iOS优化篇之App启动时间优化

原文:橘子不酸丶 [www.zyiner.com/article/5]node

前言

最近因为体验感受咱们的app启动时间过长,所以作了APP的启动优化。本次优化主要从三个方面来作了启动时间的优化,main以后的耗时方法优化premain的+load方法优化二进制重排优化premain时间ios

一般咱们对于启动时间的定义为从用户点击app到看到首屏的时间。所以对于启动时间优化就是遵循一个原则:尽早让用户看到首页内容。json

app启动过程

iOS应用的启动可分为pre-main阶段和main()阶段,pre-main阶段为main函数执行以前所作的操做,main阶段为main函数到首页展现阶段。其中系统作的事情为:markdown

premain

  • 加载全部依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 加载动态连接库加载器dyld(dynamic loader)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  • 执行声明为__attribute__((constructor))的C函数

main

  • 调用main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching

一般的premain阶段优化即为删减无用的类方法、减小+load操做、减小__attribute__((constructor))的C函数、减小启动加载的动态库。而main阶段的优化为将启动时非必要的操做延迟到首页显示以后加载、统计并优化耗时的方法、对于一些能够放在子线程的操做能够尽可能不占用主线程。app

1、耗时方法优化

1.统计启动时的耗时方法

咱们能够经过Instruments的TimeProfile来统计启动时的主要方法耗时,Call Tree->Hide System Libraries过滤掉系统库能够查看主线程下方法的耗时。 也能够经过打印时间的方式来统计各个函数的耗时。iphone

double launchTime = CFAbsoluteTimeGetCurrent();
[SDWebImageManager sharedManager];
NSLog(@"launchTime = %f秒", CFAbsoluteTimeGetCurrent() - launchTime);
复制代码

这一阶段就是须要对启动过程的业务逻辑进行梳理,确认哪些是能够延迟加载的,哪些能够放在子线程加载,以及哪些是能够懒加载处理的。同时对耗时比较严重的方法进行review并提出优化策略进行优化。ide

2、+load方法优化以及删减不用的类

2.1 +load方法统计

一样的咱们能够经过Instruments来统计启动时全部的+load方法,以及+load方法所用耗时 咱们能够对没必要要的+load方法进行优化,好比放在+initialize里。没必要要的+load进行删减。函数

2.2 使用__attribute优化+load方法

因为在咱们的工程中存在不少的+load方法,而其中一大部分为cell模板注册的+load方法(咱们的每个cell对应一个模板,而后该模板对应一个字符串,在启动时全部的模板方法都在+load中注册对应的字符串即在字典中存储字符串和对应的cell模板,而后动态下发展现对应的cell)。oop

即存在这种场景,在启动时须要大量的在+load中注册key-value。post

此时可使用__attribute((used, section("__DATA,"#sectname" ")))的方式在编译时写入"TempSection"的DATA段一个字符串。此字符串为key:value格式的字典转json。对应着key和value。

#ifndef ZYStoreListTemplateSectionName
#define ZYStoreListTemplateSectionName "ZYTempSection"
#endif

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

#define ZYStoreListTemplateRegister(templatename,templateclass) \
class NSObject; char * k##templatename##_register ZYStoreListTemplateDATA(ZYTempSection) = "{ \""#templatename"\" : \""#templateclass"\"}";
/**
经过ZYStoreListTemplateRegister(key,classname)注册处理模板的类名(类必须是ZYStoreListBaseTemplate子类)
【注意事项】
该方式经过__attribute属性在编译期间绑定注册信息,运行时读取速度快,注册信息在首次触发调用时读取,不影响pre-main时间
该方式注册时‘key’字段中不支持除下划线'_'之外的符号
【使用示例】
注册处理模板的类名:@ZYStoreListTemplateRegister(baseTemp,ZYStoreListBaseTemplate)
**/
复制代码

在使用时@ZYStoreListTemplateRegister(baseTemp,ZYStoreListBaseTemplate)即为在编译期间绑定注册信息。

读取使用__attribute在编译期间写入的key-value字符串。 关于__attribute详情能够参考__attribute黑魔法

#pragma mark - 第一次使用时读取ZYStoreListTemplateSectionName的__DATA全部数据
+ (void)readTemplateDataFromMachO {
    //1.根据符号找到所在的mach-o文件信息
    Dl_info info;
    dladdr((__bridge void *)[self class], &info);
    
    //2.读取__DATA中自定义的ZYStoreListTemplateSectionName数据
    #ifndef __LP64__
        const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
        unsigned long templateSize = 0;
        uint32_t *templateMemory = (uint32_t*)getsectiondata(mhp, "__DATA", ZYStoreListTemplateSectionName, &templateSize);
    #else /* defined(__LP64__) */
        const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
        unsigned long templateSize = 0;
        uint64_t *templateMemory = (uint64_t*)getsectiondata(mhp, "__DATA", ZYStoreListTemplateSectionName, &templateSize);
     
    #endif /* defined(__LP64__) */
    
    //3.遍历ZYStoreListTemplateSectionName中的协议数据
    unsigned long counter = templateSize/sizeof(void*);
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)templateMemory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;
        
        //NSLog(@"config = %@", str);
        NSData *jsonData = [str dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error = nil;
        id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
        if (!error) {
            if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
                NSString *templatesName = [json allKeys][0];
                NSString *templatesClass  = [json allValues][0];
                if (templatesName && templatesClass) {
                    [self registerTemplateName:templatesName templateClass:NSClassFromString(templatesClass)];
                }
            }
        }
    }
}
复制代码

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

3、二进制重排

自从抖音团队分享了这篇 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提高超15% 启动优化文章后 , 二进制重排优化 pre-main 阶段的启动时间自此被你们广为流传。

当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次 缺页中断(Page Fault)。

二进制重排,主要是优化咱们启动时须要的函数很是分散在各个页,启动时就会屡次Page Fault形成时间的损耗。

3.1 获取Order File

本次主要是经过Clang静态插桩的方式,获取到全部的启动时调用的函数符号,导出为OrderFile。

Target -> Build Setting -> Custom Complier Flags -> Other C Flags添加 -fsanitize-coverage=func,trace-pc-guard参数

而后实现hook代码获取全部启动的函数符号。启动后在首页显示以后,能够经过触发下边-getAllSymbols方法获取全部符号。

#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 symboList = OS_ATOMIC_QUEUE_INIT;
static BOOL isEnd = NO;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.
    if (isEnd) {
        return;
    }
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

- (void)getAllSymbols {
    isEnd = YES;
    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:@"linkSymbols.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"linkSymbol result %@",filePath);
    }else{
        NSLog(@"linkSymbol result文件写入出错");
    }
}
复制代码

因为咱们的工程为pod工程,若是只在主工程里添加other c flags只能获取到主工程层下的全部启动函数,若是要获取全部的包含依赖pod中启动函数符号则须要在每个pod target设置other c flags参数。

咱们能够经过添加pod脚原本对每个target添加other c flags参数。

在podfile最后添加脚原本为每个target添加编译参数。注意能够过滤掉Debug环境才加载的库。

post_install do |installer|
    pods_project = installer.pods_project
    build_settings = Hash[
    'OTHER_CFLAGS' => '-fsanitize-coverage=func,trace-pc-guard'
#    ,'OTHER_SWIFT_FLAGS' => '-sanitize=undefined -sanitize-coverage=func'
    ]
    
    pods_project.targets.each do |target|
#      if !target.name.include?('Pods-')
      if !target.name.include?('Pods-') and target.name != 'LookinServer' and target.name != 'DoraemonKit' and target.name != 'DoraemonKit-DoraemonKit'
        # 修改build_settings
        target.build_configurations.each do |config|
            build_settings.each do |pair|
                key = pair[0]
                value = pair[1]
                if config.build_settings[key].nil?
                    config.build_settings[key] = ['']
                end
                if !config.build_settings[key].include?(value)
                    config.build_settings[key] << value
                end
            end
        end
        
        puts '[Other C Flags]: ' + target.name + ' success.'
      end
    end
end
复制代码

从新install以后全部的pod target都会添加上other c flags参数。而后就能够获取到全部的函数符号(注意若是是二进制库则仍是会获取不到)。

3.1 设置Order File

经过objc的源码能够看到objc也是经过设置order file设置编译顺序的。

咱们能够在主工程的Target -> Build Setting -> Linking -> Order File添加上述步骤导出的函数符号列表linkSymbols.order。

$(SRCROOT)/linkSymbols.order 这里能够根据根目录路径而后寻找,没必要把orderfile添加到工程bundle里。若是添加到工程里则会被打包到ipa里。咱们能够只是放在工程文件夹下,只在编译的时候根据路径引用就能够了。

设置完orderfile以后咱们能够经过设置write link map file属性为YES来找到编译时生成的符号($Project)-LinkMap-normal-arm64.txt。 修改完毕后 clean 一下 , 运行工程 , Products - show in finder, 找到 macho 的上上层目录。 找到结尾为arm64.txt的文件并打开。

Intermediates -> project_ios.build -> Debug-iphoneos -> project_ios.build -> project_ios-LinkMap-normal-arm64.txt

($Project)-LinkMap-normal-arm64.txt文件里在#Symbols以后为函数符号连接的顺序,能够验证一下重排是否成功。

最后能够看一下咱们重排以后的效果,Instruments下System Trace下Page Fault的次数和耗时:

总结

最后在看一下本次优化的效果。图中为iPhone6s Plus重启后第一次启动的优化先后截屏。

参考文章:

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

iOS App启动优化

相关文章
相关标签/搜索