Toc
  1. 一. 稳定性
    1. 1. ANR
    2. 2. Crash
    3. 3. 死锁
    4. 4. Panic
  2. 二. 内存占用
    1. 内存泄漏的原因
    2. 内存泄漏的检测和定位
    3. 内存泄漏的解决
  3. 三. 流畅度
    1. 1. 界面绘制问题
    2. 2. 数据处理问题
  4. 四. 资源消耗
    1. 1. 网络流量优化
    2. 2. 电量消耗优化
  5. 五. 安装包大小
Toc
0 results found
Android 优化浅谈

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 布局文件;
    • 多使用 标签重用 layout;
    • 使用 标签替换父级布局;
    • 使用 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 的体积。

再看看这篇

打赏
支付宝
微信
本文作者:CodingRabbit
版权声明:本文首发于CodingRabbit的博客,转载请注明出处!