Android|Android性能优化详解

启动优化 【Android|Android性能优化详解】用户都希望点击应用图标后,应用能够快速地启动并响应用户操作。而随着业务逻辑的增加,要初始化的操作越来越多,尤其是第三方组件的初始化,而在应用启动时初始化会导致应用启动时间变长,因此,我们需要对启动性能(Launch-Time Performance)进行优化。
启动状态 应用会从冷启动(cold start)、热启动(warm start)、温启动(lukewarm start)三种状态中的一种启动,每种状态会影响你的应用对用户可见要花费的时间。
冷启动
冷启动(cold start)表明APP要从零启动: 系统进程还没有创建应用的进程。冷启动通常发生在开机后第一次启动应用,或者系统强制杀死了应用。这种类型的启动意味着最小化启动时间是个很大的挑战,因为此时系统和应用相对于其他启动状态有更多的工作要做:
首先,系统有三个任务要做:

  1. 加载并启动应用
  2. 启动后马上为应用显示一个空白的启动窗口
  3. 创建应用进程
系统创建完应用进程后,应用进程会马上:
  1. 创建应用对象
  2. 启动主线程
  3. 创建主Activity
  4. Inflating views
  5. 布局到屏幕上
  6. 执行最初的绘制
一旦应用进程完成了第一次绘制,系统进程就会将正在显示的启动窗口替换为主Activity,这时,用户就可以使用你的应用了:
Android|Android性能优化详解
文章图片

热启动
热启动(warm start)表明你的应用启动时比冷启动更加的简单,花费的代价更小,系统要做的只是将你的Activity移到前台就行了。如果应用的所有Activity都还驻留在内存中, 那么应用也就避免了重复初始化对象、布局及渲染了。如果由于onTrimMemory()等事件一些内存被清理了,那么一些对象将不得不重建以便热启动。
和冷启动一样,热启动时系统进程也会显示一个空白窗口直到应用完成Activity的渲染。
温启动
温启动(lukewarm start)介于冷启动和热启动之间,有些潜在的状态可以认定是温启动,如:
  • 用户按了返回键退出了你的应用,然后重新启动了应用,应用进程也许还没被杀死,所以应用进程可以继续运行,但应用的Activity必须通过onCreate()方法从零重建。
  • 由于内存紧张等原因,系统强制将你的应用从内存中清除,之后用户重新启动了应用,那么应用进程和Activity都需要重建,但这个task可以受益于onCreate()方法中传过来的已经保存的实例状态bundle。
启动性能检测 从Android 4.4 (API level 19)开始,logcat会打印一些Displayed时间,包括启动进程、初始化对象、创建并初始化Activity、inflate布局、第一次绘制应用等事件所花费的时间:
Android|Android性能优化详解
文章图片

你还可以通过Activity的reportFullyDrawn()方法测量从应用启动到所有资源和View树完全显示的时间。
或者你可以通过ADB Shell Activity Manager命令测量:
adb [-d|-e|-s ] shell am start -S -W com.example.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

或者利用Android Studio最新的Android Profiler进行测量:
  1. 通过顶部工具栏的View > Tool Windows > Android Profiler(或者直接点击底部工具栏的Android|Android性能优化详解
    文章图片
    Android Profiler)打开Android Profiler
  2. 选择你要调试的设备和应用进程
  3. 点击CPU时间轴以打开CPU Profiler
打开后你将看到应用实时的CPU使用情况和线程活动情况,对于应用中的每个线程,你都可以查看这段时间哪些方法被执行了,以及每个方法执行消耗了多少CPU资源。你还可以通过方法跟踪(method traces)确定调用者和被调用者,通过这些信息你可以确定到底是哪些方法频繁地执行耗时任务,然后就可以尝试优化你的代码以避免不必要的耗时操作了:
Android|Android性能优化详解
文章图片

①事件时间轴: 显示了用户和设备的交互,包括设备旋转事件、触屏事件等
②CPU时间轴: 显示了你的应用的实时CPU使用情况( 绿色 部分),包括占可用CPU时间的百分比、你的应用正在使用的线程总数。其中 灰色 的Others是系统进程和其它应用进程,以便你能直观地跟自己的应用对比
③线程活动时间轴: 列出了你的应用的所有线程,其中的 绿色 表明线程是活动的或者正准备使用CPU,也就是说此时线程正处于'running'或'runnable'状态。 黄色 表明线程是活动的,但是正在等待磁盘或网络I/O操作完成。 灰色 表明线程处于休眠状态不会占用CPU时间,这通常发生在线程请求当前还不可用的资源、线程自愿休眠或者内核强制线程休眠直到其请求的资源可用
④录制配置: 你可以选择profiler记录method trace的方式。Sampled: 在应用运行期间频繁间隔性地对应用调用栈进行采集,也就是采样,但基于采样的录制有个问题就是,如果在一次采样后进入了一个方法但是在下一次采样前退出了该方法,那么这次方法调用将不会被profiler记录。Instrumented: 可以在应用运行时机械地记录每个方法调用开始和结束的时间戳,然后将这些收集到的时间戳和生成的method tracing数据(包括计时信息和CPU使用)进行比较。要注意的是,机械地录制每个方法的开销会影响运行时的性能, 并可能影响分析数据,这对于具有相对较短生命周期的方法尤为明显。此外如果你的应用短时间内执行了大量的方法,那么profiler就很可能迅速达到文件大小限制,也就不会录制之后的method tracing数据了。Edit configurations: 自定义sampled或instrumented录制配置。
⑤录制按钮: 开始或停止method trace的录制,停止时profiler会自动选择录制好的时间片并显示如下图所示的method trace面板
Android|Android性能优化详解
文章图片

①已选择的时间段: 你可以点击并拖拽高亮区域的边缘去更改它的长度
②时间戳: 表明method trace录制的开始和结束时间(相对于profiler开始收集你的设备CPU使用信息的时间)。如果你录制了多个时间片,你可以点击这个时间戳来回切换
③跟踪面板: 显示了这段时间的method trace数据和你所选择的线程,在这个面板里,你可以通过④选择查看跟踪栈的方式,可以通过⑤选择测量执行时间的方式
④选择以Top Down树、Bottom Up树、Call Chart, 还是Flame Chart的方式显示method trace
⑤Wall Clock Time: 计时信息表示的是现实中所经过的时间(壁钟时间);Thread Time: 计时信息表示的是现实经过的时间减去线程未消耗CPU资源的时间(线程时间)。Thread Time可以让你更好地了解线程中的指定方法到底占用了多少CPU
Call Chart tab以调用图的方式显示method trace,水平方向表明方法的调用者,竖直方向表明被调用的方法,系统API的方法会显示为 橙色 ,应用自己的方法显示为绿色,第三方API(包括java语言API)显示为蓝色,从这张图中你可以直观的看出指定方法的self time(方法自己的代码执行所花费的时间,不包括它的callees),children time(该方法的所有callees执行所花费的时间),以及total time(Self和Children总共花费的时间,即整个方法的执行时间):
Android|Android性能优化详解
文章图片

Flame Chart tab以火焰图的方式显示method trace,相同调用序列的方法会被合并并显示为一个长条,而不像Call Chart那样显示为多个短条,这样通过图表你可以直观的看出哪个方法消耗的时间更长,也就是说水平轴不再表示时间轴,而是表明相对占用时间的多少。为了便于理解对于下面这张Call Chart:
Android|Android性能优化详解
文章图片

方法D多次调用了方法B (B1, B2, B3),有些方法B又调用了方法C (C1, C3),由于B1,B2和B3共享同一个调用序列(A → D → B)它们就可以合并,同样,C1和C3也可以合并,这些合并后的方法调用用于生成最终的flame chart,同时,消耗最多CPU时间的被调用者会显示在前面(左侧):
Android|Android性能优化详解
文章图片

Top Down tab以方法调用列表的方式显示method trace,展开方法节点以显示它的callees(调用的方法),和Flame chart一样,Top Down也会合并相同调用序列的方法:
Android|Android性能优化详解
文章图片
Android|Android性能优化详解
文章图片

Bottom Up tab以方法被调用列表的方式显示method trace,展开方法节点以显示它的唯一的callers(调用者),也就是说,虽然B调用了两次C,但在Bottom Up中展开C只会出现一次B。通过Bottom Up可以方便地根据方法消耗CPU时间排序,你可以确定哪些调用者花费了最多的CPU时间调用这些方法。
Self Children Total
顶层节点 表示该方法自己的代码执行所花费的时间,不包括它的callees,与Top Down相比这个计时信息表示录制期间内该方法所有调用所花费的时间总和 表示执行它的callees所花费的总时间,不包括它自己的代码,与Top Down相比这个计时信息表示录制期间内该方法的callees调用所花费的时间总和 self和children花费时间的总和
子节点 表示由该方法调用的每个callees的self时间之和,以上图为例,方法B的self时间等于由方法B调用的每个方法C的self时间之和 表示由该方法调用的每个callees的children时间之和,以上图为例,方法B的children时间等于由方法B调用的每个方法C的children时间之和 self和children花费时间的总
启动性能优化 Application初始化的性能瓶颈
当初始化Application对象时,你可能会做一些特别复杂的初始化工作,如你可能会在自定义的Application对象的onCreate()方法中添加一些数据库初始化逻辑、第三方组件等通用组件的初始化逻辑,或者在Application中定义并初始化全局的静态常量。但需要注意的是,任何的磁盘I/O操作、反序列化操作、循环操作、可能引起GC的操作等等都会影响Application的初始化时间。
Application初始化的优化策略
延迟对象初始化 尽量避免使用全局静态对象,而是采用单例模式等方式在需要用到对象的时候再初始化对象。同时可以考虑使用类似Dagger的依赖注入框架注入依赖的对象。
延迟通用组件初始化 对于一些可以延迟初始化的通用组件(如分享、推送),可以利用Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService或者RxJava异步初始化,甚至有些组件可以考虑延迟到Activity中进行初始化。
使用应用相关的主题 可以考虑使用应用相关的主题作为启动Activity的背景主题(Launch screens),也就是我们通常说的闪屏。只需要为启动Activity的主题设置windowBackground属性即可。如果你之后想还原该Activity的主题,只需要在super.onCreate()setContentView()之前调用setTheme()方法即可:



Android|Android性能优化详解
文章图片

Activity初始化的性能瓶颈
创建Activity时通常需要完成更多的逻辑,而最容易导致性能问题的逻辑包括:
  • Inflating大量、复杂的布局
  • 磁盘或网络I/O操作(包括SharedPreferences读写)
  • 加载、解析Bitmap
  • 栅格化VectorDrawable对象
  • 初始化其他子系统
Activity初始化的优化策略
布局优化
  • 尽量避免View嵌套以减少View层级: 推荐使用RelativeLayoutConstraintLayout容器;推荐使用CompoundDrawable(android:drawableTopandroid:drawableStart等属性);推荐在一些复用的View容器中使用标签;
  • 尽量避免Overdraw: 对于嵌套的布局,要避免重复设置各层的背景色
  • 延迟inflate暂时不用或不常用的子布局: 使用ViewStub标签作为占位符,在需要的时候再inflate
延迟资源的初始化 可以根据情况将一些资源的初始化放到异步线程中进行
预加载部分View 可以让你的应用先显示部分UI,网络图片或其它资源可以在随后加载完成后更新UI。
参考
  • Launch-Time Performance
  • Inspect CPU Activity and Method Traces with CPU Profiler

    推荐阅读