Android 优化是一个永恒的话题,主要可以入以下 5 个角度入手:

1. 稳定性
2. 内存占用
3. 流畅度
4. 资源消耗(网络流量、电量等)
5. 安装包大小

一. 稳定性

一个好的应用,首先要求的就是稳定性。如果用户点点点,就崩溃了,那用户量的损失可不是能够轻易挽回的。

一般造成应用不稳定的原因有下面几种:

  1. ANR
  2. Crash
  3. 死锁
  4. Panic

1. ANR

ANR 即 Application Not Responding,是比较常见的现象,这种现象一般出现在应用主线程无法在规定时间内取出 Looper 中的下一个消息进行处理时,屏幕上会弹出『应用无响应,是否关闭?』的对话框,是令无数开发者头疼的问题。

FUCK ANR

Android 对主线程下运行的组件都有要求:主线程5秒内无响应;BroadcastReceiver 10 秒内未返回;后台 Service 处理超时超过20秒;前台 Service 超过5秒;绑定服务超过200秒。无法达到上述要求,即被系统定义为 ANR。造成这种问题的原因的比较多,但常见的有以下几种:

  • 线程自身主线程出现问题,比如进行 IO 文件操作时间过长,进行了大量频繁的数据库操作,死循环等等。
  • 调用 AMS、PMS 等长时间未响应,这种情况有可能发生在 system_server 进程正在等待某个锁,无法响应应用当前的请求
  • io 操作时的 iowait 过高,比如下在用多线程下载文件,进行频繁写操作。
  • cpu 占用率过高,由其他应用占用了太多的 CPU 操作,本进程无法抢占到 CPU。
  • 内存过低,系统在不断地尝试进行 GC 释放内存,可能会引起 ANR。

产生 ANR 时,系统会在/data/anr下生成一个traces.txt文件,它里面记录了 ANR 产生的原因和日志,可以通过分析得出 ANR 的具体原因。

2. Crash

Crash 问题也比较常见,绝大多情况是没有进行足够的 try-catch,当异常发生时,程序就会崩溃。这些问题一般需要根据系统的 logcat 去查找对应的 StackTrace,或者使用第三方 SDK 如 Bugly 等工具,来获取崩溃时的 StackTrace。

3. 死锁

造成这种现象的原因也比较简单,简单来说,就是 A 进程正在使用某个资源,此时 B 也使用这个资源,于是等待 A 释放锁,但这时 A 又要使用 B 正在使用的资源,B 此时又无法释放该资源的锁,因为它在等待 A 释放它想要的资源的锁,就出现了你等我我等你的现象,称之为死锁。

它产生的四个必要条件是:

  1. 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;
  2. 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;
  3. 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放);
  4. 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系。

避免死锁的基本思想是:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。

4. Panic

Panic 是内核级别的崩溃。这种崩溃在通常情况下我们无能为力。但是可以通过 kernel_log 来分析并定位问题,并在代码中尽量避免使用这个功能点或者方法。

二. 内存占用

因为 Android 是移动平台,所以对每个应用来说,都是有内存限制的,如果一个应用使用的内存空间过大,则会触发 LMK(Low Memory Killer)机制,导致应用出现闪退。这种现象大部分原因来自内存泄漏。

内存泄漏的原因

一般是编码问题、第三方库问题和 Android 自身问题。

  • 编码问题:比如说静态变量引用了生命周期组件,导致该组件一直无法释放;或者非静态内部类一直持有外部类的引用,等等。
  • 第三方库:第三方的 SDK 我们无法保证其是否会有内存泄漏,一旦出现,只能定位,不太好解决。
  • Android 自身问题:比如非常著名的 WebView 内存泄漏,它的内部线程会持有 Activity 的对象,导致 Activity 对象无法释放。如何解决请继续阅读。

内存泄漏的检测和定位

如果有内存泄漏,最直观的感受,就是应用的内存占用噌噌噌地往上涨。我们可以通过一些工具来观测内存占用情况,从而确定是否有内存泄漏的现象。

  1. Memory Monitor。由 Android Studio 提供,可以监测内存的占用情况,但无法得知内存泄漏的原因。
  2. Memory Analyzer。一个快速、功能丰富的 Java 堆分析工具。会通过内存的 Snapshot 生成 HPROF 文件,可以查看每一个对象在堆中所占的大小,从而定位造成内存泄漏的对象是哪个。
  3. LeakCanary。由 Square 公司出品,可以以 SDK 的方式集成到应用中,它会在应用运行的过程中,及时地提醒开发者哪些地方出现了内存泄漏,并提供相应的 StackTrace 帮助定位,它的具体原理可以查看这篇文章
  4. Android Lint。由 Android Studio 提供,它可以基于源代码快速分析代码中可能出现内存泄漏的地方,并加以提示,从源头遏制内存泄漏的产生。它还提供了一些其他的功能,比如 Layout 优化、提示未使用的变量等等。

内存泄漏的解决

  1. 首先要注意 Activity 实例是不是被引用后无法释放。引起这种情况一般有两种原因:

    1. 内部类持有外部引用的情况下,导致 Activity 泄漏:
      这种情况下,就避免使用内部类,可以使用静态类内部来解决,静态内部类不持有外部类的引用。如果必须要使用,那么不使用这个内部类时,要强制将该类的实例置为 null。
    2. Activity 的 Context 被间接引用
      这种情况如果可以的话,可以使用 Android 的 ApplicationContext 来替代 Activity 的 Context。
  2. 然后要注意静态变量以及单例模式,静态变量它的生命周期基本与所在进程一样长,所以要小心静态变量引用其他生命周期的对象。单例模式的生命周期也与应用进程基本一致,所以与静态变量一样,要小心使用。

  3. 自定义的监听器的注销。因为监听器中一般会维护一组静态的监听者的引用队列,如果不及时注销,有可能会引起内存泄漏。

  4. 数据库 Cursor 的及时关闭。

  5. 对于 WebView 的内存泄漏,可以采取应用退出时直接调用System.exit(0)来解决,但是这种方案太暴力。第二种方案是使用新的进程来加载 WebView,但是这就涉及到进程间通信,实现起来比较麻烦。

三. 流畅度

一个 App 用起来是否『流畅』,最关键的考核指标就是『是否卡顿』。造成『卡顿』感觉一般原因是用户的输入无法得到及时响应,比如滑动时列表不流畅、页面跳转切换不流畅、事件响应不及时等等。Android 中有 VSYNC 机制,它每隔16ms就发出 VSYNC 信号,触发对 UI 的渲染,如果每次都渲染成功,则能达到如丝般顺滑的 60 帧效果(1秒=1000毫秒,1000 / 16 = 62.5)。如果某个操作花费时间超过 24ms,那收到 VSYNC 信号时就无法正常渲染,就会导致『丢帧』现象出现。

但这些原因基本上都可以归结为以下两类优化层面:

  1. 界面绘制问题
  2. 数据处理问题

1. 界面绘制问题

这种问题一般是由于 UI 布局太复杂,嵌套层级比较深,刷新机制不合理导致的。我们知道 Android 的绘制需要经过 measure、layout、draw 三个步骤,所以布局的层级越深、元素越多、耗时也就越长。

针对这种问题,一般从以下几个方面入手:

  • 布局优化

    • 减少 View 层级,优化 xml 布局文件;
    • 多使用<inclue>标签重用 layout;
    • 使用<merge>标签替换父级布局;
    • 使用 ViewStub 延迟 View 的加载;
    • 删除无用属性等。

    更多布局优化参见这篇文章

  • 渲染优化

    • 减少 Overdraw 现象,多个重叠的 View 注意背景的多次绘制问题;
    • 自定义 View 中,使用canvas.clipRect()帮助系统识别可见区域,只有在这个区域内才会被绘制。
  • 启动优化

    • 优化闪屏页布局
    • 优化启动逻辑,可以采用分步加载、延迟加载等方法提高应用启动速度
  • 动画效果优化

    好的动画效果可以让用户感觉不那么『卡顿』,在合适的情况下,可以启用『硬件加速』来帮助绘制动画效果。

2. 数据处理问题

产生这种问题一般分为三种情况:

  1. 在主线程处理数据。要尽量避免这样做,主线程尽量只用来绘制和处理 UI 层面的东西。
  2. 数据处理占用了太多 CPU。即便使用了新线程去处理数据,也可能导致主线程无法抢占到 CPU,此时可以考虑多个线程处理,或者分时、分段处理。
  3. 内存频繁 GC 引起卡顿。不要做引起频繁 GC 的操作,如使用大量临时变量等。

四. 资源消耗

资源的消耗可以从三个角度来优化:

1. 网络流量优化

虽然现在大家都不差流量,但是如果流量太多,还是会引起用户的反感。针对这种问题,我们一般从这两个层面来着手优化:

  • 图片网络优化:图片可以进行分类,一种是高清图(原图),一种是压缩图,还有缩略图。在不同的情况下引用不同版本的图片,可以在很大程度上缓解网络流量大的问题。比如在 ListView 中,就可以使用缩略图,当进入详情页时,可以使用压缩图,当点击图片时,再使用原图。还有,可以判断当前网络,如果网络是 Wifi,那可以使用原图或压缩图,如果当前是3g/4g,则要询问用户是否要使用原图/压缩图。
  • 网络请求优化连接复用、合并请求、压缩请求都是比较合理的手段。

2. 电量消耗优化

在手机电池容量已经发展到 4000mAh 的今天,电量依旧是考量手机是否强劲的重要指标之一。对于 App 来说,耗电优化是不会停止的追求,『电池终结者』最终的下场就是被卸载。

在 Android 5.0 之后,引入了一个获取设备电量消耗的 API —— Battery Historian,可以通过图形化数据分析,直观地展示手机的电量消耗过程,帮助开发者定位电量消耗的源头。

我们能做的,除了使用 Battery Historian 之外,也要少使用长时间占用后台的 Service,减少 UI 绘制的复杂度,尽量不要有死循环以及有可能大量使用 CPU 的行为。

五. 安装包大小

安装包的大小直接导致用户是否会选择下载你这个应用(当前,如果你的应用是『手机必备』的,可以另谈🥴)。另一个直观的影响就是应用的安装时间,越大的包,安装时间会越长。尤其是在 Android 5.0 之后引入了 ART 模式,由于在安装时会把程序代码转换成机器语言,安装时间会变长。所以说,安装包的大小是一个非常重要的门槛,这直接关乎到用户的使用意愿。

在 Android Studio 中,使用 Apk Analyzer 可以展示 Apk 包中每一个文件的空间占用情况,如下所示:

可见,最主要占用空间的,是 dex 文件和资源文件(包括 res、assets 等)。通常我们会采用以下几种方式优化 Apk 的体积:

  1. 代码混淆:通过 proguard 来实现,它可以在打包阶段压缩代码、优化无用代码、混淆类名等等。
  2. 资源优化:使用 Android Lint 扫描冗余资源,将文件最化;同时合理分配 drawable 的路径,对不合适的 drawable 进行删除;移除不必要的字符串资源等。
  3. 图片优化:可以对图片进行压缩处理,或者使用 webp 等格式来替换。
  4. 插件化:将功能模块分离宿主 Apk,可以大大减少 Apk 的体积。

再看看这篇