iOS 二进制重排

iOS 应用启动优化

1.启动

1.1 冷启动

  • 冷启动指的是第一次打开应用,或者打开很多其他应用后再打开该应用也可以称之为冷启动

1.2 热启动

  • 热启动指的是应用退到后台后又被唤醒

1.3 查看启动时间

Scheme -> Edit Scheme -> run -> Arguments

在Environment Variables 添加名称为 DYLD_PRINT_STATISTICS 的一项。

将自己的程序运行在真机设备上,查看启动时间(Main函数前)

Total pre-main time: 1.0 seconds (100.0%)
         dylib loading time: 252.32 milliseconds (24.8%)
        rebase/binding time:  13.32 milliseconds (1.3%)
            ObjC setup time: 356.37 milliseconds (35.1%)
           initializer time: 392.66 milliseconds (38.6%)
           slowest intializers :
             libSystem.B.dylib :   8.38 milliseconds (0.8%)
    libMainThreadChecker.dylib : 174.17 milliseconds (17.1%)
                    SchemeName : 323.75 milliseconds (31.9%)
  • dylib loading:

    加载lib库,动态库,库越多占用时间越长,所以尽量少用第三方库,苹果建议不要超过6个。如果实在太多建议合并。注: 系统库不在个数计算范围内,Apple已经做了相关的优化。

  • rebase/binding:

    rebase 就是ASLR+偏移量的时间 rebinding就是dyld对符号进行重新绑定。所以这个步骤就是重新绑定符号,得到符号的真实地址。符号少耗时就少,几乎不能优化。

  • ObjC setup:

    OC类的注册。2万个OC类大约800ms,几乎也不存在优化,Swift可优化性多一些,所以使用Swift尽量减少类的使用可能会节约一些时间。注:以上都是听说,没验证过。

  • initializer:

    load方法的加载时间。所以尽量减少load方法的使用,可以放在initializer里面配合dispathOnce进行调用。

slowest intializers: 比较耗时的操作是:libSystem.B.dylib 系统库,libMainThreadChecker.dylib 调试库。加载的时候不需要我们考虑。剩下的比较耗时的就是我们的主程序的加载了。

2.main之后

2.1 打点计时框架

BLStopwatch 里面有中文注释。

2.2 main之后的优化建议

  • 尽量使用懒加载
  • 利用CPU的多核特性使用多线程初始化(看情况用,少写bug)
  • 不要在启动的地方使用Xib或者storyboard,因为xib和storyboard也是解析成代码进行渲染的。

2.3 pre-main的优化(main函数前)

  • 减少动态库的使用(最好不要超过6个)
  • 减少load方法的使用
  • Swift减少类的使用(少了类注册)
  • 二进制重排

iOS 二进制重排

注:本文只介绍实现步骤。
详细的原理与背景详见 字节跳动技术团队分享的文章:

抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

1. 获取Linkmap

Target -> BuildSettings -> Linking -> Write Link Map File
修改为YES 即会在上面的路径写入Linkmap文件

image
  • Object files: 链接了哪些.O 文件
  • Sections: MachO 一些段的数据
  • Symbols: 符号(主要优化的地方)
几种查看符号的命令,还是以Link Map的顺序为主
cd ../DemoName.app
nm DemoName 查看此Demo的符号
nu -Up DemoName  查看此Demo的符号(自定义)
nu -up DemoName  查看此Demo的符号(系统)

2.核心原理

2.1 ld

Xcode 的连接器是ld, ld有一个不常用的参数-order_file,通过man ld可以看到详细文档:

Alters the order in which functions and data are laid out. For each section in the output file, 
any symbol in that section that are specified in the order file file is moved to the start of its 
section and laid out in the same order as in the order file file.

可以看到,order_file中的符号会按照顺序排列在对应section的开始,完美的满足了我们的需求。

配置符号在order_file 文件里,ld 会根据配置的顺序进行连接,最后达到符号加载到内存的顺序改变,满足优先调用的方法整体排在前面,减少Page Fault,从而达到启动优化。如果order_file中的符号实际不存在, ld会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。

2.2 Xcode 配置order file:

image

3. 符号获取

原理很简单,但是如何获得符号的顺序才是最难的。

3.1 通过hook方式

我们可以hook所有方法的调用,因为OC 方法的调用本质是发送 Objc_msgSend() 函数,所以我们可以 hook Objc_msgSend() 函数,但是这个函数是可变参数的,要想获取内部参数,需要通过汇编代码,从寄存器获得参数,这就加大了难度。另外还有一些C++函数,Swift的获取也不能通过Objc_msgSend() 函数。另外block作为一个特殊的单元,也不走Objc_msgSend() 函数,所以需要单独 hook。总之有很多瓶颈,详细的hook和使用可参考字节跳动技术团队分享的那篇文章

3.2 编译器插桩(Clang插桩)

Clang 文档

3.2.1 Xcode 配置

  • hook 所有方法:
    Other C Flags 配置如下项
-fsanitize-coverage=trace-pc-guard
  • 只 hook 方法 除去 while 循环啥的:
-fsanitize-coverage=func,trace-pc-guard
  • Swift hook: Other Swift Flags配置如下项
-sanitize-coverage=func
-sanitize=undefined

3.2.2 主要hook函数

来自Clang 文档的示例代码

// trace-pc-guard-cb.cc
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
extern "C" 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.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

3.2.3. 自动生成order文件

NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
    
    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];
    NSLog(@"%@",funcStr);
特别提醒!

获取order文件后可以删除上述配置和文件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容