项目赶了大半年基本进入的稳定期,剩下的就是按需的小步快跑的迭代周期,基本上半个月一个小版本,可以抽时间做一些优化型操作。正好看到抖音的启动优化文案----二进制重排,顺便就做个记录。
1 项目配置
1.1 开启 Write Liink Map File
默认情况下文件会被写在编译项目下按照对的指令集写入文件
2 文件查看
打开对应的文件 我们搜索"Symbols"
这些便是写进入的对应的文件符,这些文件如何得来的呢?和Name顺序如何来的呢?
我们大胆猜测是由文件的编译顺序导致,我们先添加一个NSObject的category--NSObject+LaunchOrder.h
@interface NSObject (LaunchOrder)
- (void) method1;
- (void) method2;
@end
调整Build Phases--Compile Souurces的文件顺序--将NSObject+LaunchOrder置顶
运行后我们重新打开launch.order文件
有此可见order中Symbols与我们的文件编译顺序相关联。那么我们怎么处理来达到项目启动优化呢?
2 理论原理
2.1 缺页中断
App启动时会将对应的符号加载如内存,iOS中默认一次按照4k空间加载符号文件,假如一次无法加载完成启动需要的资源符号,那么就会出现缺失,需要加载更多page,这个重新寻址加载是相对耗时的。假如将分散的符号按照App的启动顺序需要按需排列,那么就可减少启动耗时。
未分配前
分配后
上图展示的例子未优化前App启动共发生4次缺页中断,分配后只需1次就好,而现实中我们的项目往往更加复杂需要加载的就更多,累计起来的时间也会相对可观
3 文件符号收集
3.1 clang插桩获取符号
1 添加编译 设置
#import <stdint.h>
#import <stdio.h>
#import <sanitizer/coverage_interface.h>
#import <libkern/OSAtomic.h>
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
// printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = (uint32_t)++N; // Guards should start from 1.
}
void printInfo(void *PC) {
Dl_info info;
dladdr(PC, &info);
printf("fnam:%s \n fbase:%p \n sname:%s \n saddr:%p \n",
info.dli_fname,
info.dli_fbase,
info.dli_sname,
info.dli_saddr);
}
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct {
void *pc;
void *next;
} SymbolNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC, NULL};
OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
// printInfo(PC);
}
上述只是获取到对应的符号,我们需要将他们翻译出来
+ (BOOL)exportSymbolsWithFilePath:(nonnull NSString *)filePath
{
NSMutableArray <NSString *>* symbolNames = [NSMutableArray array];
while (YES) {
SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["]; // Objective-C method do nothing
NSString * symbolName = isObjc? name : [@"_" stringByAppendingString:name]; // c function with "_"
[symbolNames addObject:symbolName];
}
NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
NSMutableArray<NSString*>* funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
// remove current method symbol (not necessary when launch)
[funcs removeObject:[NSString stringWithFormat:@"%s", __FUNCTION__]];
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
return [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
将上述函数写到
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 函数末尾即可获取到App启动到首屏显示所需要的符号
_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
+[SMLagMonitor shareInstance]
___29+[SMLagMonitor shareInstance]_block_invoke
-[SMLagMonitor beginMonitor]
-[SMLagMonitor setIsMonitoring:]
-[SMLagMonitor setCpuMonitorTimer:]
___copy_helper_block_e8_32s
___28-[SMLagMonitor beginMonitor]_block_invoke
-[AppDelegate setWindow:]
-[SMRootViewController init]
-[AppDelegate styleNavigationControllerWithRootController:]
+[SMStyle colorPaperBlack]
+[UIColor(UIColor_Expanded) colorWithHexString:]
+[UIColor(UIColor_Expanded) colorWithRGBHex:]
+[SMStyle colorPaperDark]
_CGRectMake
-[AppDelegate window]
-[UIViewController(clsCall) clsCallHookViewWillAppear:]
-[UIViewController(clsCall) clsCallInsertToViewWillAppear]
+[SMCallTrace startWithMaxDepth:]
_smCallConfigMaxDepth
+[SMCallTrace start]
_smCallTraceStart
最后将文件粘贴入launch.order中即可实现加载的重排(下图是objc文件参考)
收集重排符号
项目编译后的符号文件
对比我们的收集到的首屏启动完成后的符号与编译项目符号一致,从而通过减少缺页中断达到优化启动的目的