一、Android 内存问题
要对Android 内存进行优化首先要知道的是Android 内存存在什么问题,哪里存在问题,才能对症下药,精准解决问题。
1.1 内存抖动
内存抖动即内存频繁分配和回收导致内存不稳定。频繁创建对象,导致内存不足或者产生内存碎片,内存碎片即内存不连续,有内存空洞, 某两个正在使用的内存中间有一个间隔, 这个间隔虽然也被算在可用内存里面, 但实际上因为它过小, 当我们申请内存的时候,经常是需要申请一定量的连续内存, 而这些碎片小内存不符合要求,是不能拿来使用的。频繁GC会导致卡顿,随后不连续的内存片无法被分配,可分配的内存减少,便最终可能导致OOM。
简单来说就是:在程序需要对象的时候,在堆当中分配出来一块空间,使用完毕以后, GC 帮我们清理掉这片内存空间,如果频繁的一直持续上述操作,就会引起内存抖动。
1.2 内存抖动常见场景
1.2.1 集合类存在不对称机制
集合类如果仅仅有添加元素的机制,而没有相应删除元素机制,这样就会造成内存被占用,如果这个类是全局性变量(比如类中有静态属性,全局性的map等即有静态引用或final一直指向它)。那么没有相应删除机制,很可能导致集合所占内存只增不减。
- 解决办法:在使用集合类时,增加删除元素机制,并适当调用减少集合所占内存。
1.2.2 不正确使用单例模式
不正确使用单例模式,也会引起内存泄漏单例对象在初始化后将在JVM的整个生命周期存在(以静态变量方式),如果单例对象持有外部对象的引用,那么这个外部对象就会一直占用着内存,可能导致内存泄漏(取决于这外部对象是否一致有用)。
- 解决办法:单例对象中避免含有不是一直都有用的外部对象引用。
1.2.3 不正确使用Android组件 或 特殊集合对象
BraodcastReceiver ,ContentObserver ,fileObserver ,Cursor,Callback 等在 Activity onDestory 或者某类生命周期结束 之后一定要 unregistere 或者 close 掉,否则这个Activity类会被system强引用,不会被回收。
不要直接对Activity进行直接引用作为成员变量,如果不得不这么做,调用 private WeakPeferense mActivity 来做,相同的,对与 Service 等其他有自己生命周期的对象来说,直接引用都需要考虑是否会存在内存泄露的可能。
1.2.4 Handler 使用问题
要知道,只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。由于 Handler 属于 TLS(Thread Local Storage)变量,生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 view 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。如上所述,Handler使用要特别小心,否则很可能内存泄漏。
- 解决办法:在view 或者Activity生命周期结束前,确保Handler已没有未处理的消息(特别是延时消息)。
1.2.5 Thread 内存泄漏
线程也是造成内存泄露的一个重要源头,线程产生内存泄露的主要原因在于线程生命周期不可控,比如线程是 Activity的内部类,则线程对象中保存了 Activity 的一个引用,当线程的 run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的Activity就出现了内存泄漏问题。
- 解决办法:
- 1. 简化线程run函数执行的任务,使他在Activity生命周期结束前,任务运行完。
- 2. 为Thread增加撤销机制,当Activity生命周期结束时,将Thread的耗时任务撤销(推荐)。
1.2.6 一些不良代码造成的内存压力
有些代码并不造成内存泄漏,但是他们是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
(1) Bitmap 没调用recycle()
Bitmap 对象在不使用时,我们应该先调用recycle()释放内存,然后才置空,因为加载bitmap对象的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过jni调用的,Android的Bitmap底层是使用skia图形库实现,skia是用c实现的)。这个recycle()函数就是针对c部分的内存释放。(2)构造Adapter时,没有使用缓存的convertView。
解决办法:使用静态holdview的方式构造Adapter。
二、Android Profile使用
手机上运行要分析 APP,再在 AS 的 Profiler 窗口选择要调试的进程,打开 CPU Profiler。得到如下图所示信息。这里有 3 个配置选项,
分别是
- AS3.0时的堆转储功能,捕获一个内存快照,
- 记录 C / C++ 层的对象
- 记录 Java / Kotlin 对象
然后我们记录一段时间的内存之后
如上图所示,我们可以获取指定时间的当前内存分配情况。在左下角的类列表中,您可以查看以下信息:
- Allocations:在选定时间段内通过 malloc() 或 new 运算符分配的对象数。
- Deallocations:在选定时间段内通过 free() 或 delete 运算符解除分配的对象数。
- Allocations Size:在选定时间段内所有分配的总大小(以字节为单位)。
- Deallocations Size:在选定时间段内所有已释放内存的总大小(以字节为单位)。
- Total Count:Allocations 列中的值减去 Deallocations 列中的值所得的结果。
- Remaining Size:Allocations Size 列中的值减去 Deallocations Size 列中的值所得的结果。
接下来先模拟一段内存抖动代码:
public class MemoryShakeActivity extends AppCompatActivity {
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 创造内存抖动(编写耗内存的操作)
for (int index = 0; index <= 100; index++) {
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_shake);
findViewById(R.id.bt_memory).setOnClickListener(v -> mHandler.sendEmptyMessage(0));
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
运行起来后,内存是平稳的:
点击按钮后,开始出现锯齿状:
可以看到上面的连续垃圾桶就代表不断地在GC,我们获取一个区域的内存信息:
我们可以看到锯齿的位置,String[] 的分配是相对比较大的; Shallow Size是该类型实例的总大小(以字节为单位)。于是现在可以锁定,String[] 是最可疑的引起内存抖动的原因, 点击左边的String[]行项,工具会在右边,弹出另外一个窗口, 窗口上边是分配出来的该类型的所有实例(<工具右上>), 点击任意一个实例, 又会在下边弹出一个该实例的内存分配的堆栈信息(<工具右下>——Allocation Call Stack), 信息即这个实例占有的这块内存是在哪里分配的。
三、内存泄漏
3.1 概况
所谓内存泄漏即是本该被回收的内存垃圾没有被回收。
与内存抖动的区别是:
内存抖动是指:在短时间内频繁发生申请内存 & 回收内存的操作,即频繁GC
本文参考:
博客:
Android | App内存优化 之 内存抖动解决实战
Android | App内存优化 之 全面理解MAT
Android | App内存优化 之 内存泄漏 要点概述 以及 解决实战
使用Android Studio和MAT进行内存泄漏分析
使用内存分析器查看应用的内存使用情况