iOS App启动优化

1.App启动过程

  • 解析Info.plist
    • 加载相关信息,例如如闪屏
    • 沙箱建立、权限检查
  • Mach-O加载
    • 如果是胖二进制文件,寻找合适当前CPU类别的部分
    • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
    • 定位内部、外部指针引用,例如字符串、函数等
    • 执行声明为attribute((constructor))的C函数
    • 加载类扩展(Category)中的方法
    • C++静态对象加载、调用ObjC的 +load 函数
  • 程序执行
    • 调用main()
    • 调用UIApplicationMain()
    • 调用applicationWillFinishLaunching

扩展

  1. Mach-O文件是什么:
  • Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。
  1. 常见的Mach-O文件类型
  • MH_OBJECT
    目标文件(.o)
    静态库文件(.a),静态库其实就是N个.o合并在一起
  • MH_EXECUTE:可执行文件
    .app/xx
  • MH_DYLIB:动态库文件
    .dylib
    .framework/xx
  • MH_DYLINKER:动态链接编辑器
    /usr/lib/dyld
  • MH_DSYM:存储着二进制文件符号信息的文件
    .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
  1. 目标文件类型
  • Mach-O 文件
    Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码
  • 通用二进制文件
    也叫作胖文件,胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件

毫无疑问移动应用的启动时间是影响用户体验的一个重要方面,那么我们究竟该如何通过启动时间来衡量一个应用性能的好坏呢?启动时间可以从冷启动和热启动两个角度去测量:

  • 冷启动:指的是应用尚未运行,必须加载并构建整个应用,完成初始化的工作,冷启动往往比热启动耗时长,而且每个应用的冷启动耗时差别也很大,所以冷启动存在很大的优化空间,冷启动时间从applicationDidFinishLaunching:withOptions:方法开始计算,很多应用会在该方法对其使用的第三方库初始化。

  • 热启动:应用已经在后台运行(常见的场景是用户按了 Home 按钮),由于某个事件将应用唤醒到前台,应用会在 applicationWillEnterForeground: 方法接收应用进入前台的事件

APP的启动时间,直接影响用户对你的APP的第一体验和判断。如果启动时间过长,不单单体验直线下降,而且可能会激发苹果的watch dog机制kill掉你的APP,那就悲剧了,用户会觉得APP怎么一启动就卡死然后崩溃了,不能用,然后长按APP点击删除键。(Xcode在debug模式下是没有开启watch dog的,所以我们一定要连接真机测试我们的APP)

1.png

t(App 总启动时间) = t1( main()之前的加载时间 ) + t2( main()之后的加载时间 )。

  • t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间;
  • t2 = main()函数执行之后到AppDelegate类中的applicationDidFinishLaunching:withOptions:方法执行结束前这段时间。

所以我们对APP启动时间的获取和优化都是从这两个阶段着手,下面先看看main()函数执行之前如何获取启动时间。

2.衡量main()函数执行之前的耗时

对于衡量main()之前也就是time1的耗时,苹果官方提供了一种方法,即在真机调试的时候,勾选DYLD_PRINT_STATISTICS选项(如果想获取更详细的信息可以使用DYLD_PRINT_STATISTICS_DETAILS),如下图:

2.png

输出结果如下:

Total pre-main time:  34.22 milliseconds (100.0%)
         dylib loading time:  14.43 milliseconds (42.1%)
        rebase/binding time:   1.82 milliseconds (5.3%)
            ObjC setup time:   3.89 milliseconds (11.3%)
           initializer time:  13.99 milliseconds (40.9%)
           slowest intializers :
             libSystem.B.dylib :   2.20 milliseconds (6.4%)
   libBacktraceRecording.dylib :   2.90 milliseconds (8.4%)
    libMainThreadChecker.dylib :   6.55 milliseconds (19.1%)
       libswiftCoreImage.dylib :   0.71 milliseconds (2.0%)

系统级别的动态链接库,因为苹果做了优化,所以耗时并不多,而大多数时候,t1的时间大部分会消耗在我们自身App中的代码上和链接第三方库上。
**main()函数之前耗时的影响因素

  • 动态库加载越多,启动越慢。
  • ObjC类越多,启动越慢
  • C的constructor函数越多,启动越慢
  • C++静态对象越多,启动越慢
  • ObjC的+load越多,启动越慢

所以我们应如何减少main()调用之前的耗时呢,我们可以优化的点有:

  1. 减少不必要的framework,特别是第三方的,因为动态链接比较耗时;
  2. check framework应设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
  3. 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具:
  • 删减一些无用的静态变量
  • 删减没有被调用到或者已经废弃的方法
  • 将不必须在+load方法中做的事情延迟到+initialize中
  • 尽量不要用C++虚函数(创建虚函数表有开销)

3.衡量main()函数执行之后的耗时

第二阶段的耗时统计,我们认为是从main ()执行之后到applicationDidFinishLaunching:withOptions:方法最后,那么我们可以通过打点的方式进行统计。 Objective-C项目因为有main文件,所以我么直接可以通过添加代码获取:

// 1. 在 main.m 添加如下代码:
CFAbsoluteTime AppStartLaunchTime;

int main(int argc, char * argv[]) {
    AppStartLaunchTime = CFAbsoluteTimeGetCurrent();
  .....
}

// 2. 在 AppDelegate.m 的开头声明
extern CFAbsoluteTime AppStartLaunchTime;

// 3. 最后在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加
dispatch_async(dispatch_get_main_queue(), ^{
  NSLog(@"App启动时间--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime));
});

main函数之后的优化:

  • 尽量使用纯代码编写,减少xib的使用;
  • 启动阶段的网络请求,是否都放到异步请求;
  • 一些耗时的操作是否可以放到后面去执行,或异步执行等。

4.applicationWillFinishLaunching的耗时

如果有这样这样的代码:

//AppDelegate.m
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.rootViewController = [[[MQQTabBarController alloc] init] autorelease];

    self.window = [[[UIWindow alloc] init] autorelease];
    [self.window makeKeyAndVisible];
    self.window.rootViewController = self.rootViewController;
    
    UITabBarController *tabBarViewController = [[[UITabBarController alloc] init] autorelease];

    NSLog(@"%s", __PRETTY_FUNCTION__);    
    return YES;
}

...
 
//MQQTabBarController.m
@implementation MQQTabBarController

- (void)viewDidLoad {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [super viewDidLoad];    
    // Do any additional setup after loading the view.

    UIViewController *tab1 = [[[MQQTab1ViewController alloc] init] autorelease];
    tab1.tabBarItem.title = @"red";
    [self addChildViewController:tab1];
    
    UIViewController *tab2 = [[[MQQTab2ViewController alloc] init] autorelease];
    tab2.tabBarItem.title = @"blue";
    [self addChildViewController:tab2];
    
    UIViewController *tab3 = [[[MQQTab3ViewController alloc] init] autorelease];
    tab3.tabBarItem.title = @"green";
    [self addChildViewController:tab3];
}

...
@end

那么[MQQTabBarController viewDidLoad][AppDelegate application:didFinishLaunchingWithOptions:][MQQTab1ViewController viewDidLoad][MQQTab2ViewController viewDidLoad][MQQTab2ViewController viewDidLoad] 完成的先后顺序是怎样的呢?

答案是:

  1. [MQQTabBarController viewDidLoad]
  2. [MQQTab1ViewController viewDidLoad]
  3. [AppDelegate application:didFinishLaunchingWithOptions:]
  4. [MQQTab2ViewController viewDidLoad] (点击了第二个tab之后加载)
  5. [MQQTab3ViewController viewDidLoad] (点击了第三个tab之后加载)

一般而言,大部分情况下我们都会把界面的初始化过程放在viewDidLoad,但是这个过程会影响消耗启动的时间。特别是在类似TabBarController这种会嵌套childViewController的ViewController的情况,它也会把部分children也初始化,因此各种viewDidLoad会递归的进行。

最简单的解决的方法,是把viewController延后加载,但实际上这属于一种掩耳盗铃,确实,applicationWillFinishLaunching的耗时是降下来了,但用户体验上并没有感觉变快。

更好一点的解决方法有点类似facebook,主视图会第一时间加载,但里面的数据和界面都会延后加载,这样用户就会阶段性的获得视觉上的变化,从而在视觉体验上感觉App启动得很快。

5. facebook启动的网络请求优化

网络请求/响应看起来像这样:

1.png

我们注意到,一旦请求正在排队,发送请求出去之后就有一个时间间隔。这很好解释 — 在冷启动中,网络连接并不是一个开放的、安全的 TCP 连接。一个连接的建立需要三次握手,平均为几百毫秒。当摘要请求第一次发送时,无法避免要花掉这些时间。长远来看,这可以通过缓存 SSL 证书来解决。但是再次强调,我们退回来的目的并不是为了发送 TCP 请求,而是为了从服务器通过任何可能的方式获得请求信息。

我们提出了一个创造性的解决方案 — UDP 启动。本质上,我们在通过 TCP 发送摘要请求时,先发送一个编码过的包含摘要请求的 UDP 包到服务器。这样做的目的是唤醒服务器更早地去获取和缓存数据。当真正的摘要请求通过 TCP 到达时,服务器只需见到地从缓存内容中构造出响应,并发回客户端。这个技术使得我们可以减少几百毫秒的耗时。

当我们持续深入研究服务器端时,我们开始尝试使用 层-取(story-fetching)策略。过去我们已经做了一批摘要请求的 3+7 层。原因很简单:下载次数和被下载的层成正比。因此,把请求分割成两块,允许开始的三层先进来,其余的七个随后进来。通过提升我们的基础设施,我们已经能够升级为 1+1+X 策略,这已经接近于流了。这样就减少了服务器必须处理第一层的时间,并且能够减少下载的时间,使得可以在最快的时间内与用户交互。通过这样的努力,这样我们又减少了几百毫秒的耗时。

总结一下facebook网络优化就是:

  1. 瘦身请求网络依赖,将相类似的多个请求归到一个请求
  2. UDP启动请求现行缓存
  3. 队列串行化处理启动响应

6.优化总结

性能上的优化:
main()函数之前

  1. 减少动态库静态库等Mach-O文件的加载
  2. 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具
  3. 尽量不要用C++虚函数(创建虚函数表有开销)
  4. 合并功能类似的类和扩展(Category)
  5. 压缩资源图片

main()函数之后

  1. 尽量使用纯代码编写,减少xib的使用
  2. 启动阶段的网络请求,是否都放到异步请求
  3. 一些耗时的操作放到后面去执行,或异步执行等
    4.优化rootViewController加载,减少或延后加载不需要的视图及逻辑
  4. 网络请求的优化。。。
  5. 数据本地缓存,先布局视图,加载本地缓存,再加载网络资源

参考文章
iOS App 启动性能优化
Facebook iOS App如何优化启动时间

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