启动优化·理论篇·浅析Android启动优化

前言:生活不止眼前的苟且,还有诗和远方。

前言

用户如果想打开一个应用,就一定要经过启动 这个步骤。APP启动时间的长短,不只是用户体验的问题,对于淘宝、京东等大型APP来说,会直接影响用户的留存和转化 等核心数据。对研发人员来说,启动速度是我们的门面,它清清楚楚可以被所有人看到,我们都希望自己应用的启动速度可以秒杀所有竞争对手。

启动是指用户从点击 icon 到看到页面首帧的整个过程 ,启动优化的目标就是减少 这一过程的耗时。启动性能是 APP 使用体验的门面,启动过程耗时较长很可能导致用户使用 APP 的兴趣骤减。提高启动速度是每一个 APP 在体验优化方向上必须要做的关键技术突破。

为了更完善系统地研究 App 启动耗时启动优化,启动优化相关知识将分为三部分来讲解,本文是第一篇:

启动优化·理论篇·浅析Android启动优化

启动优化·工具篇·启动耗时统计的六种方式(未完成)

启动优化·实战篇·启动优化落地方案(未完成)

启动优化大纲

在真正动手开始优化之前,我们需要知道一些优化应用启动时间的信息,包括启动流程内部机制的分析、优化方向与指标是什么、启动耗时方法的数据统计与分析、如何分析启动性能、启动优化的实际场景与方案、以及一些常见的启动时间问题和有关如何解决这些问题的提示。

一、启动流程的内部机制

先搞清楚从用户点击应用图标开始,整个启动过程经过哪几个关键阶段,那么启动过程究竟会出现哪些问题,又会给用户带来哪些体验问题。

1.1 进程创建流程分析

应用进程是如何被创建的?每个 App 在启动前必须先创建一个进程,该进程是由 zygote 进程 fork 出来 ,进程具有独立的资源空间,用于承载 App 上运行的各种 Activity/Service 等组件。大多数情况一个 App 就运行在一个进程中,除非在 AndroidManifest.xml 中配置 Android:process 属性,或通过 native 代码 fork 进程。

先看看进程创建流程:

  • system_server 进程:是用于管理整个 Java framework 层,包含 ActivityManager,PowerManager 等各种系统服务;

  • Zygote 进程:是 Android 系统的首个 Java 进程,Zygote 是所有 Java 进程的父进程,包括 system_server 进程以及所有的 App 进程都是 Zygote 的子进程。

这个进程的创建主要为以下四个步骤:

  1. 当点击 App 图标启动应用时或者在应用内启动一个带有 process 标签的 Activity 时,都会触发创建新进程的请求,这种请求会先通过 Binder 发送给 system_server 进程,也即是发送给 ActivityManagerService 进行处理。
  2. system_server 进程会调用 Process.start() 方法,会先收集 uid、gid 等参数,然后通过 Socket 方式发送给 Zygote 进程,请求创建新进程。
  3. Zygote 进程接收到创建新进程的请求后,调用 ZygoteInit.main() 方法进行 runSelectLoop() 循环体内,当有客户端连接时执行 ZygoteConnection.runOnce() 方法,再经过层层调用后 fork 出新的应用进程。
  4. 新创建的进程会调用 handleChildProc() 方法,最后调用 ActivityThread.main() 方法。

1.2 应用启动流程分析

应用的启动流程主要分为三步:启动主线程,创建 Application,创建 MainActivity

进程创建后,就进入应用创建启动流程。在 attachApplication 请求后将进程信息告知 AMS,system_server 在收到请求准备好一系列工作后,再通过 binder 向应用进程发送 scheduleLaunchActivity 请求,此时 binder 线程(ApplicationThread)在收到请求后,handler 向主线程发送消息执行 ActivityThread#main() 方法。

ActivityThread 就相当于我们的主线程,是应用程序的入口 。在 main() 方法里对 ActivityThread、主线程 Handler 进行初始化,然后 looper 开启消息轮询。执行到 bindApplication() 方法,开始创建 Application 并初始化,这里使用反射去创建,调用 Application 相关的生命周期。

最后,通过主线程 Handler,回到主线程中执行 Activity 的创建和启动,然后执行 Activity 的相关生命周期函数。在 Activity LifeCycle 结束之后,就会执行到 ViewRootImpl,这时才会进行真正的页面的绘制

这是进程创建和启动的整个流程。

二、应用启动状态

应用有三种启动状态:冷启动、温启动和热启动。每种状态都会影响应用向用户显示所需的时间。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台。启动优化一般是在冷启动的基础上进行优化,这样做也可以提升温启动和热启动的性能。

2.1 冷启动

冷启动是指应用从头开始启动,也就是用户点击桌面 Icon 到应用创建完成的过程 。所以系统进程是在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。常见的场景是 APP 首次启动或 APP 被完全杀死后重新启动。这种启动需要的时间最长的,因为系统和应用要做的工作比温启动和热启动状态下更多。冷启动具有耗时最多,是衡量启动耗时的标准

在冷启动开始时,系统有以下三项任务:

  1. 加载并启动应用;
  2. 在启动后立即显示应用的空白启动窗口;
  3. 创建应用进程。

然后就是创建应用进程,应用进程就负责后续阶段:

  1. 创建应用对象,并走 Application 相关生命周期;
  2. 启动主线程,Loop 消息循环;
  3. 创建主 Activity,执行 Activity 的相关生命周期;
  4. 解析视图,创建屏幕布局,执行初步绘制。

当应用进程完成第一次绘制时,系统进程就会换掉显示的后台窗口,将其替换为主 Activity。此时,用户可以开始使用应用。系统进程和应用进程之间如何交接工作如图:

2.2 温启动

温启动只是冷启动操作的一部分 。当启动应用时,后台已有该应用的进程,但是 Activity 需要重新创建。这样系统会从已有的进程中来启动这个 Activity,这个启动方式叫温启动。它的开销要比热启动高,比冷启动低。温启动常见的场景有两种:

  • 用户在退出应用后又重新启动应用。进程可能还在运行,但应用必须通过调用 onCreate() 重新创建 Activity。
  • 系统因内存不足等原因将应用回收,然后用户又重新启动这个应用。Activity 需要重启,但传递给 onCreate() 的 state bundle 已保存相关数据。

2.3 热启动

在热启动中,系统的工作就是将 Activity 带到前台。只要应用的所有 Activity 仍留在内存中,就不会重复执行进程,应用以及 Activity 的创建,避免重复初始化对象、布局解析和显现。应用的热启动开销比温启动更低,也是最简单的一种。热启动常见的场景: 当我们按了 Home 键或其它情况 App 被切换到后台,再次启动 App 的过程。

但是,如果一些内存为响应内存整理事件(如 onTrimMemory())而被完全清除,则需要为了响应热启动而重新创建相应的对象。热启动显示的屏幕上行为和冷启动场景相同。系统进程显示空白屏幕,直到应用完成 Activity 呈现。

这就是应用三种启动状态的生命周期图。

三、启动优化阶段分析

由于启动流程里面关于进程创建和 APK 资源加载的耗时,在启动阶段是无法干预的 。所以重点是关注应用进程创建完成和 MainActivity 启动两大阶段上,这是我们可以参与操作的阶段。

这里有几个关键点优化方向,创建 Application、启动主线程、创建 MainActivity、渲染 View 布局、加载数据和界面首帧绘制完成 ,整个启动流程到这里就结束了。

3.1 Application阶段分析

  • bindApplication :APP 进程由 zygote 进程 fork 出来后会执行 ActivityThread 的 main 方法,该方法最终触发执行 bindApplication(),这也是 Application 阶段的起点;
  • attachBaseContext:在应用中最早能触达到的生命周期,本阶段也是最早的预加载时机;
  • installProvider:很多三方 sdk 借助该时机来做初始化操作,很可能导致启动耗时的不可控情形,需要按具体 case 优化;
  • onCreate:这里有很多三方库和业务的初始化操作,是通过异步、按需、预加载等手段做优化的主要时机,它也是 Application 阶段的末尾。

在 Application阶段,可以在 attachBaseContext,installProvider 和 app:onCreate 三个时间段进行相关优化。

3.2 Activity LifeCycle阶段分析

来到 Activity 阶段后 会执行 Activity 的相关生命周期方法,首先经历的是 onCreate ,这里涵盖了首屏业务优化的主要场景,也是开启异步并发的主要时机,在其中有个重要的 setContentView() 方法会触发 DecorView 的 install,可尝试对 DecorView 的构建进行预加载,以及所需要使用的对象初始化优化。

3.3 View绘制阶段分析

来到 View 构建的阶段,该阶段也是比较耗时,可采用异步 Inflate 配合 X2C(编译期将 xml 布局转代码)并提升相应异步线程优先级的方法综合优化。

View 的整体渲染阶段,涵盖 measure、layout、draw 三部分,这里可尝试从层级、布局、渲染上取得优化收益。

最后是首屏数据加载阶段,这部分涵盖非常多数据相关的操作,也需要综合性优化,可尝试预加载、三级缓存或网络优先级调度等手段进行优化。

总的来说,Application atttachBaseContext、Appication onCreate 和 Activity LifeCycle,View创建绘制阶段,数据加载阶段,这几个阶段可能出现的性能问题是我们能够干预的,着重在上面进行优化。

四、启动优化起止时间

4.1 开始时间

上面启动阶段分析中,我们在应用中能触达到的 attachBaseContext 阶段,是最早的预加载时机。 可以把这个方法的回调时间当作启动开始时间 ,因为 attachBaseContext() 是应用进程的第一个生命周期。但是准确来说,应用的启动时间包括应用进程的创建,它应该是在冷启动时用户点击应用 Icon 开始计算([启动耗时统计的六种工具(未完成)]可以统计到)。但是结束时间点该如何来统计呢?

4.2 结束时间

在 MainActivity 的 onResum() 方法完成后才开始首帧的绘制。那么 View 是在 onResume() 方法之后才开始测量,布局绘制,这时用户也是不可以交互的。另外 Activity#onWindowFocusChanged() 这个方法的调用时机是用户与 Activity 交互的最佳时间点 ,当 Activity 中的 View 测量绘制完成之后就会回调 view 的 onWindowFocusChanged() 方法,也会回调 Activity 的 onWindowFocusChanged() 方法,可以选择它来当作时间结束的时间点。

但是这种还不够准确,大部分数据是通过请求接口回来之后,才能填充页面才能显示出来,当执行到 onWindowFocusChanged() 的时候,请求数据还没完成,页面上依旧是没有数据的,用户仅仅可以交互写死在 XML 布局当中的视图,更多的内容还是不可见,不可交互的。

所以结束时间点通常选择在列表上面第一个 itemView 的 perDrawCallback() 方法 的回调时机当作时间结束点,也就是首帧时间。当列表上面第一个 itemView 被显示出来的时候说明网络请求已经完成。页面上的 View 已经填充了数据,并且开始重新渲染了。此时用户是可以交互的,这个才是比较有意义的时间节点。

kotlin 复制代码
// itemView添加预绘制回调监听
itemView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
        return false
    }
})

从用户点击应用 icon 到页面生成第一帧所用的时间,就是 App 启动耗时时间。包括冷启动期间的进程初始化、冷启动或温启动期间的 Activity 创建,以及显示首帧。以时间值的形式衡量,也就是指应用与用户进入可交互状态所需的时间。表示包含以下事件序列的总经过时间:

  1. 创建和启动进程;
  2. 创建和启动 Application;
  3. 创建和初始化主 Activity;
  4. 解析布局;
  5. 首次绘制应用。

五、启动优化方向

在能够保证产品稳定,按时完成需求的前提下去做优化。同样的程序在低端配置的设备中,相同的问题暴露的更加明显。应该保持足够多的测量,用数据和事实说话(使用各种耗时检测工具以及快速定位问题)。

在分析了整个启动流程阶段之后,我们可以清楚地了解这段时间内系统、应用各个进程和线程的运行情况,那么我们可以开始真正的启动优化了。具体的优化方式,我把它们分为这里列举各优化策略下可实施的优化方面:

  • 正面优化:删减非必要的启动逻辑、开屏页与首页 Activity 合并、获取进程名从 IPC 转反射方式等;
  • 业务优化:梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载;
  • 延迟优化:主线程消息队列中非启动必要消息延迟执行、启动路径非高优业务逻辑延迟初始化等;
  • 异步优化:异步预加载(ShardPreference、实例化对象)、异步 inflate view、线程收敛,异步启动初始化任务等;
  • 综合优化:资源重排、类重排、类加载优化、启动任务调度框架、页面数据预加载优化等。

所有的耗时均因代码运行时不合理地消耗系统资源产生,而不合理的耗时点正是需要做归因分析之处

启动优化需要耐得住寂寞,把整个流程摸清摸透,一点点把时间抠出来,特别是对于低端机和系统繁忙的场景。当我们足够熟悉底层的知识时,可以利用系统的特性去做更加深层次的优化。

不管怎么说,都需要谨记一点:对于启动优化要警惕 KPI 化,我们要解决的不是一个数字,而是用户真正的体验问题

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢支持和认可,您的点赞 就是我创作的最大动力。山水有相逢,我们下篇文章见!

本人水平有限,文章难免会有错误,请批评指正,不胜感激 !

参考链接

希望我们能成为朋友,在 Github掘金 上一起分享知识,一起共勉!Keep Moving!

相关推荐
大白要努力!2 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟1 小时前
Android音频采集
android·音视频
小白也想学C2 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程2 小时前
初级数据结构——树
android·java·数据结构
乐闻x3 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
闲暇部落4 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
青云交5 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:跨数据中心环境下的挑战与对策(上)(27 / 30)
大数据·性能优化·impala·案例分析·代码示例·跨数据中心·挑战对策
诸神黄昏EX6 小时前
Android 分区相关介绍
android
大白要努力!7 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee7 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip