最近在给项目的预览图片页增加共享元素动画的时候,发现了LeakCanary一直报内存泄露。
LeakCanary日志信息
┬───
│ GC Root: Thread object
│
├─ java.lang.Thread instance
│ Leaking: NO (the main thread always runs)
│ Thread name: 'main'
│ ↓ Thread.threadLocals
│ ~~~~~~~~~~~~
├─ java.lang.ThreadLocal$ThreadLocalMap instance
│ Leaking: UNKNOWN
│ Retaining 15.5 kB in 98 objects
│ ↓ ThreadLocal$ThreadLocalMap.table
│ ~~~~~
├─ java.lang.ThreadLocal$ThreadLocalMap$Entry[] array
│ Leaking: UNKNOWN
│ Retaining 15.5 kB in 97 objects
│ ↓ ThreadLocal$ThreadLocalMap$Entry[36]
│ ~~~~
├─ java.lang.ThreadLocal$ThreadLocalMap$Entry instance
│ Leaking: UNKNOWN
│ Retaining 28 B in 1 objects
│ ↓ ThreadLocal$ThreadLocalMap$Entry.value
│ ~~~~~
├─ android.util.ArrayMap instance
│ Leaking: UNKNOWN
│ Retaining 544 B in 21 objects
│ ↓ ArrayMap.mArray
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 503 B in 19 objects
│ ↓ Object[3]
│ ~~~
├─ android.transition.Transition$AnimationInfo instance
│ Leaking: UNKNOWN
│ Retaining 141 B in 6 objects
│ ↓ Transition$AnimationInfo.transition
│ ~~~~~~~~~~
├─ android.transition.Fade instance
│ Leaking: UNKNOWN
│ Retaining 772 B in 21 objects
│ ↓ Transition.mParent
│ ~~~~~~~
├─ android.transition.TransitionSet instance
│ Leaking: UNKNOWN
│ Retaining 1.5 kB in 50 objects
│ ↓ Transition.mListeners
│ ~~~~~~~~~~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ Retaining 116 B in 5 objects
│ ↓ ArrayList[1]
│ ~~~
├─ android.transition.TransitionManager$MultiListener$1 instance
│ Leaking: UNKNOWN
│ Retaining 36 B in 2 objects
│ Anonymous subclass of android.transition.TransitionListenerAdapter
│ ↓ TransitionManager$MultiListener$1.val$runningTransitions
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ android.util.ArrayMap instance
│ Leaking: UNKNOWN
│ Retaining 541.5 kB in 8916 objects
│ ↓ ArrayMap.mArray
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 541.5 kB in 8914 objects
│ ↓ Object[8]
│ ~~~
├─ com.android.internal.policy.DecorView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ Retaining 136.4 kB in 2235 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.fengqun.whitepeachplanet.
│ activity.ImagePreviewActivity with mDestroyed = true
│ ↓ DecorView.mContentRoot
├─ android.widget.LinearLayout instance
│ Leaking: YES (DecorView↑ is leaking and View.mContext references a destroyed activity)
│ Retaining 3.0 kB in 36 objects
│ View is part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.fengqun.whitepeachplanet.activity.ImagePreviewActivity with mDestroyed = true
│ ↓ View.mContext
╰→ com.fengqun.whitepeachplanet.activity.ImagePreviewActivity instance
Leaking: YES (ObjectWatcher was watching this because com.fengqun.whitepeachplanet.activity.ImagePreviewActivit
received Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 36.0 kB in 739 objects
key = cd70fcef-af19-457e-bb88-2350945ca1c4
watchDurationMillis = 5762
retainedDurationMillis = 753
mApplication instance of com.fengqun.whitepeachplanet.MyApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
经过排查发现泄露的关键代码在这个 ActivityOptions.makeSceneTransitionAnimation 上。那么就从这里开始深入分析里面内容。
启动共享元素动画:
ActivityCompat.startActivity(
activity,
this,
ActivityOptions.makeSceneTransitionAnimation(activity, *transitionImpl)
.toBundle()
)
这会创建包含共享元素信息的ActivityOptions对象
启动Activity:
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
getAutofillClientController().onStartActivity(intent, mIntent);
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
因为携带了Bundle,
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
//...
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
//...
try {
intent.migrateExtraStreamToClipData(who);
intent.prepareToLeaveProcess(who);
int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
这里的ActivityTaskManager.getService() 实际返回的是 IActivityTaskManager 接口的 Binder 代理对象。实际上是ActivityTaskManagerService处理了startActivity()。通过在线源码阅读 可以得知他的调用应该发生在更底层的窗口。
这时我们在看看LeakCanary提供的信息。
↓ TransitionManager$MultiListener$1.val$runningTransitions
在TransitionManager找到了关键代码:
@Override
public boolean onPreDraw() {
// Add to running list, handle end to remove it
final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
getRunningTransitions();
ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
ArrayList<Transition> previousRunningTransitions = null;
if (currentTransitions == null) {
currentTransitions = new ArrayList<Transition>();
runningTransitions.put(mSceneRoot, currentTransitions);
} else if (currentTransitions.size() > 0) {
previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
}
return true;
}
这里的getRunningTransitions最终指向的是竟然是静态的成员变量:
private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
sRunningTransitions =
new ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>();
@UnsupportedAppUsage
private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>();
这个静态的成员变量里面持有了ViewGroup。sPendingTransitions记录了正在进行的过渡动画的ViewGroup,而sRunningTransitions则通过ThreadLocal存储了当前运行的过渡动画。如果这些集合没有正确清理,可能会导致Activity被持续引用,而发生内存泄露
由于它们是私有的,考虑使用反射来访问,并将他们置空处理。 最终在onDestroy()方法中调用此方法
fun leakCanaryClean() {
try {
val pendingField = TransitionManager::class.java.getDeclaredField("sPendingTransitions")
pendingField.isAccessible = true
(pendingField.get(null) as? ArrayList<ViewGroup>)?.clear()
val runningField = TransitionManager::class.java.getDeclaredField("sRunningTransitions")
runningField.isAccessible = true
val threadLocal = runningField.get(null) as? ThreadLocal<*>
threadLocal?.set(null)
} catch (e: Exception) {
LogUtils.e("清除预览图片反射异常: ${e.message}")
}
}
至此因为系统持有ViewGroup导致的泄露问题就解决了。
当然这种内存泄露也不是递增的。通过AS 的Profiler可以看到,过段时间后。 gc还是能够回收掉ViewGroup的引用。 因为在Transition执行结束后,还是会remove掉的。当然系统也不会犯这种低级错误 🥲
最后通过简单的封装,就可以调用带动画预览效果了
ImageViewer
.load(arrayList)
.selection(position)
.setShareView(viewMap.values.toList())
.start()