性能优化最简单粗暴并且最实用的方法就是:缓存、缓存、缓存...
背景
目前我们App支持 19种语言,支持多模块的独立翻译。 对于多语言功能大家可能关注的还是功能实现,忽略了其中性能优化部分,这篇文章会介绍下,我们App采用的优化策略。
先聊聊本地转国际版的过程
第一版:
由于已有代码已经很庞大,大部分文本都是直接赋值,为了快速出第一版,我们采用了 全量代码扫描 + 动态替换 + 运行时提取文案 (运行时提取有个问题,容易漏文案,但是初期扫描完整体有接近 8000个 String Key,根本翻译不过来,采用运行时提取后,只剩400个待翻译文案,后续自测又补了50多个漏的文案。 第一版只有2周的时间开发👻)。
因为翻译团队是外包的,所以不能等他们给结果后再适配,这边初步采用 机器翻译,先进行本地运行查看UI效果(翻译准确性等翻译团队给结果后在联调)。
第二版:
在已有的基础上,没有改动的代码,其实不用在提取文案了,但是我们还需要继续集成已有的模块,这时候采用对 新增模块 使用代码扫描,直接提取文案,比运行时稳妥,不用担心代码没触发的问题。
第三版:
整体稳定的情况下,使用 手工整理 + Google在线文档 + 脚本生成 Strings 文件
一、优化-获取用户首选语言
按以往的经验,是不是每次启动都执行一下 - (NSString *)getAppDefaultLanguage 可以了?
/// 根据用户系统设置,获取App支持的默认语言
- (NSString *)getAppDefaultLanguage {
/**
https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html#//apple_ref/doc/uid/10000171i-CH15-SW1
de-US,
ko-US,
zh-Hans-US,
en
*/
NSArray<NSString *> *appleLanguages = [[NSUserDefaults standardUserDefaults] stringArrayForKey:kIMYAppleLanguagesKey];
NSString *hitLanguage = nil;
for (NSString *anyLanguage in appleLanguages) {
NSString *languageID = anyLanguage;
NSString *path = [[NSBundle mainBundle] pathForResource:languageID ofType:@"lproj"];
if (!path) {
// 移除国家字段,再次尝试
languageID = [self getLanguageWithoutNation:languageID];
path = [[NSBundle mainBundle] pathForResource:languageID ofType:@"lproj"];
}
if (path != nil) {
hitLanguage = [languageID copy];
break;
}
}
// 找不到语言包则,默认使用 en
if (!hitLanguage) {
hitLanguage = @"en";
}
return hitLanguage;
}
其实考虑用户一般不会切换系统语言,所以我们完全可以缓存起来,只需要第一次启动执行。
- (void)setup {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
// 获取用户设置的语言
NSString *language = [ud stringForKey:kIMYCurrentLanguageKey];
// 如果无用户设置的key,则寻找最适合的默认语言
if (!language) {
language = [ud stringForKey:kIMYDefaultLanguageKey];
}
if (!language) {
// 遍历App存在的语言包,返回最合适的语言
language = [self getAppDefaultLanguage];
// 存储默认语言,避免每次都进行本地查找
[ud setObject:language forKey:kIMYDefaultLanguageKey];
}
// 获取远程语言包
NSString *remotePath = [ud stringForKey:kIMYRemoteLanguageKey];
_remoteRootPath = [remotePath stringByReplacingOccurrencesOfString:@"@root@" withString:NSString.imy_documentsDirectory];
// 设置当前App使用的语言
[self setCurrentLanguage:language storage:NO forced:NO];
}
二、优化-宏命令
我们内部使用宏来包裹不同翻译Key,这样开发同学可以不用单独在用 Format 进行包装。
这边使用了 RACEXTScope.h 头文件下的宏判断,区分参数个数,分别调用不同API,避免每次都调用 String Format 处理字符串
这个宏来自 ReactiveCocoa, 再溯源应该是来自 P99 :gitlab.inria.fr/gustedt/p99
有兴趣的同学可以自己去看下,P99 中的宏太丰富了~
三、优化-获取翻译文案(不加锁)
由于系统的API - (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName NS_FORMAT_ARGUMENT(1); 已有缓存,所以我们没必要自己再做一层。
不使用任何锁
我们App允许用户随意切换语言,并且不重启App,所以对于底层这种API 就会有多线程安全的要求。
那我们是如何做到不使用锁,也能满足不Crash的呢?
多线程会影响到的 主要是 NSBundle 对象,主要流程如下:
/// 设置当前语言bundle
- (void)setCurrentLanguage:(NSString *)language {
_bundle = [NSBundle bundleWithPath:...];
}
/// 获取翻译文案,可在任意线程下执行
- (NSString *)localizedStringForKey:(NSString * const)key
value:(NSString * const)value
table:(NSString * const)tableName {
...
NSString *string = [_bundle localizedStringForKey:key value:@"**!!**" table:tableName];
...
}
这边利用了一个特性就是 单线程赋值 (都在主线程),如果存在多线程赋值,则这个优化无效(该加锁还是要加锁,不管是用属性atomic,还是别的锁)
/// 设置当前语言bundle
- (void)setCurrentLanguage:(NSString *)language {
// 先用临时变量存储现在的 bundle 对象。
id oldBundle = _bundle;
_bundle = [NSBundle bundleWithPath:...];
send language did changed notification.
// block 强持有 oldBundle, 1秒后会跟随block执行完 一起释放
// 不crash的前提 就是 get方法获取到 旧_bundle 地址后 然后刚好在执行 set中间等待时间相差在 1s内 (如果都卡成这样了, 其实 async block 也不会执行的,有用iPhone6 大量压测过,未出现闪退)
async 1s block {
[oldBundle class];
}
}
/// 获取翻译文案,可在任意线程下执行
/// 目前有个前提 就是两个线程执行时间在1s内
- (NSString *)localizedStringForKey:(NSString * const)key
value:(NSString * const)value
table:(NSString * const)tableName {
...
NSString *string = [_bundle localizedStringForKey:key value:@"**!!**" table:tableName];
...
}
这部分优化成果
累计下来大概能提升启动时间 5~10ms 左右,各App逻辑有差异,不能代表全部。
其他可优化的内容:
language change UI监听逻辑,刷新逻辑,RTL布局修改等..