美文网首页
内存泄漏篇

内存泄漏篇

作者: ana生 | 来源:发表于2017-12-13 11:07 被阅读0次

1.静态变量造成的内存泄漏

public class MainActivity extends Activity{
private static final String TAG = "MainActivity";

private static Context sContext;
   
private static View sView;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //这里直接把当前Activity赋值给了静态变量sContext
    sContext = this;
    //这种写法和上面的类似
    sView = new View(this);
}
}

当MainActivity对象完成任务需要回收时,却有一个静态变量引用它(静态变量的生命周期与Application相同),造成内存泄漏

2.单例模式造成的内存泄漏

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;
}
}

我们来分析下为什么会产生内存泄漏呢?

AppManager appManager=AppManager.getInstance(this);
这句传入的是Activity的Context,我们都知道,Activty是间接继承于Context的,当这Activity退出时,Activity应该被回收, 但是单例中又持有它的引用,导致Activity回收失败,造成内存泄漏。为了以防误传Activity的Context , 我们可以修改一下单例的代码,如下:

public class AppManager {
private static AppManager instance;
private Context context;

private AppManager(Context context) {
    this.context = context.getApplicationContext();
}

public static AppManager getInstance(Context context) {
    if (instance == null) {
        instance = new AppManager(context);
    }
    return instance;
}
}

这样子修改,不管外面传入什么Context,最终都会使用Applicaton的Context,而我们单例的生命周期和应用的一样长,这样就防止了内存泄漏。

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

public class NonStaticActivity extends AppCompatActivity {
private static Config sConfig;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_non_static);
    //Config类并不是静态类,
    sConfig = new Config();
}
class Config {

}
}

首先,非静态内部类默认会持有外部类的引用。
然后又使用了该非静态内部类创建了一个静态的实例。
该静态实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
正确的做法有两种,一种是将内部类Config改成静态内部类,还有就是将Config抽取出来,封装成一个单例,如上一个例子那样,但是需要context时单例要切记注意Context的泄漏,使用applicationContext。

4.WebView造成的内存泄漏

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

mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;

当我点开了多少条新闻内存中就存在多少个BrowserActivity的实例,说明我退出时这个BrowserActivity没有被回收,这样的话当我浏览的新闻比较多时,内存就会累积存在一定的OOM风险,而且新闻界面一般存在大量图片,所以这个问题是必须要解决的。

a. 不要在.xml文件中定义webview节点

不要再布局文件中定义webview的节点,而是在需要的时候动态生成。你可以在需要webview的布局位置放一个LinearLayout,需要时在代码中动态生成webview并add进去:

mWebView=new WebView(this);
LinearLayout linearLayout  = findViewById(R.id.xxx);
linearLayout.addView(mWebView);

然后在onDestroy()方法中调用:

@Override
protected void onDestroy() {
if( mWebView!=null) {
   mWebView.setVisibility(View.GONE);
   mWebView.removeAllViews();
   mWebView.destroy();
}
super.onDestroy();
}

关于创建webview时mWebView=new WebView(this);到底是传入ApplicationContext还是Activity的context:个人建议还是传activity的context。传ApplicationContext貌似可以防止webview对activity的引用,但是在很多情况下会报错,很多都有说到webview加载的页面弹出dialog时会出现报错;所以传入activity的context然后做好销毁工作。

b. 手动删除引用

这个方法在我的项目中没有效果,但原文博主说在他的项目中效果很好,也许对其他人的情况有效,在这里也记下来。

public void setConfigCallback(WindowManager windowManager) {
try {
    Field field = WebView.class.getDeclaredField("mWebViewCore");
    field = field.getType().getDeclaredField("mBrowserFrame");
    field = field.getType().getDeclaredField("sConfigCallback");
    field.setAccessible(true);
    Object configCallback = field.get(null);

    if (null == configCallback) {
        return;
    }

    field = field.getType().getDeclaredField("mWindowManager");
    field.setAccessible(true);
    field.set(configCallback, windowManager);
} catch(Exception e) {
}
}

然后在activity中调用:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setConfigCallback(WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
}

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}
c. 进程

为加载WebView的界面开启新进程,在该页面退出之后关闭这个进程。
这个方法我没有测试,不知道应用和效果如何,有兴趣的可以试试。

d.从根源解决(划重点)

前面的方法都没有解决我内存泄漏的问题,然后我看到了一篇文章是从源码角度分析了webview内存泄漏的原因,最后按作者的方法解决了问题,后面会贴上原文地址。这里简单说一下:
原文里说的webview引起的内存泄漏主要是因为org.chromium.android_webview.AwContents 类中注册了component callbacks,但是未正常反注册而导致的。

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

if (isDestroyed()) return;

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

让onDetachedFromWindow先走,在主动调用destroy()之前,把webview从它的parent上面移除掉。
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.destroy();

完整的activity的onDestroy()方法:

@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();
}

这个方法亲测有效。

5.Handler造成的内存泄漏

我在我的项目中使用了handler,此时mHandler会隐式地持有一个外部类对象引用这里就是MainActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler();
private TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_handler);

    mTextView = (TextView) findViewById(R.id.text);//模拟内存泄露
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            mTextView.setText("test");
        }
    }, 5 * 60 * 1000);
}
}

解决办法是 在HandlerActivity onDestroy里面移除消息队列中所有消息和所有的Runnable。

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

其他原因造成的内存泄漏
造成内存泄漏的原因有很多,我们这里只是列举了其中比较典型的几种,当然还有好多原因会造成内存泄漏,比如资源开启但是未关闭、多线程等等等等。但是我们有LeakCanary这个利器哈。

相关文章

网友评论

      本文标题:内存泄漏篇

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