集成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);
}
以上大概是总结的几种类型的泄漏和修复方案,类似的问题不重复展示,后续有典型的问题,继续补充
678

被折叠的 条评论
为什么被折叠?



