Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


App启动分析与优化策略

启动过程分析

  • 解析Info.plist

    • 加载相关信息,例如闪屏
    • 沙箱建立、权限检查
  • Mach-O加载

    Mach-O 文件:我们写的程序想要跑起来,肯定它的可执行文件格式要被操作系统所理解。比如ELF是Linux下的可执行文件格式,那么对于OS X / iOS来说,Mach-O 是其可执行文件格式。

    Mach-O格式主要包括以下几种文件类型:

    • Executable:应用的主要二进制
    • Dylib:动态链接库
    • Bundle:不能被链接,只能在运行时使用dlopen加载
    • Image:包含Executable、Dylib和Bundle
    • Framework:包含Dylib、资源文件和头文件的文件夹
    • 如果是胖二进制文件,寻找合适当前CPU类别的部分
    • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
    • 定位内部、外部指针引用,例如字符串、函数等
    • 执行声明为__attribute__((constructor))的C函数
    • 加载类扩展(Category)中的方法
    • C++静态对象加载、调用ObjC的 +load 函数
  • 程序执行

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

测量App的启动时间

iOS App的启动有两种方式:冷启动和热启动,下面对这两种启动方式进行简单介绍。

冷启动:App第一次启动或者是被上滑Kill掉之后,再次从点击App应用图标到App完全启动显示主页面为止的过程成为冷启动。

热启动:当用户从当前App按下HOME键退出后,系统并不会立即kill掉App的进程,还会继续后台执行一段时间(至于什么时候会被kill,完全取决于系统内存等资源的使用情况,理想情况下可以保持后台运行15min)。然后当用户再次点击App图标时,会很快加载到上次停留的页面,几乎不需要进行资源载入,这种情况下的启动成为热启动。

下面我们对于App启动时间的测量只讨论在冷启动情形下。根据苹果官方介绍,iOS应用的启动分为两个阶段:pre-mainmain,所以App启动的总时间为:Total Time = pre-main Time + main Time。

Pre-main 阶段

Pre-mian 阶段即main()函数被调用之前的加载时间,包括dylib动态库的加载、Mach-O文件加载、Rebase/Bining、Objective-C Runtime 加载等。该过程如下图所示:

pre-main-img

main 阶段

main 阶段指的是从调用main()函数开始,调用UIApplicationMain()创建UIApplication,AppDelegate,直到回调App代理方法applicationDidBecomeActive:为止,其中包括application:didFinishLaunchingWithOptions方法中创建keyWindow、三方sdk和其他初始化项所消耗的时间,也包含RootViewController 的 ChildViewController的view完全显示出来所花费的时间。该过程如下图所示:

main-img

pre-main 阶段时间测量

那么对于pre-mainmain 启动阶段的耗时如何测量呢?苹果已为我们提供了一个简单易用的测试方法:在Xcode中选中项目的SchemeEdit Scheme...→,然后选择RunArgumentsEnvironment Variables中添加Name = DYLD_PRINT_STATISTICSvalue,Value = 1 的环境变量。如下图所示:

arguments-img

然后重新运行项目,注意控制台下面的打印输出,格式为:

Total pre-main time: 655.68 milliseconds (100.0%)
         dylib loading time: 205.67 milliseconds (31.3%)
        rebase/binding time: 320.95 milliseconds (48.9%)
            ObjC setup time:  63.07 milliseconds (9.6%)
           initializer time:  65.85 milliseconds (10.0%)
           slowest intializers :
             libSystem.B.dylib :   3.43 milliseconds (0.5%)
    libMainThreadChecker.dylib :  15.59 milliseconds (2.3%)
                       century :  76.56 milliseconds (11.6%)
                           
// 内容解读
Total pre-main time:  表示pre-main阶段总的时间消耗为655.68毫秒
dylib loading time:	 动态库加载时间
rebase/binding time: 重定位指针指向/指向镜像外部内容 所需时间
ObjC setup time: ObjC初始化时间
initializer time: 其他初始化时间的总和

slowest intializers : 最慢的三个初始化分别是libSystem.B.dyliblibMainThreadChecker.dylibcentury(项目名称)

main 阶段时间测量

由于main阶段涉及到大量的自定义代码,app主体页面的初始化、前期配置、三方库初始化等大量逻辑代理,iOS系统没有直接的测量方式,所以只能通过打点的方式计算启动开始到启动结束的时间差即可。例如下面示例代码:

// main.m 文件
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

extern CFAbsoluteTime StartTime;

int main(int argc, char * argv[]) {
    // 记录开始时间
    StartTime = CFAbsoluteTimeGetCurrent();
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

// AppDelegate.m 文件

CFAbsoluteTime StartTime;

- (void)applicationDidBecomeActive:(UIApplication *)application {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSUInteger milliseconds = (NSUInteger)((CFAbsoluteTimeGetCurrent() - StartTime) * 1000);
        NSLog(@"Loading done in %lu ms", milliseconds);
    });
}

影响启动性能的因素

接下来我们讨论在App启动的pre-main阶段和mian阶段,影响启动性能的多种因素。

Pre-main 阶段

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

我们尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象;至于ObjC的+load方法,似乎大家已经习惯不用它了。任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。

main 阶段

  • 执行main()函数的耗时
  • 执行application:didFinishLaunchingWithOptions的耗时
  • rootViewController及其childViewController的加载、view及其subviews的加载

一般的App的主体UI架构是,UITabbarViewController 作为 keyWindow的根控制器,然后包含多个ChildViewController作为每个tab的根控制器。然而在didFinishLaunchingWithOptions代理方法中各控制器的初始化顺序是怎样的呢?

答案是:

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

从上面的加载过程可以看出,在main()方法之后的启动中,只要是对页面的渲染,要减少这个阶段的耗时需要在viewDidLoad方法中做优化,减少不必要的逻辑,视图尽量使用懒加载,使用异步方式加载网络数据。

启动时间优化

苹果建议在400ms内完成pre-main 阶段的启动,App整体的启动时间不能超过20s,否则系统会被kill掉进程。

由于每个App的体量和类型不一样,对应启动过程中配置的逻辑处理复杂程度也是皆不相同,所有没有一个标准的时间来衡量启动时间是否达到最优。但是可以从用户体验上下功夫预加载占位视图,数据缓存等都可以营造出加载速度提升的感觉。

下面从实践的角度分析可优化的部分:

pre-main 阶段优化

  1. 移除不需要的动态库framework

  2. 移除不需要的类、未使用的变量、方法等

    对应项目中大量的文件,可以使用工具快速扫描整个项目找出未使用的类、变量、或方法。这里推荐一个开源工具 Fui 能准确的完成此任务,不足之处在于它处理不了动态库或静态库中提供的类,和C++ 的类模板。

    Fui使用方法:

    在终端中cd到项目根目录,然后执行fui find,就可以得到一个列表,可根据这个列表在Xcode中手动检查并删除无用代码。

  3. 合并功能类似的类和扩展(Category)

  4. +load方法中做的事情可以使用dispatch_once()来代替,因为main()方法被调用之前就会加载+load方法,会影响pre-main阶段启动事件。

  5. 压缩资源图片(对要求不高的图片可做压缩处理)

main 阶段优化

  1. 优化application: didFinishLaunchingWithOptions:内部逻辑
    1. 各种业务请求配置更新,多个配置更新可合并为单个请求,部分业务的配置延后更新。
    2. 对于新版本引导、广告闪屏逻辑,需要重构逻辑清晰
    3. 数据迁移,对太旧的无用的逻辑可以删除,不需要立即处理的数据,改为后台加载
  2. 优化rootViewController加载:在启动过程中只加载tabbarVc,主Vc即可,而且主Vc中的ViewDidLoad方法中也只加载需要立即显示出来的view,其他视图均使用懒加载,数据进行异步加载。

参考资料

iOS App 启动性能优化–腾讯Bugly

iOS启动优化—凌云的博客

[优化 App 的启动时间]

最近的文章

App常见崩溃分析

前言在iOS开发调试过程中,我们会遇到很多崩溃问题,比如数组越界、容器中插入nil、或调用不存在的方法时都会出现崩溃现象。那么,为了能更好的应对并避免这些常见崩溃问题的发生,就是我们亟待需要解决的问题。下面我们将分析iOS开发中常见的几个崩溃,并结合示例给出这类问题的解决方案。NSInvalidArgumentException从字面上来看,是无效的参数异常,但是触发这个异常的场景还有很多,最常见的就是在NSArray,NSDictionary容器中插入nil时发生,例如下面代码所示://...…

iOS继续阅读
更早的文章

反射的概念及应用

前言反射是指程序在运行时可以访问、检测、修改它本身状态或行为的一种能力。通常动态语言如ObjC具有这种特性。下面将介绍反射的特性以及在iOS中的应用举例。使用反射可以做什么 反射的应用场景 在运行时可以判断一个对象是否属于某个类、是否遵守某个协议、是否响应某个方法 在运行时可以构造任意一个类、生成一个对象、得到一个方法 在运行时动态调用一个方法ObjC中的反射由于ObjC的动态特性,下面方法可以在运行时根据字符串参数反射得到类、方法selector、协议方法等// SEL和字符串转...…

iOS继续阅读