内存泄漏检测以及如何防止

370 阅读5分钟

前言:

什么是内存泄漏? Android开发从GC root分析内存泄漏 - 简书 (jianshu.com)

如何发现内存泄漏:

一般从两个方面检查内存泄漏:

   -- back键:  进入应用->一些操作 >back键退出应用到桌面

   -- home键: 进入应用->一些操作 >home键退出应用到桌面

不断的重复上述操作, 结合LeakCanary和Profile 和adb shell dumpsys meminfo发现内存泄漏

内存泄漏的分类:

-- 显式内存泄漏:  能被检测工具发现的内存泄漏:

    不断地操作back键, 一般可以被LeakCanary和Profile 检测到,

-- 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才被释放

   可以通过 home键操作结合adb shell dumpsys meminfo发现内存不断增加

一, 检测工具

1.LeakCanary 

如何使用: Getting Started - LeakCanary (square.github.io)

分析:

一系列操作后: 检查logcat

搜索关键字:  Leaking: YES

如: Activity.shouldShowRequestPermissionRationale 导致的内存泄漏:

详情见: Android S原生系统内存泄露问题案例_reportfragment-CSDN博客

Leaking 4:  shouldShowRequestPermissionRationale 导致:

 GC Root: Global variable in native code
01-28 10:46:33.163 D/LeakCanary( 8100): ├─ android.app.AppOpsManager$4 instance
01-28 10:46:33.163 D/LeakCanary( 8100):     Leaking: UNKNOWN
01-28 10:46:33.163 D/LeakCanary( 8100):     Retaining 9.3 kB in 110 objects
01-28 10:46:33.163 D/LeakCanary( 8100):     Anonymous subclass of com.android.internal.app.IAppOpsStartedCallback$Stub
01-28 10:46:33.163 D/LeakCanary( 8100):      AppOpsManager$4.val$callback
01-28 10:46:33.163 D/LeakCanary( 8100):                       ~~~~~~~~~~~~
01-28 10:46:33.163 D/LeakCanary( 8100): ├─ android.permission.PermissionUsageHelper instance
01-28 10:46:33.163 D/LeakCanary( 8100):     Leaking: UNKNOWN
01-28 10:46:33.171 D/LeakCanary( 8100):     Retaining 126 B in 5 objects
01-28 10:46:33.171 D/LeakCanary( 8100):     mContext instance of com.xxx.MainActivity with mDestroyed = true
01-28 10:46:33.171 D/LeakCanary( 8100):      PermissionUsageHelper.mContext
01-28 10:46:33.171 D/LeakCanary( 8100):                             ~~~~~~~~
01-28 10:46:33.172 D/LeakCanary( 8100): ╰→ com.xxx.MainActivity instance
01-28 10:46:33.172 D/LeakCanary( 8100):      Leaking: YES (ObjectWatcher was watching this because com.xxx.MainActivity received
01-28 10:46:33.172 D/LeakCanary( 8100):      Activity#onDestroy() callback and Activity#mDestroyed is true)
01-28 10:46:33.172 D/LeakCanary( 8100):      Retaining 9.1 MB in 7484 objects
01-28 10:46:33.172 D/LeakCanary( 8100):      key = 0217d51a-25f2-49d8-aa59-32364d92a3b2
01-28 10:46:33.172 D/LeakCanary( 8100):      watchDurationMillis = 5835
01-28 10:46:33.172 D/LeakCanary( 8100):      retainedDurationMillis = 832
01-28 10:46:33.172 D/LeakCanary( 8100):      mApplication instance of com.xxx.MyApplication
01-28 10:46:33.172 D/LeakCanary( 8100):      mBase instance of android.app.ContextImpl

2. Profiler

如何使用: Android Studio 底部工具栏找到Profiler

-- 点击+ 选择你的应用进程:(确保应用在运行)

-- 一系列操作后 点击Memory, 选择Capture heap dump, 点击Recode按钮

-- dump完成后得到结果如图:

leaks 是泄漏的对象个数,双击可以查看具体对象, 以及该对象的引用链

3.adb shell dumpsys meminfo your pkg name  -d

有的时候我们通过LeakCanary 和Profile 检查不出内存泄漏,但是应用的内存越用越大

针对这种情况我们可以通过以下步骤检查内存问题:

-- 反复操作: 进入应用->一些操作 -> adb shell dumpsys meminfo your pkg name -d ->退出应用到桌面(home键/back键)

-- 比较meminfo中 TOTAL PSS:的变化情况, 如果TOTAL PSS有明显递增, 则说明有内存没有被释放

-- 进一步通过 对比 Activities:  Views 等 数量变化可以进一步定位泄漏点, 然后再通过Profiler工具检查具体的怀疑对象, 观察其是否随着操作有递增趋势

adb shell dumpsys meminfo your pkg name -d 打印信息

Applications Memory Usage (in Kilobytes):
Uptime: 186869 Realtime: 186869

** MEMINFO in pid 6984 [com.xxx.xxx.xxx] **
                   Pss  Private  Private     Swap      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap    50552    50540        0        0    52800    59240    58260      979
  Dalvik Heap    38909    38804        0        0    46896    62423    37847    24576
 Dalvik Other     5597     4092        0        0     8080                           
        Stack     1844     1844        0        0     1852                           
       Ashmem      998        0        0        0     2824                           
    Other dev       86        0       84        0      384                           
     .so mmap     5014      272        0        0    55336                           
    .jar mmap     1989        0        4        0    32976                           
    .apk mmap    26264       44    25320        0    29872                           
    .ttf mmap      166        0      116        0      280                           
    .dex mmap      126       96       16        0      556                           
    .oat mmap      103        0        0        0     3760                           
    .art mmap     4947     4808        0        0    14812                           
   Other mmap     2166        4      448        0     7120                           
    GL mtrack    16120    16120        0        0    16120                           
      Unknown      463      456        0        0      924                           
        TOTAL   155344   117080    25988        0   274592   121663    96107    25555
 
 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:    43612                          61708
         Native Heap:    50540                          52800
                Code:    25892                         125872
               Stack:     1844                           1852
            Graphics:    16120                          16120
       Private Other:     5060
              System:    12276
             Unknown:                                   16240
 
           TOTAL PSS:   155344            TOTAL RSS:   274592      TOTAL SWAP (KB):        0
 
 Objects
               Views:      276         ViewRootImpl:        1
         AppContexts:        7           Activities:        1
              Assets:       28        AssetManagers:        0
       Local Binders:      129        Proxy Binders:       48
       Parcel memory:       25         Parcel count:      103
    Death Recipients:        5      OpenSSL Sockets:        0
            WebViews:        0
 
 SQL
         MEMORY_USED:      806
  PAGECACHE_OVERFLOW:      315          MALLOC_SIZE:       46
 
 DATABASES
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4      136            124         7/37/9  /data/user/0/com.xxx/no_backup/androidx.work.workdb
         4        8                         0/0/0    (attached) temp
         4      136             32         2/15/3  /data/user/0/com.xxx/no_backup/androidx.work.workdb (2)
         4       44            104      141/48/12  /data/user/0/com.xxx/databases/playqueue.db
         4       36            123       67/24/10  /data/user/0/com.xxx/databases/metadata.db
         4       56            112        10/33/7  /data/user/0/xxx/databases/picnic
         4       56             54         1/16/2  /data/user/0/com.xxx/databases/picnic (1)
         4       28             65         7/22/6  /data/user/0/com.xxx/databases/browser.db

二. 自动化测试脚本

有的时候需要测试几十次上百次才能发现明显的内存泄漏, 为了节省事件我们可以写一个自动化测试脚本, 来执行测试

以shell脚本为例:

#! /bin/bash
## KEYCODE_BACK= 4;
## KEYCODE_DPAD_UP= 19;
## KEYCODE_DPAD_DOWN= 20;
## KEYCODE_DPAD_LEFT= 21;
## KEYCODE_DPAD_RIGHT= 22;
## KEYCODE_DPAD_CENTER= 23;
## KEYCODE_ENTER= 66;
## KEYCODE_HOME= 3;

function press_key() {
        echo "press key code:"$1
	adb shell input keyevent $1
	sleep 1
}

function dump_meminfo() {
        echo "start dump meminfo"
        adb shell dumpsys meminfo com.xxx.xxx -d >> meminfo.txt
}

function startplayer() {
        echo "start player"
        adb shell am start -n com.xxx/.MainActivity
}

function wait_sleep(){
	echo "wait_sleep code:"$1
	sleep $1
}


for((i=0;i<30;i++))
do
	echo "run loop "$i        startplayer
	wait_sleep 4 
	press_key 23 #enter
        wait_sleep 10 
        dump_meminfo
	press_key 3 #home
done 

三 常见的内存泄漏

1.非静态内部类/匿名内部类

由于非静态内部类/匿名内部类持有外部类的引用, 因此将非静态内部类/匿名内部类创建的对象(Object C)作为参数传递给其他对象(Object B)的时候, Object B 可能间接持有Object A的引用

这种情况下我们应该做到:

在本对象(Object A)生命周期结束前, 将Object B中持有的Object C 置空.

常见的有listener, runnable, handler 等...

例1:  listener

我们创建listener 时通常使用匿名内部类来创建 :

ActivityA.javavoid onCreate() {
  remoteService = AIDLService.getService()
  remoteService.setListtener(new AIDLService.ServiceListener(){
     @Override
     void onListenerCallback() {
     }
  } );}

void onDestory(){
  remoteService.setListtener(null);
}


AIDLService.java

AIDLService() {

   ServiceListener listener;

   public interface ServiceListener {
      void onListenerCallback();
   }
    

   public void setListtener(ServiceListener  listener) {
      this.listener = listener;
   }
}

例2:   handler

使用静态内部类来创建handler, handler 可以持有外部类的弱引用, 避免内存泄漏

Activity 生命周期结束的时候清空handler的缓存消息: handler.removeCallbacksAndMessages(null);

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

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

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

    private MyHandler handler = new MyHandler(this);


  void postDelay(){
     handler.postDelayed(runnable, delayedTime);
  }


  void onDestory(){
    handler.removeCallbacksAndMessages(null);
  }
    
}

2.静态变量

如果静态变量是Activity, Service,Fragment 类型的化, 如果Activity, Service,Fragment生命周期结束的时候, 静态变量持有的引用还指向这些对象, 就会导致这些对象不能被回收, 从而出现内存泄漏

例1: ToastUtil

如果 这样创建toast:  toast = Toast.makeText(context, text, duration);

那么Toast将持有context 的引用, 如果context是Activity 类型, 由于静态变量的生命周期长于activty , 会引起Activity内存泄漏

因此避免内存泄漏要做到以下两点:

1.Activity onDestory 的时候将toast 置空, 调用cancel()

2.使用ApplicationContext 创建Toast

 toast = Toast.makeText(context.getApplicationContext(), text,
                    duration);



public final class ToastUtil {
    private static Toast toast;

    private ToastUtil() {
        // not called
    }

 
    public static void showToast(Context context, String text, int duration) {
        makeText(context, text, duration).show();
    }

    public static void cancel() {
        if (toast != null) {
            toast.cancel();
            // When toast canceled, this toast instance can not show again.
            toast = null;
        }
    }

    private static Toast makeText(Context context, String text, int duration) {
        if (toast != null) {
            toast.setText(text);
            toast.setDuration(duration);
        } else {
            toast = Toast.makeText(context.getApplicationContext(), text,
                    duration);
        }
        return toast;
    }
}

3.IO流要正确关闭 InputStream,OutputStream,

public void readFromFile() {
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream("file.txt");
        // 读取数据
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

**4.资源未关闭造成的内存泄漏
**

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,,Cursor,SqliteDatabase, Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

 1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。

 2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。 

 3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。

 4)Bitmap, 在Android系统3.0之前,它的内存一部分在虚拟机中,一部分在虚拟机外。因此它的一部分内存不参与垃圾回收,需要我们主动调用recycler()才能回收。3.0 以后的bitmap应该是不需要手动recycle了,内存已经在java层了。bitmap = null 就可以释放内存

5.AIDL  接口中的listener 导致的内存泄漏

曾经遇到一个问题, home键退出再进, heap 内存中 MyTask 对象的数量就会增加一个

原因是传递给AIDL 的 listener 是用匿名内部类构造的, 它持有外部类MyTask 的引用,  由于AIDL接口所在进程一直没有释放listener, 所以home键回来的时候, 会重新构造一个新的listener, 之前的listener并没有释放, 进而之前的 MyTask 对象也没有释放, 导致MyTask对象越来越多, 由于是第三方的AIDL, 所以我们没有办法控制, 只能把Listener 写成单例模式, 弱引用外部类,  这样的话, 尽管AIDL进程没有释放listener, 也能保证堆内存中至多有一个listener对象,

public class MyTask {
    private IRemoteServiceListener mWrapperListener;
  

    public MyTask() {
        mWrapperListener = WrapperRemoteServiceListener.getInstance(new WeakReference<MyTask>(this));
    }

    @Override
    public void run() {
        super.run();
        RemoteServiceWrapper.getService.doSomething(mWrapperListener);
    }
    static class WrapperRemoteServiceListener extends IRemoteServiceListener.Stub {        WeakReference<MyTask> mTaskWeakReference;

        private static final WrapperRemoteServiceListener SINGLE_INSTANCE = new WrapperRemoteServiceListener ();

        private WrapperRemoteServiceListener () {        }

        public static WrapperRemoteServiceListener getInstance(WeakReference<MyTask> weakReference) {            SINGLE_INSTANCE.mTaskWeakReference= weakReference;            return SINGLE_INSTANCE;
        }


        @Override
        public void onDoSomethingResult(int result) throws RemoteException {          
        }
    }
}

6. 系统Api bug造成的内存泄漏

在Android S 上只要调用 Activity.shouldShowRequestPermissionRationale(p)

就会引起Activity 不能被回收, 导致内存泄漏:Android S原生系统内存泄露问题案例_reportfragment-CSDN博客

解决办法: 尽量不调用, 少调用Activity.shouldShowRequestPermissionRationale(p)

 if (!context.checkSelfPermission(p) == PackageManager.PERMISSION_GRANTED) {    mShouldShowRequestReadPermissionRationale =  
           mActivity.shouldShowRequestPermissionRationale(p);
 } else {
    mShouldShowRequestReadPermissionRationale  = false;
 }