这可能是最好的性能优化教程(三)

2,428 阅读12分钟

这可能是最好的性能优化教程系列专栏
这可能是最好的性能优化教程(一)
这可能是最好的性能优化教程(二)
这可能是最好的性能优化教程(三)

前言

内存泄漏从来都是我们老生常谈的话题,无论是 Android Studio 自带的内存泄漏分析工具还是专业的 Eclipse MAT 抑或是备受青睐的第三方插件 LeakCanary,都为我们的内存泄漏检测提供了便利。如果从根源上解决内存泄漏,内存优化必不可少。所以本章节我们参考扔物线胡凯的内存优化策略,直接拿出一章节来谈内存优化。

内存优化基本可以分为下面几个方面

  • 减少对象的内存占用
  • 对内存对象进行复用
  • 避免对象的内存泄漏
  • 内存使用策略优化

减少对象的内存占用

避免在 Android 里面使用 Enum

Enum 是 Java 中包含固定常量的数据类型,当需要知道预先定制的几个值,这几个值表示一些数据类,我们都可以使用 Enum。我们一般用 Enum 做一些编译时检查,以避免传入不合法的参数。

但 Enum 的每个对象都是 Object,在 Android 官网上就早已明确指出应该在 Android 开发中避免使用 Enum,因为与静态常量想必,它对内存的占用是要大很多的。

因此在实际开发中,我更加倾向于接口变量,因为接口会自动把成员变量设置为 static 和 final 的,这一点可以防止某些情况下错误地添加新的常量,这也使得代码看起来更加简单和清晰。

使用更加轻量的数据结构

前面第一节已经说过,我们应该更加倾向于考虑使用 ArrayMapSparseArray 而不是 HashMap 等传统数据结果,前面已经用图示演示了 HashMap 的简要工作原理,相比起 Android 系统专门为移动操作系统编写的 ArrayMap 容器,在大多数情况下,都显示效率低下,更占内存。通常的 HashMap 的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录 Mapping 操作。另外,SparseArray 更加高效在于他们避免了对 keyvalueautobox 自动装箱,并且避免了装箱后的解箱。

使用更小的图片

在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的 InflationException。假设有一张很大的图片被 XML 文件直接引用,很有可能在初始化视图的时候就会因为内存不足而发生 InflationException,这个问题的根本原因其实是发生了 OOM。

减少 Bitmap 对象的内存占用

Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用是很重要的,通常来说有下面2个措施:

  • inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
  • decode format:解码格式,选择 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差异。

尽量地采用 int 类型

Android 系统中 float 类型的数据存取速度是 int 类型的一半,尽量优先采用 int 类型。而同样能作为整数的代名词,采用 int 替换 Integer 会让你的内存开销更小。

对内存对象进行复用

复用系统自带的资源

Android 系统本身内置了很多的资源,例如字符串 / 颜色 / 图片 / 动画 / 样式以及简单布局等等,这些资源都可以在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小 APK 的大小,另外还可以一定程度上减少内存的开销,复用性更好。但是也有必要留意 Android 系统的版本差异性,对那些不同系统版本上表现存在很大差异,不符合需求的情况,还是需要应用程序自身内置进去。

注意 ListView / GridView 的 Adapter 对 ConvertView 进行复用

这个貌似没啥好说的,太基础了,而且我们可能现在更加青睐于 RecyclerView

尽量的采用 StringBuilder

这个也特别基础,我们点到为止。大概就是尽量的采用 StringBuilder / StringBuffer 来替换我们频繁的字符串拼接。

尽量使用原字符串的 subString

当从已经存在的数据集中抽取出 String 的时候,尝试返回原数据的 subString 对象,而不要创建一个重复的对象。

避免在 onDraw() 里面执行对象的创建

类似 onDraw() 等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的 gc,甚至是内存抖动。

避免对象的内存泄漏

内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现 OOM。显然,这还使得每级 Generation 的内存区域可用空间变小,gc 就会更容易被触发,容易出现内存抖动,从而引起性能问题。

注意 Activity 的泄漏

通常来说,Activity 的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的 Activity 泄漏:

  • 内部类引用导致 Activity 的泄漏
    最典型的场景是 Handler 导致的 Activity 泄漏,如果 Handler 中有延迟的任务或者是等待执行的任务队列过长,都有可能因为 Handler 继续执行而导致 Activity 发生泄漏。此时的引用关系链是 Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在 UI 退出之前,执行 remove Handler 消息队列中的消息与 runnable 对象。或者是使用 Static + WeakReference 的方式来达到断开 Handler 与 Activity 之间存在引用关系的目的。

  • Activity Context 被传递到其他实例中,这可能导致自身被引用而发生泄漏。
    内部类引起的泄漏不仅仅会发生在 Activity 上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用 static 类型的内部类,同时使用 WeakReference 的机制来避免因为互相引用而出现的泄露。

尽量地采用 Application Context

对于大部分非必须使用 Activity Context 的情况(Dialog 的 Context 就必须是Activity Context),我们都可以考虑使用 Application Context 而不是 Activity 的 Context,这样可以避免不经意的 Activity 泄露。

而且如果习惯 Glide 的童鞋可能会发现,Glide 需要传递的 Context 如果是 Activity 的 Context ,那么在 Activity 被销毁后还没加载出来的话还会引发崩溃。所以,请在使用 Glide 或者 Toast 等的时候,直接传递 Application Context 吧。

注意 Cursor 对象是否及时关闭

在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用 Cursor 之后没有及时关闭的情况。这些 Cursor 的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对 Cursor 对象的及时关闭。

注意 WebView 的泄漏

Android中 的 WebView 存在很大的兼容性问题,不仅仅是 Android 系统版本的不同对 WebView 产生很大的差异,另外不同的厂商出货的 ROM 里面 WebView 也存在着很大的差异。更严重的是标准的 WebView 存在内存泄露的问题,看这里。所以通常根治这个问题的办法是为 WebView 开启另外一个进程,通过 AIDL 与主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

注意临时 Bitmap 对象的及时回收

虽然在大多数情况下,我们会对 Bitmap 增加缓存机制,但是在某些时候,部分 Bitmap 是需要及时回收的。例如临时创建的某个相对比较大的 Bitmap 对象,在经过变换得到新的 Bitmap 对象之后,应该尽快回收原始的 Bitmap,这样能够更快释放原始 Bitmap 所占用的空间。

需要特别留意的是 Bitmap 类里面提供的 createBitmap() 方法:


这个函数返回的 Bitmap 有可能和 source bitmap 是同一个,在回收的时候,需要特别检查 source bitmap 与 return bitmap 的引用是否相同,只有在不等的情况下,才能够执行 source bitmap 的 recycle() 方法。

注意监听器的注销

在 Android 程序里面存在很多需要 register 与 unregister 的监听器,我们需要确保在合适的时候及时 unregister 那些监听器。自己手动 add 的 listener,需要记得及时 remove 这个 listener。

内存使用策略优化

谨慎使用 large heap

Android 设备根据硬件与软件的设置差异而存在不同大小的内存空间,他们为应用程序设置了不同大小的 Heap 限制阈值。你可以通过调用 getMemoryClass() 来获取应用的可用 Heap 大小。在一些特殊的情景下,你可以通过在 manifest 的 application 标签下添加 largeHeap = true 的属性来为应用声明一个更大的 heap 空间。然后,你可以通过 getLargeMemoryClass() 来获取到这个更大的 heap size 阈值。然而,声明得到更大 Heap 阈值的本意是为了一小部分会消耗大量 RAM 的应用 ( 例如一个大图片的编辑应用 ) 。不要轻易的因为你需要使用更多的内存而去请求一个大的 Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用 large heap。因此请谨慎使用 large heap 属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次 gc 的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap 并不一定能够获取到更大的 heap。在某些有严格限制的机器上,large heap 的大小和通常的 heap size 是一样的。因此即使你申请了 large heap,你还是应该通过执行 getMemoryClass() 来检查实际获取到的 heap 大小。

资源文件需要选择合适的文件夹进行存放

我们知道 hdpi / xhdpi / xxhdpi 等等不同 dpi 的文件夹下的图片在不同的设备上会经过 scale 的处理。例如我们只在 hdpi 的目录下放置了一张 100 x 100 的图片,那么根据换算关系,xxhdpi 的手机去引用那张图片就会被拉伸到 200 x 200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到 assets 或者 nodpi 的目录下。

Try catch某些大内存分配的操作

在某些情况下,我们需要事先评估那些可能发生 OOM 的代码,对于这些可能发生 OOM 的代码,加入 catch 机制,可以考虑在 catch 里面尝试一次降级的内存分配操作。例如 decode bitmap 的时候,catch 到 OOM,可以尝试把采样比例再增加一倍之后,再次尝试 decode。

谨慎使用 static 对象

因为 static 的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在 Android 中应该谨慎使用 static 对象。

特别留意单例对象中不合理的持有

虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。特别是持有 Context 的引用,需要谨慎对待

优化布局层次,减少内存消耗

越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的 View 无法实现足够扁平的时候考虑使用自定义 View 来达到目的。

谨慎使用多进程

使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著增加内存。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。

一个典型的例子是创建一个可以长时间后台播放的 Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些 UI 资源也没有办法得到释放。类似这样的应用可以切分成 2 个进程:一个用来操作 UI,另外一个给后台的 Service。

写在最后

内存优化并不就是说程序占用的内存越少就越好,如果因为想要保持更低的内存占用,而频繁触发执行 gc 操作,在某种程度上反而会导致应用性能整体有所下降,这里需要综合考虑做一定的权衡。

如果想第一时间收到更新信息的可以关注我的简书:简书地址
你也可以选择关注我的公众号:nanchen