处理键盘崩溃
This commit is contained in:
@@ -30,6 +30,9 @@
|
|||||||
#import "UIImage+KBColor.h"
|
#import "UIImage+KBColor.h"
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
#import <SDWebImage/SDWebImage.h>
|
#import <SDWebImage/SDWebImage.h>
|
||||||
|
#if DEBUG
|
||||||
|
#import <mach/mach.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// #import "KBLog.h"
|
// #import "KBLog.h"
|
||||||
|
|
||||||
@@ -94,6 +97,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
|
@property(nonatomic, assign) CGFloat kb_lastKeyboardHeight;
|
||||||
@property(nonatomic, strong) UIImage *kb_cachedGradientImage;
|
@property(nonatomic, strong) UIImage *kb_cachedGradientImage;
|
||||||
@property(nonatomic, assign) CGSize kb_cachedGradientSize;
|
@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) NSMutableArray<KBChatMessage *> *chatMessages;
|
||||||
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
|
@property(nonatomic, strong) AVAudioPlayer *chatAudioPlayer;
|
||||||
@property(nonatomic, assign) BOOL chatPanelVisible;
|
@property(nonatomic, assign) BOOL chatPanelVisible;
|
||||||
@@ -101,14 +106,45 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
@property(nonatomic, strong, nullable) id kb_skinObserverToken;
|
||||||
@end
|
@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
|
@implementation KeyboardViewController
|
||||||
|
|
||||||
{
|
{
|
||||||
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
BOOL _kb_didTriggerLoginDeepLinkOnce;
|
||||||
|
#if DEBUG
|
||||||
|
BOOL _kb_debugDidCountAlive;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super 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];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[self setupUI];
|
[self setupUI];
|
||||||
@@ -149,12 +185,30 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self kb_applyDefaultSkinIfNeeded];
|
[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 {
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
[super viewWillAppear:animated];
|
[super viewWillAppear:animated];
|
||||||
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
// 进入/重新进入输入界面时,清理上一次会话残留的撤销状态与缓存,避免显示“撤销删除”但实际上已不可撤销。
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
||||||
[[KBInputBufferManager shared] resetWithText:@""];
|
[[KBInputBufferManager shared] resetWithText:@""];
|
||||||
[[KBLocalizationManager shared] reloadFromSharedStorageIfNeeded];
|
[[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 可能是“截断窗口”,这里只更新
|
// 注意:微信/QQ 等宿主的 documentContext 可能是“截断窗口”,这里只更新
|
||||||
// liveText,不要把它当作全文 manualSnapshot。
|
// liveText,不要把它当作全文 manualSnapshot。
|
||||||
[[KBInputBufferManager shared]
|
[[KBInputBufferManager shared]
|
||||||
@@ -167,6 +221,17 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
- (void)viewWillDisappear:(BOOL)animated {
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
[super viewWillDisappear:animated];
|
[super viewWillDisappear:animated];
|
||||||
[[KBBackspaceUndoManager shared] registerNonClearAction];
|
[[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 {
|
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
|
||||||
@@ -196,9 +261,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
CGFloat portraitWidth = [self kb_portraitWidth];
|
CGFloat portraitWidth = [self kb_portraitWidth];
|
||||||
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
CGFloat keyboardHeight = [self kb_keyboardHeightForWidth:portraitWidth];
|
||||||
CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth];
|
CGFloat keyboardBaseHeight = [self kb_keyboardBaseHeightForWidth:portraitWidth];
|
||||||
CGFloat chatPanelHeight = [self kb_chatPanelHeightForWidth:portraitWidth];
|
|
||||||
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds);
|
||||||
CGFloat outerVerticalInset = KBFit(4.0f);
|
|
||||||
|
|
||||||
NSLayoutConstraint *h =
|
NSLayoutConstraint *h =
|
||||||
[self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
|
[self.view.heightAnchor constraintEqualToConstant:keyboardHeight];
|
||||||
@@ -231,12 +294,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
make.edges.equalTo(self.contentView);
|
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.contentView addSubview:self.keyBoardMainView];
|
||||||
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
[self.keyBoardMainView mas_makeConstraints:^(MASConstraintMaker *make) {
|
||||||
@@ -246,15 +303,6 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
make.height.mas_equalTo(keyboardBaseHeight);
|
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;
|
self.contentView.hidden = YES;
|
||||||
}
|
}
|
||||||
@@ -396,8 +444,14 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
// 简单显隐切换,复用相同的布局区域
|
// 简单显隐切换,复用相同的布局区域
|
||||||
if (show) {
|
if (show) {
|
||||||
[self showChatPanel:NO];
|
[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;
|
self.keyBoardMainView.hidden = show;
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
@@ -417,7 +471,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
// 可选:把当前显示的视图置顶,避免层级遮挡
|
// 可选:把当前显示的视图置顶,避免层级遮挡
|
||||||
if (show) {
|
if (show) {
|
||||||
[self.contentView bringSubviewToFront:self.functionView];
|
if (_functionView) {
|
||||||
|
[self.contentView bringSubviewToFront:_functionView];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
[self.contentView bringSubviewToFront:self.keyBoardMainView];
|
[self.contentView bringSubviewToFront:self.keyBoardMainView];
|
||||||
}
|
}
|
||||||
@@ -492,10 +548,13 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
self.chatPanelVisible = show;
|
self.chatPanelVisible = show;
|
||||||
if (show) {
|
if (show) {
|
||||||
|
[self kb_ensureChatPanelViewIfNeeded];
|
||||||
self.chatPanelView.hidden = NO;
|
self.chatPanelView.hidden = NO;
|
||||||
self.chatPanelView.alpha = 0.0;
|
self.chatPanelView.alpha = 0.0;
|
||||||
[self.contentView bringSubviewToFront:self.chatPanelView];
|
[self.contentView bringSubviewToFront:self.chatPanelView];
|
||||||
self.functionView.hidden = YES;
|
if (_functionView) {
|
||||||
|
_functionView.hidden = YES;
|
||||||
|
}
|
||||||
[self hideSubscriptionPanel];
|
[self hideSubscriptionPanel];
|
||||||
[self showSettingView:NO];
|
[self showSettingView:NO];
|
||||||
[UIView animateWithDuration:0.2
|
[UIView animateWithDuration:0.2
|
||||||
@@ -506,6 +565,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
completion:nil];
|
completion:nil];
|
||||||
} else {
|
} else {
|
||||||
|
// 从未创建过聊天面板时,直接返回,避免 show/hide 触发额外内存分配
|
||||||
|
if (!_chatPanelView) {
|
||||||
|
[self kb_updateKeyboardLayoutIfNeeded];
|
||||||
|
return;
|
||||||
|
}
|
||||||
[UIView animateWithDuration:0.18
|
[UIView animateWithDuration:0.18
|
||||||
delay:0
|
delay:0
|
||||||
options:UIViewAnimationOptionCurveEaseIn
|
options:UIViewAnimationOptionCurveEaseIn
|
||||||
@@ -519,6 +583,114 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
[self kb_updateKeyboardLayoutIfNeeded];
|
[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 {
|
- (void)showSubscriptionPanel {
|
||||||
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
// 1) 先判断权限:未开启“完全访问”则走引导逻辑
|
||||||
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
if (![[KBFullAccessManager shared] hasFullAccess]) {
|
||||||
@@ -831,7 +1003,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
|
|
||||||
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
- (void)chatPanelViewDidTapClose:(KBChatPanelView *)view {
|
||||||
// 清空 chatPanelView 内部的消息
|
// 清空 chatPanelView 内部的消息
|
||||||
[self.chatPanelView kb_reloadWithMessages:@[]];
|
[view kb_reloadWithMessages:@[]];
|
||||||
if (self.chatAudioPlayer.isPlaying) {
|
if (self.chatAudioPlayer.isPlaying) {
|
||||||
[self.chatAudioPlayer stop];
|
[self.chatAudioPlayer stop];
|
||||||
}
|
}
|
||||||
@@ -1553,7 +1725,11 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
(__bridge const void *)(self),
|
(__bridge const void *)(self),
|
||||||
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
|
(__bridge CFStringRef)KBDarwinSkinInstallRequestNotification, NULL);
|
||||||
#if DEBUG
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1578,6 +1754,9 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
if (self.contentView.hidden) {
|
if (self.contentView.hidden) {
|
||||||
self.contentView.hidden = NO;
|
self.contentView.hidden = NO;
|
||||||
}
|
}
|
||||||
|
if (self.kb_defaultGradientLayer) {
|
||||||
|
self.kb_defaultGradientLayer.frame = self.bgImageView.bounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillTransitionToSize:(CGSize)size
|
- (void)viewWillTransitionToSize:(CGSize)size
|
||||||
@@ -1611,10 +1790,23 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
#pragma mark - Theme
|
#pragma mark - Theme
|
||||||
|
|
||||||
- (void)kb_applyTheme {
|
- (void)kb_applyTheme {
|
||||||
|
@autoreleasepool {
|
||||||
KBSkinTheme *t = [KBSkinManager shared].current;
|
KBSkinTheme *t = [KBSkinManager shared].current;
|
||||||
UIImage *img = [[KBSkinManager shared] currentBackgroundImage];
|
UIImage *img = nil;
|
||||||
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
|
BOOL isDefaultTheme = [self kb_isDefaultKeyboardTheme:t];
|
||||||
BOOL isDarkMode = [self kb_isDarkModeActive];
|
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;
|
CGSize size = self.bgImageView.bounds.size;
|
||||||
if (isDefaultTheme) {
|
if (isDefaultTheme) {
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
@@ -1622,6 +1814,8 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
// 这样可以避免图片渲染时的色彩空间转换导致颜色不一致
|
// 这样可以避免图片渲染时的色彩空间转换导致颜色不一致
|
||||||
img = nil;
|
img = nil;
|
||||||
self.bgImageView.image = nil;
|
self.bgImageView.image = nil;
|
||||||
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
|
self.kb_defaultGradientLayer = nil;
|
||||||
// 使用与系统键盘底部完全相同的颜色
|
// 使用与系统键盘底部完全相同的颜色
|
||||||
if (@available(iOS 13.0, *)) {
|
if (@available(iOS 13.0, *)) {
|
||||||
// iOS 系统键盘使用的实际颜色 (RGB: 44, 44, 46 in sRGB, 或 #2C2C2E)
|
// iOS 系统键盘使用的实际颜色 (RGB: 44, 44, 46 in sRGB, 或 #2C2C2E)
|
||||||
@@ -1654,7 +1848,7 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
self.bgImageView.backgroundColor = darkColor;
|
self.bgImageView.backgroundColor = darkColor;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 浅色模式:使用渐变图片
|
// 浅色模式:使用渐变层(避免生成大位图导致内存上涨)
|
||||||
if (size.width <= 0 || size.height <= 0) {
|
if (size.width <= 0 || size.height <= 0) {
|
||||||
[self.view layoutIfNeeded];
|
[self.view layoutIfNeeded];
|
||||||
size = self.bgImageView.bounds.size;
|
size = self.bgImageView.bounds.size;
|
||||||
@@ -1667,9 +1861,29 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
}
|
}
|
||||||
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
|
UIColor *topColor = [UIColor colorWithHex:0xDEDFE4];
|
||||||
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
|
UIColor *bottomColor = [UIColor colorWithHex:0xD1D3DB];
|
||||||
// img = [self kb_defaultGradientImageWithSize:size
|
UIColor *resolvedTopColor = topColor;
|
||||||
// topColor:topColor
|
UIColor *resolvedBottomColor = bottomColor;
|
||||||
// bottomColor: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.contentView.backgroundColor = [UIColor clearColor];
|
||||||
self.bgImageView.backgroundColor = [UIColor clearColor];
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
||||||
}
|
}
|
||||||
@@ -1678,28 +1892,33 @@ static void KBSkinInstallNotificationCallback(CFNotificationCenterRef center,
|
|||||||
// 自定义皮肤:清除背景色,使用皮肤图片
|
// 自定义皮肤:清除背景色,使用皮肤图片
|
||||||
self.contentView.backgroundColor = [UIColor clearColor];
|
self.contentView.backgroundColor = [UIColor clearColor];
|
||||||
self.bgImageView.backgroundColor = [UIColor clearColor];
|
self.bgImageView.backgroundColor = [UIColor clearColor];
|
||||||
|
[self.kb_defaultGradientLayer removeFromSuperlayer];
|
||||||
|
self.kb_defaultGradientLayer = nil;
|
||||||
|
img = [[KBSkinManager shared] currentBackgroundImage];
|
||||||
}
|
}
|
||||||
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
NSLog(@"⌨️[Keyboard] apply theme id=%@ hasBg=%d", t.skinId, (img != nil));
|
||||||
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
[self kb_logSkinDiagnosticsWithTheme:t backgroundImage:img];
|
||||||
self.bgImageView.image = 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
|
// method declared in KBKeyBoardMainView.h
|
||||||
#pragma clang diagnostic push
|
#pragma clang diagnostic push
|
||||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
[self.keyBoardMainView performSelector:@selector(kb_applyTheme)];
|
||||||
#pragma clang diagnostic pop
|
#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 push
|
||||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
[self.functionView performSelector:@selector(kb_applyTheme)];
|
[_functionView performSelector:@selector(kb_applyTheme)];
|
||||||
#pragma clang diagnostic pop
|
#pragma clang diagnostic pop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
|
- (BOOL)kb_isDefaultKeyboardTheme:(KBSkinTheme *)theme {
|
||||||
NSString *skinId = theme.skinId ?: @"";
|
NSString *skinId = theme.skinId ?: @"";
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ static const CGFloat kKBLettersRow2EdgeSpacerMultiplier = 0.5;
|
|||||||
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
self.layoutConfig = [KBKeyboardLayoutConfig sharedConfig];
|
||||||
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
|
self.backspaceHandler = [[KBBackspaceLongPressHandler alloc] initWithContainerView:self];
|
||||||
[self buildBase];
|
[self buildBase];
|
||||||
[self reloadKeys];
|
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
#import "KBResponderUtils.h" // 查找 UIInputViewController,用于系统切换输入法
|
||||||
#import "KBBackspaceUndoManager.h"
|
#import "KBBackspaceUndoManager.h"
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
|
#import <ImageIO/ImageIO.h>
|
||||||
|
|
||||||
@interface KBToolBar ()
|
@interface KBToolBar ()
|
||||||
@property (nonatomic, strong) UIView *leftContainer;
|
@property (nonatomic, strong) UIView *leftContainer;
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
@property (nonatomic, assign) BOOL kbNeedsInputModeSwitchKey;
|
@property (nonatomic, assign) BOOL kbNeedsInputModeSwitchKey;
|
||||||
@property (nonatomic, assign) BOOL kbUndoVisible;
|
@property (nonatomic, assign) BOOL kbUndoVisible;
|
||||||
@property (nonatomic, assign) BOOL kbAvatarVisible;
|
@property (nonatomic, assign) BOOL kbAvatarVisible;
|
||||||
|
@property (nonatomic, copy, nullable) NSString *kb_cachedPersonaCoverPath;
|
||||||
|
@property (nonatomic, strong, nullable) UIImage *kb_cachedPersonaCoverImage;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation KBToolBar
|
@implementation KBToolBar
|
||||||
@@ -256,10 +259,41 @@
|
|||||||
[[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"];
|
[[containerURL path] stringByAppendingPathComponent:@"persona_cover.jpg"];
|
||||||
if (imagePath.length == 0 ||
|
if (imagePath.length == 0 ||
|
||||||
![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
|
![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
|
||||||
|
self.kb_cachedPersonaCoverPath = nil;
|
||||||
|
self.kb_cachedPersonaCoverImage = nil;
|
||||||
return 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
|
#pragma mark - Actions
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ extern NSString * const KBDarwinSkinChanged; // cross-process
|
|||||||
/// 当前背景图片(若存在)
|
/// 当前背景图片(若存在)
|
||||||
- (nullable UIImage *)currentBackgroundImage;
|
- (nullable UIImage *)currentBackgroundImage;
|
||||||
|
|
||||||
|
/// 清理运行时图片缓存(内存缓存)。键盘扩展接近内存上限时可主动调用。
|
||||||
|
- (void)clearRuntimeImageCaches;
|
||||||
|
|
||||||
/// 当前主题下,指定按键标识的文字是否应被隐藏(例如图标里已包含字母)
|
/// 当前主题下,指定按键标识的文字是否应被隐藏(例如图标里已包含字母)
|
||||||
- (BOOL)shouldHideKeyTextForIdentifier:(nullable NSString *)identifier;
|
- (BOOL)shouldHideKeyTextForIdentifier:(nullable NSString *)identifier;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#import "KBSkinManager.h"
|
#import "KBSkinManager.h"
|
||||||
#import "KBConfig.h"
|
#import "KBConfig.h"
|
||||||
|
#import <ImageIO/ImageIO.h>
|
||||||
|
|
||||||
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
|
NSString * const KBSkinDidChangeNotification = @"KBSkinDidChangeNotification";
|
||||||
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
|
NSString * const KBDarwinSkinChanged = @"com.loveKey.nyx.skin.changed";
|
||||||
@@ -59,10 +60,45 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent";
|
|||||||
|
|
||||||
@interface KBSkinManager ()
|
@interface KBSkinManager ()
|
||||||
@property (atomic, strong, readwrite) KBSkinTheme *current;
|
@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
|
@end
|
||||||
|
|
||||||
@implementation KBSkinManager
|
@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)。
|
/// 返回所有可能的皮肤根目录(优先 App Group,其次当前进程的 Caches)。
|
||||||
+ (NSArray<NSString *> *)kb_candidateBaseRoots {
|
+ (NSArray<NSString *> *)kb_candidateBaseRoots {
|
||||||
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
NSMutableArray<NSString *> *roots = [NSMutableArray array];
|
||||||
@@ -104,6 +140,14 @@ static NSString * const kKBSkinThemeStoreKey = @"KBSkinThemeCurrent";
|
|||||||
|
|
||||||
- (instancetype)init {
|
- (instancetype)init {
|
||||||
if (self = [super 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];
|
KBSkinTheme *t = [self p_loadFromStore];
|
||||||
// 若存储中的皮肤在 App Group 中找不到对应资源目录(如首次安装 / 已被清理),则回退到默认皮肤。
|
// 若存储中的皮肤在 App Group 中找不到对应资源目录(如首次安装 / 已被清理),则回退到默认皮肤。
|
||||||
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
||||||
@@ -170,6 +214,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
- (BOOL)applyTheme:(KBSkinTheme *)theme {
|
||||||
if (!theme) return NO;
|
if (!theme) return NO;
|
||||||
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
|
NSLog(@"🎨[SkinManager] apply theme id=%@ name=%@", theme.skinId, theme.name);
|
||||||
|
[self clearRuntimeImageCaches];
|
||||||
// 将主题写入 App Group 存储(失败也不影响本次进程内的使用)
|
// 将主题写入 App Group 存储(失败也不影响本次进程内的使用)
|
||||||
[self p_saveToStore:theme];
|
[self p_saveToStore:theme];
|
||||||
// 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。
|
// 始终更新当前主题并广播通知,确保当前进程和扩展之间保持同步。
|
||||||
@@ -187,6 +232,15 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
[self applyTheme:[self.class defaultTheme]];
|
[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 {
|
- (BOOL)applyImageSkinWithData:(NSData *)imageData skinId:(NSString *)skinId name:(NSString *)name {
|
||||||
// 仅作为“存在背景图”的标记使用:图像文件本身存放在 App Group 容器
|
// 仅作为“存在背景图”的标记使用:图像文件本身存放在 App Group 容器
|
||||||
// Skins/<skinId>/background.png 中,这里不再把二进制图片写入 Keychain,
|
// Skins/<skinId>/background.png 中,这里不再把二进制图片写入 Keychain,
|
||||||
@@ -216,20 +270,52 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
NSString *skinId = self.current.skinId;
|
NSString *skinId = self.current.skinId;
|
||||||
if (skinId.length == 0) return nil;
|
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];
|
NSArray<NSString *> *roots = [self.class kb_candidateBaseRoots];
|
||||||
NSFileManager *fm = [NSFileManager defaultManager];
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId];
|
NSString *relative = [NSString stringWithFormat:@"Skins/%@/background.png", skinId];
|
||||||
|
|
||||||
|
// 背景图通常远大于键盘实际显示区域,按像素上限做缩略解码,显著降低扩展内存占用。
|
||||||
|
NSUInteger maxPixel = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) ? 2048 : 1024;
|
||||||
for (NSString *base in roots) {
|
for (NSString *base in roots) {
|
||||||
NSString *bgPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
NSString *bgPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||||
BOOL isDir = NO;
|
BOOL isDir = NO;
|
||||||
if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) {
|
if (![fm fileExistsAtPath:bgPath isDirectory:&isDir] || isDir) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NSData *data = [NSData dataWithContentsOfFile:bgPath];
|
NSString *cacheKey = [NSString stringWithFormat:@"bg|%@", bgPath];
|
||||||
if (data.length == 0) continue;
|
UIImage *cached = [self.kb_fileImageCache objectForKey:cacheKey];
|
||||||
UIImage *img = [UIImage imageWithData:data scale:[UIScreen mainScreen].scale];
|
if (cached) {
|
||||||
if (img) return img;
|
@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;
|
return nil;
|
||||||
}
|
}
|
||||||
@@ -314,7 +400,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) {
|
if (![fm fileExistsAtPath:fullPath isDirectory:&isDir] || isDir) {
|
||||||
continue;
|
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 (img) return img;
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -351,7 +443,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||||
BOOL isDir = NO;
|
BOOL isDir = NO;
|
||||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
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;
|
if (img) return img;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,7 +461,13 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
NSString *fullPath = [[base stringByAppendingPathComponent:relative] stringByStandardizingPath];
|
||||||
BOOL isDir = NO;
|
BOOL isDir = NO;
|
||||||
if ([fm fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir) {
|
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;
|
if (img) return img;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,6 +553,7 @@ static void KBSkinDarwinCallback(CFNotificationCenterRef center, void *observer,
|
|||||||
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
if (!t || ![self.class kb_hasAssetsForSkinId:t.skinId]) {
|
||||||
t = [self.class defaultTheme];
|
t = [self.class defaultTheme];
|
||||||
}
|
}
|
||||||
|
[self clearRuntimeImageCaches];
|
||||||
self.current = t;
|
self.current = t;
|
||||||
if (broadcast) {
|
if (broadcast) {
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:KBSkinDidChangeNotification object:nil];
|
||||||
|
|||||||
Reference in New Issue
Block a user