App Manager: per-entry traversal fallback and stable list

This commit is contained in:
admin committed 2026-11-12 13:39:00 +08:00
1 parent 0e5851c521
commit dc51a3a3c7
4 files changed
+234 -24

No files matched your search

+3
View File
@@ -37,6 +37,9 @@ filza_develop.ipa
- App discovery uses the LaunchServices container list instead of a full - App discovery uses the LaunchServices container list instead of a full
inode scan on supported versions, so the first launch and App Manager open inode scan on supported versions, so the first launch and App Manager open
much faster. much faster.
- Entering App Manager also runs a fresh per-container worse_query/pathleak
traversal and falls back to per-app MCM/container lookups, so the list still
appears when the LaunchServices database cannot be read.
- App icons are used where iOS returns them (`_applicationIconImageForBundle-` - App icons are used where iOS returns them (`_applicationIconImageForBundle-`
`Identifier:format:scale:`); otherwise Filza's placeholder is shown. `Identifier:format:scale:`); otherwise Filza's placeholder is shown.
- Accessible roots, App Data (by app), App Data (UUID), App Groups and System - Accessible roots, App Data (by app), App Data (UUID), App Groups and System
+13
View File
@@ -941,6 +941,19 @@ static void hook_appsViewDidAppear(id self, SEL _cmd, BOOL animated)
if (orig_appsViewDidAppear) if (orig_appsViewDidAppear)
((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated); ((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated);
// App Manager entry always starts a fresh per-container traversal. Do not
// reuse the startup list: apps may have been installed since then and the
// LaunchServices database may be unavailable on this device.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
WQRequestAppManagerRescan();
WQInstalledApplications();
dispatch_async(dispatch_get_main_queue(), ^{
SEL loadSelector = NSSelectorFromString(@"loadFilesWithSortMode:");
if ([self respondsToSelector:loadSelector])
((void(*)(id, SEL, int))objc_msgSend)(self, loadSelector, 0);
});
});
static char kReloadKey; static char kReloadKey;
if (objc_getAssociatedObject(self, &kReloadKey)) return; if (objc_getAssociatedObject(self, &kReloadKey)) return;
objc_setAssociatedObject(self, &kReloadKey, @YES, objc_setAssociatedObject(self, &kReloadKey, @YES,
+3
View File
@@ -53,3 +53,6 @@ FOUNDATION_EXPORT NSArray<NSString *> * _Nonnull WQChildrenForRoot(NSString * _N
FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path); FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path);
FOUNDATION_EXPORT NSArray<NSNumber *> * _Nonnull WQActiveGrantHandles(void); FOUNDATION_EXPORT NSArray<NSNumber *> * _Nonnull WQActiveGrantHandles(void);
FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID); FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID);
/// Clears the cached app list and requests a fresh per-container traversal the
/// next time the App Manager asks for applications.
FOUNDATION_EXPORT void WQRequestAppManagerRescan(void);
+215 -24
View File
@@ -50,6 +50,30 @@ static NSMutableDictionary<NSString *, NSString *> *gDataMap;
static BOOL gBundleMapAttempted; static BOOL gBundleMapAttempted;
static NSObject *gBundleMapLock; static NSObject *gBundleMapLock;
static dispatch_queue_t gAppsQueue; static dispatch_queue_t gAppsQueue;
static BOOL gBuildingApps;
static void *kWQAppsQueueKey = &kWQAppsQueueKey;
static dispatch_queue_t WQAppsQueue(void)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(gAppsQueue, kWQAppsQueueKey,
(void *)1, NULL);
});
return gAppsQueue;
}
static void WQAppsQueueSync(dispatch_block_t block)
{
if (!block) return;
if (dispatch_get_specific(kWQAppsQueueKey)) {
block();
return;
}
dispatch_sync(WQAppsQueue(), block);
}
static dispatch_queue_t gFamiliesQueue; static dispatch_queue_t gFamiliesQueue;
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache; static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache;
static dispatch_group_t gStartupGroup; static dispatch_group_t gStartupGroup;
@@ -358,9 +382,7 @@ void WQRefreshDiscovery(void)
{ {
dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
WQDiscoverPerContainerFamilies(); WQDiscoverPerContainerFamilies();
if (!gAppsQueue) gAppsQueue = dispatch_queue_create( WQAppsQueueSync(^{
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
dispatch_sync(gAppsQueue, ^{
gAppsCache = WQBuildAppList(); gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date]; gAppsCacheDate = [NSDate date];
}); });
@@ -1123,6 +1145,143 @@ static void WQAppendMobileInstallationApps(NSMutableArray *result,
(unsigned long)apps.count, (unsigned long)result.count); (unsigned long)apps.count, (unsigned long)result.count);
} }
static BOOL gForceAppTraversal;
#pragma mark - Per-entry app scan
static void WQAddCandidateBundleID(NSMutableOrderedSet<NSString *> *ids,
id value)
{
if ([value isKindOfClass:NSString.class] &&
WQLooksLikeBundleIdentifier(value))
[ids addObject:value];
}
static void WQAddProxyBundleID(NSMutableOrderedSet<NSString *> *ids, id proxy)
{
if (!proxy) return;
NSArray<NSString *> *selectors = @[
@"applicationIdentifier", @"bundleIdentifier", @"bundleId",
@"applicationID",
];
for (NSString *name in selectors) {
SEL selector = NSSelectorFromString(name);
if (![proxy respondsToSelector:selector]) continue;
id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector);
WQAddCandidateBundleID(ids, value);
}
for (NSString *key in @[@"bundleIdentifier", @"applicationIdentifier",
@"_bundleIdentifier", @"_applicationIdentifier"]) {
@try {
WQAddCandidateBundleID(ids, [proxy valueForKey:key]);
} @catch (__unused NSException *exception) {
}
}
SEL urlSelector = NSSelectorFromString(@"bundleURL");
if ([proxy respondsToSelector:urlSelector]) {
id url = ((id(*)(id, SEL))objc_msgSend)(proxy, urlSelector);
if ([url isKindOfClass:NSURL.class]) {
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
[[(NSURL *)url path] stringByAppendingPathComponent:@"Info.plist"]];
WQAddCandidateBundleID(ids, plist[@"CFBundleIdentifier"]);
}
}
}
/// Bundle ids exposed by LaunchServices without relying on allApplications.
/// This is the fallback when the CSStore parser/layout is unavailable.
static NSArray<NSString *> *WQCandidateBundleIDs(void)
{
NSMutableOrderedSet<NSString *> *ids = [NSMutableOrderedSet orderedSet];
Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace");
id workspace = nil;
SEL defaultSelector = NSSelectorFromString(@"defaultWorkspace");
if ([workspaceClass respondsToSelector:defaultSelector])
workspace = ((id(*)(id, SEL))objc_msgSend)(workspaceClass,
defaultSelector);
NSArray<NSString *> *arraySelectors = @[
@"installedApplications", @"allInstalledApplications",
@"allApplications", @"unrestrictedApplications",
@"directionsApplications", @"placeholderApplications",
];
if (workspace) {
for (NSString *name in arraySelectors) {
SEL selector = NSSelectorFromString(name);
if (![workspace respondsToSelector:selector]) continue;
id value = ((id(*)(id, SEL))objc_msgSend)(workspace, selector);
if (![value isKindOfClass:NSArray.class]) continue;
for (id item in value) {
if ([item isKindOfClass:NSString.class])
WQAddCandidateBundleID(ids, item);
else
WQAddProxyBundleID(ids, item);
}
}
NSArray<NSString *> *typedSelectors = @[
@"applicationsOfType:", @"legacyApplicationProxiesListWithType:",
];
for (NSString *name in typedSelectors) {
SEL selector = NSSelectorFromString(name);
if (![workspace respondsToSelector:selector]) continue;
for (NSUInteger type = 0; type <= 1; type++) {
id value = ((id(*)(id, SEL, NSUInteger))objc_msgSend)(
workspace, selector, type);
if (![value isKindOfClass:NSArray.class]) continue;
for (id item in value) WQAddProxyBundleID(ids, item);
}
}
SEL enumerateSelector = NSSelectorFromString(
@"enumerateApplicationsOfType:block:");
if ([workspace respondsToSelector:enumerateSelector]) {
void (^block)(id) = ^(id proxy) {
WQAddProxyBundleID(ids, proxy);
};
((void(*)(id, SEL, NSUInteger, id))objc_msgSend)(workspace,
enumerateSelector, 0, block);
}
}
// MobileInstallation is absent on some iOS 27 builds, but present on
// others; use it when available.
NSDictionary<NSString *, NSDictionary *> *installation =
WQMobileInstallationApps();
for (NSString *key in installation) {
WQAddCandidateBundleID(ids, key);
NSDictionary *entry = installation[key];
if ([entry isKindOfClass:NSDictionary.class])
WQAddCandidateBundleID(ids, entry[@"CFBundleIdentifier"]);
}
return ids.array;
}
static NSArray<NSString *> *WQContainersForBundleIDs(NSArray<NSString *> *ids)
{
NSMutableArray<NSString *> *paths = [NSMutableArray array];
for (NSString *bundleID in ids) {
if (!bundleID.length) continue;
NSString *error = nil;
NSString *path = MCMFilzaDataContainerPath(bundleID, &error);
if (!path.length) continue;
path = WQCanonicalPath(path);
if (path.length && WQPathExists(path)) [paths addObject:path];
}
return paths;
}
void WQRequestAppManagerRescan(void)
{
gForceAppTraversal = YES;
WQAppsQueueSync(^{
gAppsCache = nil;
gAppsCacheDate = nil;
});
if (gPathleakCache) [gPathleakCache removeAllObjects];
NSLog(@"[WQ] App Manager requested a fresh per-container scan");
}
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void) static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
{ {
NSString *root = @"/private/var/mobile/Containers/Data/Application"; NSString *root = @"/private/var/mobile/Containers/Data/Application";
@@ -1133,27 +1292,44 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…"); WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
WQLoadLaunchServicesDatabase(); WQLoadLaunchServicesDatabase();
NSMutableArray<NSString *> *lsContainers = [NSMutableArray array]; BOOL forceTraversal = gForceAppTraversal;
gForceAppTraversal = NO;
NSMutableArray<NSString *> *candidates = [NSMutableArray array];
// Traditional path traversal: whenever the App Manager opens, enumerate
// the data-container root again through worse_query/pathleak. This does
// not depend on the LaunchServices database being readable.
if (forceTraversal) {
WQPostStartupProgress(0.24, @"正在逐个遍历应用数据容器…");
[candidates addObjectsFromArray:WQEnumerateContainers(root)];
}
// Names/container hints from the LaunchServices database. These are a
// supplement, never the only source.
[candidates addObjectsFromArray:WQLSContainerPaths() ?: @[]];
// Per-app container lookup by bundle id. This is the fallback for devices
// where the data-container root cannot be listed at all.
NSArray<NSString *> *bundleIDs = WQCandidateBundleIDs();
[candidates addObjectsFromArray:WQContainersForBundleIDs(bundleIDs)];
// If neither source found anything, do the traditional enumeration even
// when this was not an App Manager-triggered scan.
if (candidates.count == 0) {
[candidates addObjectsFromArray:WQEnumerateContainers(root)];
}
NSString *lsPrefix = [root stringByAppendingString:@"/"]; NSString *lsPrefix = [root stringByAppendingString:@"/"];
for (NSString *path in WQLSContainerPaths() ?: @[]) { NSMutableOrderedSet<NSString *> *unique = [NSMutableOrderedSet orderedSet];
for (NSString *path in candidates) {
NSString *canonical = WQCanonicalPath(path); NSString *canonical = WQCanonicalPath(path);
if (![canonical hasPrefix:lsPrefix]) continue; if (![canonical hasPrefix:lsPrefix]) continue;
if (canonical.length != lsPrefix.length + 36) continue; if (canonical.length != lsPrefix.length + 36) continue;
if (![[NSUUID alloc] initWithUUIDString: if (![[NSUUID alloc] initWithUUIDString:
[canonical substringFromIndex:lsPrefix.length]]) continue; [canonical substringFromIndex:lsPrefix.length]]) continue;
[lsContainers addObject:canonical]; [unique addObject:canonical];
}
NSArray<NSString *> *containers = nil;
if (lsContainers.count >= 2) {
containers = lsContainers;
WQPostStartupProgress(0.30, [NSString stringWithFormat:
@"应用数据库已列出 %lu 个数据容器,正在授权…",
(unsigned long)containers.count]);
} else {
WQPostStartupProgress(0.26, @"正在扫描应用数据容器…");
containers = WQEnumerateContainers(root);
} }
NSArray<NSString *> *containers = unique.array;
WQPostStartupProgress(0.40, [NSString stringWithFormat: WQPostStartupProgress(0.40, [NSString stringWithFormat:
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]); @"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
NSUInteger granted = 0; NSUInteger granted = 0;
@@ -1176,17 +1352,21 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void) NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
{ {
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
__block NSArray *value = nil; __block NSArray *value = nil;
dispatch_sync(gAppsQueue, ^{ WQAppsQueueSync(^{
NSTimeInterval age = gAppsCacheDate NSTimeInterval age = gAppsCacheDate
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9; ? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
if (gBuildingApps) {
value = gAppsCache ?: @[];
return;
}
// Never cache an empty result: the first call can happen before the // Never cache an empty result: the first call can happen before the
// worse_query grants and app-container scan have finished. // worse_query grants and app-container scan have finished.
if (!gAppsCache || gAppsCache.count == 0 || age > 120) { if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
gBuildingApps = YES;
gAppsCache = WQBuildAppList(); gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date]; gAppsCacheDate = [NSDate date];
gBuildingApps = NO;
} }
value = gAppsCache; value = gAppsCache;
}); });
@@ -1507,15 +1687,14 @@ void WQFilzaStart(void)
WQScheduleFavorites(); WQScheduleFavorites();
// Build the app-container index before the user opens Apps manager. // Build the app-container index before the user opens Apps manager.
if (!gAppsQueue) gAppsQueue = dispatch_queue_create( WQAppsQueue();
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create( if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create(
"com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL); "com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL);
// App containers are the priority: the Apps manager must populate before // App containers are the priority: the Apps manager must populate before
// the slower system-group sweep begins. // the slower system-group sweep begins.
dispatch_group_enter(gStartupGroup); dispatch_group_enter(gStartupGroup);
dispatch_async(gAppsQueue, ^{ dispatch_async(WQAppsQueue(), ^{
WQPostStartupProgress(0.24, @"正在枚举应用数据容器…"); WQPostStartupProgress(0.24, @"正在枚举应用数据容器…");
if (!gAppsCache) gAppsCache = WQBuildAppList(); if (!gAppsCache) gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date]; gAppsCacheDate = [NSDate date];
@@ -1587,6 +1766,18 @@ void WQFilzaStart(void)
- (NSNumber *)dynamicDiskUsage { return @0; } - (NSNumber *)dynamicDiskUsage { return @0; }
- (BOOL)isDeletable { return YES; } - (BOOL)isDeletable { return YES; }
- (BOOL)isPlaceholder { return NO; } - (BOOL)isPlaceholder { return NO; }
- (BOOL)isSystem { return NO; } - (BOOL)isSystem {
return [_bundleIdentifier hasPrefix:@"com.apple."];
}
- (BOOL)isInstalled { return YES; }
- (BOOL)isNewsstandApp { return NO; }
- (BOOL)isRestricted { return NO; }
- (BOOL)isLaunchProhibited { return NO; }
- (BOOL)isHidden { return NO; }
- (NSString *)minimumSystemVersion { return @"15.0"; }
- (NSString *)teamID { return @""; }
- (long long)bundleModTime {
return (long long)NSDate.date.timeIntervalSinceReferenceDate;
}
@end @end