Android内存泄露浅析

基础知识

内存分配策略

根据编译原理观点,程序运行时的内存分配主要分为三块——静态存储区、堆区和栈区。

静态存储区

也称为方法区。
这块内存在程序编译的时候就分配好了,在程序整个运行期间都存在。
主要存放静态数据,全局static数据以及常量。

栈区(Stack)

执行函数时,函数参数值、局部变量等存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的容量有限。

堆区(Heap)

也称为动态内存分配区。
程序运行期间用malloc或new申请任意大小的内存,程序员自己负责适当的时候free或delete释放内存(Java则依赖垃圾回收器)。
动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。
良好的编程习惯是:如果动态内存不再使用,就需要将其释放掉

栈与堆区别

  1. 在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配
  2. 堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组
  3. 栈区的数据结构类似于先进后出的栈,进出一一对应,不产生碎片。
  4. 堆区的数据结构类似于链表,是不连续的内存区域。

Android之OOM起因

Android对dalvik的vm heapsize作了限制,当Java进程申请的内存超过阈值时,会抛出OOM异常。
程序发生OOM并不能说明RAM不足,只能说明是Java heap超出了dalvik vm heapsize的阈值。
当RAM内存不足时,memory killer会杀掉低优先级进程,保证高优先级进程有更多内存。
Google之所以这样设计,是为了让比较多的进程同时常驻内存,这样程序启动时不需要每次都加载到内存,能够给用户更快的响应。
通过限制每个应用程序的内存,使得Android系统内存中同时常驻多个进程。

dalvik虚拟机GC原理

Android虚拟机的垃圾回收采用的是根搜索算法
gc_0
如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。
最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。
如下图蓝色部分。
gc_1

内存优化

减少Service的使用

Service启动的时候,其所在进程会保持运行状态,Service所占内存不会释放,使得进程占用资源过多。
这样的话,系统LRU Cache中同时存在的进程数就会减少,应用程序切换效率也会降低。
如果没有足够的进程来处理系统中的Service,也会导致系统稳定性变差。
所以要做到:

  • 尽量少用Service,当后台任务运行完成时及时关闭Service
  • 使用IntentService代替Service,后台任务完成时自动结束服务本身

UI不可见或者内存紧张时,释放内存

在Activity的回调方法onTrimMemory(int level)中根据level的不同释放内存。

进程不在缓存中

  • TRIM_MEMORY_RUNNING_MODERATE 应用程序正在运行,并且处于非killable状态,此时设备内存低(low),系统主动杀LRU缓存中的进程
  • TRIM_MEMORY_RUNNING_LOW 应用程序正在运行,并且处于非killable状态,此时设备内存很低(much lower),需要释放没用的资源
  • TRIM_MEMORY_RUNNING_CRITICAL 应用程序正在运行,但是系统已杀死LRU缓存中的大部分进程,此时需要释放所有不至关重要的资源。如果系统不能回收足够的内存,就会清掉LRU中所有的进程以及服务进程。

进程在LRU缓存中

  • TRIM_MEMORY_BACKGROUND 系统低内存下运行,程序进程位于LRU缓存列表的开头位置。虽然程序进程被kill的概率不大,但是系统可能正在杀LRU中的进程。你需要释放容易恢复的资源以便程序进程还在LRU list中,当从其他App返回时,能快速恢复现场。
  • TRIM_MEMORY_MODERATE 系统低内存下运行,程序进程位于LRU缓存列表的中间位置。你的进程被杀掉的可能性变大。
  • TRIM_MEMORY_COMPLETE 系统低内存下运行,程序进程最先容易被系统杀死。你需要释放所有对于恢复程序状态不至关重要的资源。

API14开始有onTrimMemory()回调;API 14以下使用的是onLowMemory(),等价于TRIM_MEMORY_COMPLETE

恰当使用Bitmap

加载Bitmap的时候,尽量保证Bitmap分辨率和屏幕分辨率匹配,对于大分辨率的Bitmap需要进行压缩。

  • Android 2.3.x(API 10)及以下的系统,bitmap像素数据实际存储于native内存中,在java heap中只是保留对象的引用,因此在java heap中内部都显示同一个大小。内存回收需主动调用recycle(),GC失效。
  • 在Android 3.0(API 11)及以上的系统,bitmap像素数据存储于java heap中,无需主动调用recycle(),由GC管理内存。
  • 通过内存分析工具调试bitmap内存时,在Android 3.0的系统上进行,因为大部分内存分析工具只能分析java内存。

其他内存优化

  • 使用SparseArray,SparseBooleanArray和LongSparseArray等优化的数据容器代替HashMap
  • 使用static const代替enum
  • 非必要情况下,少用抽象
  • 对于序列化数据,使用nano protobuf
  • 尽量少使用依赖注入框架
  • 谨慎使用第三方库
  • 使用ProGuard去除不必要的代码
  • apk打包签名时,使用zipalign工具对齐
  • 使用多进程

内存泄露

内存泄露简介

内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。
无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。

引起内存泄露的因素

  • 长时间保持对Activity,Context,View,Drawable和其他对象的引用
  • 非静态内部类
  • 持有对象的时间超出需要的时间

常见内存泄露

  • 非静态内部类的静态实例:
    非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被回收掉。
  • Activity使用静态成员:
    静态变量长期维持到大数据对象的引用,阻止垃圾回收。
  • Handler、HandlerThread使用时的问题:
    下文有具体举例。
  • register某个对象后缺少对应的unregister操作:
    未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。
  • 集合对象未清理,资源对象未关闭:
    资源性对象如Cursor、File、Socket,应该在使用后及时关闭。未在finally中关闭,会导致异常情况下资源对象未被释放的隐患。
  • Dialog或PopupWindow未关闭引起的window leak
  • 不良代码造成的压力:
    如Bitmap使用不当;构造adapter时,没有使用缓存的convertView;在循环方法中创建对象。

改进建议

  • 与View无关的操作尽量使用Application Context
  • 使用静态内部类
  • Activity中尽量不要使用非静态内部类,可以使用静态内部类和WeakReference代替
  • 不要维持到Activity的长久引用,对activity的引用应该和activity本身有相同的生命周期

内存泄露举例

Handler

看一段常见的代码:

1
2
3
4
5
6
7
8
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
}

打完这段代码,IDE应该就会提醒你:

In Android, Handler classes should be static or leaks might occur.

Handler到底是怎么造成内存泄露的呢?

  1. 当你启动一个application时,它会自动在主线程创建一个Looper对象,用于处理Handler中的message。
    Looper实现了简单的消息队列,在循环中一个接一个的处理Message对象。
    大多数Application框架事件(比如Activity生命周期调用,按钮点击等)都在Message中,它们在Looper的消息队列中一个接一个的处理。
    注意Looper是存在于application整个生命周期中。
  2. 当新建了一个handler对象后,它会被分配给Looper的消息队列。
    被发送到消息队列的Message将保持对Handler的引用,因为当消息队列处理到这个消息时,需要使用Handler#handleMessage(Message)
    也就是说,只要没有处理到这个Message,Handler就一直在队列中被引用
  3. 在java中,非静态的内部Class与匿名Class对它们外部的Class有强引用。
    static inner class除外
    handler_memory_leak

尝试运行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleActivity extends Activity {

private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

这段代码发送了一个Message,将在十分钟后运行,也就是说Message将被保持引用达到10分钟,这就照成了至少10分钟的内存泄露。
所以正确的代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class SampleActivity extends Activity {

/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/

private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);

/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/

private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

结论

  • 匿名类/非静态类内部class会保持对它所在Activity的引用,使用时要注意它们的生命周期不能超过Activity,否则要用static inner class
  • 善于在Activy中的生命周期(比如onPause)中手动控制其他类的生命周期

HandlerThread使用

当我们在activity里面创建了一个HandlerThread,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public classMainActivity extends Activity
{
@Override
public void onCreate(BundlesavedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Thread mThread = newHandlerThread("demo", Process.THREAD_PRIORITY_BACKGROUND);
mThread.start();
MyHandler mHandler = new MyHandler( mThread.getLooper( ) );
...
...
...
}

@Override
public void onDestroy() {
super.onDestroy();
}
}

这个代码存在泄漏问题,因为HandlerThread实现的run方法是一个无限循环,它不会自己结束,线程的生命周期超过了Activity生命周期。
当横竖屏切换,HandlerThread线程的数量会随着activity重建次数的增加而增加。
应该在onDestroy时将线程停止掉:mThread.getLooper().quit();
另外,对于不是HandlerThread的线程,也应该确保activity消耗后,线程已经终止
可以这样做:在onDestroy时调用mThread.join();

onDestroy()或者onPause()中未及时关闭对象

线程泄漏

当你执行耗时任务,在onDestroy()的时候考虑调用Thread.close(),如果对线程的控制不够强的话,可以使用RxJava自动建立线程池进行控制,并在生命周期结束时取消订阅

Handler泄露

当退出activity时,要注意所在Handler消息队列中的Message是否全部处理完成,可以考虑removeCallbacksAndMessages(null)手动关闭

广播泄露

手动注册广播时,记住退出的时候要unregisterReceiver()

第三方SDK/开源框架泄露

ShareSDK, JPush等第三方SDK需要按照文档控制生命周期,它们有时候要求你继承它们丑陋的activity,其实也为了帮你控制生命周期

各种callBack/Listener的泄露

要及时设置为Null,特别是static的callback

EventBus等观察者模式的框架

需要手动解除注册

Service要及时关闭

比如图片上传,当上传成功后,要stopself()

Static的使用

static class/method/variable 的区别

static inner class 与 non static inner class 的区别

static inner class 即静态内部类,它只会出现在类的内部,在某个类中写一个静态内部类其实同你在IDE里新建一个.java 文件是完全一样的
下面为其对比:

static inner class non-static inner class
与外部class引用关系 如果没有传入参数,就无引用关系 自动获得强引用(implicit reference)
被调用时需要外部实例 不需要(比如Bulider类) 需要
能否调用外部class中的变量与方法 不能
生命周期 自主的生命周期 依赖于外部类,甚至比外部类更长

可以看到,非静态内部类埋下了内存泄露的隐患,如果其生命周期比Activity长的话,就会发生泄露,还可能发生难以预防的空指针问题。
上面的Handler就是典型的例子。

static inner method

静态内部方法,可以被直接调用,而不用去依赖它所在的类。
比如你需要随机数,只用调用Math.random()即可,而不用实例化Math这个对象。
在工具类(Utils)中,建议用static修饰方法。
static方法的调用不会泄露内存

static inner variable

static 变量称为静态变量或者类变量,它由类的所有实例共享。
当ClassLoader停止加载这个Class时,它才会回收。
在Android中,需要手动置空才会卸掉ClassLoader,才能出现GC。
下面这段谷歌博客上的著名代码演示了一次内存泄露的,当你旋转屏幕后,Drawable就会泄露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);

TextView label = new TextView(this);
label.setText("Leaks are bad");

if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);

setContentView(label);
}

不过这个问题貌似在新的API中用弱引用修复了

单例导致内存泄露

看下面这个例子:
single_instance_leak_1
single_instance_leak_2
可以看出ImageUtil这个工具类是一个单例,并引用了activity的context。
试想这个场景,应用起来以后,转屏。
转屏以后,旧MainActivity会destroy,新MainActivity会重建,导致单例ImageUtil重新getInstance。
不幸的是,由于instance已经不是空的了,所以ImageUtil不会重建,还持有之前的Context,也就是之前的那个MainActivity实例的context,因此会造成两个问题:

  1. 功能问题:使用ImageUitl访问context相关内容时可能会发生异常(因为当前context并不是当前activity的context)
  2. 内存泄露:旧context被生命周期更长的静态变量持有而导致activity无法释放造成泄漏!(因此静态变量是很容易因此内存泄露的!)

使用内部匿名类要注意什么?

匿名内部类实际上就是non-static inner class,比如某些初学者经常一个new Handler就写出来了,它对外部类有一个强引用。
建议单独写出来这个类并继承,并加入static修饰。