启动优化

何为启动

在苹果WWDC上面定义过何为启动. 大致分为3个阶段

  • 第一个是在用户看到app的第一个界面之间
  • 第二个是缓存数据出现在第一帧之后的渲染
  • 第三个是真正的网络数据加载完毕的真实界面.

在dyld加载libsystem系统底层依赖库大概需要100ms, 用户main函数之前各种依赖库, c函数之类的加载直到main函数, 苹果官方建议300ms之内. 也就是加上系统的100ms, 苹果建议应用在出现第一帧的时间最好限制在400ms之内.

所以, 如果我们想要做好启动优化, 我们就必须知道在这400ms内系统做了什么事情, 我们又可以做什么事情.

main函数之前做了什么

待更
复制代码

我们可以做哪些优化

待更
复制代码

二进制重排

核心点记录, 下面是原文, 本人只是做了验证和测试.

iOS 优化篇 – 启动优化之Clang插桩实现二进制重排

对于三方pod库的快捷符号导入, 可以看这一篇
懒人版二进制重排这里面直接pod install之后会运行一个脚本, 执行相关设置和操作, 会更好用.

  • post_install寻找脚本的路径会报错, 把脚本的路径直接拖进工程修改为绝对路径即可.

其实如果我们研究一个第三方的话, 可以通过这种方式, 初始化调用看看第三方的符号走向 ,可以帮助我们更快捷的了解整个架构以及调用.

查看pageFault次数

实际应用情况

  • 应用第一次安装时启动
  • 手机开机无后台程序状态应用启动
  • 应用在后台热启动
  • 杀掉后台后多打开一些其他应用再次启动

添加环境变量

DYLD_PRINT_STATISTICS = 1

使用instruments查看虚拟内存的缺页异常

选择System Trace查看MainThread查看VM虚拟内存

工程设置

  • build setting搜索order, 找到link下的Order File设置为当前目录${SRCROOT}/xx.order.
  • 搜索other c, 找到Other C Flags修改为-fsanitize-coverage=func,trace-pc-guard

注意创建order文件的时候, 要在当前目录使用touch命令创建, 不然会出现无法解读或者打不开的情况.

Other C Flags设置为-fsanitize-coverage=trace-pc-guard在读取循环调用的时候会出现问题

想要查看自己的符号顺序的

  • Link Map 是编译期间产生的产物,它记录了二进制文件的布局. 通过设置 Write Link Map File 来设置输出与否 , 默认是 no .
  • Products – show in finder找到真机或者模拟器的上层目录里面会出现一个txt的文本可供查看.

添加hook代码

找个地方写就可以, 这里写在VC里. 下述是最终的解决方案.

#import <dlfcn.h>
#import <libkern/OSAtomic.h>

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, 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:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //将结果写入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件写入出错");
    }
    
}

//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

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 = ++N;  // Guards should start from 1.
}


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};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

复制代码

问题

→ : 多线程问题

  • 代码中的多线程调用非常频繁, 所以这个地方选择的是原子队列. 如果使用锁的话, 自旋锁更为合适.

→ : 解决循环

  • 上述代码中, 解决循环-fsanitize-coverage=func,trace-pc-guard表示针对func进行hook.

→ : load 方法时 , __sanitizer_cov_trace_pc_guard 函数的参数 guard 是 0.

  • 屏蔽掉__sanitizer_cov_trace_pc_guard中的if (!*guard) return;

→ : 细节处理

  • 队列先进后出, 所以从队列中拿完数据需要倒序
  • 去重
  • order文件格式要求c函数 , block调用前面还需要加_下划线 .
  • 写入文件, 复制进自己的工程.

swift 工程 / 混编工程问题

通过如上方式适合纯OC工程获取符号方式 .
由于swift的编译器前端是自己的swift编译前端程序 , 因此配置稍有不同 .
搜索Other Swift Flags, 添加两条配置即可 :

  • -sanitize-coverage=func
  • -sanitize=undefined

swift 类通过上述方法同样可以获取符号 .

实现原理, 静态插桩

静态插桩实际上是在编译期就在每一个函数内部二进制源数据添加hook代码 ( 我们添加的 __sanitizer_cov_trace_pc_guard函数 ) 来实现全局的方法hook的效果 .

为什么使用原子性而不使用锁

这里是在跳转指令中加入非常小的一段执行代码, 并不需要因此而产生线程的高度竞争, 因为这里的多线程的执行和调用是已经在已经开辟好处理过的情况下插入了一段非常简单的且并不耗时操作, 所以在这里使用原子操作会好一些, 如果线程发生竞争而产生的等待时间长原子属性就非常不合适, 如果几乎没有什么耗时以及竞争, 原子属性优于锁.

  • 当线程之间高度竞争的时候,锁的性能会比原子变量高。
  • 锁在高度竞争时会不断挂起恢复线程从而让出cpu使用权给其余的计算资源
  • 原子变量在高度竞争时会一直占用cpu因此其余的计算资源因缺少导致变慢。

总结

更新中…

重要的事情, swift已经是苹果的核心, 现在从编译器到整个维护已经推广程度, 官方更新效率, 以及现在API相对的文档等. 早点学习Swift是iOS程序员的必备了.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享