记共享元素动画导致的内存泄露
最近在给项目的预览图片页增加共享元素动画的时候,发现了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 itfinal 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 = trueval 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()