处理键盘崩溃
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBConfig.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
|
||||
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
|
||||
@@ -59,10 +60,45 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent";
|
||||
|
||||
@interface KBSkinManager ()
|
||||
@property (atomic, strong, readwrite) KBSkinTheme *current;
|
||||
@property (nonatomic, strong) NSCache<NSString *, UIImage *> *kb_fileImageCache;
|
||||
@property (nonatomic, copy, nullable) NSString *kb_cachedBgSkinId;
|
||||
@property (nonatomic, assign) BOOL kb_cachedBgResolved;
|
||||
@property (nonatomic, strong, nullable) UIImage *kb_cachedBgImage;
|
||||
@end
|
||||
|
||||
@implementation KBSkinManager
|
||||
|
||||
/// 从文件路径解码图片,并按 maxPixel 限制最长边像素(避免加载超大背景图导致键盘扩展内存飙升)。
|
||||
+ (nullable UIImage *)kb_imageAtPath:(NSString *)path maxPixel:(NSUInteger)maxPixel {
|
||||
if (path.length == 0) return nil;
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
|
||||
if (!source) return nil;
|
||||
NSDictionary *opts = @{
|
||||
(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
|
||||
(__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
|
||||
(__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(MAX(1, (NSInteger)maxPixel)),
|
||||
};
|
||||
CGImageRef cg = CGImageSourceCreateThumbnailAtIndex(source, 0, (__bridge CFDictionaryRef)opts);
|
||||
CFRelease(source);
|
||||
if (!cg) return nil;
|
||||
UIImage *img = [UIImage imageWithCGImage:cg scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp];
|
||||
CGImageRelease(cg);
|
||||
return img;
|
||||
}
|
||||
|
||||
static inline NSUInteger KBApproxImageCostBytes(UIImage *img) {
|
||||
if (!img) return 0;
|
||||
CGFloat scale = img.scale > 0 ? img.scale : [UIScreen mainScreen].scale;
|
||||
CGSize s = img.size;
|
||||
double px = (double)s.width * scale * (double)s.height * scale;
|
||||
if (px <= 0) return 0;
|
||||
// RGBA 4 bytes/pixel
|
||||
double cost = px * 4.0;
|
||||
if (cost > (double)NSUIntegerMax) return NSUIntegerMax;
|
||||
return (NSUInteger)cost;
|
||||
}
|
||||
|
||||
/// 返回所有可能的皮肤根目录(优先 App Group,其次当前进程的 Caches)。
|
||||
+ (NSArray<NSString *> *)kb_candidateBaseRoots {
|
||||
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
||||
@@ -104,6 +140,14 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent";
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_kb_fileImageCache = [NSCache new];
|
||||
// 键盘扩展内存上限较小,缓存要保守一些;主 App 也共用该实现但不会出问题。
|
||||
// iPad 的键盘背景可能更大,适当放宽。
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
_kb_fileImageCache.totalCostLimit = 24 * 1024 * 1024;
|
||||
} else {
|
||||
_kb_fileImageCache.totalCostLimit = 12 * 1024 * 1024;
|
||||
}
|
||||
KBSkinTheme *t = [self p_loadFromStore];
|
||||
// 若存储中的皮肤在 App Group 中找不到对应资源目录(如首次安装 / 已被清理),则回退到默认皮肤。
|
||||
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
||||
@@ -170,6 +214,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
||||
if (!theme) return NO;
|
||||
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
|
||||
[self clearRuntimeImageCaches];
|
||||
// 将主题写入 App Group 存储(失败也不影响本次进程内的使用)
|
||||
[self p_saveToStore:theme];
|
||||
// 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。
|
||||
@@ -187,6 +232,15 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
[self applyTheme:[self.class defaultTheme]];
|
||||
}
|
||||
|
||||
- (void)clearRuntimeImageCaches {
|
||||
@synchronized (self) {
|
||||
[self.kb_fileImageCache removeAllObjects];
|
||||
self.kb_cachedBgSkinId = nil;
|
||||
self.kb_cachedBgResolved = NO;
|
||||
self.kb_cachedBgImage = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name {
|
||||
// 仅作为“存在背景图”的标记使用:图像文件本身存放在 App Group 容器
|
||||
// Skins/<skinId>/background.png 中,这里不再把二进制图片写入 Keychain,
|
||||
@@ -216,20 +270,52 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
NSString *skinId = self.current.skinId;
|
||||
if (skinId.length == 0) return nil;
|
||||
|
||||
// 同一个 skinId 在键盘的生命周期内会被频繁读取;缓存一份避免反复解码导致内存上涨。
|
||||
@synchronized (self) {
|
||||
if (self.kb_cachedBgResolved && [self.kb_cachedBgSkinId isEqualToString:skinId]) {
|
||||
return self.kb_cachedBgImage;
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSString *> *roots = [self.class kb_candidateBaseRoots];
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId];
|
||||
|
||||
// 背景图通常远大于键盘实际显示区域,按像素上限做缩略解码,显著降低扩展内存占用。
|
||||
NSUInteger maxPixel = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? 2048 : 1024;
|
||||
for (NSString *base in roots) {
|
||||
NSString *bgPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) {
|
||||
continue;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfFile:bgPath];
|
||||
if (data.length == 0) continue;
|
||||
UIImage *img = [UIImage imageWithData:data scale:[UIScreen mainScreen].scale];
|
||||
if (img) return img;
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"bg|%@", bgPath];
|
||||
UIImage *cached = [self.kb_fileImageCache objectForKey:cacheKey];
|
||||
if (cached) {
|
||||
@synchronized (self) {
|
||||
self.kb_cachedBgSkinId = skinId;
|
||||
self.kb_cachedBgResolved = YES;
|
||||
self.kb_cachedBgImage = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
UIImage *img = [self.class kb_imageAtPath:bgPath maxPixel:maxPixel];
|
||||
if (img) {
|
||||
NSUInteger cost = KBApproxImageCostBytes(img);
|
||||
[self.kb_fileImageCache setObject:img forKey:cacheKey cost:cost];
|
||||
@synchronized (self) {
|
||||
self.kb_cachedBgSkinId = skinId;
|
||||
self.kb_cachedBgResolved = YES;
|
||||
self.kb_cachedBgImage = img;
|
||||
}
|
||||
return img;
|
||||
}
|
||||
}
|
||||
@synchronized (self) {
|
||||
self.kb_cachedBgSkinId = skinId;
|
||||
self.kb_cachedBgResolved = YES;
|
||||
self.kb_cachedBgImage = nil;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -314,7 +400,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) {
|
||||
continue;
|
||||
}
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"icon|%@", fullPath];
|
||||
UIImage *img = [self.kb_fileImageCache objectForKey:cacheKey];
|
||||
if (img) return img;
|
||||
img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) {
|
||||
[self.kb_fileImageCache setObject:img forKey:cacheKey cost:KBApproxImageCostBytes(img)];
|
||||
}
|
||||
if (img) return img;
|
||||
}
|
||||
#if DEBUG
|
||||
@@ -351,7 +443,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"icon|%@", fullPath];
|
||||
UIImage *img = [self.kb_fileImageCache objectForKey:cacheKey];
|
||||
if (img) return img;
|
||||
img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) {
|
||||
[self.kb_fileImageCache setObject:img forKey:cacheKey cost:KBApproxImageCostBytes(img)];
|
||||
}
|
||||
if (img) return img;
|
||||
}
|
||||
}
|
||||
@@ -363,7 +461,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||
BOOL isDir = NO;
|
||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
||||
UIImage *img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"icon|%@", fullPath];
|
||||
UIImage *img = [self.kb_fileImageCache objectForKey:cacheKey];
|
||||
if (img) return img;
|
||||
img = [UIImage imageWithContentsOfFile:fullPath];
|
||||
if (img) {
|
||||
[self.kb_fileImageCache setObject:img forKey:cacheKey cost:KBApproxImageCostBytes(img)];
|
||||
}
|
||||
if (img) return img;
|
||||
}
|
||||
}
|
||||
@@ -449,6 +553,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
||||
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
||||
t = [self.class defaultTheme];
|
||||
}
|
||||
[self clearRuntimeImageCaches];
|
||||
self.current = t;
|
||||
if (broadcast) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
||||
|
||||
Reference in New Issue
Block a user