1 Commits
Author SHA1 Message Date
admin 168a89cb8a Fix iOS 26 app manager blank state 2026-11-11 15:16:54 +08:00
4 changed files with 361 additions and 15 deletions

No files matched your search

+236 -9
View File
@@ -407,6 +407,8 @@ static id hook_unZipFilePassword(id self, SEL _cmd, id zipPath, id toPath, id cu
@interface LSApplicationWorkspace : NSObject
+ (id)defaultWorkspace;
- (NSArray *)allApplications;
- (NSArray *)allInstalledApplications;
- (void)enumerateApplicationsOfType:(NSUInteger)type block:(void (^)(id proxy))block;
@end
// --- Helper: find app bundle path from bundleId ---
@@ -490,6 +492,114 @@ static NSString *findIconPath(NSString *bundlePath, NSDictionary *infoPlist) {
return bestIcon;
}
// LaunchServices changed several proxy getters on iOS 26. Derive the bundle
// identifier from every available source instead of dropping the proxy.
static NSString *WQProxyString(id proxy, NSString *selectorName)
{
SEL selector = NSSelectorFromString(selectorName);
if (!proxy || ![proxy respondsToSelector:selector]) return nil;
id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector);
return [value isKindOfClass:NSString.class] ? value : nil;
}
static NSString *WQBundleIdentifierFromProxy(id proxy)
{
if (!proxy) return nil;
NSArray<NSString *> *selectors = @[
@"applicationIdentifier", @"bundleIdentifier", @"bundleId",
@"applicationID",
];
for (NSString *name in selectors) {
NSString *value = WQProxyString(proxy, name);
if (value.length) return value;
}
NSArray<NSString *> *keys = @[
@"bundleIdentifier", @"applicationIdentifier",
@"_bundleIdentifier", @"_applicationIdentifier",
];
for (NSString *key in keys) {
@try {
id value = [proxy valueForKey:key];
if ([value isKindOfClass:NSString.class] && [value length])
return value;
} @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]) {
NSString *plistPath = [[(NSURL *)url path]
stringByAppendingPathComponent:@"Info.plist"];
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:
plistPath];
NSString *identifier = info[@"CFBundleIdentifier"];
if ([identifier isKindOfClass:NSString.class] && identifier.length)
return identifier;
}
}
return nil;
}
static NSArray *WQLSApplicationSources(id workspace, NSArray *original)
{
static __thread BOOL collecting;
if (collecting) return original ?: @[];
collecting = YES;
NSMutableArray *result = [NSMutableArray array];
NSMutableSet<NSValue *> *seen = [NSMutableSet set];
void (^append)(id) = ^(id proxy) {
if (!proxy) return;
NSValue *value = [NSValue valueWithNonretainedObject:proxy];
if ([seen containsObject:value]) return;
[seen addObject:value];
[result addObject:proxy];
};
for (id proxy in original ?: @[]) append(proxy);
NSArray<NSString *> *arraySelectors = @[
@"allInstalledApplications", @"unrestrictedApplications",
@"directionsApplications", @"placeholderApplications",
];
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])
for (id proxy in value) append(proxy);
}
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])
for (id proxy in value) append(proxy);
}
}
SEL enumerateSelector = NSSelectorFromString(
@"enumerateApplicationsOfType:block:");
if ([workspace respondsToSelector:enumerateSelector]) {
void (^enumerateBlock)(id) = ^(id proxy) { append(proxy); };
((void(*)(id, SEL, NSUInteger, id))objc_msgSend)(workspace,
enumerateSelector, 0, enumerateBlock);
}
collecting = NO;
if (result.count != (original ?: @[]).count)
NSLog(@"[Tweak] LaunchServices sources: original=%lu merged=%lu",
(unsigned long)(original ?: @[]).count,
(unsigned long)result.count);
return result;
}
// --- Hook: allApplications fallback ---
static IMP orig_allApplications = NULL;
static id hook_allApplications(id self, SEL _cmd) {
@@ -497,15 +607,20 @@ static id hook_allApplications(id self, SEL _cmd) {
? ((id(*)(id,SEL))orig_allApplications)(self, _cmd) : @[];
NSMutableArray *apps = [NSMutableArray array];
NSMutableSet<NSString *> *known = [NSMutableSet set];
for (id proxy in origResult ?: @[]) {
NSString *identifier = [proxy respondsToSelector:@selector(applicationIdentifier)]
? [proxy applicationIdentifier] : nil;
if (!identifier) identifier = [proxy respondsToSelector:@selector(bundleIdentifier)]
? [proxy bundleIdentifier] : nil;
if (identifier.length) {
[known addObject:identifier];
[apps addObject:proxy];
NSMutableSet<NSString *> *knownBundlePaths = [NSMutableSet set];
NSArray *sources = WQLSApplicationSources(self, origResult);
for (id proxy in sources) {
NSString *identifier = WQBundleIdentifierFromProxy(proxy);
if (identifier.length) [known addObject:identifier];
SEL urlSelector = NSSelectorFromString(@"bundleURL");
if ([proxy respondsToSelector:urlSelector]) {
id url = ((id(*)(id,SEL))objc_msgSend)(proxy, urlSelector);
if ([url isKindOfClass:NSURL.class] && [(NSURL *)url path].length)
[knownBundlePaths addObject:[(NSURL *)url path]];
}
// Keep proxies even when iOS 26 hides their identifier; the
// ApplicationItem hook derives it again from bundleURL/Info.plist.
[apps addObject:proxy];
}
// Apps hidden from LaunchServices enumeration are recovered from their
@@ -528,6 +643,48 @@ static id hook_allApplications(id self, SEL _cmd) {
[known addObject:bundleID];
merged++;
}
// Some iOS 26 builds expose only bundle identifiers here.
SEL identifiersSelector = NSSelectorFromString(@"installedApplications");
if ([self respondsToSelector:identifiersSelector]) {
id identifiers = ((id(*)(id, SEL))objc_msgSend)(self,
identifiersSelector);
if ([identifiers isKindOfClass:NSArray.class]) {
for (id value in identifiers) {
if (![value isKindOfClass:NSString.class] ||
[known containsObject:value]) continue;
NSString *bundleID = value;
NSString *bundlePath = WQBundlePathForIdentifier(bundleID);
if (bundlePath.length &&
[knownBundlePaths containsObject:bundlePath]) continue;
id proxy = nil;
if ([proxyClass respondsToSelector:
NSSelectorFromString(@"applicationProxyForIdentifier:")]) {
proxy = ((id(*)(id,SEL,id))objc_msgSend)(proxyClass,
NSSelectorFromString(@"applicationProxyForIdentifier:"),
bundleID);
}
if (!proxy) {
NSString *error = nil;
NSString *dataPath = MCMFilzaDataContainerPath(bundleID,
&error);
NSMutableDictionary *info = [@{
@"bundleID": bundleID,
@"name": WQFallbackDisplayName(bundleID),
@"version": @"1.0",
} mutableCopy];
if (bundlePath.length) info[@"bundlePath"] = bundlePath;
if (dataPath.length) info[@"dataPath"] = dataPath;
proxy = [[WQApplicationProxy alloc] initWithAppInfo:info];
}
if (proxy) {
[apps addObject:proxy];
[known addObject:bundleID];
merged++;
}
}
}
}
if (merged > 0 || apps.count > 0) {
NSLog(@"[Tweak] Apps Manager: LaunchServices=%lu worse_query merged=%lu total=%lu",
(unsigned long)(origResult.count), (unsigned long)merged,
@@ -546,6 +703,17 @@ static id hook_allApplications(id self, SEL _cmd) {
NSString *bid = info[@"CFBundleIdentifier"];
if (bid) {
id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid];
if (!proxy) {
proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
@"bundleID": bid,
@"bundlePath": [uuidPath stringByAppendingPathComponent:item],
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
@"name": info[@"CFBundleDisplayName"] ?:
info[@"CFBundleName"] ?: bid,
@"version": info[@"CFBundleShortVersionString"] ?:
info[@"CFBundleVersion"] ?: @"1.0",
}];
}
if (proxy) [apps addObject:proxy];
}
}
@@ -560,6 +728,17 @@ static id hook_allApplications(id self, SEL _cmd) {
NSString *bid = info[@"CFBundleIdentifier"];
if (bid) {
id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid];
if (!proxy) {
proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{
@"bundleID": bid,
@"bundlePath": [@"/Applications" stringByAppendingPathComponent:item],
@"dataPath": WQDataPathForIdentifier(bid) ?: @"",
@"name": info[@"CFBundleDisplayName"] ?:
info[@"CFBundleName"] ?: bid,
@"version": info[@"CFBundleShortVersionString"] ?:
info[@"CFBundleVersion"] ?: @"1.0",
}];
}
if (proxy) [apps addObject:proxy];
}
}
@@ -574,7 +753,13 @@ static void hook_setAppProxy(id self, SEL _cmd, id proxy) {
((void(*)(id,SEL,id))orig_setAppProxy)(self, _cmd, proxy);
NSString *bundleId = [self performSelector:NSSelectorFromString(@"bundleId")];
if (!bundleId) return;
if (!bundleId.length) {
bundleId = WQBundleIdentifierFromProxy(proxy);
if (bundleId.length)
((void(*)(id,SEL,id))objc_msgSend)(self,
NSSelectorFromString(@"setBundleId:"), bundleId);
}
if (!bundleId.length) return;
NSString *bundlePath = nil;
NSString *currentFilePath = [self performSelector:NSSelectorFromString(@"filePath")];
@@ -675,6 +860,43 @@ static void hook_didSelectItem(id self, SEL _cmd, id browserView, id indexPath)
((void(*)(id,SEL,id,id))orig_didSelectItem)(self, _cmd, browserView, indexPath);
}
static IMP orig_appsViewDidAppear;
static void hook_appsViewDidAppear(id self, SEL _cmd, BOOL animated)
{
if (orig_appsViewDidAppear)
((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated);
static char kReloadKey;
if (objc_getAssociatedObject(self, &kReloadKey)) return;
objc_setAssociatedObject(self, &kReloadKey, @YES,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
__block NSInteger attempts = 0;
__block __weak void (^weakRetry)(void);
void (^retry)(void) = ^{
NSArray *list = nil;
SEL listSelector = NSSelectorFromString(@"fileList");
if ([self respondsToSelector:listSelector])
list = ((id(*)(id, SEL))objc_msgSend)(self, listSelector);
if ([list isKindOfClass:NSArray.class] && list.count > 0) return;
if (attempts++ >= 12) return;
SEL loadSelector = NSSelectorFromString(@"loadFilesWithSortMode:");
if ([self respondsToSelector:loadSelector])
((void(*)(id, SEL, int))objc_msgSend)(self, loadSelector, 0);
if (attempts == 1)
NSLog(@"[Tweak] Apps Manager was empty; retrying after discovery");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC),
dispatch_get_main_queue(), ^{
void (^strongRetry)(void) = weakRetry;
if (strongRetry) strongRetry();
});
};
weakRetry = retry;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),
dispatch_get_main_queue(), retry);
}
#pragma mark - License / Integrity Bypass
// Suppress "Main binary was modified" and "Not activated" alerts.
@@ -2091,6 +2313,11 @@ static void installHooks(void) {
if (appsVC) {
Method m = class_getInstanceMethod(appsVC, NSSelectorFromString(@"browserView:didSelectItemAtIndexPath:"));
if (m) { orig_didSelectItem = method_getImplementation(m); method_setImplementation(m, (IMP)hook_didSelectItem); }
m = class_getInstanceMethod(appsVC, @selector(viewDidAppear:));
if (m) {
orig_appsViewDidAppear = method_getImplementation(m);
method_setImplementation(m, (IMP)hook_appsViewDidAppear);
}
}
NSLog(@"[Tweak] All hooks installed");
+1
View File
@@ -52,3 +52,4 @@ FOUNDATION_EXPORT void WQRefreshDiscovery(void);
FOUNDATION_EXPORT NSArray<NSString *> * _Nonnull WQChildrenForRoot(NSString * _Nonnull root);
FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path);
FOUNDATION_EXPORT NSArray<NSNumber *> * _Nonnull WQActiveGrantHandles(void);
FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID);
+113 -3
View File
@@ -698,7 +698,7 @@ static void WQInstallAppLinks(NSArray<NSDictionary<NSString *, NSString *> *> *a
(unsigned long)uuidLinked, (unsigned long)apps.count);
}
static NSString *WQFallbackDisplayName(NSString *bundleID)
NSString *WQFallbackDisplayName(NSString *bundleID)
{
static NSDictionary<NSString *, NSString *> *names;
static dispatch_once_t onceToken;
@@ -968,6 +968,101 @@ static void WQDiscoverPerContainerFamilies(void)
}
}
static NSDictionary<NSString *, NSDictionary *> *WQMobileInstallationApps(void)
{
static NSDictionary *apps;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation",
RTLD_LAZY);
if (!handle) {
NSLog(@"[WQ] MobileInstallation framework unavailable: %s", dlerror());
return;
}
NSDictionary *(*lookup)(NSDictionary *, NSError **) =
(NSDictionary *(*)(NSDictionary *, NSError **))dlsym(handle,
"MobileInstallationLookup");
if (lookup) {
NSError *error = nil;
id value = lookup(nil, &error);
if ([value isKindOfClass:NSDictionary.class]) apps = [value copy];
NSLog(@"[WQ] MobileInstallationLookup apps=%lu error=%@",
(unsigned long)apps.count,
error.localizedDescription ?: @"none");
} else {
NSLog(@"[WQ] MobileInstallationLookup symbol missing");
}
dlclose(handle);
});
return apps ?: @{};
}
static void WQAppendMobileInstallationApps(NSMutableArray *result,
NSMutableSet<NSString *> *known)
{
NSDictionary<NSString *, NSDictionary *> *apps = WQMobileInstallationApps();
for (NSString *key in apps) {
NSDictionary *entry = apps[key];
if (![entry isKindOfClass:NSDictionary.class]) continue;
NSString *bundleID = entry[@"CFBundleIdentifier"];
if (![bundleID isKindOfClass:NSString.class] || !bundleID.length)
bundleID = key;
if (!bundleID.length || [known containsObject:bundleID]) continue;
id containerValue = entry[@"Container"];
NSString *dataPath = [containerValue isKindOfClass:NSURL.class]
? [(NSURL *)containerValue path] : containerValue;
if (![dataPath isKindOfClass:NSString.class] || !dataPath.length) {
NSString *error = nil;
dataPath = MCMFilzaDataContainerPath(bundleID, &error);
}
id bundleValue = entry[@"Path"];
NSString *bundlePath = [bundleValue isKindOfClass:NSURL.class]
? [(NSURL *)bundleValue path] : bundleValue;
if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length) {
bundleValue = entry[@"BundlePath"];
bundlePath = [bundleValue isKindOfClass:NSURL.class]
? [(NSURL *)bundleValue path] : bundleValue;
}
if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length)
bundlePath = WQBundlePathForIdentifier(bundleID);
NSMutableDictionary *info = [@{
@"bundleID": bundleID,
@"name": bundleID,
@"version": @"1.0",
} mutableCopy];
if (dataPath.length) info[@"dataPath"] = dataPath;
if (bundlePath.length) {
info[@"bundlePath"] = bundlePath;
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:
[bundlePath stringByAppendingPathComponent:@"Info.plist"]];
NSString *name = plist[@"CFBundleDisplayName"];
if (!name.length) name = plist[@"CFBundleName"];
if (name.length) info[@"name"] = name;
NSString *version = plist[@"CFBundleShortVersionString"];
if (!version.length) version = plist[@"CFBundleVersion"];
if (version.length) info[@"version"] = version;
}
NSString *name = entry[@"CFBundleDisplayName"];
if (!name.length) name = entry[@"CFBundleName"];
if ([name isKindOfClass:NSString.class] && name.length)
info[@"name"] = name;
NSString *version = entry[@"CFBundleShortVersionString"];
if (!version.length) version = entry[@"CFBundleVersion"];
if ([version isKindOfClass:NSString.class] && version.length)
info[@"version"] = version;
if ([info[@"name"] isEqualToString:bundleID])
info[@"name"] = WQFallbackDisplayName(bundleID);
[result addObject:info];
[known addObject:bundleID];
}
if (apps.count)
NSLog(@"[WQ] MobileInstallation merged=%lu total=%lu",
(unsigned long)apps.count, (unsigned long)result.count);
}
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
{
NSString *root = @"/private/var/mobile/Containers/Data/Application";
@@ -984,7 +1079,12 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
if (WQGrantContainer(container)) granted++;
}
WQPostStartupProgress(0.48, @"正在从数据目录识别应用…");
NSArray *apps = WQAppInfoFromContainers(containers);
NSArray *containerApps = WQAppInfoFromContainers(containers);
NSMutableArray *apps = [containerApps mutableCopy];
NSMutableSet<NSString *> *known = [NSMutableSet set];
for (NSDictionary *info in containerApps)
if (info[@"bundleID"]) [known addObject:info[@"bundleID"]];
WQAppendMobileInstallationApps(apps, known);
WQInstallAppLinks(apps);
NSLog(@"[WQ] app discovery containers=%lu granted=%lu apps=%lu",
(unsigned long)containers.count, (unsigned long)granted,
@@ -1000,7 +1100,9 @@ NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
dispatch_sync(gAppsQueue, ^{
NSTimeInterval age = gAppsCacheDate
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
if (!gAppsCache || age > 120) {
// Never cache an empty result: the first call can happen before the
// worse_query grants and app-container scan have finished.
if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
gAppsCache = WQBuildAppList();
gAppsCacheDate = [NSDate date];
}
@@ -1133,6 +1235,14 @@ NSString *WQBundlePathForIdentifier(NSString *bundleID)
NSDictionary *info = gBundleInfo[bundleID];
if ([info[@"path"] isKindOfClass:NSString.class]) return info[@"path"];
NSDictionary *installation = WQMobileInstallationApps()[bundleID];
if ([installation isKindOfClass:NSDictionary.class]) {
NSString *path = installation[@"Path"];
if (![path isKindOfClass:NSString.class] || !path.length)
path = installation[@"BundlePath"];
if ([path isKindOfClass:NSString.class] && path.length) return path;
}
Class proxyClass = NSClassFromString(@"LSApplicationProxy");
id proxy = [proxyClass respondsToSelector:
NSSelectorFromString(@"applicationProxyForIdentifier:")]
+11 -3
View File
@@ -49,9 +49,9 @@ def exclude_framework(name):
if n in ('libgdal', 'openblas', 'libgfortran', 'libproj', 'libgeos',
'libgeos_c', 'libspatialindex', 'libspatialindex_c'):
return 'Python geo/math'
# Duplicate variant frameworks (A/B/C/D/E) are runtime alternatives.
if re.search(r'(dash|perl|ssh_cmd|openrsync|xetex|luahbtex|luatex|pdftex|euptex)[A-E]$', n):
return 'duplicate variant'
# Keep the A-E variants. ios_system picks a free variant for each
# concurrent script/shell/SSH session, so stripping them breaks dash
# scripts and parallel sessions.
return None
fw_size = {}
@@ -133,6 +133,14 @@ while pending:
selected.add(d)
pending.append(d)
# ios_system appends A-E when it starts concurrent dash scripts. These
# variants are not referenced by commandDictionary.plist, so add them
# explicitly; otherwise `dash script.sh` fails to load dashA.framework.
for variant in ('dashA', 'dashB', 'dashC', 'dashD', 'dashE'):
framework = variant + '.framework'
if framework in fw_size:
selected.add(framework)
print('selected frameworks:', len(selected), 'size:', round(sum(fw_size[f] for f in selected)/1e6, 1), 'MB')
# extract frameworks