写给程序员的内存泄漏治理手册

作者: Geekholt | 来源:发表于2019-11-05 17:40 被阅读0次

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

当一个短生命周期对象销毁且不会再被使用时,长生命周期对象依然持有了短生命周期的对象的引用,这样这个短生命周期对象就一直不会被GC释放,这样就造成了内存泄漏。内存泄漏显然是对内存的一种浪费,当一个项目中有多处内存泄漏时,就非常容易产生OOM。在Android中,一般内存泄漏都是由于Activity destory后,其引用不能释放导致的,本文主要介绍一些内存泄漏的常见场景及其解决方案

内存泄漏的场景

单例造成内存泄漏

单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏
如下这个典例:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

这样不管传入什么Context最终将使用ApplicationContext,而单例的生命周期和应用的一样长,这样就防止了内存泄漏

非静态内部类创建静态实例造成内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
        //...
    }
    class TestResource {
    //...
    }
}

这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext

Handler造成内存泄漏

Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:

public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    //...
    }
};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

这种创建Handler的方式会造成内存泄漏,由于mHandlerHandler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference reference;
        public MyHandler(Context context) {
        reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
            activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在ActivityDestroy时或者Stop时应该移除消息队列中的消息,更准确的做法如下:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference reference;
        public MyHandler(Context context) {
        reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
            activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();mHandler.removeMessages();来移除指定的RunnableMessage

线程造成内存泄漏

对于线程造成的内存泄漏,也是平时比较常见的,异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成,
那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:

static class MyAsyncTask extends AsyncTask {
    private WeakReference weakReference;

    public MyAsyncTask(Context context) {
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params) {
        SystemClock.sleep(10000);
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity) weakReference.get();
        if (activity != null) {
        //...
        }
    }
}
static class MyRunnable implements Runnable{
    @Override
    public void run() {
        SystemClock.sleep(10000);
    }
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();

这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。

资源未关闭造成内存泄漏

对于使用了BraodcastReceiverContentObserverFileCursorStreamBitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

集合类造成内存泄漏

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。如下面的例子,当然实际上我们在项目中肯定不会这么写,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

WebView造成的内存泄漏

混合开发时经常用到WebView加载html等页面,而WebView的内存泄漏就是最经常遇到的问题,尤其是当项目中需要用Webview加载的页面比较多时

即使当我退出页面时在我的BrowserActivityonDestroy()方法中进行内存占用回收(如下)但并没有效果:

mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;
  • 方法一

很多文章的给出方法是这样的:不要在布局文件中定义WebView的节点,而是在需要的时候动态生成。你可以在需要WebView的布局位置放一个LinearLayout,需要时在代码中动态生成WebViewadd进去,然后在Activity onDestory()的时候销毁WebView。这个方法的关键点在于动态创建WebView时传入的是ApplicationContext,而不是ActivityContext,但是在很多情况下会报错,这个出错应该是WebView的某些特殊动作产生由ApplicationActivity的类型转换错误,所以这个方法不是特别推荐

  • 方法二

为加载WebView的界面开启新进程,在该页面退出之后关闭这个进程

  • 方法三

从根源解决(推荐)。WebView引起的内存泄漏主要是因为org.chromium.android_webview.AwContents 类中注册了component callbacks,但是未正常反注册而导致的

org.chromium.android_webview.AwContents 类中有这两个方法 onAttachedToWindowonDetachedFromWindow,系统会在attachdetach处进行注册和反注册component callback
onDetachedFromWindow() 方法的第一行中:

if (isDestroyed()) return;, 

如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister的操作。我们的activity退出的时候,都会主动调用 WebView.destroy() 方法,这会导致 isDestroyed() 返回 truedestroy()的执行时间又在onDetachedFromWindow之前,所以就会导致不能正常进行unregister
解决方法就是:让onDetachedFromWindow先走,在主动调用destroy()之前,把webview从它的parent上面移除掉

ViewParent parent = mWebView.getParent();
if (parent != null) {
    ((ViewGroup) parent).removeView(mWebView);
}

mWebView.destroy();

完整的activityonDestroy()方法如下

@Override
protected void onDestroy() {
    if( mWebView!=null) {

        // 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
        // destory()
        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();

    }
    super.on Destroy();
}

EditText造成内存泄漏

Android 输入法会导致内存泄露,基本原因就是inputMethodManager持有了EditText的引用,进而持有了activity的引用导致的内存泄露,现在提供的基本方法就是通过反射把inputMethodManager以及相关的持有引用赋值null,但因为Android平台的多样性,内部代码被修改的乱七八糟,该种方法适用性不高

推荐使用下面这个方法

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;

import java.lang.reflect.Field;

/**
 * 防止出现内存泄漏
 */
@SuppressLint("AppCompatCustomView")
public class BaseEditText extends EditText {
    private static Field mParent;

    static {
        try {
            mParent = View.class.getDeclaredField("mParent");
            mParent.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public BaseEditText(Context context) {
        super(context.getApplicationContext());
    }

    public BaseEditText(Context context, AttributeSet attrs) {
        super(context.getApplicationContext(), attrs);
    }

    public BaseEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context.getApplicationContext(), attrs, defStyleAttr);
    }

    @Override
    protected void onDetachedFromWindow() {
        try {
            if (mParent != null)
                mParent.set(this, null);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
        super.onDetachedFromWindow();
    }
}

属性动画造成内存泄漏

属性动画不用的时候要及时cancel()

if (mUpAnimatorSet != null) {
     if (mUpAnimatorSet.isRunning()) {
         mUpAnimatorSet.cancel();
     }
     mUpAnimatorSet.removeAllListeners();
     for (Animator animator : mUpAnimatorSet.getChildAnimations()) {
         animator.removeAllListeners();
     }
     mUpAnimatorSet = null;
}

Fragment中使用RecyclerView造成内存泄漏

Fragemnt中使用RecyclerView可能导致内存泄漏,需要小心

 @Override
    public void onDestroyView() {
        super.onDestroyView();
        mRecyclerView = null;
        mSwipeRefreshLayout = null;
        mAdapter = null;
        notDataView = null;
        errorView = null;
    }

内存泄漏的检测工具

  • Android Profiler

操作方式:如果想测试某个Activity有没有内存泄漏,可以将当前Activity不断进行横竖屏切换(或不断重复进入Activity又返回的操作),如果结果如下图所示,那基本可以确定这个Activity发生了内存泄漏


具体使用可以参考:https://www.jianshu.com/p/a934df19c42e

  • LeakCanary

LeakCanary官方文档:https://github.com/square/leakcanary

LeakCanary工作原理:LeakCanary2.0使用及原理分析 — Kotlin重构版

可以在项目中直接集成LeakCanary,非常方便,一般内存泄漏问题都能够得到及时的监控和解决,如果碰到比较棘手的问题,无法正确跟踪到内存泄漏的位置,可以使用Android Profiler或MAT等工具进行进一步分析

总结

内存泄漏是”内存杀手“,但是往往非常隐蔽,所以个人比较建议集成LeakCanary,同时要重视CodeReview,在开发测试过程中及时发现并解决内存泄漏问题。如果有必要,还可以做一些线上内存监控,但是线上内存监控往往成本比较大(在监控的同时也会有一些性能影响),需要制定针对自己实际项目的更加精细化的方案

相关文章

网友评论

    本文标题:写给程序员的内存泄漏治理手册

    本文链接:https://www.haomeiwen.com/subject/pojebctx.html