Android性能优化流程与思路

流程

  1. 确定指标:明确要优化哪些指标,指标如何定义和计算。
  2. 测试工具:定位问题使用到的工具,第三方工具,或者自行开发。
  3. 定位问题:使用工具定位问题,例如页面滚动卡顿,要定位到具体哪些代码逻辑耗时较多。
  4. 需求文档:编写技术需求文档。
  5. 任务分配、版本排期:如果是多个人负责,需要根据实际情况分配任务,并进行版本排期。
  6. 技术优化:具体进行优化。
  7. 成果验收:优化完成后,使用测试工具再次测试,对比分析优化前后的效果。
  8. 规范制定:如果优化过程中,发现了一些业务代码的写法容易导致性能问题(例如在BindView过程中打Log而且线上包没有删掉,容易引起卡顿),可以针对性的制定一些代码规范。还可以封装基础工具类解决这类问题(封装Log工具类,统一控制Log输出),借助静态代码检查工具进行约束(例如可参考 美团外卖Android Lint代码检查实践 )。
  9. 持续监测:借助监控SDK、CI工具等,持续监控性能指标,避免之后性能持续下降。

分析和监控工具

  1. Android Studio 提供的 Profiler
    • 内存
      • 内存消耗监控
      • HeapDump内存分析
    • CPU
      • CPU占用率监控
      • 方法耗时火焰图
    • 网络
    • 耗电量
  2. Android Studio 提供的 Layout Inspector:View布局分析
  3. TimeTracer:方法耗时分析,可用于分析冷启动、页面滚动卡顿等
  4. LeakCanary:分析内存泄露
  5. Nanoscope:方法耗时分析
  6. 自动化测试技术,在真机、Jenkins虚拟机上运行
  7. 自研SDK,例如美团的 Hertz

指标设计

冷启动时间:进程启动到首页加载完成,读取/proc/pid/stat可以获取进程启动时间。另一种常见的思路是以Application启动作为起始时间。

页面加载时间:从Activity对象创建到数据加载刷新完成。可以参考 Android自动化页面测速在美团的实践

滚动FPS:页面滚动时检测FPS,可使用Choreographer.doFrame接口实现。FPS主要是可以衡量View滚动期间主线程是否有阻塞现象。

滚动平滑度:如果滚动组件的事件处理逻辑有问题(例如Fling机制出现问题),组件虽然滚动很不平滑,但是并不会影响FPS。这里提出一个滚动平滑度的指标,思路是正常的Fling流程应该是匀减速运动,加速度是固定的,但是如果Fling机制有问题,或者是常规的主线程阻塞,加速度会不稳定。于是可以通过计算加速度的变异系数,来衡量滚动是否平滑。

OOM崩溃率:应用的内存消耗其实对于用户而言没有很直接的感知,真正最影响用户体验的是OOM,发生OOM说明内存问题已经很严重了,需要引起重视。Crash上报可以使用现成的第三方SDK,也可以参考 Android Crash监控SDK设计思路

App内存消耗:onResume-onPause期间,多次采样取平均值。

  • 内存消耗的指标参考价值有限
  • Java虚拟机并不会立即回收无用内存,常常会到内存消耗较多时才回收;”内存大户“图片库常常会用LRU Cache之类内存缓存,只有在内存不够时才会清理资源;后台Activity只有在内存不足时才销毁,否则会继续留在后台。这些相似的因素都会导致App的内存占用看起来比较高,但是实际上并没有明显的内存问题。
  • 如果按照内存消耗的指标盲目做优化,反而可能导致CPU消耗大大增加,最终损害用户体验。

内存稳定度:如果内存波动很大,说明有频繁的内存分配和回收,会导致过多CPU消耗,内存使用可能存在问题。

CPU占用率:onResume-onPause期间,多次采样取平均值。

耗电量。

流量消耗。

优化思路

冷启动

App初始化框架。当App中的初始化项很多时,可以实现一个初始化框架,把初始化操作拆分成一个个独立的Init,统一管理。

  1. 依赖管理和流程分析。方便统一分析初始化流程,找到互相依赖关系。对于减少BUG也有很大帮助。
  2. 耗时统计,可以在基类中给每个Init做耗时统计。
  3. 线程管理,同步 / 异步初始化。
  4. 进程管理。在不同的进程中,初始化不同的模块;只初始化必要的模块,减少性能损耗。
  5. 可以借助CI工具统计Init耗时(Jenkins虚拟机)。新增Init耗时太长的,代码不能合并。

初始化项的优化。耗时较多的初始化项,针对性的去做优化。

流程优化。

  1. 串行 --> 并行,部分初始化从主线程挪到后台线程,避免阻塞主线程,并且多个初始化可以在不同的后台线程进行(可以使用线程池)。
  2. 很多App启动都有倒计时广告,一方面,这个广告图尽可能提前加载到内存,而不是启动到这个页面时才加载,另一方面,充分利用这个倒计时的时间,在后台做其他初始化。
  3. 提前加载首页数据,可能包括定位、网络请求等。
  4. 对首页View的加载进行优化也可以减少App启动时间,参考下文的页面启动。

页面启动

页面启动的大部分时间通常消耗在网络请求上。网络请求可以使用长连接,减少DNS解析、HTTP连接等耗时,例如美团Shark

除了网络请求,最耗时的通常是View初始化。优化思路包括降低View层级,提前异步创建View,多Tab页面按需加载Tab等。

按照 Hertz 中的页面测速模型,从Activity启动到发起网络请求的时间也可以优化。常规的代码思路是先加载View,在发起网络请求,最后填充数据。可以改成启动时立即发网络请求,同时加载View,当View加载完成、网络数据也返回后,再填充数据。

FPS、页面卡顿

  1. 异步创建View,例如AsyncLayoutInflater。
  2. List二级View做缓存,例如List每个Item中又有很多小标签,这些标签可以放到一个缓存池中。具体实现是在bindView时,Container不是直接removeAllViews,而是将View保存到List中,然后在添加View时先从List取,取不到再创建。这样就避免了每次bindView时反复创建View。
  3. Release包移除Log,统计埋点移到后台。Log和埋点之类操作通常会有大量字符串拼接操作,特别是String.format耗时很多。
  4. 布局层级降低,使用ConstraintLayout或自定义Layout。
  5. 监控滚动速度,快速滚动时暂停图片加载。快速滚动时大量图片加载,频繁内存分配和回收,性能消耗大。参考 Android滚动组件图片加载优化与滚动速度的精确监听
  6. 过度绘制优化。
  7. 特殊滚动组件的事件处理,要保证没有明显BUG,否则对用户体验影响很大。

CPU

  1. 图片、网络库的线程池合并。网络请求和图片加载的时机通常不一样,网络请求很长时间才发一次,之后线程池就一直处于空闲状态,而图片加载可能会随着页面滚动不断发生,合并线程池可以促进线程的充分利用,避免创建过多线程。
  2. 规范线程使用。封装线程基础工具类,禁止使用new Thread。
  3. 可以配合Lint检查,参考 美团外卖Android Lint代码检查实践

内存

  1. 网络图片使用CDN服务器压缩尺寸,这里指的是图片的长宽,因为会影响最终Bitmap的内存消耗,和图片文件尺寸无关。
  2. 不需要透明区域的图片,使用RGB_565代替ARGB_8888。
  3. ShapeDrawable代替Bitmap。
  4. 监控滚动速度,快速滚动时暂停图片加载。快速滚动时大量图片加载,来不及回收,可能会产生OOM。
  5. Lottie矢量动画代替图片逐帧动画。
  6. 加载本地大图要做压缩,设置inSampleSize参数。
  7. 避免使用多个图片库,因为每个图片库都有自己的内存缓存。特别是在接入业务SDK,或者是多个AAR工程独立开发的情况下,容易引入不同的图片库,需要做好代码规范。
  8. 下拉动画、加载动画在播放完成后,及时释放动画占用的内存。
  9. 解决内存泄露问题。

流量

流量的几个优化点,都需要后台服务器支持。

  1. 使用webp代替 jpg / png / gif。
  2. 网络图片使用CDN服务器压缩尺寸,这里指的是文件尺寸。
  3. 网络请求启用gzip压缩。

还可以参考

https://jsonchao.github.io/categories/性能优化/