iOS-App启动优化

手动目录

  • Main之前
  • Main 之后
  • 二进制重排
    系统默认加载方式
    1)、查看PageFault 次数
    2)、查看系统默认的链接符号
    指定加载方式

随着App的不管更新、功能增加等,app工程会原来越大,需要在启动就加载的功能模块也可能越来越多,这个是很耗时的,App的启动时间过久,多用户是很不友好的。所以App的启动优化是一个非常有必要的。

App的启动分为:热启动、冷启动
一般我们说的启动优化 是指冷启动

启动优化,从2方面入手:Main函数之前、Main函数之后。

Main函数之前 主要是由系统决定。
Main函数之后:由用户的加在内容决定。

Main之前

如何检测Main函数之前的启动时间?
添加一个环境变量:DYLD_PRINT_STATISTICS

iOS-App启动优化_第1张图片
添加环境变量

重新启动 打印出Main启动时间

   Total pre-main time: 704.32 milliseconds (100.0%)               // 总共启动时间
         dylib loading time: 174.75 milliseconds (24.8%)               // 动态库加载时间
        rebase/binding time:  36.61 milliseconds (5.1%)                // 修复内部指针地址(ASLR 随机偏移值)/外部符号绑定(DYLD去做的)
            ObjC setup time: 170.39 milliseconds (24.1%)               // OC类注册的耗时时间
           initializer time: 322.29 milliseconds (45.7%)               // load 的时间 
           slowest intializers :                                //  启动最耗时的内容
             libSystem.B.dylib :  10.14 milliseconds (1.4%)                  // 系统库  
    libMainThreadChecker.dylib :  85.19 milliseconds (12.0%)                  //系统库 
                  (App名称) : 411.80 milliseconds (58.4%)                      // 主程序

从上面的耗时来分析

在Main函数之前,有哪些是可以 做 优化的:

  • 1、库的加载
    系统库经过优化处理的,本身加载就 很快。
    自己倒入的库: 苹果给出的建议是不超过6个。如果超过6个,可以采用合并的方式。
  • 2、减少不必要的类 、资源图片等。
    比如随着版本更新迭代,有些类、图片等被弃用了的。
    可以使用工具检测没有用到的类
    这个操作相对来说优化的成效不高。有人说 减少2w个类,启动时间只少了800 毫秒。
  • 3、能不在Load里面做的操作,就不要在Load操作。
  • 4、二进制重排
    这个主要是正对binding阶段的操作。

Main之后

Main 之后的时间 自己用计时器打印。推荐一个工具:BLStopwatch打点计时器。 里面也链接了一篇作者自己的关于Main之后启动优化的文章 一次立竿见影的启动时间优化。

// 打点计时器用法

//在didFinishLaunchingWithOptions 里面加入计时操作
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [[BLStopwatch sharedStopwatch] start];
//    一些列操作
  [[BLStopwatch sharedStopwatch] splitWithDescription:@"didFinishLaunchingWithOptions"];
  return YES;
}

// 在其他启动阶段操作的地方 
[[BLStopwatch sharedStopwatch] refreshMedianTime];
//一些列操作
[[BLStopwatch sharedStopwatch] splitWithDescription:@"这是进行了某一个操作"];

// 显示第一个界面的地方进行操作
[[BLStopwatch sharedStopwatch] refreshMedianTime];
//一些列操作
[[BLStopwatch sharedStopwatch] splitWithDescription:@"第一个界面显示耗时"];、
[[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset ];      // 停止计时并打印所有的计时。

根据打印的时间,可以做相应的优化

Main 之后的优化
1、能懒加载的就懒加载
2、发货CPU的性能(多线程初始化)
3、启动阶段的尽量不要用Xib、stroyboard。 Xib、storyboard都是需要进行xml解析,相对纯代码来讲,是比较耗时的。

网上关于Main之后的优化内容比较多。包括上面提到的 打点计时器作者的那片文章一次立竿见影的启动时间优化。

二进制重排

这篇文章主要讲Main之前的启动优化。 二进制重排
在19年 抖音团队的一篇关于二进制重排火了。我们这篇文章主要讲如何操作

关于二进制重排,需要先了解虚拟内存物理内存

早期的计算机都是使用物理内存来处理:一次全部把App加载进入内存。这也就是出现了当内存满了之后,在打开一个应用,就会报错。而且也不安全(因为 全部都加载进入内存,而且是连续的,当拿到内存地址开始的位置,其他的内存信息,都可以通过内存偏移来拿到)。

虚拟内存就是为了解决这些问题的,先加载必须的信息,其他的信息当你用到的时候再在物理内存上去分配。
虚拟地址于物理地址 中间通过一张映射表(页表)进行管理。由硬件mmu(CPU里的一个单元)来管理。

我们在Xcode里面打印出来的地址 都是虚拟地址,这个时候的地址是连续的。但是实际地址需要通过映射表(页表)去寻址。 在物理地址上可能是不连续的

物理内存是分页的(在iOS设备 一页16K,Mac 一页4K)。当App加载的时候,会将虚拟内存映射到物理内存,这个时候,物理内存大概是这样的


iOS-App启动优化_第2张图片
物理内存分页

App先加载部分进入内存,当App使用某一功能的时候,发现在物理内存里面没有,这个时候会发生
缺页中断(PageFault) ---- 这个时候会先阻塞进程,先将虚拟内存加入物理内存中去。
加载的原则就是哪里有空的,加载哪里,没有空的,覆盖掉其他的。(这也是为什么当我们App开的比较多的时候,最开始打开的App会被重新启动加载)。
还有一个问题:当App编译好之后,他的虚拟内存的地址就固定了,这样会很容易黑客攻击,为了解决这个问题 ,就出现了ASLR - 地址空间布局随机化(就是在每个内存前面 加一个随机偏移值)。

因为内存加载是进行分页加载。那么我要先加载那些,后加载那些 。能不能自己进行指定?答案是可以的。Xcode支持指定符号进行加载。

系统默认加载方式

查看PageFault 次数

我们借助 系统自带调试工具:Xcode -> Instruments -> System Trace


iOS-App启动优化_第3张图片
System Trace

步骤一: 将应用安装到手机上
步骤二:打开System Trace 清空筛选条件,输入 Main Thread (下图一)
步骤三:点击start ,等状态变成黑色方形图标 点击停止 (下图二)
步骤四:选中Main Thread 选择主线程 - > Summary : Virtual Memory(下图三)
查看缺页中断次数:


iOS-App启动优化_第4张图片
图一
iOS-App启动优化_第5张图片
图二
iOS-App启动优化_第6张图片
图三

这样就看到了缺页中断的次数(File Backed Page In):这里是2747次。总耗时:814ms。
(注意:如果不是第一次启动。尽量多的点开其他应用,把物理内存中的page 尽量清空)。

后台退出App,在进行一次 操作,这个时候,File Backed page In 可能就很小(几十----一百多)。

查看系统默认的链接符号

直接在Xcode 设置改。


iOS-App启动优化_第7张图片
修改Link map

编译之后找到 编译好的.app 工程 -> Products —> xxx.app Show In Finder

iOS-App启动优化_第8张图片
找到相应目录

按照这个目录去找
Intermediates.noindex -> 工程名.build -> Debug-iphoneos(跑的机器不同,这个路径也不同) -> 工程名.bulid —> 工程名-LinkMap-normal-arm64.txt
打开这个文件 往下翻 找到这样的地方# Symbols:

# Symbols:
# Address   Size        File  Name
0x100007F1C 0x000001F0  [  1] -[OneClass mj_newValueFromOldValue:property:]
0x10000810C 0x000000CC  [  1] +[OneClass mj_objectClassInArray]
0x1000081D8 0x0000002C  [  1] -[OneClass buyPrice]
0x100008204 0x00000034  [  1] -[OneClass setBuyPrice:]
........

0x100008238 0x0000002C  [  2] -[TwoClass deliveryType]
0x100008264 0x00000048  [  2] -[TwoClass setDeliveryType:]
0x1000082AC 0x00000030  [  2] -[TwoClass isHot]
.......

我们发现 系统默认是按照 Bulid Phases -> Compile Source 里面的类的顺序去排列符号的。
但是一般来说,我们启动的时候,需要的类并不完全都在前面,这样就导致不必要的 缺页中断的发生。

指定加载方式

指定加载符号order文件

其实在下载系统的源码中,就有这样的配置。只不过之前没太注意。
打开下载的源码,在目录下就可以看到一个 libobjc.order文件


iOS-App启动优化_第9张图片
源码里的order文件

打开这个文件,里面就是指定的加载内容。

那么我们自己如何使用这个功能

指定符号顺序
1、 创建一个 .order 文件 (filename.order) 并放入工程目录下
2、在Build Settings 下搜索 order File (下图一) ,输入 路径 ./filename.order (因为我放在了根目录 下)

这样 Xcode就会按照我们指定的符号去加载到内存。

iOS-App启动优化_第10张图片
添加order支持
获取启动需要的符号

说了那么多,我知道怎么用了,但是我要如何获取启动时,需要重排的符号?

我们使用Clang 插桩的方式 【官方网站】,它可以捕获所有的方法、block、函数的调用。
他的作用就是:在编译的时候 ,在每个函数、方法、block 调用的时候,插入一个 __sanitizer_cov_trace_pc_guard

类似这样

- (void)callTask {
__sanitizer_cov_trace_pc_guard  () ;    // 插入这行代码
//  你要操作的任务
}

获取符号列表步骤

  • 步骤一:With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge。给compiler添加一个参数 (下图一)
  • 步骤二:在合适的位置写入它指定的函数。
  • 步骤三:根据打印的出来的地址,根据方法地址,找到方法符号。
  • 步骤四:保存找到的符号。
    因为编译插入的函数可能是在子线程,所以不能直接用数组来保存。
iOS-App启动优化_第11张图片
图一 ---- 添加配置

步骤二中的函数

// 新建一个AppDelegate 分类  :AppDelegate+Hook
extern  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.
}

extern  void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  
  void *PC = __builtin_return_address(0);        // 拿到被调用函数的地址。
  char PcDescr[1024];
//  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));    这一个函数在官网中没有说明,我们屏蔽掉
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

在这里面拿到了被调用方法的地址,我们根据地址拿符号。
步骤三中的代码

#import         // 引入系统库

void *PC = __builtin_return_address(0);      //  在这个位置插入下面的代码
//    typedef struct dl_info {
//            const char      *dli_fname;     /* Pathname of shared object */               文件路径
//            void            *dli_fbase;     /* Base address of shared object */           文件地址
//            const char      *dli_sname;     /* Name of nearest symbol */                  所需要的符号
//            void            *dli_saddr;     /* Address of nearest symbol */               函数所在的起始地址
//    } Dl_info;
    Dl_info info;           // 所有信息都在这个结构体里面  
    dladdr(PC, &info);
    NSLog(@"dli_fname : %s  \ndli_fbase : %p  \ndli_sname : %s  \ndli_saddr : %p\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);

// 其中一个打印的内容
dli_fname : /Users/xxxx/Library/Developer/CoreSimulator/Devices/C09425DB-F468-4BB4-BE33-42845DFEAE07/data/Containers/Bundle/Application/D14C571E-54D1-4ABD-85B5-3CE16A46744C/我的App名.app/我的App名
dli_fbase : 0x10af7c000  
dli_sname : -[UIView(SDLayoutExtention) sd_equalWidthSubviews]  
dli_saddr : 0x10afb3bd0

步骤四的操作:
不能用数组直接操作,那么换个方式:原子队列

//原子队列
static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
}SYNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;  // Duplicate the guard check.                 不屏蔽的话  打印出来load方法
    /*  精确定位 哪里开始 到哪里结束!  在这里面做判断写条件!*/
    
    void *PC = __builtin_return_address(0);
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //进入
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    
    
// NSLog(@"dli_fname : %s  \ndli_fbase : %p  \ndli_sname : %s  \ndli_saddr : %p\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
}

原子队列先进后出,我们需要对保存的符号进行 去翻、去重、重组(Block、函数 符号不完整)。

最后完成的一个完整类:#import "AppDelegate+Hook.h"


#import "AppDelegate+Hook.h"

#import "Aspects.h"

#import 
#import 


@implementation AppDelegate (Hook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class  class= NSClassFromString(@"SU_UnionHomeVC");
        
        [class aspect_hookSelector:@selector(viewDidLoad)
                       withOptions:AspectPositionBefore
                        usingBlock:^(id info) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                   [self methodSymbolList];
            });
        } error:NULL];
        
    });
}


//原子队列
static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
    void *pc;
    void *next;
}SYNode;

    
#ifdef DEBUG
    
    
#endif


+ (void)methodSymbolList {
    
    
    NSMutableArray  * 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 *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:@"fileName.order"];
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    NSLog(@"%@",funcStr);
}


#ifdef DEBUG

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

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;  // Duplicate the guard check.
    /*  精确定位 哪里开始 到哪里结束!  在这里面做判断写条件!*/
    
    void *PC = __builtin_return_address(0);
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //进入
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    
    
//    printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
//           info.dli_fname,
//           info.dli_fbase,
//           info.dli_sname,
//           info.dli_saddr);
//
}


#endif

@end

最后取出methodSymbolList 方法里面打印的路径里面的文件, 替换掉根目录下的.order文件。

你可能感兴趣的:(iOS-App启动优化)