iOS启动优化之二进制重排

496 阅读3分钟

二进制重排这个概念,最早提出是由抖音团队技术分享的一篇文章,使大家能知道这个技术,当时是在19年,里面运用的是针对OC、C++、C、block的分别单独hook处理,但是只有OC能进行全部获取,C++、C、block、swift等处理依旧有缺陷,这篇文章会利用抖音结尾的进一步优化操作进行获取全部符号表。

原因

由于现在的操作系统中都引入了虚拟内存,由虚拟内存转换为物理内存的映射表(页表)是分页的,macOS每页大小为4kb,iOS大小为16kb,在内存分页触发会出现缺页异常,iOS 系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 所产生的耗时要更多。

image.png

image.png

上图1就是就是虚拟内存通过页表转换为物理内存
上图2是页表的显示,前面的0、1表示的是否已经加载到内存中,后面的是实际的物理地址

当我们启动程序,系统会把所有的文件加载到内存中,加载顺序即是Build Phases,Compile Sources中的文件顺序,如图

image.png

加载进去的类会把依次把每个方法加载,但是当前类中的方法有可能是启动时不需要,所以未加入到内存的,这时系统就会触发一次缺页中断,即 Page Fault。

在未优化前,我们当前的程序就有3426次page fault,需要535.79ms image.png

怎么做

APP 的二进制重排很容易做,因为xcode中是可以配置的,在Build Setting中查找 Write Link Map File 改为 YES, 再查找 Order File ,把你编写好的重排文件填入,这样二进制重排就完成了

image.png

image.png

所以配置二进制重排是很容易的,难的是我们的Order File文件怎么生成,总不能一个个方法去写,然后排列吧。

如何生成Order

生成Order文件,就需要我们知道自己的程序在启动时调用了哪些方法,这些方法的调用顺序是如何的,如果是纯OC代码其实很简单,因为所有的OC代码都会走 objc_msgSend 消息转发方法,所以我们只需要用方法交换进行获取方法列表即可,但是现在的程序很多都会用swift、c、c++混编,这些都是不走 objc_msgSend 方法的,所以获取不全。现在我们可以用编译期插桩方案来 100% 获取方法。

我们可以通过这个地址 clang.llvm.org/docs/Saniti… 去了解,其中给了我们例子。

Other C flags 添加参数 -fsanitize-coverage=trace-pc-guard


    

    **while** (**YES**) {

        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, 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];

        [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];

        }

    }

    //干掉自己!

    [funcs removeObject:[NSString stringWithFormat:@"%s", **__FUNCTION__** ]];

    //将数组变成字符串

    NSString * funcStr = [funcs  componentsJoinedByString:@"\n"];

    

    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];

    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];

    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:**nil**];

如果是OC与Swift混编的项目,需要在Other swift flags 中添加-sanitize=undefined 、 -sanitize-coverage=func,这样就能获取Swift方法

检测

运行了程序之后,我们怎么知道程序是否进行二进制重排了呢,在生成的项目里会有 项目名-LinkMap-normal-arm64.txt文件,其中可以查看

运行前 image.png

运行后

image.png

显示加载的顺序是按照我们order文件中配置的顺序加载的,到此就完成了二进制重排

再运行一下APP

image.png 由300ms提升到224ms,与抖音提出的提升15%以上的效率结论也基本一致。

总结

LLVM里提供了很多辅助工具,可以实现一些高端操作,这是在我们业务代码之外供我们的提升。现在虽然clang可以获取全部的OC、Swift,但是在flutter、RN里还没找到好的解决方案,需要进一步的去探索。