谈谈android中的内存泄漏

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/H_Gao/article/details/78893156

写在前面

内存泄漏实际上很多时候,对于开发者来说不容易引起重视。因为相对于crash来说,android中一两个地方发生内存泄漏的时候,对于整体没有特别严重的影响。但是我想说的是,当内存泄漏多的时候,很容易造成他OOM的,因为android给每个app的分配的内存是有限的,而且当发生内存泄漏的时候,对于app的体验也会不好,容易造成卡顿等不好的体验。

Java内存结构

这里写图片描述
上面展示的是Java虚拟机运行时数据区的模型图

这里简单介绍下

  • 方法区:存储已加载的类信息,常量池,静态变量(静态变量与app的生命周期同步)
  • 虚拟机栈:存储基本类型和对象引用
  • 堆:存储新建的对象或数组对象
  • 程序计数器:线程执行字节码文件位置的指示器,用于线程切换后,再次切换回来能够准确执行上次执行到的字节码文件位置。
  • 本地方法栈:用于记录Native方法。

我们主要关注这几个内存区域,new出来的对象,放在Heap堆这个区域,这块内存区由GC(Garbage Collector)负责内存的回收。对于Java程序员来说,很多时候我们不需要关心内存的分配与回收问题,但是这并不表示Java没有内存泄漏,Java的内存泄漏显得更为隐蔽,于是这不仅需要我们在开发时避免写出容易造成内存泄漏的问题代码,还需要我们在出现内存泄漏时掌握相应的技巧去定位和发现问题。

Java 对象在内存中的三种状态

对象的三种状态

  1. 可达状态
    当一个对象被创建后,有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,那它就处于可达状态,程序可通过引用变量来调用该对象的属性和方法。
  2. 可恢复状态
    如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象,在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存。在回收该对象之前,系统会调用可恢复状态的对象的finalize方法进行资源清理,如果系统在调用finalize方法重新让一个以上的引用变量引用该对象,则这个对象会再次变为可达状态;否则,该对象进入不可达状态。
  3. 不可达状态
    当对象的所有关联被切断,且系统调用所有对象的finalize方法依然没有使得该对象变为可达状态,则这个对象将永久性地失去引用,最后变为不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。
    GC Root
    左边的object1,object2,object3和object4是仍然存活的对象,或者说可达状态的对象,而右边的object5,object6和object7则是判定可回收的状态,或者说不可达状态的对象

这里说到的GC Roots,一般作为GC Roots的对象有下面几个(书本上的)可达性算法的根节点(简单来说,这些对象一般不会被GC 回收):
a 虚拟机栈(栈桢中的本地变量表)中的引用的对象

b.方法区中的类静态属性引用的对象

c.方法区中的常量引用的对象

d.本地方法栈中JNI的引用的对象

为什么会有内存泄漏

上面所说的不可达状态的判断,在Java中是由 可达性分析算法 来实现的。其基本思路就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可达的。
在android中尤为明显,因为android中很多对象都是有生命周期的。当它们的任务完成之后,它们将被垃圾回收。如果在对象的生命周期本该结束的时候,这个对象还被一系列的引用,这就会导致内存泄漏。

简单来说,没有被GC ROOTS间接或直接引用的对象的内存会被回收。

那么为什么会有内存泄漏呢
来看一段Java代码:

List list = new ArrayList();
for (int i = 1; i < 100; i++) {
    Object object = new Object();
    list.add(object);
    object = null;   
}

针对上面的代码我大致画了下简图,我们将object置为null,object变为无用对象,但是GC这时并不能回收object的内存,因为list仍然在引用object对象,故这就会导致内存泄漏。
image

我们循环申请Object对象,并将所申请的对象放入一个 ArrayList 中,如果我们仅仅释放引用本身,那么 ArrayList 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到ArrayList后,还必须从 ArrayList 中删除。或者我们将ArrayList 对象设置为 null。

其实简单来说内存泄漏就是当对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用。

android中的内存泄漏

来到我们的主题,android中的内存泄漏大部分是指Activity的内存泄漏,因为Activity对象会间接或者直接引用View,Bitmap等,所以一旦无法释放,会占用大量内存。

android内存泄漏的具体场景

错误使用Context

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) {
            instance = new Singleton(context);
        }
        return instance;
    }
}

项目中封装许多工具类,这些工具类有的会被设计成单例模式。

很正常的一个单例模式,可就由于传入的是一个 Context,而这个 Context 的生命周期的长短就尤为重要了。如果我们传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出的时候,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会回收,最后造成内存泄漏。

这里我们在设计单例的时候尽量把这部分拦截下来,尽量使用ApplicationContext,而不用生命周期短的Activity,容易造成Activity的内存泄漏。

Handler使用造成内存泄漏

我们在界面上经常会有自动滚动的Banner以及用TextSwitcher实现的自动轮播的公告消息的功能,这样的自动滚动通常通常都是通过Handler来不断的发消息来实现自动滚动,如下面的代码。由于这样的是一个耗时操作,当前Activity被finish掉的时候,Handler仍然持有外部类Activity的引用,导致Activity不能被GC回收,所以导致内存泄露。

import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;


public class MainActivity extends AppCompatActivity {


    private static final int TYPE_CHANGE_AD = 1;
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            if(message.what == TYPE_CHANGE_AD) {
                //do something
            }
            return false;
        }
    }) ;

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

       mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(TYPE_CHANGE_AD);
            }
        },5000);

    }
}

针对Handler造成的内存泄漏,一般有两种方法来解决:

  • 第一种是在Activity onDestroy的时候,将一些已经在排队的msg remove掉,通过removeCallbacksAndMessages来把在当前Handler持有的消息队列的msg移除掉。
  • 第二种是使用WeakReference来处理。
public class MainActivity extends Activity {

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

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

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

  private final MyHandler mHandler = new MyHandler(this);
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mHandler.sendEmptyMessage(TYPE_CHANGE_AD);
            }
        },5000);
    finish();
  }
}

强引用,软引用,弱引用,虚引用

与Handler有点类似的,我们在Activity中使用Thread(或者TimeTask)的时候去做一些耗时操作的时候,也很容易造成内存泄漏。通常我们在Activity的里面通常都是采用内部类的时候来用的,这种跟Handler机制一样,因为内部类默认都会持有外部类的引用,当Activity finish的时候,Thread里面的操作可能还没结束,这时Activity被Thread所占用,导致无法回收。针对这种情况,网上建议是采用静态内部类,因为静态内部类是不会持有外部类的引用的。

针对这种情况,最好的方式我觉得还是用rxjava来做,能用rxjava来做的尽量用rxjava做。

InputMethodManager造成的内存泄漏

使用LeakCanary检测到的内存泄漏,LeakCanary是Square公司是开发的用于检测内存泄漏的开源库,我们知道Java最常见的内存分析工具是MAT,MAT的使用比较简单,但是用MAT分析内存相对比较麻烦。而LeakCanary这个开源库则不一样,在简单的代码引入LeakCanary的时候,它就能够替我们检测程序中的内存泄漏,当出现内存泄漏的时候还能以一个非常友好的节目来显示内存泄漏的引用链。如下图所示:

InputMethodManager造成的内存泄露

上面展示的是InputMethodManager造成的内存泄漏,InputMethodManager造成的内存泄漏看起来比较奇怪,毕竟是Google设计的API,查了一些资料,InputMethodManager.mServedView持有一个最后聚焦View的引用,直到另外的一个View聚焦后才会释放当前的View。当发生GC时mServedView持有的View的引用不会被回收,导致了内存泄漏。

针对InputMethodManager造成的内存泄漏,LeakCanary这个库在AndroidExcludedRefs.java页列举了很多内存泄漏的场景,同时针对InputMethodManager也有提供了相应的解决方法。

在反编译今日头条的代码发现有下面这段代码,发现这段代码是通过反射来解决InputMethodManager造成的内存泄露,反射这种方式相对比较暴力点,但是针对InputMethodManager造成的内存泄漏目前没发现比较好的方式。

 public static void releaseInputMethodManagerFocus(Activity paramActivity)
  {
    if (paramActivity == null);
    while (true)
    {
      return;
      try
      {
        InputMethodManager localInputMethodManager = (InputMethodManager)paramActivity.getSystemService("input_method");
        if (localInputMethodManager != null)
        {
          Method localMethod = InputMethodManager.class.getMethod("windowDismissed", new Class[] { IBinder.class });
          if (localMethod != null)
            localMethod.invoke(localInputMethodManager, new Object[] { paramActivity.getWindow().getDecorView().getWindowToken() });
          paramActivity = InputMethodManager.class.getDeclaredField("mLastSrvView");
          if (paramActivity != null)
          {
            paramActivity.setAccessible(true);
            paramActivity.set(localInputMethodManager, null);
            return;
          }
        }
      }
      catch (Throwable paramActivity)
      {
        paramActivity.printStackTrace();
      }
    }
  }

使用高德地图来地位出现的内存泄露,由于定位会在多个页面使用,所以讲高德地图定位抽取成了一个工具类,

下面这个是高德地图的内存泄漏

image

这里出现内存泄漏场景一般是都是我们写的代码不正确导致的。
拿高德地图举例:

第一是我们通过AMapLocationClient去开启地位的时候,不要忘了在Activity Destroy的时候调用stopLocation和onDestroy方法。
第二是我们的MapView需要与Activity的生命相绑定,不然也容易造成内存泄漏。

与高德地图这样的类似就是,SensorManager,EventBus,BroadCast的注册,在调用register方法后不要忘记调用unregister方法。

WedView造成的内存泄漏

WebView跟MapView有点类似,对于WebView首先要注意的是,Activity onDestroy的时候注意将WebView移除掉:

 @Override
    public void onDestroy() {
        super.onDestroy();
        if (mWebview != null) {
            if (mWebview.getParent() != null) {
                ((ViewGroup) (mWebview.getParent())).removeView(mWebview);
            }
            mWebview.destroy();
            mWebview = null;
        }
    }

但是对于WebView需要加载大量数据的时候,例如需要加载很多图片的时候,这个时候对于这种场景可以考虑将WebView放进一个垃圾进程,在Activity onDestroy的时候,需要调用Process.killProcess(Process.myPid())将当前进程kill掉。

@Override
    public void onDestroy() {
        super.onDestroy();
       Process.killProcess(Process.myPid())
    }
  • 有效增大App的运存,减少由webview引起的内存泄露对主进程内存的占用。
  • 避免WebView的Crash影响App主进程的运行。
  • 拥有对WebView独立进程操控权。

采用这种方式可能需要设计到进程间通信相关,aidl,messager。

关于Activity内存泄漏的场景很多,但是无论是何种案例,都离不开一些表格中集中大的分类。

引用的方式/GC Root Class-(静态变量) 活着的线程 生命周期跟随app的特殊存在
mContext间接引用 静态View,InputMethodManager SensorManager、WifiManager(其他Service进程都可以) ViewRootImpl
(this$0间接引用) 内类引用 匿名类/Timer/TimerTask/Handler
数据库,流等资源未释放

这里首先简单说明下,

  • 第一种:Activity的Context被静态变量所持有导致内存泄漏
class Person {
    static Object obj = new Object();
}

根据jvm的分代的回收机制,Person类的信息将会被存入方法区永久(Permanent)代。也就是说,Person类、obj引用变量都将存在Permanent里,这会导致obj对象一直有效,从而使得obj对象不能被回收。
同样的道理,Activity里面的静态变量同样会造成Activity不能被回收从而导致内存泄漏。

  • 第二种:内部类(this$0)造成Activity的泄漏
    内部类是很容易造成内存泄漏的,因为内部类都能够访问外部类的成员变量,默认内部类都会持有外部类的引用,即this$0这样的字串,当内部类和外部类的生命周期不一致的时候,在android中最典型就是Handler了。

Message对象有个target字段,该字段是Handler类型,引用了当前Handler对象。一句话就是:你通过Handler发往消息队列的Message对象持有了Handler对象的引用。假如Message对象一直在消息队列中未被处理释放掉,你的Handler对象就不会被释放,进而你的Activity也不会被释放。这种现象很常见,当消息队列中含有大量的Message等待处理,你发的Message需要等几秒才能被处理,而此时你关闭Activity,就会引起内存泄露。如果你经常send一些delay的消息,即使消息队列不繁忙,在delay到达之前关闭Activity也会造成内存泄露。

  • 第三种 资源没释放(数据库连接,Bitmap,IO流)
3634 3644 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
3634 3644 E JavaBinder: android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=866 (# cursors opened by pid 1565=866)
3634 3644 E JavaBinder: at android.database.CursorWindow.(CursorWindow.java:104)
3634 3644 E JavaBinder: at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
3634 3644 E JavaBinder: at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:141)
3634 3644 E JavaBinder: at android.database.CursorToBulkCursorAdaptor.getBulkCursorDescriptor(CursorToBulkCursorAdaptor.java:143)
3634 3644 E JavaBinder: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
3634 3644 E JavaBinder: at android.os.Binder.execTransact(Binder.java:367)
3634 3644 E JavaBinder: at dalvik.system.NativeStart.run(Native Method)

数据库的Cursor在使用完要记得close,Bitmap如果是采用createBitmap这种方法创建的在释放的时候要记得调用recycle方法,IO流在使用之后要记得close,这种比较小的地方虽然不是必现问题,但是会占用资源,这种未释放的资源多了场景多了很容易造成内存泄漏。

写在最后

  • 内类是非常危险的编码
  • 在使用Handler,Thead,TimeTask的时候需要需要注意
  • 对于资源一定要记得释放
  • 对于一些系统API容易造成内存泄漏的地方可以重点关注下。
展开阅读全文

没有更多推荐了,返回首页