Android 内存泄漏分析总结
GitHub地址
https://github.com/realxz/MemoryLeak
GitHub 代码只包含泄漏情况,不包括修改后的代码,大家可以下载下来后,自行修改。
什么是内存
Android 系统我们的 APP 分配的内存大小是有限的,我现在用的手机小米 4c 为我自己开发的应用 分配的256MB的内存大小,不同的手机型号,不同的 ROM 分配的内存大小不一定一样,这里面所提 到的内存一般是指 Android 手机的 RAM。
RAM 包含寄存器,栈,堆,静态存储区域,常量池。通常我们所说的 Android 内存泄漏中的内存, 指的是其中的堆内存。一般来来说,我们 new 出来的对 象都会存储在堆内存中,这部分的内存由 GC 进行回收管理。
GC 是什么,它如何进行内存管理
GC 指垃圾回收器 「Garbage Collection」。Java 使用 GC 进行内存回收理,不用我们手动释放内 存,提升了我们的开发效率。那GC回收对象的依据是什么呢 ?简单的说,对于一个对象,若果不存 在从 GC 根节点到该对象的引用链 (从根节点不可到达的 (从根节点不可到达的),那么对于 GC 来说这个对象就是需要被回收的,反之该对象是从根节点可到达的,那么这个对象就不会被 GC 回 收。
根节点:在 Java 中可以作为根节点的对象有很多,这块内容我理解的不是很到位。我很简单的把它理解为 Android 应用的主线程,存活的子线程,栈中的对象以及静态属性引用的对象。
注意:这里的引用是指强引用,在 Java 当中存在4种引用类型分别是「强引用」、「软引用」、「弱引用」、「虚引用」。如果没有特别指定,我们所说的引用都是指强引用,GC 不会回收具有 强引用的对象。
什么是内存泄漏
我们已经知道了,如果某个对象,从根节点可到达,也就是存在从根节点到该对象的引用链,那么该对象是不会被 GC 回收的。如果说这个对象已经不会再被使用到了,是无用的,我们依然持有他的引用的话,就会造成内存泄漏,例如 一个长期在后台运行的线程持有 Activity 的引用,这个时 候 Activity 执行了 onDestroy 方法,那么这个 Activity 就是从根节点可到达并且无用的对象, 这个 Activity 对象就是泄漏的对象,给这个对象分配的内存将无法被回收。
内存泄漏的影响
- 内存很宝贵,即使从效率,责任的角度上,我们也应该降低内存的使用,减少内存的浪费。
- 内存泄漏导致可用内存越来越少,最终导致OOM。
- 可用内存减少,GC 被触发,虽然 GC 可以帮助我们回收无用内存,但是频繁的触发 GC 也会影响性能,可能造成程序卡顿。
如何查找、定位内存泄漏
- MAT「Memory AnalysisTools」,网上有很多的使用教程,个人感觉使用较繁琐,需要耐心分析和一定的经验才能定位出内存泄漏。
- LeakCanary,Square 公司开源作品,使用方便,可以直接定位到泄漏的对象,并且给出调用链。
内存泄漏事例
非静态内部类
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class InnerThreadActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_thread);
RunningThread runningThread = new RunningThread();
runningThread.start();
}
class RunningThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void closeActivity(View view) {
this.finish();
}
}
当我们运行这段代码的时候,LeakCanary 会帮我们检测出来内存泄漏,如图:
![image_1bbluquud1ndr12k41mdnpbmiv9.png-59.9kB][1]
在 Java 中,内部类会隐式的持有外部类的引用。我们可以很清楚的看见 RunningThread 对象持有 了 InnerThreadActivity 的引用,由于 RunningThread 线程会一直运行下去,我 finish 掉当前 的 Activity 就会导致 InnerThreadActivity 实例发生泄漏。我们可以采用静态内部类的方式来解 除这种内存泄漏的隐患,代码如下:
private static class RunningThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:尽量使用静态内部类来替代内部类,同时避免让长期运行的任务( 线程 )持有 Activity的引用。
匿名内部类
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class AnonymousThreadActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anonymous_thread);
Thread anonymousThread = new Thread() {
@Override
public void run() {
while (true) {
//do something
try {
Thread.sleep(60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
}
};
anonymousThread.start();
}
public void closeActivity(View view) {
this.finish();
}
}
LeakCanary 同样会检测出内存泄漏,如图:
![image_1bbluvkf51um53c04fucmr1g8cm.png-69.7kB][2]
在 Java 中,匿名内部类和非静态内部类一样,都会持有外部类的引用。上面的代码正式由于 Thread 的匿名类持有了 AnonymousThreadActivity 的引用,并且匿名类的运行时间长达 1 分钟, 在这段时间内,我 finish 掉了 Activity 导致了内存泄漏,解决方式和非静态内部类的方法一样,使用静态内部类来代替匿名内部类,这里就不贴代码了。
Handler 内存泄漏
package com.example.xiezhen.memoryleak;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class HandlerActivity extends AppCompatActivity {
private TextView tvShowMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
MemoryLeakHandler handler = new MemoryLeakHandler();
handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
}
class MemoryLeakHandler extends Handler {
@Override
public void handleMessage(Message msg) {
tvShowMessage.setText("MemoryLeak");
Toast.makeText(HandlerActivity.this, "memory leak", Toast.LENGTH_SHORT).show();
}
}
public void closeActivity(View view) {
this.finish();
}
}
![image_1bblv16hj148t1bvmjge13jd5if13.png-79.1kB][3]
LeakCanary 为我们展示了内存泄漏的引用链,这段代码泄漏的原因也是因为非静态内部类持有了外部类的引用。图中的引用链涉及到 Android 中的消息机制 「Handler」、「MessageQueue」、 「Looper」。大致叙述一下,我们的 MemoryLeakHandler 因为内部类的关系会持有 HandlerActivity 实例的引用,我们使用 Handler 来发送消息,这个Handler 会被消息中 target 属性引用,这个 Message 会在我们主线程的消息队 列中存活 10 秒钟,在这段时间内,我 finish 掉当前 Activity 就会造成内存泄漏,并且依然会弹出 Toast 尽管我们已经开不见这个 Activity了。
解决方案依然是采用静态内部类来替代非静态内部类,并且使用 WeakReference 来引用 Activity,如果对象只存在弱引用的话,GC 是会回收这部分内存的。
package com.example.xiezhen.memoryleak;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
public class HandlerActivity extends AppCompatActivity {
private TextView tvShowMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
MemoryLeakHandler handler = new MemoryLeakHandler(this);
handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
}
private static class MemoryLeakHandler extends Handler {
private WeakReference<HandlerActivity> weakReference;
public MemoryLeakHandler(HandlerActivity activity) {
this.weakReference = new WeakReference<HandlerActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
HandlerActivity activity = weakReference.get();
if (activity != null) {
activity.tvShowMessage.setText("MemoryLeak");
Toast.makeText(activity, "memory leak", Toast.LENGTH_SHORT).show();
}
}
}
public void closeActivity(View view) {
this.finish();
}
}
单例/静态引用
static volatile EventBus defaultInstance;
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class EventBusActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_event_bus);
EventBus.getDefault().register(this);
EventBusThread thread=new EventBusThread();
thread.start();
}
public void closeActivity(View view) {
this.finish();
}
private static class EventBusThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
EventBus.getDefault().post("eventbus");
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void receiveMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
EventBus 我相信大家都不陌生,我们一般在使用 EventBus 的时候,都会使用EventBus.getDefault( ) 方法来获取一个 EventBus 单例,这个单例是静态的,全局可访问的。上面的代码我们在获取 EventBus 单例后调用 register 方法,将 EventBusActivity 注册到 EventBus 中,这个时候 EventBus 就会持有 Activity 的引用,由于单例是静态的,生命周期和整个 App 生命周期 一致,如果我们不调用 unRegister 方法的话,EventBusActivity 实例就会泄漏。上述代码中,我特意没有去调用 unRegister 方法,我们来看看 LeakCanary 的结果:
![image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png-88kB][4]
- 不要让我们的对象被静态属性所引用,这很容易造成内存泄漏。
- 一般来说我们在使用注册方法的时候,library 都会提供相对应的解除注册方法,不要忘了调用!
Activity Context & Application Context
package com.example.xiezhen.memoryleak;
import android.content.Context;
import android.content.pm.PackageInfo;
/**
* Created by xiezhen on 2017/3/16.
*/
public class CommonHelper {
private Context context;
private static CommonHelper commonHelper = null;
private CommonHelper(Context context) {
this.context = context;
}
public static CommonHelper getCommonHelper(Context context) {
if (commonHelper == null) {
commonHelper = new CommonHelper(context);
}
return commonHelper;
}
public int getVersionCode() {
PackageInfo packInfo = getPackageInfo(context);
if (packInfo != null) {
return packInfo.versionCode;
} else {
return -1;
}
}
public String getVersionName() {
PackageInfo packInfo = getPackageInfo(context);
if (packInfo != null) {
return packInfo.versionName;
} else {
return "";
}
}
private PackageInfo getPackageInfo(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (Exception e) {
return null;
}
}
}
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class ContextActivity extends AppCompatActivity {
private TextView tvVersionName;
private TextView tvVersionCode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_context);
tvVersionName = (TextView) findViewById(R.id.tv_version_name);
tvVersionCode = (TextView) findViewById(R.id.tv_version_code);
}
@Override
protected void onResume() {
super.onResume();
setVersionCode(CommonHelper.getCommonHelper(this).getVersionCode());
setVersionName(CommonHelper.getCommonHelper(this).getVersionName());
}
private void setVersionName(String versionName) {
tvVersionName.setText(versionName);
}
private void setVersionCode(int versionCode) {
tvVersionCode.setText(String.valueOf(versionCode));
}
public void closeActivity(View view) {
this.finish();
}
}
我写了一个工具类,来获取 App 的 Version Code 和 Version Name,这段代码同样会导致内存泄漏,下面是 LeakCanary 的泄露图。
![image_1bblvctrqieh39h9asvsm9p91t.png-60.1kB][5]
发现泄露了一个 Context 实例,在调用 CommonHelper 中的方法时候的时候,我们将 ContextActivity 作为一个 Context 对象传递了进去,Context 对象的引用被长期持有导致内存泄漏。处理这种泄漏的方法很简单,使用 Application Context 来代替 Activity Context 即可,Application Context 在整个 App 生命周期内适用。
结论
一般来说,内存泄漏都是因为泄漏对象的引用被传递到该对象的范围之外,或者说内存泄漏是因为持有对象的长期引用,导致对象无法被 GC 回收。为了避免这种情况,我们可以选择在对象生命周期结束的时候,解除绑定,将引用置为空,或者使用弱引用。
- 由于 Context 导致内存泄漏。使用 Application Context 代替 Activity Context,避免长期持有 Context 的引用,引用应该和 Context 自身的生命周期保持一致。
- 由于非静态内部类、匿名内部类导致内存泄。它们会隐式的持有外部类的引用,一不小心长期持有该引用就会导致内存泄漏,使用静态内部类来代替它们。
- Handler 导致内存泄漏。原因和第二点一样,同样使用静态内部类的实现方式,同时对需要引用的对象/资源采用弱引用的方式。
- EventBus导致内存泄漏。EventBus 的单例特性,会长期持有注册对象的引用,一定要在对象生命周期结束的时候,接触注册,释放引用。同样对于系统提供的一些成对出现的方法,我们也需要成对的调用,例如 BroadcastReceiver 的 registerReceiver( ) 方法和 unRegisterReceiver( ) 方法。
- 线程导致内存泄漏。我们经常会执行一些长期运行的任务,避免在这些任务中持有 Activity 对象的引用,如果持有了引用的话,我们应该在对象生命周期结束的时候,释放引用。
参考链接
- http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
- http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html
- https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
- https://medium.com/freenet-engineering/memory-leaks-in-android-identify-treat-and-avoid-d0b1233acc8#.45ldsvqll
-
http://droidyue.com/blog/2016/11/23/memory-leaks-in-android/index.html
[1]: http://static.zybuluo.com/xiezhen/odev0wja0q36wc92pdrmpd22/image_1bbluquud1ndr12k41mdnpbmiv9.png
[2]: http://static.zybuluo.com/xiezhen/ak2lk5h73l0axpcf7kmz8ib5/image_1bbluvkf51um53c04fucmr1g8cm.png
[3]: http://static.zybuluo.com/xiezhen/ddv0e7hhwbtvf22ev4tkkt6e/image_1bblv16hj148t1bvmjge13jd5if13.png
[4]: http://static.zybuluo.com/xiezhen/58kb04ti3bgd5ttfgxcajx9b/image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png
[5]: http://static.zybuluo.com/xiezhen/kvxq3e0vkmd7n5fxd8g4zlgf/image_1bblvctrqieh39h9asvsm9p91t.png