优化app运行时间,WWDC之优化App运转速度

参考资料:深入理解iOS App的启动过程iOS 应用程序启动过程及原理总结iOS:App启动过程详解

启动时间是衡量应用品质的重要指标。

1. App启动过程

图片 1

图片 2title-6.png

iOS APP启动函数调用顺序~详解

本文首先会从原理上出发,讲解iOS系统是如何启动App的,然后从main函数之前和main函数之后两个角度去分析如何优化启动时间。

    解析Info.plist

        加载相关信息,例如如闪屏

        沙箱建立、权限检查

在WWDC 2016上首次提到了关于App应用启动速度优化的话题:Session 406 Optimizing App Startup Time .该Session上Apple建议一个App完整的启动时间应该保证400ms之内,而若超过20s后还未完全启动App,那么App进程就会被系统杀死.而如何Debug和优化应用启动的时间,官方提出一系列方法来关注应用启动时执行main()前究竟干了些什么.而通过这个Session,你会了解到以下内容:

[]

准备知识

    Mach-O加载

        如果是胖二进制文件,寻找合适当前CPU类别的部分

        加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)

        定位内部、外部指针引用,例如字符串、函数等

        执行声明为__attribute__((constructor))的C函数

        加载类扩展(Category)中的方法

        C 静态对象加载、调用ObjC的 load函数

  • 如何获得应用加载的时间
  • 执行程序入门的代码前App加载过程中的流程
  • 如何优化App的加载时间

启动时间

Mach-O

    程序执行

        调用main()

        调用UIApplicationMain()

        调用applicationWillFinishLaunching

一个App在执行main函数前包括app delegate的系列方法如applicationWillFinishLaunching时,会做许多系统级别的准备.而在iOS10之前,开发者很难清楚自己App为何启动加载慢.而通过在工程的scheme中添加环境变量DYLD_PRINT_STATISTICS,设置Value为1,App启动加载时就会有启动过程的日志输出. 现在(iOS 10之后)Apple对DYLD_PRINT_STATISTICS的日志输出结果进行了简化,使得更容易让开发者理解.

冷启动 VS 热启动

如果你刚刚启动过App,这时候App的启动所需要的数据仍然在缓存中,再次启动的时候称为热启动。如果设备刚刚重启,然后启动App,这时候称为冷启动。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS 和 DYLD_PRINT_STATISTICS_DETAILS。

图片 3image.png

Total pre-main time: 43.00 milliseconds  dylib loading time: 19.01 milliseconds  rebase/binding time: 1.77 milliseconds  ObjC setup time: 3.98 milliseconds  initializer time: 18.17 milliseconds  slowest intializers : libSystem.B.dylib : 2.56 milliseconds  libBacktraceRecording.dylib : 3.00 milliseconds  libMainThreadChecker.dylib : 8.26 milliseconds  ModelIO : 1.37 milliseconds 

对于这个libMainThreadChecker.dylib估计很多同学会有点陌生,这是XCode 9新增的动态库,用来做主线成检查的。

哪些名词指的是Mach-o

2. 如何测量启动过程耗时

图片 4指定Scheme上添加DYLD_PRINT_STATISTICS环境变量

优化启动时间

启动时间这个名词,不同的人有不同的定义。在我看来,

启动时间是用户点击App图标,到第一个界面展示的时间。

以main函数作为分水岭,启动时间其实包括了两部分:main函数之前和main函数到第一个界面的viewDidAppear:。所以,优化也是从两个方面进行的,个人建议优先优化后者,因为绝大多数App的瓶颈在自己的代码里。

我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。

  • 执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions
  • 初始化Window,初始化基础的ViewController结构(一般是UINavigationController UITabViewController)
  • 获取数据(Local DB/Network),展示给用户。

Executable 可执行文件

    mian()之前

        在Xcode的菜单中选择Project→Scheme→Edit Scheme...,然后找到        Run→Environment Variables→ ,添加name为DYLD_PRINT_STATISTICSvalue为1的环境变量。

图片 5

        在Xcode运行App时,会在console中得到一个报告。例如,我在WiFi管家中加入以上设置之后,会得到这样一个报告:

图片 6

        main()函数之前总共使用了94.33ms

        在94.33ms中,加载动态库用了61.87ms,指针重定位使用了3.09ms,ObjC类初始化使用了10.78ms,各种初始化使用了18.50ms。

        在初始化耗费的18.50ms中,用时最多的三个初始化是libSystem.B.dylib、libBacktraceRecording.dylib以及GTFreeWifi。

尝试在iPad (使用iOS10 beta 3)上对一个纯OC项目设置该环境变量后,有了以下输出信息.

UIViewController

延迟初始化那些不必要的UIViewController。

比如网易新闻:

图片 7image.png

在启动的时候只需要初始化首页的头条页面即可。像“要闻”,“我的”等页面,则延迟加载,即启动的时候只是一个UIViewController作为占位符给TabController,等到用户点击了再去进行真正的数据和视图的初始化工作。

Dylib 动态库

    main()函数之后

        从main()函数开始至applicationWillFinishLaunching结束,我们统一称为main()函数之后的部分。

Total pre-main time: 74.37 milliseconds  dylib loading time: 41.05 milliseconds  rebase/binding time: 8.10 milliseconds  ObjC setup time: 9.87 milliseconds  initializer time: 15.23 milliseconds  slowest intializers : libSystem.B.dylib : 6.58 milliseconds  libBacktraceRecording.dylib : 6.27 milliseconds 
AppDelegate

通常我们会在AppDelegate的代理方法里进行初始化工作,主要包括了两个方法:

  • didFinishLaunchingWithOptions
  • applicationDidBecomeActive

优化这些初始化的核心思想就是:

能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

这些工作主要可以分为几类:

  • 三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。
  • 初始化某些基础服务,比如WatchDog,远程参数。
  • 启动相关日志,日志往往涉及到DB操作,一定要放到后台去做
  • 业务方初始化,这个交由每个业务自己去控制初始化时间。

对于didFinishLaunchingWithOptions的代码,建议按照以下的方式进行划分:

@interface AppDelegate ()//业务方需要的生命周期回调@property (strong, nonatomic) NSArray<id<UIApplicationDelegate>> * eventQueues;//主框架负责的生命周期回调@property (strong, nonatomic) id<UIApplicationDelegate> basicDelegate;@end

然后,你会得到一个非常干净的AppDelegate文件:

- application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { for (id<UIApplicationDelegate> delegate in self.eventQueues) { [delegate application:application didFinishLaunchingWithOptions:launchOptions]; } return [self.basicDelegate application:application didFinishLaunchingWithOptions:launchOptions];}

由于对这些初始化进行了分组,在开发期就可以很容易的控制每一个业务的初始化时间:

CFTimeInterval startTime = CACurrentMediaTime();//执行方法CFTimeInterval endTime = CACurrentMediaTime();

Bundle 无法被连接的动态库,只能通过dlopen()加载

3. 影响启动性能的因素

        App启动过程中每一个步骤都会影响启动性能,但是有些部分所消耗的时间少之又少,另外有些部分根本无法避免,考虑到投入产出比,我们只列出我们可以优化的部分:

输出内容展示了系统调用main()函前主要进行的工作内容和时间花费,Session上也对每一阶段加载过程具体内容进行了详细的叙述,有兴趣地可观看该Session.

用Time Profiler找到元凶

Time Profiler在分析时间占用上非常强大。实用的时候注意三点

  • 在打包模式下分析(一般是Release),这样和线上环境一样。
  • 记得开启dsym,不然无法查看到具体的函数调用堆栈
  • 分析性能差的设备,对于支持iOS 8的,一般分析iphone 4s或者iphone 5。

一个典型的分析界面如下:

图片 8image.png

几点要注意:

  • 分析启动时间,一般只关心主线程
  • 选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码
  • 右侧可以看到详细的调用堆栈信息

在某一行上双击,我们可以进入到代码预览界面,去看看实际每一行占用了多少时间:

图片 9image.png

不同的App在启动的时候做的事情往往不同,但是优化起来的核心思想无非就两个:

  • 能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。
  • 不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。

Image 指的是Executable,Dylib或者Bundle的一种,文中会多次使用Image这个名词。

    main()函数之前耗时的影响因素

        动态库加载越多,启动越慢。

        ObjC类越多,启动越慢

        C的constructor函数越多,启动越慢

        C 静态对象越多,启动越慢

        ObjC的 load越多,启动越慢

        实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难查察觉得出,但1000个类和10000个类的分别就开始明显起来。

        同样的,尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C 的静态对象;至于ObjC的 load方法,现在建议使用  initialize。任何情况下,能用dispatch_once()来完成的,就尽量不要用到以上的方法。

那么如何尽可能的减少pre-main花费的时间呢,主要就从输出日志给出的四个阶段下手:

Main函数之前

Main函数之前是iOS系统的工作,所以这部分的优化往往更具有通用性。

启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量。

  • 合并动态库,比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。

Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

  • 减少__DATA段中的指针数量。
  • 合并Category和功能类似的类。比如:UIView Frame,UIView AutoLayout…合并为一个
  • 删除无用的方法和类。
  • 多用Swift Structs,因为Swfit Structs是静态分发的。感兴趣的同学可以看看我之前这篇文章:《Swift进阶之内存模型和方法调度》

通常,我们会在 load方法中进行method-swizzling,这也是Nshipster推荐的方式。

  • 用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。
  • 减少atribute((constructor))的使用,而是在第一次访问的时候才* * 用dispatch_once等方式初始化。
  • 不要创建线程
  • 使用Swfit重写代码。

Framework 动态库和对应的头文件和资源文件的集合

    main()函数之后耗时的影响因素

        执行main()函数的耗时

        执行applicationWillFinishLaunching的耗时

        rootViewController及其childViewController的加载、view及其subviews的加载

  • 对动态库加载的时间优化.每个App都进行动态库加载,其中系统级别的动态库占据了绝大数,而针对系统级别的动态库都是经过系统高度优化的,不用担心时间的花费.开发者应该关注于自己集成到App的那些动态库,这也是最能消耗加载时间的地方.对此Apple建议减少在App里开发者的动态库集成或者有可能地将其多个动态库最终集成一个动态库后进行导入, 尽量保证将App现有的非系统级的动态库个数保证在6个以内.

  • 减少Appp的Objective-C类,分类和的唯一Selector的个数.这样做主要是为了加快程序的整个动态链接, 在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正的使用,加快程序机器码的生成.

  • 减少Objc运行初始化的时间花费.主要是类的注册,分类的注册,唯一选择器的存在,以及涉及子父类内存布局的Non Fragile ivars偏移的更新,都会影响Objective-C运行时初始化的时间消耗.

  • 使用initialize方法进行必要的初始化工作.用 initialize方法替换调用原先在OC的 load方法中执行初始代码工作,从而加快所有类文件的加载速度.

Apple出品的操作系统的可执行文件格式几乎都是mach-o,iOS当然也不例外。

    applicationWillFinishLaunching的耗时 

       app结构为TabBarViewController->NavgationViewController->ViewController。启动时的顺序是[TabBarViewController viewDidLoad]->[NavgationViewController viewDidLoad]->[ViewController viewDidLoad]->[AppDelegate application:didFinishLaunchingWithOptions:]。(TabBarViewController下多个NavgationViewController只执行要显示的NavgationViewController和ViewController)。

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

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

图片 10

最后演讲者对加载启动优化的整体概括了Session所要传达的内容:

mach-o可以大致的分为三部分:

4优化的目标

        由于每个App的情况有所不同,需要加载的数据量也有所不同,事实上我们无法使用一种统一的标准来衡量不同的App。

            应该在400ms内完成main()函数之前的加载

            整体过程耗时不能超过20秒,否则系统会kill掉进程,App启动失败

        如何定制优化的目标呢?首先,要确定启动性能的界限,例如,在各种App性能的指标中,哪一此属于启动性能的范畴,哪一些则于App的流畅度性能?我认为应该首先把启动过程分为四个部分:

                1.main()函数之前

                2.main()函数之后至applicationWillFinishLaunching完成

                3.App完成所有本地数据的加载并将相应的信息展示给用户

                4.App完成所有联网数据的加载并将相应的信息展示给用户

        1 2一起决定了我们需要用户等待多久才能出现一个主视图,同时也是技术上可以精确测量的时长,1 2 3决定了用户视觉上的等待出现有用信息所需要的时长,1 2 3 4决定了我们需要多少时间才能让我们需要展示给用户的所有信息全部出现。

  • 使用DYLD_PRINT_STATISTICS测试启动加载时间
  • 减少自定义的动态库集成
  • 精简原有的Objective-C类和代码
  • 移除静态的初始化操作
  • 使用更多的Swift代码

图片 11

5.启动优化

  1. 移除不需要用到的动态库.

  2. 移除不需要用到的类.

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

        由于Category的实现原理,和ObjC的动态绑定有很强的关系,所以实际上类的扩展是比较占用启动时间的。尽量合并一些扩展,会对启动有一定的优化作用。

  1. 压缩资源图片

        压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了。

  1. 优化applicationWillFinishLaunching

        对于 didFinishLaunchingWithOptions,这里面的初始化是必须执行的,但是我们可以适当的根据功能的不同对应的适当延迟启动的时机。

  1. 优化rootViewController加载。

Header 头部,包含可以执行的CPU架构,比如x86,arm64

总结

  • WWDC 2016 Session 406 Optimizing App Startup Time
  • Objective-C Runtime

Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式

    利用DYLD_PRINT_STATISTICS分析main()函数之前的耗时

        重新梳理架构,减少动态库、ObjC类的数目,减少Category的数目

        定期扫描不再使用的动态库、类、函数,例如每两个迭代一次

        用dispatchonce()代替所有的__attribute__((constructor))函数、C 静态对象初始化、ObjC的 load

        在设计师可接受的范围内压缩图片的大小,会有意外收获

Data,数据,包含load commands中需要的各个段的数据,每一个Segment都得大小是Page的整数倍。

    利用锚点分析applicationWillFinishLaunching的耗时

        将不需要马上在applicationWillFinishLaunching执行的代码延后执行

        rootViewController的加载,适当将某一级的childViewController或subviews延后加载

        如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController

我们用MachOView打开Demo工程的可以执行文件,来验证下mach-o的文件布局:

    不应放过的一些小细节

        异步操作并不影响指标,但有可能影响交互体验,例如大量网络请求导致数据拥堵。

图片 12

图中分析的mach-o文件来源于PullToRefreshKit,这是一个纯Swift的编写的工程。

那么Data部分又包含哪些segment呢?绝大多数mach-o包括以下三个段(支持用户自定义Segment,但是很少使用)

__TEXT 代码段,只读,包括函数,和只读的字符串,上图中类似__TEXT,__text的都是代码段

__DATA 数据段,读写,包括可读写的全局变量等,上图类似中的__DATA,__data都是数据段

__LINKEDIT __LINKEDIT包含了方法和变量的元数据,以及代码签名等信息。

关于mach-o更多细节,可以看看文档:《Mac OS X ABI Mach-O File Format Reference》。

dyld

dyld的全称是dynamic loader,它的作用是加载一个进程所需要的image,dyld是开源的。

Virtual Memory

虚拟内存是在物理内存上建立的一个逻辑地址空间,它向上提供了一个连续的逻辑地址空间,向下隐藏了物理内存的细节。

虚拟内存使得逻辑地址可以没有实际的物理地址,也可以让多个逻辑地址对应到一个物理地址。

虚拟内存被划分为一个个大小相同的Page(64位系统上是16KB),提高管理和读写的效率。 Page又分为只读和读写的Page。

虚拟内存是建立在物理内存和进程之间的中间层。在iOS上,当内存不足的时候,会尝试释放那些只读的Page,因为只读的Page在下次被访问的时候,可以再从磁盘读取。如果没有可用内存,会通知在后台的App(也就是在这个时候收到了memory warning),如果在这之后仍然没有可用内存,则会杀死在后台的App。

Page fault

在应用执行的时候,它被分配的逻辑地址空间都是可以访问的,当应用访问一个逻辑Page,而在对应的物理内存中并不存在的时候,这时候就发生了一次Page fault。当Page fault发生的时候,会中断当前的程序,在物理内存中寻找一个可用的Page,然后从磁盘中读取数据到物理内存,接着继续执行当前程序。

Dirty Page & Clean Page

如果一个Page可以从磁盘上重新生成,那么这个Page称为Clean Page

如果一个Page包含了进程相关信息,那么这个Page称为Dirty Page

像代码段这种只读的Page就是Clean Page。而像数据段这种读写的Page,当写数据发生的时候,会触发COW(Copy on write),也就是写时复制,Page会被标记成Dirty,同时会被复制。

想要了解更多细节,可以阅读文档:Memory Usage Performance Guidelines

启动过程

使用dyld2启动应用的过程如图:

图片 13

大致的过程如下:

加载dyld到App进程

加载动态库(包括所依赖的所有动态库)

Rebase

Bind

初始化Objective C Runtime

其它的初始化代码

加载动态库

dyld会首先读取mach-o文件的Header和load commands。

接着就知道了这个可执行文件依赖的动态库。例如加载动态库A到内存,接着检查A所依赖的动态库,就这样的递归加载,直到所有的动态库加载完毕。通常一个App所依赖的动态库在100-400个左右,其中大多数都是系统的动态库,它们会被缓存到dyld shared cache,这样读取的效率会很高。

查看mach-o文件所依赖的动态库,可以通过MachOView的图形化界面(展开Load Command就能看到),也可以通过命令行otool。

192:Desktop Leo$ otool -L demo demo:@rpath/PullToRefreshKit.framework/PullToRefreshKit (compatibilityversion1.0.0, currentversion1.0.0) /System/Library/Frameworks/Foundation.framework/Foundation (compatibilityversion300.0.0, currentversion1444.12.0) /usr/lib/libobjc.A.dylib (compatibilityversion1.0.0, currentversion228.0.0)@rpath/libswiftCore.dylib (compatibilityversion1.0.0, currentversion900.0.65)@rpath/libswiftCoreAudio.dylib (compatibilityversion1.0.0, currentversion900.0.65)//...

Rebase && Bind

这里先来讲讲为什么要Rebase?

有两种主要的技术来保证应用的安全:ASLR和Code Sign。

ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址 偏移量找到函数的地址。

Code Sign相信大多数开发者都知晓,这里要提一点的是,在进行Code sign的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。

mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?

mach-o中采用了PIC技术,全称是Position Independ code。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分

Rebase 修正内部(指向当前mach-o文件)的指针指向

Bind 修正外部指针指向

图片 14

之所以需要Rebase,是因为刚刚提到的ASLR使得地址随机化,导致起始地址不固定,另外由于Code Sign,导致不能直接修改Image。Rebase的时候只需要增加对应的偏移量即可。待Rebase的数据都存放在__LINKEDIT中。

可以通过MachOView查看:Dynamic Loader Info -> Rebase Info

也可以通过命令行:

192:Desktop Leo$ xcrun dyldinfo -bind demo bind information:segment section address type addend dylib symbol__DATA __got0x10003C038pointer0PullToRefreshKit __T016PullToRefreshKit07DefaultC4LeftC9textLabelSo7UILabelCvWvd__DATA __got0x10003C040pointer0PullToRefreshKit __T016PullToRefreshKit07DefaultC5RightC9textLabelSo7UILabelCvWvd__DATA __got0x10003C048pointer0PullToRefreshKit __T016PullToRefreshKit07DefaultC6FooterC9textLabelSo7UILabelCvWvd__DATA __got0x10003C050pointer0PullToRefreshKit __T016PullToRefreshKit07DefaultC6HeaderC7spinnerSo23UIActivityIndicatorViewCvWvd//...

Rebase解决了内部的符号引用问题,而外部的符号引用则是由Bind解决。在解决Bind的时候,是根据字符串匹配的方式查找符号表,所以这个过程相对于Rebase来说是略慢的。

同样,也可以通过xcrun dyldinfo来查看Bind的信息,比如我们查看bind信息中,包含UITableView的部分:

192:DesktopLeo$ xcrun dyldinfo -bind demo | grepUITableView__DATA __objc_classrefs0x100041940pointer0UIKit_OBJC_CLASS_$_UITableView__DATA __objc_classrefs0x1000418B0pointer0UIKit_OBJC_CLASS_$_UITableViewCell__DATA __objc_data0x100041AC0pointer0UIKit_OBJC_CLASS_$_UITableViewController__DATA __objc_data0x100041BE8pointer0UIKit_OBJC_CLASS_$_UITableViewController__DATA __objc_data0x100042348pointer0UIKit_OBJC_CLASS_$_UITableViewController__DATA __objc_data0x100042718pointer0UIKit_OBJC_CLASS_$_UITableViewController__DATA __data0x100042998pointer0UIKit_OBJC_METACLASS_$_UITableViewController__DATA __data0x100042A28pointer0UIKit_OBJC_METACLASS_$_UITableViewController__DATA __data0x100042F10pointer0UIKit_OBJC_METACLASS_$_UITableViewController__DATA __data0x1000431A8pointer0UIKit_OBJC_METACLASS_$_UITableViewController

保障是必须的,你支付成功后,你在天猫上面支付是保障你的消费权益的,稍后我们售后 老师会跟你签学习合同的。发电子版的合同给你,你填好之后回传给我,然后我这边签字 盖章之后发扫描件到你的邮箱,如果你有需要还可以把纸质合同寄给你的

Objective C

Objective C是动态语言,所以在执行main函数之前,需要把类的信息注册到一个全局的Table中。同时,Objective C支持Category,在初始化的时候,也会把Category中的方法注册到对应的类中,同时会唯一Selector,这也是为什么当你的Cagegory实现了类中同名的方法后,类中的方法会被覆盖。

另外,由于iOS开发时基于Cocoa Touch的,所以绝大多数的类起始都是系统类,所以大多数的Runtime初始化起始在Rebase和Bind中已经完成。

Initializers

接下来就是必要的初始化部分了,主要包括几部分:

load方法。

C/C 静态初始化对象和标记为__attribute__(constructor)的方法

这里要提一点的就是, load方法已经被弃用了,如果你用Swift开发,你会发现根本无法去写这样一个方法,官方的建议是实用initialize。区别就是,load是在类装载的时候执行,而initialize是在类第一次收到message前调用。

dyld3

上文的讲解是dyld2的加载方式。而最新的是dyld3加载方式略有不同:

图片 15

dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。

dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情:

分析Mach-o Headers

分析依赖的动态库

查找需要Rebase & Bind之类的符号

把上述结果写入缓存

这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。

启动时间

冷启动 VS 热启动

如果你刚刚启动过App,这时候App的启动所需要的数据仍然在缓存中,再次启动的时候称为热启动。如果设备刚刚重启,然后启动App,这时候称为冷启动。

启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

在Xcode中,可以通过设置环境变量来查看App的启动时间,DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。

图片 16

Total pre-maintime:43.00milliseconds dylib loadingtime:19.01milliseconds rebase/bindingtime:1.77milliseconds ObjC setuptime:3.98milliseconds initializertime:18.17milliseconds slowest intializers : libSystem.B.dylib :2.56milliseconds libBacktraceRecording.dylib :3.00milliseconds libMainThreadChecker.dylib :8.26milliseconds ModelIO :1.37milliseconds

对于这个libMainThreadChecker.dylib估计很多同学会有点陌生,这是XCode 9新增的动态库,用来做主线成检查的。

优化启动时间

启动时间这个名词,不同的人有不同的定义。在我看来,

启动时间是用户点击App图标,到第一个界面展示的时间。

以main函数作为分水岭,启动时间其实包括了两部分:main函数之前和main函数到第一个界面的viewDidAppear:。所以,优化也是从两个方面进行的,个人建议优先优化后者,因为绝大多数App的瓶颈在自己的代码里。

Main函数之后

我们首先来分析下,从main函数开始执行,到你的第一个界面显示,这期间一般会做哪些事情。

执行AppDelegate的代理方法,主要是didFinishLaunchingWithOptions

初始化Window,初始化基础的ViewController结构(一般是UINavigationController UITabViewController)

获取数据(Local DB/Network),展示给用户。

UIViewController

延迟初始化那些不必要的UIViewController。

比如网易新闻:

图片 17

在启动的时候只需要初始化首页头条页面即可。像“要闻”,“我的”等页面,则延迟加载,即启动的时候只是一个UIViewController作为占位符给TabController,等到用户点击了再去进行真正的数据和视图的初始化工作。

AppDelegate

通常我们会在AppDelegate的代理方法里进行初始化工作,主要包括了两个方法:

didFinishLaunchingWithOptions

applicationDidBecomeActive

优化这些初始化的核心思想就是:

能延迟初始化的尽量延迟初始化,不能延迟初始化的尽量放到后台初始化。

这些工作主要可以分为几类:

三方SDK初始化,比如Crash统计; 像分享之类的,可以等到第一次调用再出初始化。

初始化某些基础服务,比如WatchDog,远程参数。

启动相关日志,日志往往涉及到DB操作,一定要放到后台去做

业务方初始化,这个交由每个业务自己去控制初始化时间。

对于didFinishLaunchingWithOptions的代码,建议按照以下的方式进行划分:

@interfaceAppDelegate()//业务方需要的生命周期回调@property(strong,nonatomic)NSArray> * eventQueues;//主框架负责的生命周期回调@property(strong,nonatomic)id basicDelegate;@end

然后,你会得到一个非常干净的AppDelegate文件:

- application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions {for(id delegateinself.eventQueues) { [delegateapplication:applicationdidFinishLaunchingWithOptions:launchOptions]; } return[self.basicDelegateapplication:applicationdidFinishLaunchingWithOptions:launchOptions];}

由于对这些初始化进行了分组,在开发期就可以很容易的控制每一个业务的初始化时间:

CFTimeInterval startTime = CACurrentMediaTime();

//执行方法

CFTimeInterval endTime = CACurrentMediaTime();

用Time Profiler找到元凶

Time Profiler在分析时间占用上非常强大。实用的时候注意三点

在打包模式下分析(一般是Release),这样和线上环境一样。

记得开启dsym,不然无法查看到具体的函数调用堆栈

分析性能差的设备,对于支持iOS 8的,一般分析iphone 4s或者iphone 5。

一个典型的分析界面如下:

图片 18

几点要注意:

分析启动时间,一般只关心主线程

选择Hide System Libraries和Invert Call Tree,这样我们能专注于自己的代码

右侧可以看到详细的调用堆栈信息

在某一行上双击,我们可以进入到代码预览界面,去看看实际每一行占用了多少时间:

图片 19

小结

不同的App在启动的时候做的事情往往不同,但是优化起来的核心思想无非就两个:

能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。

不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。

Main函数之前

Main函数之前是iOS系统的工作,所以这部分的优化往往更具有通用性。

dylibs

启动的第一步是加载动态库,加载系统的动态库使很快的,因为可以缓存,而加载内嵌的动态库速度较慢。所以,提高这一步的效率的关键是:减少动态库的数量

合并动态库,比如公司内部由私有Pod建立了如下动态库:XXTableView, XXHUD, XXLabel,强烈建议合并成一个XXUIKit来提高加载速度。

Rebase & Bind & Objective C Runtime

Rebase和Bind都是为了解决指针引用的问题。对于Objective C开发来说,主要的时间消耗在Class/Method的符号加载上,所以常见的优化方案是:

减少__DATA段中的指针数量。

合并Category和功能类似的类。比如:UIView Frame,UIView AutoLayout…合并为一个

删除无用的方法和类。

多用Swift Structs,因为Swfit Structs是静态分发的。感兴趣的同学可以看看我之前这篇文章:《Swift进阶之内存模型和方法调度》

Initializers

通常,我们会在 load方法中进行method-swizzling,这也是Nshipster推荐的方式。

用initialize替代load。不少同学喜欢用method-swizzling来实现AOP去做日志统计等内容,强烈建议改为在initialize进行初始化。

减少__atribute__((constructor))的使用,而是在第一次访问的时候才用dispatch_once等方式初始化。

不要创建线程

使用Swfit重写代码。

本文由星彩网app下载发布于计算机编程,转载请注明出处:优化app运行时间,WWDC之优化App运转速度

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。