0 App内存优化
纸上谈兵系列第二期,关于App的内存优化。
0.1 内存管理
Android系统是基于Linux内核开发的开源操作系统,而linux系统的内存管理有其独特的动态存储管理机制。不过Android系统对Linux的内存管理机制进行了优化,Linux系统会在进程活动停止后就结束该进程,而Android把这些进程都保留在内存中,直到系统需要更多内存为止。这些保留在内存中的进程通常情况下不会影响整体系统的运行速度,并且当用户再次激活这些进程时,提升了进程的启动速度。
0.2 垃圾回收
无论是ART还是Dalvik虚拟机,都和众多Java虚拟机一样,属于一种托管内存环境(程序员不需要显示的管理内存的分配与回收,交由系统自动管理)。托管内存环境会跟踪每个内存分配, 一旦确定程序不再使用一块内存,它就会将其释放回堆中,而无需程序员的任何干预。 回收托管内存环境中未使用内存的机制称为垃圾回收。
Android的内存堆是分代式(Generational)的,意味着它会将所有分配的对象进行分代,然后分代跟踪这些对象。 例如,最近分配的对象属于年轻代(Young Generation)。 当一个对象长时间保持活动状态时,它可以被提升为年老代(Older Generation),之后还能进一步提升为永久代(Permanent Generation),这一点和JVM的垃圾回收基本一致。
0.3 内存限制
Dalvik堆被限制为每个应用程序进程的单个虚拟内存范围。这定义了逻辑堆大小,它可以根据需要增长,但最多只能达到系统为每个应用程序定义的限制。
Dalvik堆不会压缩堆的逻辑大小,这意味着Android不会对堆进行碎片整理以关闭空间。 Android只能在堆末尾有未使用的空间时缩小逻辑堆大小。但是,系统仍然可以减少堆使用的物理内存。垃圾收集后,Dalvik遍历堆并找到未使用的页面,然后使用madvise将这些页面返回到内核。因此,大块的配对分配和解除分配应该导致回收所有(或几乎所有)所使用的物理内存。但是,从小分配中回收内存可能效率低得多,因为用于小分配的页面仍可能与尚未释放的其他内容共享。
0.4 限制应用的内存
为了维护高效的多任务环境,Android为每个应用程序设置了堆大小的硬性限制。 该限制因设备而异,取决于设备总体可用的RAM。 如果应用程序已达到该限制并尝试分配更多内存,则会收到 OutOfMemoryError 。
在某些情况下,你可能希望查询系统以准确确定当前设备上可用的堆空间大小,例如,确定可以安全地保留在缓存中的数据量。 你可以通过调用 getMemoryClass() 来查询系统中的这个数字。 此方法返回一个整数,指示应用程序堆可用的兆字节数。
0.5 切换应用
当用户在应用程序之间切换时,Android会将非前台应用程序(即用户不可见或并没有运行诸如音乐播放等前台服务的进程)缓存到一个最近最少使用缓存(LRU Cache)中。例如,当用户首次启动应用程序时,会为其创建一个进程; 但是当用户离开应用程序时,该进程不会退出。 系统会缓存该进程。 如果用户稍后返回应用程序,系统将重新使用该进程,从而使应用程序切换更快。
如果你的应用程序具有缓存进程并且它保留了当前不需要的内存,那么即使用户未使用它,你的应用程序也会影响系统的整体性能。 当系统内存不足时,就会从最近最少使用的进程开始,终止LRU Cache中的进程。另外,系统还会综合考虑保留了最多内存的进程,并可能终止它们以释放RAM。
当系统开始终止LRU Cache中的进程时,它主要是自下而上的。 系统还会考虑哪些进程占用更多内存,因为在它被杀时会为系统提供更多内存增益。 因此在整个LRU列表中消耗的内存越少,保留在列表中并且能够快速恢复的机会就越大。
1 内存抖动
现象:内存锯齿状,频繁GC导致卡顿。
原因:频繁创建对象和回收对象,导致GC过于频繁。
构造带有内存抖动的代码:
final Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
String s = "";
for (int i = 0; i < 1000; i++) {
s += i;
}
Log.d(TAG, "run: " + s);
handler.post(this);
}
});
这里会在for循环时不断的进行String的拼接操作,会造成不断创建对象并且回收对象,所以这就是很好的内存抖动的代码。
内存抖动分析图:
使用Android Studio自带的Profile工具进行Memory分析,可以看到内存下面有很多回收的标记(垃圾桶)。选择一段时间,此时可以看到内存的分配(方框1)和释放数量(方框2)。点击数量对应的类,可以在右边查看调用栈,我们可以看到调用栈为MainActivity的匿名内部类的run方法。
避免内存抖动需要注意:
- for/onDraw循环以及其他会被多次调用的方法里不要直接new对象
- 对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
- 使用StringBuilder代替频繁的"+"号操作
2 内存泄露
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。[百度百科]
在Android中,内存泄露可能会由于内存无法被释放,导致内存使用逐渐增加,最终可能会引起OOM(OutOfMemoryError)。
2.1 Android中检测内存的泄露
在App的开发阶段,使用Proflie工具进行内存分析,检查打开某个页面并且关闭后,强制GC完成,内存并没有减少或者回到主页面后,GC完成,内存与最初始内存相差很大如果上述现象发生,那么可以考虑是由于内存泄漏引起的。
2.2 内存泄露定位
- 如果明确知道是某个页面导致的内存泄露,可以使用Profile工具进行分析,在GC完成后,查看当前内存中存在的对象。查找并不应该存在的对象,通过查找引用树即可定位。
- 开发阶段使用LeakCannary,可以有效地在开发阶段发现内存泄露。有关LeakCannary原理,查看这里
- 线上内存泄露检测比较难,而且需要消耗流量,总体来说弊大于利,不建议线上检测。
- 设定场景线上Dump(例如超过最大内存80%),使用
Debug.dumpHprofData(“fileName”);
进行获取当前堆栈信息,并上传服务器,最终通过MAT手动分析。 - 将LeakCannary带到线上,发生内存泄露时上报服务器,并且将文件上传。
- 设定场景线上Dump(例如超过最大内存80%),使用
3 内存泄露需要关注点
- 通过addListener/addXXXz形式添加的各种回调,需要在相关的位置remove。
- 通过Handler执行post/postDelay时,可以考虑在destory位置remove相关回调。
- 使用匿名内部类/内部类时需谨慎。匿名内部类/会隐式持有外部类的引用,例如我们在定义成员变量时,
new Handler(){}
并重写方法,此时Android Studio会提示我们This Handler class should be static or leaks might occur (anonymous android.os.Handler)
。我们可以使用静态匿名内部类/内部类,通过WeakReference将需要的外部对象包装,从而解决上面的问题。 - 使用Application Context代替Activity Context(允许的情况下)
- Bitmap对象需要及时回收
- 游标(Cursor)注意关闭
- 谨慎使用static对象
3 内存溢出
做了几年的Android开发,没有遇到过几次OOM都不好意思说自己做过Android。OOM的原因上面也说了,Android系统会为每个App分配最大的内存,当App使用的内存超过了该最大内存,就会出现OutOfMemoryError。
OOM出现的原因基本上由以下几点造成:
- Bitmap
- 内存泄露
- 过多的对象存储,并且不能释放(ListView/RecyclerView数据特别多,导致内存中存在特别多的对象)
如何优化内存占用,从而降低OOM的发生呢?可以从以下几点考虑:
- 图片加载/处理时要慎重,可以通过压缩/缓存/设置inBitmap等等方式减少内存消耗,关于Bitmap使用的优化,可以查看google官方文档
- 使用轻量级的数据结构
例如:可以考虑使用SparseArray代替Map,SparseArray是Android对特殊Map的优化。 - 资源压缩
可以考虑将设计给的资源图片进行压缩(在可压缩的前提下),从而降低内存的使用。 - 可以尝试使用对象池进行内存的复用
- ListView/GridView/RecyclerView要将item进行复用
- 优化布局层级,减少内存消耗
- 使用protobuffer/flatbuffers进行序列化数据
4 思考
关于App的内存优化,是个持续的过程。随着时间的推移,会有更多更先进的技术出现,任何技术都需要更新。因此关于App的内存优化并没有一劳永逸的优化方案,只有持续并且不断寻找出优化点才是最好的解决方案。