1 内存泄漏简介
内存泄漏是指内存空间使用完毕后无法被释放的现象。尽管Java有垃圾回收机制(GC),但是对于还保持着引用,逻辑上却已经不会再用到的对象,垃圾回收器不会回收它们。
内存泄漏带来的危害:
- 用户对单次的内存泄漏并没有什么感知,但当可用的空闲空间越来越少,GC就会更容易被触发,GC进行时会停止其他线程的工作,因此有可能会造成界面卡顿等情况。
- 后续需要分配内存的时候,很容易导致内存空间不足而出现 OOM(内存溢出)。
2 常见的内存泄漏场景
2.1 static 关键字修饰成员变量
被 static 关键字修饰的成员变量的生命周期等于应用程序的生命周期。若使被 static 关键字修饰的成员变量引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期大于引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露。
-
static Activity
这里也会提示有内存泄漏。 -
static View
如果一个 View 初始化耗费大量资源,而且在一个 Activity 生命周期内保持不变,那可以把它变成 static,加载到视图树上(View Hierachy)。当 Activity 被销毁时,应当释放资源,否则就会导致内存泄漏。
解决方案:
- 尽量避免 static 成员变量引用资源耗费过多的实例(如 Context),若需引用 Context,则尽量使用Applicaiton的 Context。
- 使用弱引用(WeakReference) 代替强引用持有实例。
2.2 非静态内部类/ 匿名类
非静态内部类 / 匿名类默认持有外部类的引用,而静态内部类则不会。常见的情况有以下三种。
2.2.1 非静态内部类
如果非静态内部类所创建的实例是静态的,其生命周期等于应用的生命周期。非静态内部类默认持有外部类的引用而导致外部类无法释放,最终造成内存泄露。即外部类中持有非静态内部类的静态对象。
public class MainActivity extends AppCompatActivity {
//非静态内部类的静态实例引用
public static InnerClass innerClass = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//保证非静态内部类的实例只有1个
if (innerClass == null) {
innerClass = new InnerClass();
}
}
// 非静态内部类
private class InnerClass {
//...
}
}
当 MainActivity 销毁时,因非静态内部类单例的引用,innerClass 的生命周期等于应用的生命周期,持有外部类 MainActivity 的引用,故 MainActivity 无法被 GC 回收,从而导致内存泄漏。
解决方案:
- 将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
- 该内部类抽取出来封装成一个单例
- 尽量避免非静态内部类所创建的实例是静态的。
2.2.2 多线程:AsyncTask、实现 Runnable 接口、继承 Thread 类
当工作线程正在处理任务时,如果外部类销毁, 由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成内存泄露。
2.2.2.1 AsyncTask
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startAsyncTask();
}
private void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
//执行耗时操作
while(true);
}
}.execute();
}
}
2.2.2.2 实现 Runnable 接口
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
class MyRunnable implements Runnable {
@Override
public void run() {
//执行耗时操作
}
}
}
2.2.2.3 继承 Thread 类
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyThread().start();
}
private class MyThread extends Thread {
@Override
public void run() {
//执行耗时操作
}
}
}
解决方案:
- 使用静态内部类的方式,静态内部类不默认持有外部类的引用。
private static class MyThread extends Thread {
@Override
public void run() {
//执行耗时操作
}
}
- 当外部类结束生命周期时,强制结束线程。使得工作线程实例的生命周期与外部类的生命周期同步。
@Override
protected void onDestroy() {
super.onDestroy();
myThread.interrupt();
}
2.3 Handler
在 Handler 消息队列还有未处理的消息 / 正在处理消息时,消息队列中的 Message 持有 Handler 实例的引用。如果 Handler 是非静态内部类 / 匿名内部类(2种使用方式),就会默认持有外部类的引用(如 MainActivity 实例)。
上述的引用关系会一直保持,直到 Handler 消息队列中的所有消息被处理完毕。在 Handler 消息队列还有未处理的消息 / 正在处理消息时,此时若需销毁外部类 MainActivity,但由于上述引用关系,垃圾回收器(GC)无法回收 MainActivity,从而造成内存泄漏。
public class MainActivity extends AppCompatActivity {
private MyHandler myHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myHandler = new MyHandler();
new Thread() {
@Override
public void run() {
try {
//执行耗时操作
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//发送消息
myHandler.sendEmptyMessage(1);
}
}.start();
}
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
//处理消息事件
}
}
}
解决方案:
- 使用静态内部类+弱引用的方式,保证外部类能被回收。因为弱引用的对象拥有短暂的生命周期,在垃圾回收器线程扫描时,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
public class MainActivity extends AppCompatActivity {
private MyHandler myHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myHandler = new MyHandler(this);
new Thread() {
@Override
public void run() {
try {
//执行耗时操作
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//发送消息
myHandler.sendEmptyMessage(1);
}
}.start();
}
public void test() {
Log.d("MainActivity", "test");
}
private static class MyHandler extends Handler {
//定义弱引用实例
private WeakReference<Activity> reference;
//在构造方法中传入需持有的Activity实例
public MyHandler(Activity activity) {
//使用 WeakReference 弱引用持有 Activity 实例
reference = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
//处理消息事件
//调用Activity实例中的方法
((MainActivity) reference.get()).test();
}
}
}
- 当外部类结束生命周期时,清空 Handler 内消息队列。
使用建议:
为了保证 Handler 中消息队列中的所有消息都能被执行,此处推荐使用解决方案1,即静态内部类+弱引用的方式。
2.4 资源对象使用后未关闭
对于资源的使用(如广播 BraodcastReceiver、文件流 File、数据库游标 Cursor、图片资源 Bitmap等),若在 Activity 销毁时无及时关闭 / 注销这些资源,则这些资源将不会被回收,从而造成内存泄漏。
解决方案:
//对于广播BroadcastReceiver:注销注册
unregisterReceiver(broadcastReceiver);
//对于文件流File:关闭流
inputStream / outputStream.close();
//对于数据库游标cursor:使用后关闭游标
cursor.close();
//对于图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null
bitmap.recycle();
bitmap = null;
// 对于动画(属性动画),将动画设置成无限循环播放setRepeatCount(ValueAnimator.INFINITE);后
// 在Activity退出时记得停止动画
animator.cancel();
关闭以上对象的时候注意做非空判断。
2.5 WebView内存泄露
WebView 内部的一些线程持有 Activity 对象,使得 Activity 无法释放,从而导致内存泄漏。
解决方案:
@Override
protected void onDestroy() {
if (mWebView != null) {
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
不建议在 xml 中创建 WebView,因为在 xml 中创建的 WebView 会持有 Activity 的 Context 对象。
2.6 单例模式造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton(Context context){
this.mContext = context;
}
public static Singleton getInstance(Context context){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton(context);
}
}
}
return instance;
}
}
这是一个单例模式,当创建这个单例的时候,由于需要传入一个 Context:
- 如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这没有问题。
- 如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 的内存并不会被回收,这就造成泄漏了。
解决方案:
将 new Singleton(context) 改为 new Singleton(context.getApplicationContext()) 即可,这样便和传入的 Activity 没关系了。
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton(Context context){
this.mContext = context;
}
public static Singleton getInstance(Context context){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton(context.getApplicationContext());// 使用Application 的context
}
}
}
return instance;
}
}
3 内存泄漏分析工具
3.1 lint
lint 是一个静态代码分析工具,同样也可以用来检测部分会出现内存泄露的代码,平时编程注意 lint 提示的各种黄色警告即可。如:
也可以手动检测,在 Android Studio 中选择 Analyze->Inspect Code。
然后会弹出弹窗选择检测范围。
点击 OK 等待分析结果:
这个工具除了会检测内存泄漏,还会检测代码是否规范、是否有没用到的导包、可能的bug、安全问题等等。
3.2 Memory Profile
3.3 LeakCanary