第十七篇:iOS启动优化

APP启动分为pre-main和mian两个过程,首先我们需要进行如下图设置,设置DYLD_PRINT_STATISTICS为1,这样我们就可以打印出来APP在启动的时候所花费的时间。


WechatIMG2107.jpeg

下面是各个阶段的消耗时间:

1. dylib loading time

这个时间优化,也就是dylib链接动态库的优化,可以减少动态库个数(指自己写的库),苹果给我们建议是我们自己写的动态库不要超过6个是最好的。

2.ObjC setup time

这个是注册objc所有类所需时间,在mach-0里读取所有文件的类,把它们会放到一张表里面。我们可以安装一个fui,通过终端输入 fui find命令可以打印出没有被使用的类,我们把这些没使用的类进行一些删除。还可以通过工具检测是否工厂里有没有使用的图片。

3. rebase/binding time

binding就是一个符号绑定,其会把一个函数具体实现地址和对应函数符号进行链接。比如printf函数打印,其内部是通过一个binging链接到printf具体函数实现和printf这个符号,然后进行打印。

rebase是一个进行指针的修复,这里有个虚拟内存的概念,在早期时候都是通过物理内存,这样会出现内存不够用,不安全。但是虚拟内存也会不安全,因为其地址都是从0开始,为了解决这个问题用到了ASLR(地址空间随机话),

4. initializer time

初始化时间优化,一边我们再load里不要添加耗时操作,因为load加载会在mian函数之前。
Total pre-main time: 2.0 seconds (100.0%)
         dylib loading time: 232.43 milliseconds (11.4%)
        rebase/binding time: 1.0 seconds (53.4%)
            ObjC setup time: 224.46 milliseconds (11.0%)
           initializer time: 486.86 milliseconds (23.9%)
           slowest intializers :
             libSystem.B.dylib :  61.13 milliseconds (3.0%)
             libprotobuf.dylib :  55.00 milliseconds (2.7%)
                          Test : 185.69 milliseconds (9.1%)

二进制重排

每个page fault消耗时间虽然很少,但是多个时间就会很多。所以这个是我们优化的重点。App在执⾏的过程中会存在⼤量的Page Fault,⼀个Page Fault
的耗时很少,但是当⼤量的Page Fault存在时,就会影响到代码的执⾏速度。同理,在App启动的时候,就可能会出现⼤量的Page Fault。⼆进制重排就是把在启动过程中需要使⽤到的符号,重新排列在⼀个或者⼏个Page⾥⾯,减少Page Fault的次数,从⽽达到减少启动时间的⽬的。

在build phrase里我们可以重新排列调用文件顺序,但是这样不是一个好办法。


WechatIMG2108.jpeg

可以通过设置.order文件,因为系统在启动的时候是会先读.order文件进行配置启动。首先通过终端创建一个lg.order文件。然后我们可以看到就是按我们.order文件进行排序的。


WechatIMG2110.jpeg
WechatIMG2109.jpeg

那我们如何知道哪些函数在启动的时候使用需要放到前面,这个就需要clang插桩技术,首先我们开启clang插桩,然后需要添加几个头文件#include <stdint.h>,#include <stdio.h>,#include <sanitizer/coverage_interface.h>。

WechatIMG2111.jpeg

在viewController里也添加了如下的两个方法:


void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;
    NSLog(@"%d",N);
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    NSLog(@"%s",__func__);
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,0};
    
    //插入结构体
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
    
//    printf("%s\n",info.dli_sname);
}

上面运行后会打印如下,也就是stop和start的打印,下面30代表是的30个方法。下面是4个字节的存储01 00 00 00,其存的值就是1,2,3,4,5,6,7,8。通过

1.__sanitizer_cov_trace_pc_guard_init这个方法也就知道了在项目中用到的符号(方法)的个数。

2.系统每调用一个方法,都会回调这个void __sanitizer_cov_trace_pc_guard(uint32_t *guard) 这个方法,我们发现这个方法会到各个方法调用之前会被调用,那么就可以在这里获取到启动时候调用了哪些方法。

INIT: 0x1009e1750 0x1009e17c8
(lldb) x 0x1009e1750
0x1009e1750: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x1009e1760: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x1009e17c8
0x1009e17c8: 00 00 00 00 00 00 00 00 1e 00 00 00 00 00 00 00  ................
0x1009e17d8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
2022-06-13 16:18:47.368235+0800 TraceDemo[43903:514812] 30

下面是获取一个函数的名称以及地址:
Dl_info info;
dladdr(node->pc, &info);
NSString *name = @(info.dli_sname);

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

下面dli_fbase是文件地址,dli_sname是名字也就是符号,dli_saddr符号的地址。其实我们只需要dli_sname就可以。


WechatIMG2116.jpeg

下面是dli_sname的打印,也就是启动后的程序所运行的函数,其中对于swift也有效,s9TraceDemo9SwiftTestC05swiftD0yyFZTo这些方法就是swift的,因为swift有自带混淆。

-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
_$s9TraceDemo9SwiftTestC05swiftD0yyFZTo
_$s9TraceDemo9SwiftTestC05swiftD0yyFZ
_$ss27_finalizeUninitializedArrayySayxGABnlF
_$sSa12_endMutationyyF
_$ss5print_9separator10terminatoryypd_S2StFfA0_
_$ss5print_9separator10terminatoryypd_S2StFfA1_
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
-[ViewController touchesBegan:withEvent:]

通过如下操作可以进行二进制重排:可以直接生成.oder文件,这里是把二进制符号文件存储在symbolList里符号列表里。

//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

typedef struct {
    void * pc;
    int abc;
} SYNode;

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;
    NSLog(@"%d",N);
}

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,0};
    
    //插入结构体
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
    }

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //创建数组
    NSMutableArray *symbleNames = [NSMutableArray array];
    while (YES) {
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, abc));//取出symbolList里的方法符号
        if (node == NULL) {
            break;;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        //转为OC字符串方便操作
        NSString *name = @(info.dli_sname);
        //OC方法 直接添加到数组
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbleName = isObjc ? name : [@"_" stringByAppendingString:name];
        [symbleNames addObject:symbleName];
    }
    NSEnumerator *em = [symbleNames reverseObjectEnumerator];//去重
    //新数组存储
    NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
    NSString *name;
    while (name = [em nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    
   //生成.order文件
    //数组转为字符串
    NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
    //文件路径
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lg.order"];
    //文件内容
    NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    //写入文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    //获取沙盒主目录路径
    NSLog(@"%@",NSHomeDirectory());
    
    NSLog(@"%@",funcStr);
}

二进制重排总结

  1. 在二进制重排里我们首先需要进行设置:


    WechatIMG2121.jpeg

2.然后其会调用__sanitizer_cov_trace_pc_guard_init(这里可以打印出启动调用了几个方法) 和void __sanitizer_cov_trace_pc_guard(这里就是启动时候每调用一个方法就会回调这个)这两个方法,然后生成.order文件。

3.在生成了.order文件之后,需要进行配置处理,放了之后的话编译器就会按照.order的顺序进行排列了。


WechatIMG2122.jpeg
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容