当前位置: 首页 > article >正文

记共享元素动画导致的内存泄露

最近在给项目的预览图片页增加共享元素动画的时候,发现了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()

在这里插入图片描述

http://www.lryc.cn/news/2384791.html

相关文章:

  • Flyweight(享元)设计模式 软考 享元 和 代理属于结构型设计模式
  • Win/Linux安装flash attention2
  • 【原创】ubuntu22.04下载编译AOSP 15
  • 服务器网络配置 netplan一个网口配置两个ip(双ip、辅助ip、别名IP别名)
  • 响应面法(Response Surface Methodology ,RSM)
  • 针对面试-java集合篇
  • Spring Boot 拦截器:解锁5大实用场景
  • 展锐 Android 15 锁定某个App版本的实现
  • 有两个Python脚本都在虚拟环境下运行,怎么打包成一个系统服务,按照顺序启动?
  • 【Linux cmd】查找进程信息
  • 与网格共舞 - 服务网格的运维与问题排查 (Istio 实例)
  • Python 脚本执行命令的深度探索:方法、示例与最佳实践
  • PotPlayer 4K 本地万能影音播放器
  • 2025年电工杯A题第一版本Q1-Q4详细思路求解+代码运行
  • 基于阿里云DashScope API构建智能对话指南
  • HOW - 基于组件库组件改造成自定义组件基本规范
  • 九州未来十三载:开源赋能 智启未来
  • 2025年AI搜索引擎发展洞察:技术革新与市场变革
  • dify调用Streamable HTTP MCP应用
  • HCIP实验五
  • java将图片转Base64字符串存储mysql数据库
  • 题目 3330: 蓝桥杯2025年第十六届省赛真题-01 串
  • 初识 Flask 框架
  • MYSQL故障排查和环境优化
  • vivado fpga程序固化
  • OpenCV CUDA模块图像特征检测与描述------图像中快速检测特征点类cv::cuda::FastFeatureDetector
  • SpringMVC(结合源码浅析工作流程)
  • 学习STC51单片机13(芯片为STC89C52RC)
  • Claude 4 系列 Opus 4 与 Sonnet 4正式发布:Claude 4新特性都有哪些?
  • Swagger API 未授权访问漏洞【原理扫描】修复