美文网首页
第十七篇:iOS启动优化

第十七篇:iOS启动优化

作者: 坚持才会看到希望 | 来源:发表于2022-06-15 14:38 被阅读0次

    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

    相关文章

      网友评论

          本文标题:第十七篇:iOS启动优化

          本文链接:https://www.haomeiwen.com/subject/yvtnmrtx.html