考拉app内存泄露实践&总结

摘要
  • 什么是内存泄露
  • 内存泄露的危害
  • 常见的内存泄露问题
  • 实践

什么是内存泄露

内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,

  • 这些对象是可达的,即在有向图中,存在通路可以与其相连;
  • 这些对象是无用的,即程序以后不会再使用这些对象。

如果对象满足这两个条件,这些对象就可以判定为内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

简单一句话总结就是:不再用到的对象因为被错误引用而无法进行回收。

内存泄露的危害

系统分配给应用程序的内存资源是一定的。对象的创建消耗内存后,因为内存泄露导致这一部分对象不再会被使用到却永远不会被回收,如果该现场高频次发生,会导致:

  1. app可用内存减少,造成卡顿
  2. 因为严重内存泄露造成运行时异常OutOfMemeryError,直接反应就是在使用中直接Crash

常见的内存泄露问题

  1. 单例 ( Context 使用不当造成内存泄露)
  2. 匿名内部类 ( Handler 使用不当造成的内存泄露)
  3. 资源使用完未关闭( ContentObserver,File,Cursor,Stream,Bitmap )
  4. 对象的注册与反注册没有成对出现 ( BroadcastReceiver )
  5. 线程未及时终止
  6. 动画没有及时cancel
  7. 静态引用

实践

对象的注册与反注册没有成对出现


  • 过程分析:BaseFragment#onCreateView 中已经注册 PigeonAutoUnregisterObserver ,当时的本意就是让业务方在页面 onDestroy 中无需反注册,但debug发现该 getLifecycle().addObserver(this); 没有被执行,导致泄露。

  • 解决方法:将 BaseFragment 的生命周期的检测移动到 onCreate 中

匿名内部类+静态引用


  • 过程分析:SpecialFollowTipsDecoration 的构造方法中,调用 FollowManager.addOnPositionChangeListener 添加 new 匿名内部类,而 FollowManager 中存放刚刚添加的回调 sOnPositionChangeListenerList 为静态集合,没有 remove 方法对应。导致 listener没有及时被移除,一直被静态集合持有,无法被回收,又因为匿名内部类会持有外部类,导致 SpecialFollowTipsDecoration 无法被回收,导致泄露

  • 解决方法:
    解决方法

动画没有及时cancel


  • 过程分析:CommentBubbleView 中使用 ObjectAnimator,在 ObjectAnimator#start 方法调用时,如下图所示,最终向单例 AnimationHandler 中添加了 callback,但是在页面 onDetachedFromWindow 的回调中却没有及时cancel 动画,导致泄露

过程分析

  • 解决方法:在 onDetachedFromWindow 中 cancel 动画并 remove runnable;cancel 调用流程如下:

过程分析

Kotlin中的陷阱导致静态引用

以下为一段Kotlin代码

1
2
3
4
5
6
7
8
9
10
object VideoTimeLimitHelper {
private var mDotContext: DotContext? = null
...
@JvmStatic
fun setPublishVideoView(view: View, request: Boolean, setListener: Boolean, dotAction: BaseAction?, dotContext: DotContext?) {
mDotContext = dotContext
}
...
}

我们通过Tools->Kotlin->Show Kotlin Bytecode工具,将Kotlin代码编译成字节码,再点击Decompile转成Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class VideoTimeLimitHelper {
private static DotContext mDotContext;
public static final VideoTimeLimitHelper INSTANCE;
@JvmStatic
public static final void setPublishVideoView(@NotNull View view, boolean request, boolean setListener, @Nullable BaseAction dotAction, @Nullable DotContext dotContext) {
Intrinsics.checkParameterIsNotNull(view, "view");
mDotContext = dotContext;
}
static {
VideoTimeLimitHelper var0 = new VideoTimeLimitHelper();
INSTANCE = var0;
}
}
  • 过程分析:在VideoTimeLimitHelper类加载的静态代码块内,我们创建了一个静态的INSTANCE对象,mDotContext参数也是一个静态成员变量,当setPublishVideoView被调用后,传入的activity被长期持有,导致发生内存泄露。
  • 解决方法:将mDotContext参数声明为弱引用,代码如下:
1
2
3
4
5
6
7
8
private var mDotContextRef: WeakReference<DotContext>? = null
@JvmStatic
fun setPublishVideoView(view: View, request: Boolean, setListener: Boolean, dotAction: BaseAction?, dotContext: DotContext?) {
if (dotContext != null) {
mDotContextRef = WeakReference(dotContext)
}
}

参考

  1. Eight Ways Your Android App Can Leak Memory
  2. 内存泄露从入门到精通三部曲之常见原因与用户实践