进阶篇|大厂常用的启动优化有哪些?

前言

之前有和各位同学分享过启动的两篇文章:

第一篇《Android启动这些事儿,你都拎得清吗?》从源码的角度分析了启动流程。

第二篇《进阶应用启动分析,这一篇就够了!》讲了了如何使用工具测量启动流程。

今天我将结合自己的过往工作经验,分享一下常见的启动优化和一些黑科技的实操。

一、准备

在正式讲优化的方法之前,默认各位同学已经掌握了:

  1. 启动的源码分析
  2. 启动时长的监控

因为在实际的分析过程,一定是我们懂得了自己应用的启动阶段的各个耗时点,然后对这些流程分析,最终做出针对性的优化策略。

最简单来讲,我们自己的应用的启动时长怎么定义的,启动的开始点在哪里,结束点在哪里。举个例子,我们App之前定义的两个点:

  1. 开始点:拦截的 ActivityThread 里面的消息机制 Application 创建的点
  2. 结束点:第一个 ActivityonWindowsFocusChanged 方法

看一下 Android 官方的应用启动图:

Android启动时长

我们知道,onWindowsFocusChanged 回调发生在 Activity#onCreate 之后,又在第一帧vSync之前,也就是途中的的 Displayed Time 和 reportFullyDrawn 之间。所以对于我来说,就可以知道我们的应用的优化范围在 Application#onCreate 和闪屏页,后面就可以持续的对这一块儿做优化。

另外一个重点就是关于启动工具的选择。对于启动流程的分析,我强烈建议使用 Android Studio Profiler 工具,使用其中的 TraceSystemCall 功能,优点如下:

  1. 分析各种系统资源:CPU使用情况、显示(Vsync信号、卡顿市场)、一些核心函数的耗时。
  2. 函数插桩:对于想关注的其他方法耗时,可以通过函数插装来实现。

系统资源的分析:

系统资源分析

可以看到,系统资源的展示是比较全面的。

二、常用优化

先聊聊常用的优化策略吧。

1、梳理冗余逻辑

梳理冗余逻辑这个词看着比较简单,实际做起来也是不难,主要是各种业务的权衡与取舍。

闭嘴

有如下两点。

1.1 去除历史包袱

做启动优化的第一步是梳理启动业务流程,如果我们开发的是一个中大型应用,那么其中的很多流程是把握不准的,因此我之前的策略是和同事在周会上review代码,将启动过程的业务一个个的过,标记下不用的业务,然后在后续开发中下线。

对于更大型的团队,如大众点评,遇到相关的不熟的业务,不仅要和团队内沟通,还需要和团队外的其他业务方进行沟通,了解相关的启动业务的使用情况。

1.2 了解业务使用时机

梳理完启动业务的流程以后,我们需要对启动的业务的使用有一定的了解。对于时间偏长的任务,我们去思考一下,这个任务真的有必要在启动中去使用吗?是否可以使用懒加载,这可能需要和相关的业务方进行Argue。

2、启动框架

我想各位同学一定知道,在启动过程中,如果遇到耗时任务,可以根据情况放到异步线程,但是异步线程也会有一些特殊情况:

  • 时机保证:有一些重要的异步线程任务,如何保证在在启动结束后,能够及时使用到对应的功能。
  • 效率保证:如何保证异步线程的数量。
  • 任务时机:有一些任务是有依赖关系的,如何保证任务的执行顺序。

这也是我们使用启动框架原因,利用多核的CPU + 多线程,高效率、并行、有序、按时执行启动任务。

如果要做好一个启动框架,有几个重要的点:

启动框架重点

2.1 基础框架

我之前使用过的启动框架有:android-startup

在这个框架中,启动任务分为三种:

  1. 主线程重要的任务:Application#onCreate结束前主线程执行完成
  2. 子线程重要任务:交给线程池执行,也会在Application#onCreate结束前执行完成
  3. 字线程不重要的任务:交给线程池后台执行

还有一些点处理的比较好:

  1. 任务排序:很好的处理了任务依赖关系,如果发生了循环依赖,可以在任务的拓扑排序阶段,就对外抛出异常
  2. 记录任务耗时:统计好各个任务的时长,记录下来,后期有需要,可以上传到埋点

然后,这个框架有一些点还需要改进:

  1. 主线程不重要任务:我们可能还有主线程不太重要的任务,这个时候可以交给idleHandler执行
  2. 更多的执行时机:这个启动框架主要针对的时机是Application#onCreate,我们可以有更多的时机,比如首页初始化、首页空闲时、次级页面打开时,通过划分更多的时机,可以缓解CPU、线程池和内存的压力,从而降低启动时长。
2.2 动态调整启动任务的执行顺序

通常启动框架去执行启动任务的时候,顺序都是确定的。有时,我们会对外进行广告投放,想给用户一个比较有吸引力的落地页,比如我在腾讯体育看到一个京东的目的商品的广告投放页,像这样:

广告投放

这个时候落地页可能是一个活动页,那这个活动页的技术栈和首页的技术栈大概率是不一样的,那么我们是否可以针对活动页的技术栈调整一下启动任务顺序,从而降低启动时长。

简单来说,这个步骤有如下几步:

  1. 加入标记:在对外投放的Deeplink中,加入相关的标记
  2. 识别标记:在Android或者iOS启动过程中,可以通过Hook的方式或者其他方式,在启动的早期,拿到相关的参数
  3. 改变任务执行优先级:可以在系统中静态注册相关标记下的另外一套任务执行级顺序,或者动态下发也可以

核心的想法就是跟落地页相关的技术栈的任务时机往前挪,不相关的任务往后挪,从而保证启动时长的最低。

3、线程梳理

在启动过程中,如果线程资源不加以限制,线程数量可能就有几百个,这会有什么问题呢?

  1. 资源消耗过高:每个线程都需要一定的系统资源,包括内存、CPU时间等
  2. 上下文切换开销:操作系统需要在多个线程之间进行上下文切换,以便让每个线程都有机会执行。频繁的上下文切换会带来额外的开销,影响应用程序的整体性能

线程的数量可以在性能分析工具中查看。具体的治理策略有:

  1. 避免使用new Thread的方式创建线程
  2. 对于一些可以替换线程池的第三方库,替换成内部使用的线程池
  3. 将第三方SDK中开源库,在核心线程空闲的时候,也能够进行释放

先讲一下第一点,如果项目团队不大,开发的人员都在一个项目中开发,那么我们使用全局搜索就可以定位new Thread的位置,但对于第三方库中的创建却无从定位。那如果是大的项目,每个团队都有自己的开发模块,这种怎么定位呢?

Booster有给我们具体的解决方案,在字节码Transform的时候,将线程的调用方,然后传递给线程,运行的时候给它打印出来。

再简单讲一下第三点吧,可以看Booster框架,它提供了一些思路,也是在Transform的时候,将第三发的线程池的allowCoreThreadTimeOut设置为true,让它可以在空闲的时候能够进行释放,除此以外,还可以:

  • 线程池的corePoolSize设置为0
  • maxPoolSize设置上限

4、闪屏页优化

根据我的经验,闪屏一般有两种:

  1. 有对应业务的闪屏:比如说可以自定义闪屏页、或者承接开屏广告的工作,如起点读书,B站
  2. 纯闪屏:如大众点评,京东类
4.1 纯闪屏页

对于纯闪屏的应用,给人的感觉就是启动速度非常快,因为启动第一个Activity可能就是我们的首页。

这里有一个优化措施就是利用StartWindow机制,简单介绍一下,在Android的启动过程中,在第一个Activity真正显示之前,系统会会提供一个页面来进行过渡,我们称之为StartWindow。StartWindow会在应用的第一个Activity绘制完成以后被移除。

默认情况下根据主题而定,白色或者黑色,我们也可以设置成自定义的颜色或者图片。

具体的优化策略:

  1. 启动的时候,为首个Activity提供一个带闪屏页的主题。
  2. 在进入Activity以后,在onCreate方法中设置透明主题。

通过在onCreate中设置透明主题,我们可以减少绘制一层背景,通常我们在首页中,也不需要带背景。

4.2 携带业务的闪屏页

对于承接业务的闪屏页,这类应用启动的第一个页面一般就不是首页,它就是一个单独的闪屏页Activity,里面会有一些广告处理的逻辑。

这里也有一些具体的优化措施,除了上述的StartWindow机制以外,还有:

  1. 将xml布局的方式改成动态的创建View
  2. 预加载闪屏页的背景图片

一般这类的闪屏页的元素也比较简单,将xml布局改成动态创建View,可以减少读取xml文件和反射创建View的时间,在中低端的效果还是比较明显的。

5、系统资源处理

系统资源指的是锁竞争、IO治理、CPU治理,通过监控这些数据,然后分析一下其中的不合理之处,这个其实是一个细活,需要通过Perfetto和Profiler工具查看。

6、Baseline Profile

早期的Android虚拟机采用的是Dalvik,为了提高Java执行的效率,在虚拟机中采用JIT(Just in time)技术,在运行的时候将高频的方法编译成机器码,但是JIT编译的机器码是存在内存中的,下次冷启动,这些数据会丢失,对于类似服务端长期运行的Java应用来讲,提效明显。对于Android应用来讲,应用可能需要经常重启,显然就不是那么友好了。

AOT(Ahead of time)是一种预编译机制,可以将Apk中的字节码编译成二进制的机器码,减少运行时间。Android 7.0以前,在安装的时候,会将全部的字节码编译成机器码,但这会有两个问题:

  1. 安装时间长
  2. 安装包体积大

Android 7.0以后支持JIT和AOT并存的编译模式,其中AOT中有两种编译策略值得关注:

  1. quicken:应用安装时的编译模式,相对编译速度较快,占用空间合理
  2. speed-profile:系统后台触发的编译模式,按照用户的习惯进行特定的优化

所谓的Baseline Profile,指的是提前扫描我们的热点代码,生成配置文件,然后在安装的时候,对这些热点代码做AOT,可以看一下谷歌官方给的流程图:

baselineprofile_workflow

这个是借助Google Play实现的,所以针对国内的应用,可行吗?

根据网易云音乐得出来的结果,AOT其实有两种场景:

  1. 安装时AOT:在安装或者更新过程中,提前对这些热点代码aot,从而降低我们的启动时长,国内厂商对这一块儿支持的比较少,仅少数厂商支持。
  2. 还有一种场景就是启动后对Profile文件进行aot,流程如下图:

流程

从网易云优化的结果来看,第一种方案提升明显,可以降低30%的启动时长(应该是安装或者更新后的首次启动时长),第二种仅有5%。

三、黑科技

我们再来聊聊黑科技,其中的一些策略需要投入比较长的时间,一个人还是比较难搞定的。

1、Apk资源重排

1.1 背景

Android底层运行着Linux系统,当App启动时,需要通过Linux系统从磁盘中加载很多文件到内存中,比如代码、资源文件(Manifest文件、布局、图片),把这些文件加载到内存中。

Linux加载文件有两种方式:

  1. 普通文件读取
  2. 内存映射

两种文件加载方式都会把文件内容加载到pagecache中,如果读取文件已经在pagecache中,就不会发生真正的磁盘IO,而是直接从pagecache中读取,这就大大提升读的速度。

流程如下:

pagecache流程

1.2 内部优化策略

为了提升磁盘读取效率,Linux采取了预读机制。简单来说:

  1. 单个文件的第一次读取,系统读入所请求页面的后面几个页面作为缓存。
  2. 如果下次要读取的页面不在缓存中,则表明此次的文件访问不是顺序访问,系统会采用之前的同步预读方式。
  3. 如果读的页面命中,系统会把之前预读的页面扩大一倍,但是这个过程时异步的。

如果我们启动过程中,读取的apk文件按实际加载顺序排列,就能充分的利用Linux预读机制,减少启动过程中的磁盘IO,从而降低启动时间。

1.3 技术策略

那么我们能做的就是统计这些资源文件的命中率,代码文件其实受AOT影响,拿到启动资源文件的顺序以后,重新打包,中间涉及的流程还是挺复杂的。

可以参考:《支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能》

2、dex2aot触发

dex2aot指的就是我们在Baseline Profile方案中说的aot,aot的时机有很多种,常见的有:

  1. 安装或者更新的时候触发
  2. 应用空闲的时候,处在后台触发aot
  3. 系统空闲

这些其实是系统帮我们触发的,并且也具有不确定性,我们是否可以让应用处在后台的时候主动触发这些流程吗?

答案是肯定的,查看源码的时候发现,aot的流程都是由PackageManagerService触发的,其中的函数pefromdexOpt可以通过一些手段被我们主动触发。

可以参考:《Android ART dex2oat 浅析》

3、启动阶段抑制GC

在启动过程中,我们希望合理的使用CPU,避免启动过程中CPU被一些任务长时间的占用。下图是通过使用字节的Btrace结合Perfetto分析得出来启动流程中的HeapTaskDaemon执行情况:

ME1728267175297.png

我们可以发现了HeapTaskDaemon线程占用了比较高CPU时间片,这个线程实际上是虚拟机执行GC操作的。

简单介绍一下,HeapTaskDaemon是一个守护线程,随着Zygote线程一起启动,HeapTaskDaemon做的就是无限从执行GC的HeapTask集合里面取任务执行,对于需要延时的任务,会阻塞到目标执行。

那么我们可以通过获取系统的HeapTask,并让这个HeapTask休眠,同样能达到抑制HeapTaskDaemon线程执行的目的。这个过程比较复杂,可以参考:

《速度优化:GC抑制》

需要指出的是,在 Android 8.0 以后,在应用启动的时候,会默认执行TriggerPostForkCCGcTask,该任务可以将GC延后2秒执行,所以我们看到,上面的HeapTaskDaemon并不是一开始就执行的。所以我们需要分析一下,在启动还没完成的场景下,就GC的场景是不是很多。

4、保活

现在大部分包活策略,都不太行的通了,但是之前看过某个开源项目,安装以后,即使用户手动点击强行停止,该软件也能重新启动。

开源项目:

AndroidKeepAlive:github.com/fgkeepalive…

TechMerger里面的一篇文章也有介绍,原因如下:

保活

地址:mp.weixin.qq.com/s/E038lXvQw…

里面的作者反编译后得出的结论是,文中的流氓软件主要做了:

  1. 被杀后重启:通过高优先级的native进程进行监听
  2. 通过各种手段提高进程优先级

而其中提高进程的优先级有:

  1. UI进程与Service进程分离
  2. 使用MediaPlayer播放无声音乐
  3. 使用AccountManager备份数据
  4. 注册无障碍服务
  5. 注册设备管理器

可以看到,流氓软件还是做了很多东西,有兴趣的读者可以看一下原文。

总结

本文中涉及到的很多内容都没有深入讲解,只是提供了一个思路和一些策略,希望做一个抛砖引玉。如果你有更好的想法,欢迎评论区留言。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21235,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?