处理键盘崩溃

This commit is contained in:
2026-02-10 13:22:19 +08:00
parent 4c57f16058
commit 3c71797b7b
5 changed files with 459 additions and 99 deletions

View File

@@ -30,6 +30,9 @@
#import "UIImage+KBColor.h"
#import <AVFoundation/AVFoundation.h>
#import <SDWebImage/SDWebImage.h>
#if DEBUG
#import <mach/mach.h>
#endif
// #import "KBLog.h"
@@ -94,6 +97,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
@property(nonatomic, strong) UIImage *kb_cachedGradientImage;
@property(nonatomic, assign) CGSize kb_cachedGradientSize;
@property(nonatomic, strong, nullable) CAGradientLayer *kb_defaultGradientLayer;
@property(nonatomic, copy, nullable) NSString *kb_lastAppliedThemeKey;
@property(nonatomic, strong) NSMutableArray<KBChatMessage *> *chatMessages;
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
@property(nonatomic, assign) BOOL chatPanelVisible;
@@ -101,14 +106,45 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
@end
#if DEBUG
static NSInteger sKBKeyboardVCAliveCount = 0;
static uint64_t KBPhysFootprintBytes(void) {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO,
(task_info_t)&vmInfo, &count);
if (kr != KERN_SUCCESS) {
return 0;
}
return (uint64_t)vmInfo.phys_footprint;
}
static NSString *KBFormatMB(uint64_t bytes) {
double mb = (double)bytes / 1024.0 / 1024.0;
return [NSString stringWithFormat:@"%.1fMB", mb];
}
#endif
@implementation KeyboardViewController
{
BOOL _kb_didTriggerLoginDeepLinkOnce;
#if DEBUG
BOOL _kb_debugDidCountAlive;
#endif
}
- (void)viewDidLoad {
[super viewDidLoad];
#if DEBUG
if (!_kb_debugDidCountAlive) {
_kb_debugDidCountAlive = YES;
sKBKeyboardVCAliveCount += 1;
}
NSLog(@"[Keyboard] KeyboardViewController viewDidLoad alive=%ld self=%p mem=%@",
(long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes()));
#endif
// /
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self setupUI];
@@ -149,12 +185,30 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
[self kb_applyDefaultSkinIfNeeded];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
//
self.kb_cachedGradientImage = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
[[KBSkinManager shared] clearRuntimeImageCaches];
[[SDImageCache sharedImageCache] clearMemory];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// /
[[KBBackspaceUndoManager shared] registerNonClearAction];
[[KBInputBufferManager shared] resetWithText:@""];
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
// HUD viewDidDisappear /
[KBHUD setContainerView:self.view];
[self kb_ensureKeyBoardMainViewIfNeeded];
[self kb_applyTheme];
#if DEBUG
NSLog(@"[Keyboard] viewWillAppear self=%p mem=%@",
self, KBFormatMB(KBPhysFootprintBytes()));
#endif
// /QQ 宿 documentContext
// liveText manualSnapshot
[[KBInputBufferManager shared]
@@ -167,6 +221,17 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[KBBackspaceUndoManager shared] registerNonClearAction];
[self kb_releaseMemoryWhenKeyboardHidden];
#if DEBUG
NSLog(@"[Keyboard] viewWillDisappear self=%p mem=%@",
self, KBFormatMB(KBPhysFootprintBytes()));
#endif
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 宿 willDisappear didDisappear
[self kb_releaseMemoryWhenKeyboardHidden];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
@@ -196,9 +261,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth];
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
CGFloat outerVerticalInset = KBFit(4.0f);
NSLayoutConstraint *h =
[self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
@@ -231,12 +294,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
//
self.functionView.hidden = YES;
[self.contentView addSubview:self.functionView];
[self.functionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
[self.contentView addSubview:self.keyBoardMainView];
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -246,15 +303,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
make.height.mas_equalTo(keyboardBaseHeight);
}];
[self.contentView addSubview:self.chatPanelView];
[self.chatPanelView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.keyBoardMainView.mas_top);
self.chatPanelHeightConstraint =
make.height.mas_equalTo(chatPanelHeight);
}];
self.chatPanelView.hidden = YES;
//
self.contentView.hidden = YES;
}
@@ -396,8 +444,14 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
//
if (show) {
[self showChatPanel:NO];
[self kb_ensureFunctionViewIfNeeded];
}
if (_functionView) {
_functionView.hidden = !show;
} else if (show) {
// ensure
self.functionView.hidden = NO;
}
self.functionView.hidden = !show;
self.keyBoardMainView.hidden = show;
if (show) {
@@ -417,7 +471,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
//
if (show) {
[self.contentView bringSubviewToFront:self.functionView];
if (_functionView) {
[self.contentView bringSubviewToFront:_functionView];
}
} else {
[self.contentView bringSubviewToFront:self.keyBoardMainView];
}
@@ -492,10 +548,13 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
}
self.chatPanelVisible = show;
if (show) {
[self kb_ensureChatPanelViewIfNeeded];
self.chatPanelView.hidden = NO;
self.chatPanelView.alpha = 0.0;
[self.contentView bringSubviewToFront:self.chatPanelView];
self.functionView.hidden = YES;
if (_functionView) {
_functionView.hidden = YES;
}
[self hideSubscriptionPanel];
[self showSettingView:NO];
[UIView animateWithDuration:0.2
@@ -506,6 +565,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
}
completion:nil];
} else {
// show/hide
if (!_chatPanelView) {
[self kb_updateKeyboardLayoutIfNeeded];
return;
}
[UIView animateWithDuration:0.18
delay:0
options:UIViewAnimationOptionCurveEaseIn
@@ -519,6 +583,114 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
[self kb_updateKeyboardLayoutIfNeeded];
}
// /
- (void)kb_ensureFunctionViewIfNeeded {
if (_functionView && _functionView.superview) {
return;
}
KBFunctionView *v = self.functionView;
if (!v.superview) {
v.hidden = YES;
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.contentView);
}];
}
}
// /
- (void)kb_ensureChatPanelViewIfNeeded {
if (_chatPanelView && _chatPanelView.superview) {
return;
}
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
KBChatPanelView *v = self.chatPanelView;
if (!v.superview) {
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.keyBoardMainView.mas_top);
self.chatPanelHeightConstraint =
make.height.mas_equalTo(chatPanelHeight);
}];
v.hidden = YES;
}
}
//
- (void)kb_ensureKeyBoardMainViewIfNeeded {
if (_keyBoardMainView && _keyBoardMainView.superview) {
return;
}
CGFloat portraitWidth = [self kb_portraitWidth];
CGFloat keyboardBaseHeight =
[self kb_keyboardBaseHeightForWidth:portraitWidth];
KBKeyBoardMainView *v = self.keyBoardMainView;
if (!v.superview) {
[self.contentView addSubview:v];
[v mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.contentView);
make.bottom.equalTo(self.contentView);
self.keyBoardMainHeightConstraint =
make.height.mas_equalTo(keyboardBaseHeight);
}];
}
[self.contentView bringSubviewToFront:v];
}
// //
- (void)kb_releaseMemoryWhenKeyboardHidden {
[KBHUD setContainerView:nil];
self.bgImageView.image = nil;
self.kb_cachedGradientImage = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
[[SDImageCache sharedImageCache] clearMemory];
// /
if (self.chatAudioPlayer) {
[self.chatAudioPlayer stop];
self.chatAudioPlayer = nil;
}
if (_chatMessages.count > 0) {
NSString *tmpRoot = NSTemporaryDirectory();
for (KBChatMessage *msg in _chatMessages.copy) {
if (tmpRoot.length > 0 && msg.audioFilePath.length > 0 &&
[msg.audioFilePath hasPrefix:tmpRoot]) {
[[NSFileManager defaultManager] removeItemAtPath:msg.audioFilePath
error:nil];
}
}
[_chatMessages removeAllObjects];
}
if (_keyBoardMainView) {
[_keyBoardMainView removeFromSuperview];
_keyBoardMainView = nil;
}
self.keyBoardMainHeightConstraint = nil;
if (_functionView) {
[_functionView removeFromSuperview];
_functionView = nil;
}
if (_chatPanelView) {
[_chatPanelView removeFromSuperview];
_chatPanelView = nil;
}
self.chatPanelVisible = NO;
if (_subscriptionView) {
[_subscriptionView removeFromSuperview];
_subscriptionView = nil;
}
if (_settingView) {
[_settingView removeFromSuperview];
_settingView = nil;
}
}
- (void)showSubscriptionPanel {
// 1) 访
if (![[KBFullAccessManager shared] hasFullAccess]) {
@@ -831,7 +1003,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
// chatPanelView
[self.chatPanelView kb_reloadWithMessages:@[]];
[view kb_reloadWithMessages:@[]];
if (self.chatAudioPlayer.isPlaying) {
[self.chatAudioPlayer stop];
}
@@ -1553,7 +1725,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
(__bridge const void *)(self),
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
#if DEBUG
NSLog(@"[Keyboard] KeyboardViewController dealloc");
if (_kb_debugDidCountAlive) {
sKBKeyboardVCAliveCount -= 1;
}
NSLog(@"[Keyboard] KeyboardViewController dealloc alive=%ld self=%p mem=%@",
(long)sKBKeyboardVCAliveCount, self, KBFormatMB(KBPhysFootprintBytes()));
#endif
}
@@ -1578,6 +1754,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
if (self.contentView.hidden) {
self.contentView.hidden = NO;
}
if (self.kb_defaultGradientLayer) {
self.kb_defaultGradientLayer.frame = self.bgImageView.bounds;
}
}
- (void)viewWillTransitionToSize:(CGSize)size
@@ -1611,93 +1790,133 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
#pragma mark - Theme
- (void)kb_applyTheme {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
BOOL isDarkMode = [self kb_isDarkModeActive];
CGSize size = self.bgImageView.bounds.size;
if (isDefaultTheme) {
if (isDarkMode) {
@autoreleasepool {
KBSkinTheme *t = [KBSkinManager shared].current;
UIImage *img = nil;
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
BOOL isDarkMode = [self kb_isDarkModeActive];
NSString *skinId = t.skinId ?: @"";
NSString *themeKey =
[NSString stringWithFormat:@"%@|default=%d|dark=%d",
skinId, isDefaultTheme, isDarkMode];
BOOL themeChanged =
(self.kb_lastAppliedThemeKey.length == 0 ||
![self.kb_lastAppliedThemeKey isEqualToString:themeKey]);
if (themeChanged) {
self.kb_lastAppliedThemeKey = themeKey;
}
CGSize size = self.bgImageView.bounds.size;
if (isDefaultTheme) {
if (isDarkMode) {
// 使使
//
img = nil;
self.bgImageView.image = nil;
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
// 使
if (@available(iOS 13.0, *)) {
// iOS 使 (RGB: 44, 44, 46 in sRGB, #2C2C2E)
// 使
UIColor *kbBgColor =
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle ==
UIUserInterfaceStyleDark) {
//
return [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
} else {
return [UIColor colorWithRed:209.0 / 255.0
green:211.0 / 255.0
blue:219.0 / 255.0
alpha:1.0];
}
}];
self.contentView.backgroundColor = kbBgColor;
self.bgImageView.backgroundColor = kbBgColor;
} else {
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
self.contentView.backgroundColor = darkColor;
self.bgImageView.backgroundColor = darkColor;
}
// iOS 使 (RGB: 44, 44, 46 in sRGB, #2C2C2E)
// 使
UIColor *kbBgColor =
[UIColor colorWithDynamicProvider:^UIColor *_Nonnull(
UITraitCollection *_Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle ==
UIUserInterfaceStyleDark) {
//
return [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
} else {
return [UIColor colorWithRed:209.0 / 255.0
green:211.0 / 255.0
blue:219.0 / 255.0
alpha:1.0];
}
}];
self.contentView.backgroundColor = kbBgColor;
self.bgImageView.backgroundColor = kbBgColor;
} else {
UIColor *darkColor = [UIColor colorWithRed:43.0 / 255.0
green:43.0 / 255.0
blue:43.0 / 255.0
alpha:1.0];
self.contentView.backgroundColor = darkColor;
self.bgImageView.backgroundColor = darkColor;
}
} else {
// 使
if (size.width <= 0 || size.height <= 0) {
[self.view layoutIfNeeded];
size = self.bgImageView.bounds.size;
// 使
if (size.width <= 0 || size.height <= 0) {
[self.view layoutIfNeeded];
size = self.bgImageView.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = self.view.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = [UIScreen mainScreen].bounds.size;
}
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
UIColor *resolvedTopColor = topColor;
UIColor *resolvedBottomColor = bottomColor;
if (@available(iOS 13.0, *)) {
resolvedTopColor =
[topColor resolvedColorWithTraitCollection:self.traitCollection];
resolvedBottomColor = [bottomColor
resolvedColorWithTraitCollection:self.traitCollection];
}
CAGradientLayer *layer = self.kb_defaultGradientLayer;
if (!layer) {
layer = [CAGradientLayer layer];
layer.startPoint = CGPointMake(0.5, 0.0);
layer.endPoint = CGPointMake(0.5, 1.0);
[self.bgImageView.layer insertSublayer:layer atIndex:0];
self.kb_defaultGradientLayer = layer;
}
layer.colors = @[
(id)resolvedTopColor.CGColor,
(id)resolvedBottomColor.CGColor
];
layer.frame = (CGRect){CGPointZero, size};
img = nil;
self.bgImageView.image = nil;
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
}
if (size.width <= 0 || size.height <= 0) {
size = self.view.bounds.size;
}
if (size.width <= 0 || size.height <= 0) {
size = [UIScreen mainScreen].bounds.size;
}
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
// img = [self kb_defaultGradientImageWithSize:size
// topColor:topColor
// bottomColor:bottomColor];
NSLog(@"===");
} else {
// 使
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
[self.kb_defaultGradientLayer removeFromSuperlayer];
self.kb_defaultGradientLayer = nil;
img = [[KBSkinManager shared] currentBackgroundImage];
}
NSLog(@"===");
} else {
// 使
self.contentView.backgroundColor = [UIColor clearColor];
self.bgImageView.backgroundColor = [UIColor clearColor];
}
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
self.bgImageView.image = img;
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
self.bgImageView.image = img;
// [self.chatPanelView kb_setBackgroundImage:img];
BOOL hasImg = (img != nil);
//
if ([self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
//
if (themeChanged &&
[self.keyBoardMainView respondsToSelector:@selector(kb_applyTheme)]) {
// method declared in KBKeyBoardMainView.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
if ([self.functionView respondsToSelector:@selector(kb_applyTheme)]) {
}
// 访 self.functionView
if (themeChanged && _functionView &&
[_functionView respondsToSelector:@selector(kb_applyTheme)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.functionView performSelector:@selector(kb_applyTheme)];
[_functionView performSelector:@selector(kb_applyTheme)];
#pragma clang diagnostic pop
}
}
}

View File

@@ -49,7 +49,6 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
[self buildBase];
[self reloadKeys];
}
return self;
}

View File

@@ -9,6 +9,7 @@
#import "KBResponderUtils.h" // UIInputViewController
#import "KBBackspaceUndoManager.h"
#import "KBSkinManager.h"
#import <ImageIO/ImageIO.h>
@interface KBToolBar ()
@property (nonatomic, strong) UIView *leftContainer;
@@ -20,6 +21,8 @@
@property (nonatomic, assign) BOOL kbNeedsInputModeSwitchKey;
@property (nonatomic, assign) BOOL kbUndoVisible;
@property (nonatomic, assign) BOOL kbAvatarVisible;
@property (nonatomic, copy, nullable) NSString *kb_cachedPersonaCoverPath;
@property (nonatomic, strong, nullable) UIImage *kb_cachedPersonaCoverImage;
@end
@implementation KBToolBar
@@ -256,10 +259,41 @@
[[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"];
if (imagePath.length == 0 ||
![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
self.kb_cachedPersonaCoverPath = nil;
self.kb_cachedPersonaCoverImage = nil;
return nil;
}
return [UIImage imageWithContentsOfFile:imagePath];
if (self.kb_cachedPersonaCoverImage &&
[self.kb_cachedPersonaCoverPath isEqualToString:imagePath]) {
return self.kb_cachedPersonaCoverImage;
}
// 40pt full decode JPG
NSUInteger maxPixel = 256;
NSURL *url = [NSURL fileURLWithPath:imagePath];
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
if (!source) {
return nil;
}
NSDictionary *opts = @{
(__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
(__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(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);
self.kb_cachedPersonaCoverPath = imagePath;
self.kb_cachedPersonaCoverImage = img;
return img;
}
#pragma mark - Actions

View File

@@ -54,6 +54,9 @@ extern NSString * const KBDarwinSkinChanged; // cross-process
/// 当前背景图片(若存在)
- (nullable UIImage *)currentBackgroundImage;
/// 清理运行时图片缓存(内存缓存)。键盘扩展接近内存上限时可主动调用。
- (void)clearRuntimeImageCaches;
/// 当前主题下,指定按键标识的文字是否应被隐藏(例如图标里已包含字母)
- (BOOL)shouldHideKeyTextForIdentifier:(nullable NSString *)identifier;

View File

@@ -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];