启动优化

本文为个人已知启动优化的总结,如有问题请指教

APP启动主要分为main函数前和main函数后

1. pre-main阶段:即main函数之前,操作系统加载App可执行文件到内存,执行一系列的加载&链接等工作,简单来说,就是dyld加载过程.

Edit Scheme -> Run -> Arguments ->Environment Variables点击+添加环境变量 DYLD_PRINT_STATISTICS 设为 1


pre-main启动时间示例

说明

  • dylib loading time:(加载动态库耗时)
  1. 系统自带的动态库,苹果都已经做过优化,所以不需要再进行优化.
  2. 手动添加的动态库,苹果建议不要超过6个.
  • rebase/binding time(偏移修正/符号绑定耗时):
    rebase(偏移修正):任何一个二进制文件,内部所有的方法、函数都是一个偏移地址。一旦运行到虚拟内存中,每次系统都会随机分配一个ASLR地址值,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移地址。
    binding(绑定):,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定)。
  • ObjC setup time:(OC类注册的耗时)OC类越多,越耗时
  • initializer time:(执行+load和 C++构造函数的耗时)
针对pre_main的优化
  • 尽量少用外部动态库,苹果官方建议自定义的动态库最好不要超过6个,如果超过6个,需要合并动态库
  • 减少OC类,因为OC类越多,越耗时
  • 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数
  • 如果是swift,尽量使用struct
1. 对于必须在+load方法中实现的逻辑可用_attribute替代
以BeeHive举例

#define BeeHiveDATA(sectname) __attribute((used, section("__DATA, "#sectname" ")))

#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";

#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

@BeeHiveMod(ModuleAModule)
输出:@class BeeHive; char * kModuleAModule_mod __attribute((used, section("__DATA, ""BeehiveMods"" "))) = """ModuleAModule""";
说明:__attribute((used, section("__DATA, ""BeehiveMods"" ")))表示在项目的mach-o文件的名字为__DATA的segment中添加一个名字为BeehiveMods的section,并将其值设置为字符串"ModuleAModule"

@BeeHiveService(ModuleAServiceProtocol, ModuleAService)
输出:@class BeeHive; char * kModuleAServiceProtocol_service __attribute((used, section("__DATA, ""BeehiveServices"" "))) = "{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}";
说明:__attribute((used, section("__DATA, ""BeehiveServices"" ")))表示在项目的mach-o文件的名字为__DATA的segment中添加一个名字为BeehiveServices的section,并将其值设置为json格式的字符串"{ \"ModuleAServiceProtocol\" : \"ModuleAService\"}"

这样我们就可以优化大量的重复+load方法。而且使用__attribute属性为编译期间绑定注册信息,运行时读取速度快,注册信息在首次触发调用时读取,不影响pre-main时间

2.二进制重排

当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次 缺页中断(Page Fault)。
二进制重排,主要是优化我们启动时需要的函数非常分散在各个页,启动时就会多次Page Fault造成时间的损耗

  • 步骤一:添加 Build Setting 设置
Target -> Build Setting -> Custom Complier Flags ->
OC项目:Other C Flags 添加
-fsanitize-coverage=func,trace-pc-guard

Swift项目:Other Swift Flags 添加
-sanitize-coverage=func
-sanitize=undefined

如果项目有引用第三方,需要在Podfile中添加
post_install do |installer|
 installer.pods_project.targets.each do |target|
   target.build_configurations.each do |config|
     config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
     config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
   end
 end
end
  • 步骤二:在调用最早的VC中添加代码
#import "dlfcn.h"
#import <libkern/OSAtomic.h>

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.
}


//初始化原子队列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
//定义节点结构体
typedef struct {
    void *pc;   //存下获取到的PC
    void *next; //指向下一个节点
} Node;


void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
     void *PC = __builtin_return_address(0);
     Node *node = malloc(sizeof(Node));
     *node = (Node){PC, NULL};
     // offsetof() 计算出列尾,OSAtomicEnqueue() 把 node 加入 list 尾巴
     OSAtomicEnqueue(&list, node, offsetof(Node, next));
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSMutableArray *arr = [NSMutableArray array];
     while(1){
         //有进就有出,这个方法和 OSAtomicEnqueue() 类比使用
         Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
         //退出机制
         if (node == NULL) {
             break;
         }
         //获取函数信息
         Dl_info info;
         dladdr(node->pc, &info);
         NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
         printf("%s \n", info.dli_sname);
         //处理c函数及block前缀
         BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
         //c函数及block需要在开头添加下划线
         sname = isObjc ? sname: [@"_" stringByAppendingString:sname];
         
         //去重
         if (![arr containsObject:sname]) {
             //因为入栈的时候是从上至下,取出的时候方向是从下至上,那么就需要倒序,直接插在数组头部即可
             [arr insertObject:sname atIndex:0];
         }
     }
       
     //去掉 touchesBegan 方法 启动的时候不会用到这个
     [arr removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
     //数组合成字符串
     NSString * funcStr = [arr  componentsJoinedByString:@"\n"];
     //写入文件
     NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
     NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
     // NSLog(@"%@", filePath);
     [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
  • 步骤四:设置 order file

把 link.order 的路径放到工程根目录
Target -> Build Setting -> Linking -> Order File 设置路径./link.order

  • 步骤五:clean后重新编译

验证:在Build Settings -> Write Link Map File 设置为YES,通过Path To Lingk Map File 查看文件中函数符号链接的顺序

2. main函数之后:即从main函数开始,到AppDelegate 的didFinishLaunching方法执行完成为止,主要是构建第一个界面,并完成渲染.

针对main函数之后的优化:
  • 减少启动初始化的流程,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间
  • 优化代码逻辑,去除非必须的代码逻辑,减少每个流程的消耗时间
  • 启动阶段能使用多线程来初始化的,就使用多线程
  • 尽量使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
  • 删除废弃类、方法
  • 将启动时非必要的操作延迟到首页显示之后加载

参考:

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

推荐阅读更多精彩内容