内存泄漏修复示例
集成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.javapublic class MyBinder extends Binder {public MyBinder() {}public void clearDevice() {BaseBleService.this.mBleScanManager.clear();}...}
修改方案将MyBinder 改为静态内部类,并通过WeakReference对BaseBleService进行弱引用,从而解决泄漏
BaseBleService.javapublic 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.ktbinder.setConnectDeviceLister { connected ->LogUtils.v("蓝牙基类 -- > 设备连接监听 $connected $activelyDisconnect ").......
}
BaseBluetoothFragment在 Service bind之后,通过lambda设置了setConnectDeviceLister 监听器,这个匿名内部类隐式的持有fragment的this对象,这个对象最终被baseBleService的lConnect持有。
修复方案
在 fragment销毁的时候,断开引用链路,将setConnectDeviceLister 设置为null
BaseBluetoothFragment.ktoverride 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.ktNetworkChange.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.javaprivate Runnable mCounter =new Runnable() {@Overridepublic 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);
}
以上大概是总结的几种类型的泄漏和修复方案,类似的问题不重复展示,后续有典型的问题,继续补充