启动优化

2022-05-11  本文已影响0人  呆呆笨

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

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的优化
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造成时间的损耗

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
#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];
}

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

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

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

针对main函数之后的优化:

参考:

  1. attribute详解及应用
  2. 阿里组件化框架BeeHive解析
  3. 组件化工具BeeHive(一):事件分发
  4. iOS-底层原理 32:启动优化(三)二进制重排
  5. iOS优化篇之App启动时间优化
  6. iOS App启动优化(六):实用党直接看这里
上一篇下一篇

猜你喜欢

热点阅读