iOSApp启动优化方案

前言

        最近和公司iOS小组伙伴讨论准备对各自手上的产品做一次优化,确实对比很多产品来看,(支付宝,淘宝,几乎1~2s内完成启动并展现出来)因为我们产品因为历史原因,遗留了很多问题,版本多次的迭代更新需求变更,不断的引入新的库,第三方开源库,和很多无效代码,长时间的积累没有很好的对冗余代码和不再使用的库进行清除,所以导致APP启动速度简直不要太慢!关于这个也查阅很多了资料,借鉴了很多写得很好的文章对未来的优化之路做好铺垫,以下部分内容引用自不同大大的文章分享,并附上原文链接!


1.APP的启动过程

    1.1)解析Info.plist        

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

        1.1.2)沙箱建立、权限检查

    1.2)Mach-O加载

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

        1.2.2)加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)定位内部、外部指针引用,例如字符串、函数等

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

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

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

    1.3)程序执行

        1.3.1)调用main()

        1.3.2)调用UIApplicationMain()

        1.3.3)调用applicationWillFinishLaunching

 2.冷启动/热启动概念

        当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。理想情况下,用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。这种持续存活的情况下启动App,我们称为热启动,相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。我们这里只讨论App冷启动的情况。

    对于冷启动来说,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。我们进行优化的时候,我们将启动时间分为 pre-main() 时间和 main() 函数到第一个界面渲染完成时间这两个部分。因为 APP 的入口在 main() 函数 ,在  main() 函数之后我们的代码才会执行。为什么这么划分呢?大家都知道 APP 的入口是  main() 函数,在 main() 之前,我们自己的代码是不会执行的。而进入到 main() 函数以后,我们的代码都是从

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;

开始执行的,所以很明显,优化这两部分的思路是不一样的。为了方便起见,我们将 pre- main() 时间成为 t1时间,而将 main() 函数到第一个界面渲染完成这段时间称为 t2时间

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

t1 =pre-main() 系统dylib(动态链接库)和自身App可执行文件的加载。

t2 = main() 方法执行之后到AppDelegate类中的- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

pre-main() (main() 函数之前)加载过程以及时间测量

    App开始启动后, 系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接库dyld(dynamic loade)是苹果的动态链接器是一个专门用来加载动态链接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

       在不越狱的情况下,以往很难精确的测量在 main() 函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。小型App确实不需要太过关注这部分。但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),这部分耗时将会变得突出。所幸,苹果已经在Xcode中加入这部分的支持,下面两种方法测量这段时间。

Time Profile(方法一)

启动Time Profile:Xcode ——> Product ——> Profile ——> Time Profile

使用Time Profiler调试程序,能获取到整个应用程序运行中所消耗的时间分布和百分比

作者:然哥哥  原文链接

苹果提供的方法(方法二)

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

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

   main() 函数之前总共使用了615ms,因为我在测试+load方法里面写了个耗时操作,报告显示这部分的耗时操作占了35%。

如何解读

main()函数之前总共使用了615.20ms,在615.20ms中

    dylib loading :加载动态库用了71.68ms。

    rebase/binging :指针重定位使用了254.99ms。

    ObjC setup :ObjC类初始化使用了49.79ms。

    initializer: 各种初始化使用了238.58ms,在初始化耗费的18.50ms中。

    耗时最多的三个初始化是TestYuxingjin、libMainThreadChecker、libSystem.B.dylib以及。

main()函数之后 加载过程及时间测量

    从 main() 函数开始至 applicationWillFinishLaunching 结束,我们统一称为 main()  函数之后的部分。(执行main()函数,执行applicationWillFinishLaunching,rootViewController及其childViewController的加载、view及其subviews的加载)。

    时间测量的使用工具来自NewPan大大的打点计时器BLStopwatch

3. 影响启动性能的因素

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

    3.1)main() 函数之前耗时的影响因素及优化方案:

        3.1.1)减少不必要的framework,动态库加载越多,启动越慢,因为动态链接比较耗时,如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库方法。 实验证明,在ObjC类的数目一样多的情况下,需要加载的动态库越多,App启动就越慢。同样的,在动态库一样多的情况下,ObjC的类越多,App的启动也越慢。需要加载的动态库从1个上升到10个的时候,用户几乎感知不到任何分别,但从10个上升到100个的时候就会变得十分明显。同理,100个类和1000个类,可能也很难查察觉得出,但1000个类和10000个类的分别就开始明显起来。

        3.1.2)ObjC类越多,启动越慢,合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode(获取激活码破解方法)代码检查功能。

        3.1.3)尽量不要写__attribute__((constructor))的C函数,C的constructor函数越多,启动越慢,

        3.1.4)C++静态对象越多,启动越慢,尽量不要用C++虚函数(创建虚函数表有开销)

        3.1.5)ObjC的+load越多,启动越慢(当类被引用进项目的时候就会执行load函数,在 main() 函数开始执行之前与这个类是否被用到无关)

        3.1.6)检查 framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查

        3.1.7)将不必须在+load方法中做的事情延迟到+initialize中(initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用)

        3.1.8)在可接受的范围内压缩图片的大小,压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法TinyPNG,或者工具ImageOptim

    3.2)main() 函数之后耗时的影响因素及优化方案

        3.2.1)执行main()函数的耗时

        3.2.2)执行didFinishLaunchingWithOptions的耗时,我们平常在这里做的    1.初始化第三方SDK ,2.配置 APP 运行需要的环境,3.自己的一些工具类的初始化。这里的优化空间相对来说挺大的(尽量根据业务需求, 将多个二方/三方库延迟加载,减少启动初始化的流程,能懒加载的就懒加载,能放后台初始化的就放后台,能够延时初始化的就延时,不要卡主线程的启动时间)

        3.2.3)rootViewController及其childViewController的加载、view及其subviews的加载,所以对于第一个页面渲染的优化思路就是数据缓存,先展示本地数据,然后在 viewDidAppear方法里进行数据加载解析渲染等一系列操作,这样一来,用户已经看到界面了,就不会觉得是启动慢,这个时候的等待就变成等待数据请求了,这样就把这部分时间转嫁出去了。

        3.2.4)applicationWillFinishLaunching的耗时,将不需要马上在这里执行的代码延后执行

        3.2.5)使用纯代码而不是xib或者storyboard来进行UI框架的搭建,尤其是主UI框架比如TabBarController这种,尽量避免使用xib和storyboard,因为xib和storyboard也还是要解析成代码来渲染页面,多了一些步骤。

        3.2.6)NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这里推荐一个腾讯内部开源库MMKV(MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。从 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和稳定性经过了时间的验证。效率和速度见原文链接:凌国 WeMobileDev)。

        3.2.7)每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log。

        3.2.8)梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求。

总结

1.APP架构的时候加载首页或者Tabbar的时候尽量不要使用Xib,Storyboard。

2.对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。

3.用dispatchonce()代替所有的__attribute__((constructor))函数、C++静态对象初始化、ObjC的+load,对实现了+load()方法的类进行分析,尽量将load里的代码延后调用,在initialize里面调用。

4.对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。尽量将不需要的耗时操作延迟到首屏展示之后在viewDidAppear中执行。

5.利用DYLD_PRINT_STATISTICS分析 main() 函数之前的耗时,重新梳理架构,减少动态库、ObjC类的数目,减少Category的数目,由于Category的实现原理,和ObjC的动态绑定有很强的关系,所以实际上类的扩展是比较占用启动时间的。尽量合并一些扩展,会对启动有一定的优化作用。不过个人认为也不能因为它占用启动时间而去逃避使用扩展,这里只是强调要合并一些在工程、架构上没有太大意义的扩展。

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

7.图片在可接受范围内进行一次压缩。

8.异步操作并不影响指标,但有可能影响交互体验(比如黑屏问题)多线程跑初始化任务的时候,可能主线程会有空闲等待子线程的阶段,而主线程一旦空闲,iOS系统的启动画面就会消失,如果此时APP尚未初始化完全,则可能会黑屏。为了避免黑屏,我们需要一个假的启动画面,在刚开始跑初始化任务时,就生成这个启动画面,启动过程完全结束后再去掉。或者当一个状态机里的主线程跑完时检查下是否所有线程都执行完任务了,如果没有则去生成这个假的初始化页面,避免黑屏。

9.数据持久化,在加载第一个页面的时间用本地数据快速展示首页,以给用户认为快速启动APP的效果。

注意

    优化应该在项目完成稳定之后进行,避免过早优化。结合项目自身情况制定优化方案。

参考资源

iOS App 启动性能优化

阿里数据iOS端启动速度优化的一些经验

优化APP的启动时间实践iOS

[贝聊科技]一次立竿见影的启动时间优化 中提到的管理器类下一步将会实现这种思想

今日头条iOS客户端启动速度优化

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,506评论 2 59
  • 背景 一个项目做的时间长了,启动流程往往容易杂乱,库也用的越来越多,APP的启动时间也会慢慢变长。本次将针对iOS...
    酱油瓶2阅读 3,421评论 0 12
  • 我有一个很棒很棒的爸爸。 我的爸爸长的一米七,一张经典的国字脸。一双小心的眼睛,总是闪烁着 狡黠的眼...
    小学生哥哥阅读 225评论 2 2
  • 秋已浓阅读 85评论 0 0