Android性能优化之启动加速35%

一、前言

随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我将开启一个Android应用性能优化的专题,从理论到实战,从入门到深挖,手把手将性能优化实践到项目中,欢迎持续关注!

那么第一篇文章我就从应用的启动优化开始,根据实际案例,打造闪电般的App启动速度。

二、初识启动加速

来看一下Google官方文档《Launch-Time Performance》 对应用启动优化的概述;

应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!
应用在冷启动之前,要执行三个任务:

  • 加载启动App;

  • App启动之后立即展示出一个空白的Window;

  • 创建App的进程;

而这三个任务执行完毕之后会马上执行以下任务:

  • 创建App对象;

  • 启动Main Thread;

  • 创建启动的Activity对象;

  • 加载View;

  • 布置屏幕;

  • 进行第一次绘制;

而一旦App进程完成了第一次绘制,系统进程就会用Main Activity替换已经展示的Background Window,此时用户就可以使用App了。

Picture

作为普通应用,App进程的创建等环节我们是无法主动控制的,可以优化的也就是Application、Activity创建以及回调等过程

同样,Google也给出了启动加速的方向

  • 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;

  • 避免在启动时做密集沉重的初始化(Heavy app initialization);

  • 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。

备注:方向1属于治标不治本,只是表面上快;方向2、3可以真实的加快启动速度。
接下来我们就在项目中实际应用。

三、启动加速之主题切换

按照官方文档的说明:使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable。
Layout XML file:

Picture

Manifest file:

Picture
Picture

这样在启动的时候,会先展示一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而产生一种快的感觉。不过如上文总结这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。
**备注:截图同样来自官方文档《Launch-Time Performance》 **。

四、启动加速之Avoid Heavy App Initialization

通过代码分析我们可以得到App启动的业务工作流程图:

Picture

这一章节我们重点关注初始化的部分:在Application以及首屏Activity中我们主要做了:

  • **MultiDex以及Tinker的初始化,最先执行;关于MultiDex的优化本文不再赘述,参考我之前

  • Application中主要做了各种三方组件的初始化;

项目中除听云之外其余所有三方组件都抢占先机,在Application主线程初始化。这样的初始化方式肯定是过重的:

  • 考虑异步初始化三方组件,不阻塞主线程;

  • 延迟部分三方组件的初始化;实际上我们粗粒度的把所有三方组件都放到异步任务里,可能会出现WorkThread中尚未初始化完毕但MainThread中已经使用的错误,因此这种情况建议延迟到使用前再去初始化;

  • 而如何开启WorkThread同样也有讲究,这个话题在下文详谈。

项目修改:

  • 将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;

  • 延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader
    因为调用关系不能异步以及过久延迟,初始化从Application延迟到SplashActivity;而EventBus因为再Activity中使用所以必须在Application中初始化。

Picture

注意:闪屏页的2秒停留可以利用,把耗时操作延迟到这个时间间隔里。

五、启动加速之Diagnosing The Problem

本节我们实际定位耗时的操作,在开发阶段我们一般使用BlockCanary或者ANRWatchDog找耗时操作,简单明了,但是无法得到每一个方法的执行时间以及更详细的对比信息。我们可以通过Method Tracing或者DDMS来获得更全面详细的信息。
启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。

Picture
Picture

左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。注意两列:Real Time/Call(实际发生时间),Calls+RecurCalls/Total(发生次数);
上图我们可以得到以下信息:

  • 可以直观看到MainThread的时间轴很长,说明大多数任务都是在MainThread中执行;

  • 通过Real Time/Call 降序排列可以看到程序中的部分代码确实非常耗时;

  • 在下一页可以看出来部分三方SDK也比较耗时;

即便是耗时操作,但是只要正确发生在WorkThread就没问题。因此我们需要确认这些方法执行的线程以及发生的时机。这些操作如果发生在主线程,可能不构成ANR的发生条件,但是卡顿是再算难免的!结合上章节图App冷启动业务工作流程图中业务操作以及分析图,再次查看代码我们可以看到:部分耗时操作例如IO读取等确实发生在主线程。事实上在traceview里点击执行函数的名称不仅可以跟踪到父类及子类的方法耗时,也可以在方法执行时间轴中看到具体在哪个线程以及耗时的界面闪动。

分析到部分耗时操作发生在主线程,那我们把耗时操作都改到子线程是不是就万事大吉了?非也!!

  • 卡顿不能都靠异步来解决,错误的使用工程线程不仅不能改善卡顿,反而可能加剧卡顿。是否需要开启工作线程需要根据具体的性能瓶颈根源具体分析,对症下药,不可一概而论;

  • 而如何开启线程同样也有学问:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService等都各有利弊;例如通常情况下ThreadPoolExecutor比Thread更加高效、优势明显,但是特定场景下单个时间点的表现Thread会比ThreadPoolExecutor好:同样的创建对象,ThreadPoolExecutor的开销明显比Thread大;

  • 正确的开启线程也不能包治百病,例如执行网络请求会创建线程池,而在Application中正确的创建线程池势必也会降低启动速度;因此延迟操作也必不可少。

通过对traceview的详细跟踪以及代码的详细比对,我发现卡顿发生在

  • 部分数据库及IO的操作发生在首屏Activity主线程;

  • Application中创建了线程池;

  • 首屏Activity网络请求密集;

  • 工作线程使用未设置优先级;

  • 信息未缓存,重复获取同样信息;

  • 流程问题:例如闪屏图每次下载,当次使用;

以及其它细节问题:

  • 执行无用老代码;

  • 执行开发阶段使用的代码;

  • 执行重复逻辑;

  • 调用三方SDK里或者Demo里的多余代码;
    项目修改:
    1. 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程最多能获取到10%的时间片,优先保证主线程执行。

2. 流程梳理,延后执行;
实际上,这一步对项目启动加速最有效果。通过流程梳理发现部分流程调用时机偏早、失误等,例如:

  • 更新等操作无需在首屏尚未展示就调用,造成资源竞争;

  • 调用了IOS为了规避审核而做的开关,造成网络请求密集;

  • 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争,在上图traceview功能说明图中最后一行可以看到编号12执行5次,耗时排名前列;此处线程池的创建是必要但可以延后的。

  • 修改广告闪屏逻辑为下次生效。

3.其它优化;

  • 去掉无用但被执行的老代码;

  • 去掉开发阶段使用但线上被执行的代码;

  • 去掉重复逻辑执行代码;

  • 去掉调用三方SDK里或者Demo里的多余代码;

  • 信息缓存,常用信息只在第一次获取,之后从缓存中取;

  • 项目是多进程架构,只在主进程执行Application的onCreate();

Picture

通过以上三步及三方组件的优化:Application以及首屏Activity回调期间主线程就没有耗时、争抢资源等情况了。此外还涉及布局优化、内存优化等部分技术,因对于应用冷启动一般不是瓶颈点,这里不展开详谈,可根据实际项目实际处理。

六、对比效果:

通过ADB命令统计应用的启动时间:adb shell am start -W 首屏Activity。
同等条件下使用MX3及Nexus6P,启动5次,比较优化前与优化后的启动时间;

优化前:
MX3

ThisTime TotalTime WaitTime
1237 2205 2214
1280 2181 2189
1622 2508 2513
1485 2434 2443
1442 2418 2429

Nexus6P

ThisTime TotalTime WaitTime
1229 1832 1868
1268 1849 1880
1184 1780 1812
1262 1845 1876
1164 1766 1807

优化后:
MX3

ThisTime TotalTime WaitTime
865 1516 1523
911 1565 1573
812 1406 1418
962 1564 1574
925 1566 1577

Nexus6P

ThisTime TotalTime WaitTime
603 1192 1243
614 1076 1115
650 1120 1163
642 1107 1139
624 1084 1124

对比:
MX3提升35%

对比 ThisTime平均数 TotalTime平均数 WaitTime平均数
优化前 1413 2349 2357
优化后 895 1523 1533

Nexus6P提升39%

对比 ThisTime平均数 TotalTime平均数 WaitTime平均数
优化前 1221 1814 1848
优化后 626 1115 1156
  • 命令含义:
    ThisTime:最后一个启动的Activity的启动耗时;

TotalTime:自己的所有Activity的启动耗时;
WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)。

七、问题:

1、还可以继续优化的方向?

  • 项目里使用Retrofit网络请求库,FastConverterFactory做Json解析器,TraceView中看到FastConverterFactory在创建过程中也比较耗时,考虑将其换为GsonConverterFactory。但是因为类的继承关系短时间内无法直接替换,作为优化点暂时遗留;

  • 可以考虑根据实际情况将启动时部分接口合并为一,减少网络请求次数,降低频率;

  • 相同功能的组件只保留一个,例如:友盟、GrowingIO、自有统计等功能重复;

  • 使用ReDex 进行优化;实验Redex发现Apk体积确实是小了一点,但是启动速度没有变化,或许需要继续研究。

2、异步、延迟初始化及操作的依据?
注意一点:并不是每一个组件的初始化以及操作都可以异步或延迟;是否可以取决组件的调用关系以及自己项目具体业务的需要。保证一个准则:可以异步的都异步,不可以异步的尽量延迟。让应用先启动,再操作。

3、通用应用启动加速套路?

  • 利用主题快速显示界面;

  • 异步初始化组件;

  • 梳理业务逻辑,延迟初始化组件、操作;

  • 正确使用线程;

  • 去掉无用代码、重复逻辑等。

4、其它

  • 将启动速度加快了35%不代表之前的代码都是问题,从业务角度上将,代码并没有错误,实现了业务需求。但是在启动时这个注重速度的阶段,忽略的细节就会导致性能的瓶颈。

  • 开发过程中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。

    360°Android app全方位性能调优.png

大纲与资料.png
高清视频资料.jpg

+qq群:853967238。获取以上高清技术思维图,以及相关技术的免费视频学习资料

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

推荐阅读更多精彩内容

  • 转载自一触即发 App启动优化最佳实践 一触即发 App启动优化最佳实践 文中的很多图都是Google性能优化指南...
    CP9阅读 601评论 0 3
  • 应用启动类型 冷启动场景:开机后第一次启动应用 或者 应用被杀死后再次启动生命周期:Process.start->...
    please边去阅读 2,180评论 0 2
  • 青的故事阅读 358评论 0 0
  • 最近一段时间,我在朋友圈很不活跃,而且就算发圈,也是比较负面情绪。 非常感谢默默关心,关注和支持我的朋友们,很感激...
    是晶晶啊阅读 271评论 0 4
  • 1.什么是runloop? RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。 事件循环 没...
    细雨菲菲v阅读 372评论 0 1