内存泄漏修复示例

集成leakcanary

usdebugImplementation libs.leakcanary.android 
debuggable true

工具介绍

集成leakcanary之后,在运行过程中,如果有内存泄漏,会有弹窗提示,通过如下命令可启动泄漏日志页面

adb shell am start -n com.xxx.xxx/leakcanary.internal.activity.LeakLauncherActivity

泄漏修复示例

列举1.0版本中检测并修复的内存泄漏示例

1. BaseBleService非静态内部类MyBinder泄漏

泄漏日志如下:

┬───
                 │ GC Root: Global variable in native code
                 │
                 ├─ com.xxxxx.bluetooth.service.BaseBleService$MyBinder instance
                 │    Leaking: UNKNOWN
                 │    Retaining 2.2 kB in 20 objects
                 │    this$0 instance of com.xxx.bluetooth.service.BaseBleService
                 │    ↓ BaseBleService$MyBinder.this$0
                 │                              ~~~~~~
                 ╰→ com.xxxxxx.bluetooth.service.BaseBleService instance
                 •     Leaking: YES (ObjectWatcher was watching this because com.xxxx.bluetooth.service.BaseBleService received
                 •     Service#onDestroy() callback and Service not held by ActivityThread)
                 •     Retaining 1.6 kB in 19 objects
                 •     key = eca2c1ba-6c10-43a1-90b0-1d12bc112f11
                 •     watchDurationMillis = 11852
                 •     retainedDurationMillis = 6640
                 •     mApplication instance of com.xxxx.xxxx.MyApplication
                 •     mBase instance of android.app.ContextImpl

大致定位到BaseBleService 的MyBinder 因持有this对象导致BaseBleService 泄漏

查看代码,在 BaseBleService 中有一个非静态的内部类 MyBinder ,在内部类中通过this隐式持有外部BaseBleService 实例,导致在 BaseBleService destroy的时候,但 MyBinder 仍持有其引用,阻止垃圾回收

BaseBleService.java

public class MyBinder extends Binder {
    public MyBinder() {
    }

    public void clearDevice() {
        BaseBleService.this.mBleScanManager.clear();
    }
    ...
  }

修改方案将MyBinder 改为静态内部类,并通过WeakReference对BaseBleService进行弱引用,从而解决泄漏

BaseBleService.java

public static class MyBinder extends Binder {
    private WeakReference<BaseBleService> serviceRef;

    public MyBinder(BaseBleService service) {
        this.serviceRef = new WeakReference<>(service);
    }

    public void clearDevice() {
        BaseBleService baseBleService = serviceRef.get();
        if (baseBleService != null) {
            baseBleService.mBleScanManager.clear();
        }
    }
    ....
    
  }

2. BaseBluetoothFragment onNewDataCallbackLister泄漏

泄漏日志:

                 ┬───
                 │ GC Root: System class
                 │
                 ├─ android.app.ActivityThread class
                 │    Leaking: NO (ActivityThread↓ is not leaking and a class is never leaking)
                 │    ↓ static ActivityThread.sCurrentActivityThread
                 ├─ android.app.ActivityThread instance
                 │    Leaking: NO (BaseBleService↓ is not leaking and ActivityThread is a singleton)
                 │    mInitialApplication instance of com.xxx.xxx.MyApplication
                 │    mSystemContext instance of android.app.ContextImpl
                 │    mSystemUiContext instance of android.app.ContextImpl
                 │    ↓ ActivityThread.mServices
                 ├─ android.util.ArrayMap instance
                 │    Leaking: NO (BaseBleService↓ is not leaking)
                 │    ↓ ArrayMap.mArray
                 ├─ java.lang.Object[] array
                 │    Leaking: NO (BaseBleService↓ is not leaking)
                 │    ↓ Object[7]
                 ├─ com.xxxx.bluetooth.service.BaseBleService instance
                 │    Leaking: NO (Service held by ActivityThread)
                 │    mApplication instance of com.xxx.xxxx.MyApplication
                 │    mBase instance of android.app.ContextImpl
                 │    ↓ BaseBleService.lConnect
                 │                     ~~~~~~~~
                 ├─ com.xxxxx.common.base.BaseBluetoothFragment$$ExternalSyntheticLambda3 instance
                 │    Leaking: UNKNOWN
                 │    Retaining 12 B in 1 objects
                 │    ↓ BaseBluetoothFragment$$ExternalSyntheticLambda3.f$0
                 │                                                      ~~~
                 ╰→ com.xxxx.xxx.ui.exercisepreparation.LandExercisePreparationFragment instance
                 •     Leaking: YES (ObjectWatcher was watching this because com.xxxx.xxxx.ui.exercisepreparation.
                 •     xxxxxxFragment received Fragment#onDestroy() callback. Conflicts with Fragment.
                 •     mLifecycleRegistry.state is INITIALIZED)
                 •     Retaining 1.2 MB in 3977 objects
                 •     key = 39fc801c-5bf6-45ea-86ec-29621ba613aa
                 •     watchDurationMillis = 5995
                 •     retainedDurationMillis = 993

链路说明,LandExercisePreparationFragment 因BaseBluetoothFragment中的this对象被BaseBleService 中的lConnect持有,导致LandExercisePreparationFragment 在destroy的时候,因为还有持有链路而无法销毁,造成泄漏

结合代码


BaseBluetoothFragment.kt

binder.setConnectDeviceLister { connected ->
    LogUtils.v("蓝牙基类 -- > 设备连接监听 $connected  $activelyDisconnect ")
    .......
}

BaseBluetoothFragment在 Service bind之后,通过lambda设置了setConnectDeviceLister 监听器,这个匿名内部类隐式的持有fragment的this对象,这个对象最终被baseBleService的lConnect持有。

修复方案

在 fragment销毁的时候,断开引用链路,将setConnectDeviceLister 设置为null


BaseBluetoothFragment.kt

override fun onDestroy() {
    if (::binder.isInitialized) {
            binder.setNewDataLister(null)
            binder.setConnectDeviceLister(null)
        }
  }

3. MainFragment 因被静态变量持有导致泻漏

 ┬───
                 │ GC Root: Thread object
                 │
                 ├─ android.net.ConnectivityThread instance
                 │    Leaking: NO (PathClassLoader↓ is not leaking)
                 │    Thread name: 'ConnectivityThread'
                 │    ↓ Thread.contextClassLoader
                 ├─ dalvik.system.PathClassLoader instance
                 │    Leaking: NO (NetworkChange↓ is not leaking and A ClassLoader is never leaking)
                 │    ↓ ClassLoader.runtimeInternalObjects
                 ├─ java.lang.Object[] array
                 │    Leaking: NO (NetworkChange↓ is not leaking)
                 │    ↓ Object[3764]
                 ├─ com.xxxx.common.utils.network.NetworkChange class
                 │    Leaking: NO (a class is never leaking)
                 │    ↓ static NetworkChange.listener
                 │                           ~~~~~~~~
                 ╰→ com.xxxxx.main.MainFragment instance
                 •     Leaking: YES (ObjectWatcher was watching this because com.xxxxx.main.MainFragment received
                 •     Fragment#onDestroy() callback. Conflicts with Fragment.mLifecycleRegistry.state is INITIALIZED)
                 •     Retaining 493.7 kB in 8263 objects
                 •     key = ecc337e3-aef3-45eb-8995-8a2008ea86e4
                 •     watchDurationMillis = 12461
                 •     retainedDurationMillis = 7461
                 

链路分析:MainFragment 因被NetworkChange 中的listener持有,导致onDestroy的时候无法回收

结合代码

public class NetworkChange {
    public static final String TAG = "NetworkChange";
    private static boolean isRegister = false;
    private static NetStateChangeObserver listener;
    
     public static void registerReceiver(Context context, NetStateChangeObserver lis) {
        listener = lis;
        ........
        isRegister = true;
    }
    .....
}

MainFragment.kt
        NetworkChange.registerReceiver(requireContext(), this)

NetworkChange 中持有一个静态变量NetStateChangeObserver listener,在MainFragment registerReceiver的时候直接将this对象传递给了静态变量。导致在fragment销毁的时候,因为还被静态引用,所以无法销毁,导致泄漏

解决方案:在取消注册的时候,将静态listener置为null

    public static void unRegisterReceiver(Context context) {
     .......
        if (listener != null) {
            listener = null;
        }
    }

4.服务广播未解绑导致泻漏

 ┬───
                 │ GC Root: System class
                 │
                 ├─ android.provider.FontsContract class
                 │    Leaking: NO (MyApplication↓ is not leaking and a class is never leaking)
                 │    ↓ static FontsContract.sContext
                 ├─ com.xxxx.xxxx.MyApplication instance
                 │    Leaking: NO (Application is a singleton)
                 │    mBase instance of android.app.ContextImpl
                 │    ↓ Application.mLoadedApk
                 │                  ~~~~~~~~~~
                 ├─ android.app.LoadedApk instance
                 │    Leaking: UNKNOWN
                 │    Retaining 4.3 MB in 50494 objects
                 │    mApplication instance of com.xxxx.xxxx.MyApplication
                 │    Receivers
                 │    ..xxxxActivity@342657624
                 │    ....xxxxFragment$onCreate$usBroadcastReceiver$1@359382944
                 │    ..MyApplication@319447736
                 │    ....CJ@359062408
                 │    ....v4@359062472
                 │    ....ZV@359062536
                 │    ....ProxyChangeListener$ProxyReceiver@359062600
                 │    ....MediaRouter$VolumeChangeReceiver@359062664
                 │    ....MediaRouter$WifiDisplayStatusChangedReceiver@359062720
                 │    ....cs@359062776
                 │    ....v4@359062840
                 │    ....v4@359062904
                 │    ....d@359062968
                 │    ....C4@359063032
                 │    ....VisibilityTracker@359063096
                 │    ....RegisteredMediaRouteProviderWatcher$1@359063168
                 │    ....FI@359063232
                 │    ....jV@359063288
                 │    ....z10@358194080
                 │    ....NetworkTypeObserver$Receiver@359063392
                 │    ....o@359063456
                 │    ..ControllerService@319399632
                 │    ....ControllerService$1@340346088
                 │    ↓ LoadedApk.mServices
                 │                ~~~~~~~~~
                 ├─ android.util.ArrayMap instance
                 │    Leaking: UNKNOWN
                 │    Retaining 4.3 MB in 50395 objects
                 │    ↓ ArrayMap.mArray
                 │               ~~~~~~
                 ├─ java.lang.Object[] array
                 │    Leaking: UNKNOWN
                 │    Retaining 4.3 MB in 50393 objects
                 │    ↓ Object[2]
                 │            ~~~
                 ╰→ com.xxxxx.main.MainActivity instance
                 •     Leaking: YES (ObjectWatcher was watching this because com.xxx.main.MainActivity received
                 •     Activity#onDestroy() callback and Activity#mDestroyed is true)
                 •     Retaining 8.9 kB in 338 objects
                 •     key = f273584b-ac53-45b9-926a-8c4dddbecb3d
                 •     watchDurationMillis = 12488
                 •     retainedDurationMillis = 7482
                 •     mApplication instance of com.xxxx.xxxx.MyApplication
                 •     mBase instance of androidx.appcompat.view.ContextThemeWrapper

日志分析,MainActivity 有已关联的广播,服务,但是在destroy的时候未注销关联,导致MainActivity 无法回收,可以看到 usBroadcastReceiver,ControllerService等等

结合代码

SocketService

/**
 * 绑定长连接服务
 */
fun bindSocketService(context: Activity, serviceConnection: ServiceConnection){
    LogUtils.v("长链接  -->   绑定长连接服务")
    val service = Intent(context, SocketService::class.java)
    context.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE)
}

解决方案

在销货的时候添加解绑

/**
 * 解绑长连接服务
 */
fun untieSocketService(context: Activity, serviceConnection: ServiceConnection){
    LogUtils.v("长链接  -->   解绑长连接服务")
    context.unbindService(serviceConnection)
}

usBroadcastReceiver:

val filter = IntentFilter()
filter.addAction(ACTION)
requireContext().registerReceiver(usBroadcastReceiver, filter)

解决方案

override fun onDestroy() {
    .....
    requireContext().unregisterReceiver(usBroadcastReceiver)
}

6.播发器SubtitleView 泄漏

 ┬───
09:37:07.203  D  │ GC Root: Thread object
09:37:07.203  D  │
09:37:07.203  D  ├─ android.net.ConnectivityThread instance
09:37:07.203  D  │    Leaking: NO (PathClassLoader↓ is not leaking)
09:37:07.203  D  │    Thread name: 'ConnectivityThread'
09:37:07.203  D  │    ↓ Thread.contextClassLoader
09:37:07.203  D  ├─ dalvik.system.PathClassLoader instance
09:37:07.203  D  │    Leaking: NO (GSYExoSubTitleVideoManager↓ is not leaking and A ClassLoader is never leaking)
09:37:07.203  D  │    ↓ ClassLoader.runtimeInternalObjects
09:37:07.203  D  ├─ java.lang.Object[] array
09:37:07.203  D  │    Leaking: NO (GSYExoSubTitleVideoManager↓ is not leaking)
09:37:07.204  D  │    ↓ Object[4815]
09:37:07.204  D  ├─ com.xxxxx.common.view.video.GSYExoSubTitleVideoManager class
09:37:07.204  D  │    Leaking: NO (a class is never leaking)
09:37:07.204  D  │    ↓ static GSYExoSubTitleVideoManager.videoManager
09:37:07.204  D  │                                        ~~~~~~~~~~~~
09:37:07.204  D  ├─ com.xxxx.common.view.video.GSYExoSubTitleVideoManager instance
09:37:07.204  D  │    Leaking: UNKNOWN
09:37:07.204  D  │    Retaining 460.0 kB in 3925 objects
09:37:07.204  D  │    context instance of com.xxxx.xxxx.MyApplication
09:37:07.204  D  │    ↓ GSYVideoBaseManager.playerManager
09:37:07.204  D  │                          ~~~~~~~~~~~~~
09:37:07.204  D  ├─ com.xxxx.common.view.video.GSYExoSubTitlePlayerManager instance
09:37:07.204  D  │    Leaking: UNKNOWN
09:37:07.204  D  │    Retaining 459.8 kB in 3919 objects
09:37:07.204  D  │    context instance of com.xxxx.xxx.MyApplication
09:37:07.204  D  │    ↓ GSYExoSubTitlePlayerManager.mediaPlayer
09:37:07.204  D  │                                  ~~~~~~~~~~~
09:37:07.204  D  ├─ com.xxx.common.view.video.GSYExoSubTitlePlayer instance
09:37:07.204  D  │    Leaking: UNKNOWN
09:37:07.205  D  │    Retaining 459.8 kB in 3918 objects
09:37:07.205  D  │    mAppContext instance of com.xxx.xxx.MyApplication
09:37:07.205  D  │    ↓ GSYExoSubTitlePlayer.mTextOutput
09:37:07.205  D  │                           ~~~~~~~~~~~
09:37:07.205  D  ├─ com.google.android.exoplayer2.ui.SubtitleView instance
09:37:07.205  D  │    Leaking: YES (View.mContext references a destroyed activity)
09:37:07.205  D  │    Retaining 375.1 kB in 3382 objects
09:37:07.205  D  │    View not part of a window view hierarchy
09:37:07.205  D  │    View.mAttachInfo is null (view detached)
09:37:07.205  D  │    View.mID = R.id.sub_title_view
09:37:07.205  D  │    View.mWindowAttachCount = 1
09:37:07.205  D  │    mContext instance of com.xxx.login.ui.EntranceActivity with mDestroyed = true
09:37:07.205  D  │    ↓ View.mContext
09:37:07.205  D  ╰→ com.xxx.login.ui.EntranceActivity instance
09:37:07.205  D  •     Leaking: YES (ObjectWatcher was watching this because com.xxx.login.ui.EntranceActivity received
09:37:07.205  D  •     Activity#onDestroy() callback and Activity#mDestroyed is true)
09:37:07.205  D  •     Retaining 47.4 kB in 942 objects
09:37:07.205  D  •     key = 49ed1eca-f233-43c3-96d4-37500f393238
09:37:07.205  D  •     watchDurationMillis = 8566
09:37:07.205  D  •     retainedDurationMillis = 3068
09:37:07.206  D  •     mApplication instance of com.xxx.xxx.MyApplication
09:37:07.206  D  •     mBase instance of androidx.appcompat.view.ContextThemeWrapper

泄漏日志分析:

EntranceActivity 无法回收,是因为被 SubtitleView 持有,此链路比较深,需要结合代码分析,

GSYExoSubTitlePlayerManager 中的GSYExoSubTitlePlayer mediaPlayer通过TextOutput mTextOutput的 SubtitleView

GSYExoSubTitleVideoManager(单例)→ GSYExoSubTitlePlayerManager → GSYExoSubTitlePlayer → SubtitleView → 已销毁的EntranceActivity。

`GSYExoSubTitlePlayer`中的`mTextOutput`(即`SubtitleView`)未及时释放,其`mContext`引用了已销毁的Activity。

单例`GSYExoSubTitleVideoManager`长期持有`PlayerManager`及`Player`实例,间接导致`SubtitleView`无法释放。

修复方案

GSYExoSubTitlePlayerManager.java

@Override
public void release() {
    if (mediaPlayer != null) {
        .....
        mediaPlayer.setTextOutput(null);
        mediaPlayer.release();
    }
  }
  
  public static void releaseAllVideos() {
    if (GSYExoSubTitleVideoManager.instance().listener() != null) {
        GSYExoSubTitleVideoManager.instance().listener().onCompletion();
    }
    GSYExoSubTitleVideoManager.instance().releaseMediaPlayer();
    videoManager = null;
}

在播放器释放的时候将setTextOutput置空,断开Manager和 SubtitleView的引用,从而阻断SubtitleView中context对EntranceActivity的引用链路

7.Handler message未取消导致泄漏

泄漏日志:

┬───
                 │ GC Root: System class
                 │
                 ├─ android.app.ActivityThread class
                 │    Leaking: NO (MessageQueue↓ is not leaking and a class is never leaking)
                 │    ↓ static ActivityThread.sMainThreadHandler
                 ├─ android.app.ActivityThread$H instance
                 │    Leaking: NO (MessageQueue↓ is not leaking)
                 │    ↓ Handler.mQueue
                 ├─ android.os.MessageQueue instance
                 │    Leaking: NO (MessageQueue#mQuitting is false)
                 │    HandlerThread: "main"
                 │    ↓ MessageQueue[1]
                 │                  ~~~
                 ├─ android.os.Message instance
                 │    Leaking: UNKNOWN
                 │    Retaining 1.3 MB in 3549 objects
                 │    Message.what = 0
                 │    Message.when = 20885133 (134 ms after heap dump)
                 │    Message.obj = null
                 │    Message.callback = instance @322976080 of com.xxx.xxx.service.CadenceDeviceService$1
                 │    Message.target = instance @321540304 of android.os.Handler
                 │    ↓ Message.callback
                 │              ~~~~~~~~
                 ├─ com.xxxx.bluetooth.service.CadenceDeviceService$1 instance
                 │    Leaking: UNKNOWN
                 │    Retaining 1.3 MB in 3547 objects
                 │    Anonymous class implementing java.lang.Runnable
                 │    this$0 instance of com.xxxx.bluetooth.service.CadenceDeviceService
                 │    ↓ CadenceDeviceService$1.this$0
                 │                             ~~~~~~
                 ╰→ com.xxx.bluetooth.service.CadenceDeviceService instance
                 •     Leaking: YES (ObjectWatcher was watching this because com.xxx.bluetooth.service.CadenceDeviceService
                 •     received Service#onDestroy() callback and Service not held by ActivityThread)
                 •     Retaining 1.3 MB in 3546 objects
                 •     key = 57d1c887-d6d4-46ad-b2fe-6670be28b88b
                 •     watchDurationMillis = 7292
                 •     retainedDurationMillis = 2291
                 •     mApplication instance of com.xxx.xxx.MyApplication
                 •     mBase instance of android.app.ContextImpl
                 

因有未处理的message导致CadenceDeviceService 无法回收导致泄漏

结合代码

CadenceDeviceService.java

private Runnable mCounter =new Runnable() {
    @Override
    public void run() {
        time++;
        hourMeterHandler.postDelayed(this,1000);
    }
};


 this.mHandler.postDelayed(new Runnable() {
                public void run() {
                    CadenceDeviceService.this.mScanning = false;
                    CadenceDeviceService.this.mBluetoothAdapter.stopLeScan(CadenceDeviceService.this.mLeScanCallback);
                }
            }, SCAN_PERIOD);
            

CadenceDeviceService中的mHandler,hourMeterHandler都有延时任务,这些任务在CadenceDeviceService销毁的时候,因为延时还没得到处理,从而阻止了CadenceDeviceService的回收

解决方案:

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

以上大概是总结的几种类型的泄漏和修复方案,类似的问题不重复展示,后续有典型的问题,继续补充

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值