在讨论 Android 性能问题的时候,卡顿、响应速度、ANR 这三个性能相关的知识点通常会放到一起来讲,因为引起卡顿、响应慢、ANR 的原因类似,只不过根据重要程度,被人为分成了卡顿、响应慢、ANR 三种,所以我们可以定义广义上的卡顿,包含了卡顿、响应慢和 ANR 三种,所以如果用户反馈说手机卡顿或者 App 卡顿,大部分情况下都是广义上的卡顿,需要搞清楚,到底出现了哪一种问题。
如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢 ,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲。
另外在 App 或者厂商内部,卡顿、响应速度、ANR 这几个性能指标都是有单独的标准的,比如 掉帧率、启动速度、ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了。
性能是充满挑战的
系统性能工程是一个充满挑战的领域,具体原因有很多,其中包括以下事实,系统性能是主观的、复杂的,而且常常是多问题并存的
性能是主观的
技术学科往往是客观的,太多的业界人士审视问题非黑即白。在进行软件故障查找的时候,判断 bug 是否存在或 bug 是否修复就是这样。bug 的出现总是伴随着错误信息,错误信息通常容易解读,进而你就明白错误为什么会出现了
与此不同,性能常常是主观性的。开始着手性能问题的时候,对问题是否存在的判断都有可能是模糊的,在问题被修复的时候也同样,被一个用户认为是“不好”的性能,另一个用户可能认为是“好”的
系统是复杂的
除了主观性之外,性能工程作为一门充满了挑战的学科,除了因为系统的复杂性,还因为对于性能,我们常常缺少一个明确的分析起点。有时我们只是从猜测开始,比如,责怪网络,而性能分析必须对这是不是一个正确的方向做出判断
性能问题可能出在子系统之间复杂的互联上,即便这些子系统隔离时表现得都很好。也可能由于连锁故障(cascading failure)出现性能问题,这指的是一个出现故障的组件会导致其他组件产生性能问题。要理解这些产生的问题,你必须理清组件之间的关系,还要了解它们是怎样协作的
瓶颈往往是复杂的,还会以意想不到的方式互相联系。修复了一个问题可能只是把瓶颈推向了系统里的其他地方,导致系统的整体性能并没有得到期望的提升。
除了系统的复杂性之外,生产环境负载的复杂特性也可能会导致性能问题。在实验室环境很难重现这类情况,或者只能间歇式地重现
解决复杂的性能问题常常需要全局性的方法。整个系统——包括自身内部和外部的交互——都可能需要被调查研究。这项工作要求有非常广泛的技能,一般不太可能集中在一人身上,这促使性能工程成为一门多变的并且充满智力挑战的工作
可能有多个问题并存
找到一个性能问题点往往并不是问题本身,在复杂的软件中通常会有多个问题
性能分析的又一个难点:真正的任务不是寻找问题,而是辨别问题或者说是辨别哪些问题是最重要的
要做到这一点,性能分析必须量化(quantify)问题的重要程度。某些性能问题可能并不适用于你的工作负载或者只在非常小的程度上适用。理想情况下,你不仅要量化问题,还要估计每个问题修复后能带来的增速。当管理层审查工程或运维资源的开销缘由时,这类信息尤其有用。
有一个指标非常适合用来量化性能,那就是 延时(latency)
响应速度概述
响应速度是应用App性能的重要指标之一。响应慢通常表现为点击效果延迟,操作等待或白屏时间长等,主要场景包括:
-
应用启动场景,包括冷启动、热启动、温启动等。
-
界面跳转场景,包括应用内页面跳转、App之间跳转。
-
其他非跳转的点击场景(开关、弹窗、长按、控件选择、单击、双击等)。
-
亮灭屏、开关机、解锁、人脸识别、拍照、视频加载等场景。
从原理上来说,响应速度场景往往是由一个 input 事件(以 Message 的形式给到需要处理的应用主线程)触发(比如点击、长按、电源键、指纹等),由一个或者多个 Message 的执行结束为结尾,而这些 Message 中一般都有关键的界面绘制相关的 Message 。衡量一个场景的响应速度,我们通常从事件触发开始计时,到应用处理完成计时结束,这一段时间就称为响应时间。
如下图所示,响应速度的问题,通常就是这些Message的某个执行超过预期(主观),导致最终完成的时间长于用户期待的时间。
由于响应速度是一个比较主观的性能指标(而流畅度就是一个很精确的指标,掉一帧就是掉一帧),而且根据角色的不同,对这个性能指标的判定也不同,比如 Android 系统开发者和应用开发者以及测试同学,对 应用冷启动 的起点和终点就有不同的判定:
-
系统开发者 往往从 input 中断开始看,部分以应用第一帧为结束点(因为比较好计算),部分以应用加载完成为结束点(比较主观,除非结束点比较容易通过工具去判断),主要是以优化应用的整体性能为主,涉及到的方面就比较广,包括 input 事件传递、SystemServer、SurfaceFlinger、Kernel 、Launcher 等。
-
App 开发者 一般从 Application 的 onCreate 或者 attachContext 开始看,大部分以界面完全加载或者用户可操作为结束点,因为是自己的应用,结束点在代码里面可以主动加,主要还是以优化应用自身的启动速度为主,市面上讲启动速度优化的,大部分是讲这部分。
-
测试同学 则更多从用户的真实体验角度来看,以桌面点击应用图标且应用图标变色为第一帧,内容完全加载为结束点。测试过程一般使用 高速相机 + 自动化,通过机械手和图形识别技术,可以自动进行响应速度测试并抓取相关的测试数据。
响应速度问题分析思路
分清起点和终点
分析响应速度,最重要的是要找到起点和终点。不同角色的开发者,对这个性能指标的判定起点和终点都不一样;而且这个指标有很主观的成分,所以在开始的时候,就要跟各方来确定好起点和终点,具体的数值标准,下面一些手段可以帮助大家来确定:
-
竞品分析。一般来说,响应速度这个指标都会有一个对标的竞品,竞品手机或者竞品 App,相同的条件下,竞品手机或者竞品 App 从点击到响应花费了多少时间,可以作为一个标准。
-
对比前一个版本。有时候系统进行大版本升级或者 App 进行版本迭代,那么上一个版本的数据就可以拿来作为标准进行对比。
一般来说,起点都比较好确定,无非是一个点击事件或者一个自定义的触发事件;而终点的确定就比较麻烦,比如如何确定一个复杂的 App (比如淘宝)启动完成的时间点,用 Systrace 的第一帧或者 Log 输出的 Displayed 时间或者 onWindowFocusChange 回调的时间显然是不准确的。目前市面上使用高速相机 + 图像识别来做是一个比较主流的做法。
响应速度常见问题
Android系统自身原因导致响应慢
下面这些列举的是 Android 系统自身的原因,与 Android 机器的性能有比较大的关系,性能越差,越容易出现响应速度问题。下面就列出了 Android 系统原因导致的 App 响应速度出现问题的原因,以及这个时候 App 端在 Systrace 中的表现。
CPU频率不足。
App端的表现:主线程处于Running状态,但是执行耗时变长。
CPU大小核调度:关键任务跑到了小核。
App端的表现:Systrace看主线程处于Running状态,但是执行耗时变长。
System Server繁忙,主要影响。
响应App主线程Binder调用处理耗时。
App端的表现:Systrace看主线程处于Sleep状态,在等待Binder调用返回。
应用启动过程逻辑处理耗时。
App端的表现:Systrace看主线程处于Sleep状态,在等待Binder调用返回。
SurfaceFlinger繁忙,主要影响应用的渲染线程的dequeueBuffer、queueBuffer。
App端的表现:Systrace看应用渲染线程的dequeueBuffer、queueBuffer处于Binder等待状态。
系统低内存,低内存的时候,很大概率出现下面几种情况,都会对System Server和应用有影响。
低内存的时候,有些应用会频繁被杀和启动,而应用启动时一个重操作,会占用CPU资源,导致前台App启动变慢。
App端的表现:Systrace看应用主线程Runnable状态变多,Running状态变少,整体函数执行耗时增加。
低内存的时候,很容易触发各个进程的GC,用于内存回收的HeapTaskDeamon、kswapd0出现非常频繁。
App端的表现:Systrace看应用主线程Runnable状态变多,Running状态变少,整体函数执行耗时增加。
低内存会导致磁盘IO变多,如果频繁进行磁盘IO,由于磁盘IO很慢,那么主线程会有很多进程处于等IO的状态,也就是我们经常看到的Uninterruptible Sleep。
App端的表现:Systrace看应用主线程Uninterruptible Sleep和Uninterruptible Sleep - IO状态变多,Running状态变少,整体函数执行耗时增加。
系统触发温控频率被限制:由于温度过高,CPU最高频率被限制。
App端的表现:主线程处于Running状态,但是执行耗时变长。
整机CPU繁忙:可能有多个高负载进程同时在运行,或者有单个进程负载过高跑满了CPU。
App端的表现:从Systrace来看,CPU区域的任务非常满,所有的核心都有任务在执行,App的主线程和渲染线程多处于Runnable状态,或者频繁在Runnable和Running之间切换。
应有自身原因
应用自身原因主要是应用启动时候的组件初始化、View 初始化、数据初始化耗时等,具体包括:
-
Application.onCreate:应用自身的逻辑 + 三方 SDK 初始化耗时。
-
Activity 的生命周期函数:onStart、onCreate、onResume 耗时。
-
Services 的生命周期函数耗时。
-
Broadcast 的 onReceive 耗时。
-
ContentProvider 初始化耗时(注意已经被滥用)。
-
界面布局初始化:measure、layout、draw 等耗时。
-
渲染线程初始化:setSurface、queueBuffer、dequeueBuffer、Textureupload 等耗时。
-
Activity 跳转:从 SplashActivity 到 MainActivity 耗时。
-
应用向主线程 post 的耗时 Message 耗时。
-
主线程或者渲染线程等待子线程数据更新耗时。
-
主线程或者渲染线程等待子进程程数据更新耗时。
-
主线程或者渲染线程等待网络数据更新耗时。
-
主线程或者渲染线程 binder 调用耗时。
-
WebView 初始化耗时。
-
初次运行 JIT 耗时。
响应速度问题分析套路(以 Systrace 为主)
确认前提条件(老化,数据量、下载等)、操作步骤、问题现象,本地复现。
需要明确测试标准。
启动时间的起点是哪里。
启动时机的终点是哪里。
抓取所需的日志信息(Systrace、常规 log 等)。
首先分析 Systrace,大概找出差异的点。
首先查看应用耗时点,分析对比机差异,这里可以把应用启动阶段分成好几段来看,来对比分析是哪部分时间增加。
Application 创建。
Activity 创建。
第一个 doFrame。
后续内容加载。
应用自己的 Message。
分析应用耗时的点。
是否某一段方法自身执行耗时比较久(Running 状态) –> 应用自身问题。
主线程是否有大段 Running 状态,但是底下没有任何堆栈 –> 应用自身问题,加 TraceTag 或者使用 TraceView 来找到对应的代码逻辑。
是否在等 Binder 耗时比较久(Sleep 状态) –> 检测 Binder 服务端,一般是 SystemServer。
是否在等待子线程返回数据(Sleep 状态) –> 应用自身问题,通过查看 wakeup 信息,来找到依赖的子线程。
是否在等待子进程返回数据(Sleep 状态) –> 应用自身问题,通过查看 wakeup 信息,来找到依赖的子进程或者其他进程(一般是 ContentProvider 所在的进程)。
是否有大量的 Runnable –> 系统问题,查看 CPU 部分,看看是否已经跑满。
是否有大量的 IO 等待(Uninterruptible Sleep | WakeKill - Block I/O) –> 检查系统是否已经低内存。
RenderThread 是否执行 dequeueBuffer 和 queueBuffer 耗时 –> 查看 SurfaceFlinger。
如果分析是系统的问题,则根据上面耗时的点,查看系统对应的部分,一般情况要优先查看系统是否异常,参考上面列出的的系统原因,主要看下面四个区域(Systrace)。
查看关键任务是否跑在了小核 –> 一般小核是 0-3(也有特例),如果启动时候的关键任务跑到了小核,执行速度也会变慢。
查看频率是否没有跑满 –> 表现是核心频率没有达到最大值,比如最大值是 2.8Ghz,但是只跑到了 1.8Ghz,那么可能是有问题的。
查看 CPU 使用率,是否已经跑满了 –> 表现是 CPU 区域八个核心上,任务和任务之间没有空隙。
查看是否低内存。
应用进程状态有大量的 Uninterruptible Sleep | WakeKill - Block I/O。
HeapTaskDeamon 任务执行频繁。
kswapd0 任务执行频繁。
input 事件读取和分发是否有异常 –> 表现是 input 事件传递耗时,比较少见。
binder 执行是否耗时 –> 表现是 SystemServer 对应的 Binder 执行代码逻辑耗时。
binder 等 am、wm 锁是否耗时–> 表现是 SystemServer 对应的 Binder 都在等待锁,可以通过 wakeup 信息跟踪等锁情况,分析等锁是不是由于应用导致的。
是否有应用频繁启动或者被杀 –> 在 Systrace 中查看 startProcess,或者查看 Event Log。
dequeueBuffer 和 queueBuffer 是否执行耗时 –> 表现是 SurfaceFlinger 的对应的 Binder 执行 dequeueBuffer 和 queueBuffer 耗时。
主线程是否执行耗时 –> 表现是 SurfaceFlinger 主线程耗时,可能是在执行其他的任务。
Launcher 进程区域(冷热启动场景)。
Launcher 进程处理点击事件是否耗时 –> 表现在处理 input 事件耗时。
Launcher 自身 pause 是否耗时 –> 表现在执行 onPause 耗时。
Launcher 应用启动动画是否耗时或者卡顿 –> 表现在动画耗时或者卡顿。
初步分析有怀疑的点之后。
如果是系统的原因,首先需要看应用自身是否能规避,如果不能规避,则转给系统来处理。
如果是应用自身的原因,可以使用 TraceView(AS 自带的 CPU Profiler)、Simple Perf 等继续查看更加详细的函数调用信息,也可以使用 TraceFix 插件,插入更多的 TraceTag 之后,重新抓取 Systrace 来对比分析。
问题可能有很多个原因。
首先要把影响最大的因素找出来优化,影响比较小的因素可以先忽略。
有些问题需要系统的配合才能解决,这时候需要跟系统一起进行调优(比如各大 App 厂商就会有专门跟手机厂商打交道的,手机厂商会以 SDK 的形式,暴露部分系统接口给 App 来使用,比如 Oppo 、华为、Vivo 等)。
有些问题影响很小或者无解,这时候需要跟测试同学沟通清楚。
有些问题是重复问题或不同平台的相同,可以在 Bug 库中搜索是否有案例。
准备工作
以 Systrace 为主线,讲解应用启动的时候各个关键模块的大概工作流程。了解大概流程之后,就可以分段去深入自己感兴趣或者自己负责的部分,这里首先放一张 Systrace 和手机截图所对应的图,大家可以先看看这个图,然后再往下看。
为了更方便分析应用冷启动,我们需要做下面的准备工作:
打开 Binder 调试,方便在 Trace 中显示 Binder 信息(即可以在 Systrace 中看到 Binder 调用的函数)- 需要 Root。
开启 ipc debug:
adb shell am trace-ipc start
。抓取结束后,可以执行下面的命令关闭
adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
。Trace 命令加入 irq tag,默认的命令不包含 irq,需要自己加 irq 的 TAG,这样打开 Trace 之后,就可以看到 irq 相关的内容,最后的抓 trace 命令如下:
python /mnt/d/Android/platform-tools/systrace/systrace.py gfx input view webview wm am sm rs bionic power pm ss database network adb idle pdx sched irq freq idle disk workq binder_driver binder_lock -a com.xxx.xxx
,注意这里的 com.xxx.xxx 换成自己的包名,如果不是调试特定的包名,可以去掉 -a com.xxx.xxx。推荐 :如果要 Debug 的 App 可以进行编译(即可以使用 Gradle 编译,一般自己开发的项目都可以),可以在分析响应速度问题的时候,引入 TraceFix 库。接入之后,编译的时候就会进行代码插桩,在 App 代码的每一个函数中都插入 Trace 点,这样在分析的时候可以看到更详细的 App 的信息。
使用插件前,只能看到 Framework 里面的 Trace 点。
使用插件后,可以看到 Trace 中显示的信息多了很多(App 自身的代码逻辑,Framework 的代码没法插桩) 。
Android App冷启动流程分析
本文以在桌面上冷启动一个Android App为例,应用冷启动的整个流程包含了从用户触摸屏幕到应用完全显示的整个流程,其中涉及到:
-
触摸屏中断处理阶段。
-
InputReader 和 InputDispatcher 处理 input 事件阶段。
-
Launcher 处理 input 事件阶段。
-
SystemServer 处理启动事件。
-
启动动画。
-
应用启动和自身逻辑阶段。
响应速度的问题,需要搞清楚 起点 和 终点,对于应用冷启动来说,起点就是 input 事件,终点就是应用完全展示给用户(用户可操作)。下面将从上面几个关键流程,通过 Systrace 的来介绍整个流程。
触摸屏中断处理阶段
由于我们的案例是在桌面冷启动一个 App,那么在手指触摸手机屏幕的时候,触摸屏会触发中断,这个中断我们最早能在 Systrace 中看到的地方如下:
对应的 cpu ss 区域和 中断区域(加了 irq 的 tag 才可以看到)。
一般来说,点击屏幕会触发若干个中断,这些信号经过处理之后,触摸屏驱动会把这些点更新到 EventHub 中,让 InputReader 和 InputDIspatcher 进行进一步的处理。这一步一般不会出现什么问题,厂商这边对触摸屏的调教可能会关注这里。
InputReader 和 InputDispatcher 处理Input事件阶段
InputReader 和 InputDispatcher 这两个线程跑在 SystemServer 里面,专门负责处理 Input 事件。
这里由于我们是点击桌面上的一个 App 的图标,可以看到底层上报上来的事件包括一个 Input_Down 事件 + 若干个 Input Move 事件 + 一个 Input Up 事件,组成了一个完整的点击事件。
由于 Launcher 在进程创建的时候就注册了 Input 监听,且此时 Launcher 在前台且可见,所以 Launcher 进程可以收到这些 Input 事件,并根据 Input 事件的类型进行处理,input 事件在 SystemServer 和 App 的流转在 Systrace 中的具体表现。
Input事件在System Server中流转
Input事件在Launcher进程流转
Launcher进程处理Input事件阶段
Launcher 处理 Input 事件也是响应时间的一个重要阶段,主要包括两个响应速度指标:
点击桌面到桌面第一帧响应(一般 Launcher 会在接收到 Down 事件的时候,将 App 图标置灰,以表示接收到了事件;有的定制桌面 App 图标会有一个缩小的动画,表示被按压)。
桌面第一帧响应到启动 App(这段时间指的是桌面在收到 Down 对 App 图标做处理后,到收到 Up 事件判断需要启动 App 的时间)。
另外提一下,滑动桌面到桌面第一帧响应时间(这个指的是滑动桌面的场景,左右滑动桌面的时候,用高速相机拍摄,从手指动开始,到桌面动的第一帧的时间)也是一个很重要的响应速度指标,部分厂商也会在这方面做优化,感兴趣的可以自己试试主流厂商的桌面滑动场景(跟原生的机器对比 Systrace 即可)。
在冷启动的场景里面,Launcher 在收到 up 事件后,会进行逻辑判断,然后启动对应的 App(这里主要是交给 AMS 来处理,又回到了 SystemServer 进程)。
这个阶段通常也是做系统优化的会比较关注,做 App 的同学还不需要关注到这里(Launcher App 的除外);另外在最新的版本,应用启动的动画是由 Launcher 和 SystemServer 共同完成的,目的就是可以做一些复杂的动画而没有割裂感,大家可以用慢镜头拍一下启动时候和退出应用的动画,可以看到有的应用图标是分层的,甚至会动,这是之前纯粹由 SystemServer 这边来做动画所办不到的。
System Server处理StartActivity阶段
System Server处理:
处理启动命令
通知 Launcher 进入 Pause 状态
fork 新的进程
处理启动命令
这个 SystemServer 进程中的 Binder 调用就是 Launcher 通过ActivityTaskManager.getService().startActivity 调用过来的。
fork 新的进程,则是在判断启动的 Activity 的 App 进程没有启动后,需要首先启动进程,然后再启动 Activity,这里是冷启动和其他启动不一样的地方。fork 主要是 fork Zygote64 这个进程(部分 App 是 fork 的 Zygote32 )。
Zygote 64 位进程执行Fork操作
对应App进程出现
对应的代码如下,这里就正式进入了 App 自己的进程逻辑了。
应用启动后,SystemServer 会记录从 startActivity 被调用到应用第一帧显示的时长,在 Systrace 中的显示如下(注意结尾是应用第一帧,如果应用启动的时候是 SplashActivity -> MainActivity,那么这里的结尾只是 SplashActivity,MainActivity 的完全启动需要自己查看)。
应用进程启动阶段
通常的大型应用,App 冷启动通常包括下面三个部分,每一个部分耗时都会导致应用的整体启动速度变慢,所以在优化启动速度的时候,需要明确知道应用启动结束的点(需要跟测试沟通清楚,一般是界面保持稳定的那个点)。
应用进程启动到 SplashActivity 第一帧显示(部分 App 没有 SplashActivity,所以可以省略这一步,直接到进程启动到 主 Activit 第一帧显示 )。
SplashActivity 第一帧显示到主 Activity 第一帧显示。
主 Activity 第一帧显示到界面完全显示。
下面针对这三个阶段来具体分析(当然你的 App 如果简单的话,可能没有 SplashActivity ,直接进的就是主 Activity,那么忽略第二步就可以了)。
应用进程启动到SplashActivity第一帧显示
由于是冷启动,所有App进程在Fork之后,需要首先执行bindApplication ,这个也是区分冷热启动的一个重要的点。Application 的环境创建好之后,就开始组件的启动(这里是 Activity 组件,通过 Service、Broadcast、ContentProvider 组件启动的进程则会在 bindApplication 之后先启动这些组件)。
Activity 的生命周期函数会在 Activity 组件创建的时候执行,包括 onStart、onCreate、onResume 等,然后还要经过一次 Choreographer#doFrame 的执行(包括 measure、layout、draw)以及 RenderThread 的初始化和第一帧任务的绘制,再加上 SurfaceFlinger 一个 Vsync 周期的合成,应用第一帧才会真正显示(也就是下图中 finishDrawing 的位置)。
SplashActivity 第一帧显示到主 Activity 第一帧显示
大部分的 App 都有 SplashActivity 来播放广告,播放完成之后才是真正的主 Activity 的启动,同样包括 Activity 组件的创建,包括 onStart、onCreate、onResume 、自有启动逻辑的执行、WebView 的初始化等等,直到主 Activity 的第一帧显示。
主 Activity 第一帧显示到界面完全加载并显示
一般来说,主 Activity 需要多帧才能显示完全,因为有很多资源(最常见的是图片)是异步加载的,第一帧可能只加载了一个显示框架、而其中的内容在准备好之后才会显示出来。这里也可以看到,通过 Systrace 不是很方便来判断应用冷启动的终点(除非你跟测试约定好,在某个 View 显示之后就算启动完成,然后你在这个 View 里面打个 Systrace 的 Tag,通过跟踪这个 Tag 就可以粗略判断具体 Systrace 里面哪一帧是启动完成的点)。
我制作了一个 Systrace + 截图的方式,来进行演示,方便你了解 App 启动各个阶段都对应在 Systrace 的哪里(使用的是一个开源的 WanAndroid 客户端)。
Systrace 中进程三种状态解读
Systrace 中,进程的任务最常见的有三种状态:Sleep、Running、Runnable。在优化的过程中,这几个状态也需要我们关注。进程任务状态在最上面,以颜色来做区分:
-
绿色:Running
-
蓝色:Runnable
-
白色:Sleep
如何分析Sleep状态的Task
一般白色的 Sleep 有两种,即应用主动 Sleep 和被动 Sleep。
-
nativePoll 这种,一般属于主动 Sleep,因为没有消息处理了,所以进入 Sleep 状态等待 Message,这种一般是正常的,我们不需要去关注。比如两帧之间的那段,就是主动 sleep 的。
-
被动 Sleep 一般是由用户主动调用 sleep,或者用 Binder 与其他进程进行通信,这个是我们最常见的,也是分析性能问题的时候经常会遇到的,需要重点关注。
如下图,这种在启动过程中,有较长时间的 sleep 情况,一般下面就可以看到是否在进行 Binder 通信,如果在启动过程中有频繁的 Binder 通信,那么应用等待的时间就会变长,导致响应时间变慢。
这种一般可以点击这个 Task 最下面的 binder transaction 来查看 Binder 调用信息,比如:
有时候没有 Binder 信息,是被其他的等待的线程唤醒,那么可以查看唤醒信息,也可以找到应用是在等待什么。
放大上图中我们点击的 Runnable 的地方。
如何分析Running状态的Task
Running 状态的任务就是目前在 CPU 某一个核心上运行的任务,如果某一段任务是 Running 状态,且耗时变长,那么需要分析:
-
是否应用的本身逻辑耗时,比如新增了某些代码逻辑。
-
是否跑在了对应的核心上。
在某些 Android 机器上,大家一般会对 App 的主线程和渲染线程进行调度方面的优化:一般前台应用的 UI Thread 和 RenderThread 都是跑在大核上的。
如何分析Runnable状态的Task
一个 Task 要从 Sleep 状态转到 Running 状态,必须先变成 Runnable 状态,其状态转换图如下:
在 Systrace 上的表现如下:
正常情况下,应用进入 Runnable 状态之后,会马上被调度器调度,进入 Running 状态,开始干活;但是在系统繁忙的时候,应用就会有大量的时间在 Runnable 状态,因为 cpu 已经跑满,各种任务都需要排队等待调度。
如果应用启动的时候出现大量的 Runnable 任务,那么需要查看系统的状态。
TraceView工具在响应速度方面的使用
TraceView 指的是我们在 AS Profiler 里面抓取 CPU 信息的时候出现的那个,大家看下面的截图就知道了。
如何抓取应用启动的时候的TraceView
使用下面的命令可以抓取应用的冷启动,这些命令也可以分开执行,需要把里面的包名和 Activity 名切换成自己应用的包名。
adb shell am start -n com.aboback.wanandroidjetpack/.splash.SplashActivity --start-profiler /data/local/tmp/traceview.trace --sampling 1 && sleep 10 && adb shell am profile stop com.aboback.wanandroidjetpack && adb pull /data/local/tmp/traceview.trace .
或者分开执行上面的命令。
// 1. 冷启动 App,sampleing = 1 意思是 1ms 采样一次
adb shell am start -n com.aboback.wanandroidjetpack/.splash.SplashActivity --start-profiler /data/local/tmp/traceview.trace --sampling 1
// 2. 等待应用完全启动之后,结束 profile
adb shell am profile stop com.aboback.wanandroidjetpack
// 3. 将 Trace 文件从手机里面 pull 出来
adb pull /data/local/tmp/traceview.trace .
// 4. 使用 Android Studio 打开 traceview.trace 文件
TraceView工具怎么看
抓出来的 TraceView 可以直接在 Android Studio 中打开。其中图里面用绿色标记的函数,就是应用自己的函数,黄色标注的是系统的函数。
Application.onCreate
Activity.onCreate
doFrame
WebView 初始化
TraceView工具的弊端
由于采样比较细,所以会性能损耗比较大,所以抓出来的 TraceView,其中每个方法的执行时间是不准的,所以不可用作为真实的时间参考,但是可以用来定位具体的函数调用栈。需要跟 Systrace 来进行互补。
SimplePerf 工具在启动速度分析的使用
使用 SimplePerf 工具也可以抓取启动时候的堆栈信息,既包括 Java 也包括 Native。
比如我们要抓取 com.aboback.wanandroidjetpack 这个应用的冷启动,可以执行下面的命令(SimplePerf 的环境初始化参考 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md 这篇文章 ,其中 app_profiler.py 就是 SimplePerf 的工具)。
python app_profiler.py -p com.aboback.wanandroidjetpack
执行上面的命令之后,需要手动在手机上启动 App,然后主动结束。
$ python app_profiler.py -p com.aboback.wanandroidjetpack
INFO:root:prepare profiling
INFO:root:start profiling1
INFO:root:run adb cmd: ['adb', 'shell', '/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data', '-e task-clock:u -f 1000 -g --duration 10', '--log', 'info', '--app', 'com.aboback.wanandroidjetpack'] simpleperf I environment.cpp:601] Waiting for process of app com.aboback.wanandroidjetpack
simpleperf I environment.cpp:593] Got process 32112 for package com.aboback.wanandroidjetpack
抓取结束之后,调用解析脚本来生成 html 报告。
python report_html.py
就会得到下面这个。
不仅可以看到 Java 层的堆栈,也可以看到 Native 的堆栈,这里只是简单的使用,更详细的方法可以参考下面几个文档:
SimplePerf 初步试探 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/README.md
Android application profiling https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md
Android platform profiling https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_platform_profiling.md
Executable commands reference https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/executable_commands_reference.md
Scripts reference https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/scripts_reference.md
其他组件启动时在 Systrace 中的位置
Service 的启动
public final void scheduleCreateService(IBinder token,ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
sendMessage(H.CREATE_SERVICE, s);
}
public final void scheduleBindService(IBinder token, Intent intent, boolean rebind, int processState) {
updateProcessState(processState, false);
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
s.rebind = rebind;
sendMessage(H.BIND_SERVICE, s);
}
可以看到,代码执行都是往 H 这个 Handler 中发送 Message,所以如果我们在代码里面启动 Service,并不是马上就执行的,而是由 MessageQueue 里面的 Message 顺序决定的。
放大真正执行的部分可以看到,其执行的时机是在 MessageQueue 按照 Message 的顺序执行(这里是在应用第一帧执行结束后),后面的 Message 就是应用自己的 Message、启动 Service、执行广播接收器。
执行自己的 Message
执行自定义的 Message 在 Systrace 中的显示。
启动 Service
Service 启动在 Systrace 中的显示。
启动 BroadcastReceiver
执行 Receiver 在 Systrace 中的显示。
Broadcast 的注册:一般是在 Activity 生命周期函数中注册,在哪里注册就在哪里执行。
ContentProvider 的启动时机
AppStartup 是否能优化启动速度?
三方库的初始化
很多三方库都需要在 Application 中进行初始化,并顺便获取到 Application 的上下文。
但是也有的库不需要我们自己去初始化,它偷偷摸摸就给初始化了,用到的方法就是使用 ContentProvider 进行初始化,定义一个 ContentProvider,然后在 onCreate 拿到上下文,就可以进行三方库自己的初始化工作了。而在 APP 的启动流程中,有一步就是要执行到程序中所有注册过的 ContentProvider 的 onCreate 方法,所以这个库的初始化就默默完成了。
这种做法确实给集成库的开发者们带来了很大的便利,现在很多库都用到了这种方法,比如 Facebook,Firebase,WorkManager。
ContentProvider 的初始化时机如下:
但是当大部分三方库使用这种方法初始化的时候,就会有下面几个问题:
启动过程中的 ContentProvider 过多。
应用开发者无法控制使用这种方式初始化的库的初始化时机。
无法处理这些三方库的依赖。
AppStartup 库
针对上面的情况,Google 推出了 AppStartup 库,AppStartup 库的优点:
-
可以共享单个 Contentprovider。
-
可以明确地设置初始化顺序。
-
通过这个库可以移除三方库的 ContentProvider 启动时候自动初始化的步骤,手动通过 LazyLoad 的方式启动,这样可以起到优化启动速度的作用。
根据测算结果来看,使用 AppStartup 库并不能显著加快应用启动速度,除非你有非常多 (50+)的 ContentProvider 在应用启动的时候初始,那么 AppStartup 才会有比较明显的效果。
如果三方的 SDK 使用 ContentProvider 初始化耗时,那么可以考虑针对这个 ContentProvider 进行延迟初始化,比如:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="com.example.ExampleLoggerInitializer"
tools:node="remove" />
</provider>
ExampleLoggerInitializer 的 meta-data 当中加入了一个 tools:node=”remove”的标记。
总结
App Startup 的设计是为了解决一个问题:即不同的库使用不同的 ContentProvider 进行初始化,导致 ContentProvider 太多,管理杂乱,影响耗时的问题。
App Startup 具体能减少多少耗时时间:根据测试,如果二三十个三方库都集成了 App Startup,减少的耗时大概在 20ms 以内。
App Startup 的使用场景应该。
APK 有很多的 ContentProvider 在启动时候初始化。
APK 中有的三方库 ContentProvider 初始化很耗时,但是又不是必须要在启动的时候初始,可以按需初始化。
应用开发者想自己控制各个库的初始化时机或者初始化顺序。
需要 App 开发同学验证
检查打包出来的 apk 的配置文件里面看一下,有多少个三方库是利用 ContentProvider 初始化的(或者在 AS 的 src\main\AndroidManifest.xml 文件最下面打开 Merged Manifest 标签查看)。
确认这些 ContentProvider 在启动时候的耗时。
确认哪些 ContentProvider 可以延迟加载或者用时加载。
如果需要的话,接入 AppStartup 库。
IdleHandler 在 App 启动场景下的使用
在启动优化的过程中,idleHandler 可以在 MessageQueue 空闲的时候执行任务,如下图,可以很清晰地查看 idleHandler 的执行时机。
其使用场景如下:
-
在启动的过程中,可以借助 idleHandler 来做一些延迟加载的事情。比如在启动过程中 Activity 的 onCreate 里面 addIdleHandler,这样在 Message 空闲的时候,可以执行这个任务。
-
进行启动时间统计:比如在页面完全加载之后,调用 activity.reportFullyDrawn 来告知系统这个 Activity 已经完全加载,用户可以使用了,比如下面的例子,在主页的 List 加载完成后,调用 activity.reportFullyDrawn。
其对应的 Systrace 如下:
这时候得到的应用的冷启动时间才是正常的。
另外系统有些功能,也会依赖于 FullyDrawn,所以建议主动上报(即主动在 App 完全启动后调用 activity.reportFullyDrawn)。