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

news2025/5/24 18:32:33

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2384791.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flyweight(享元)设计模式 软考 享元 和 代理属于结构型设计模式

1.目的&#xff1a;运用共享技术有效地支持大量细粒度的对象 Flyweight&#xff08;享元&#xff09;设计模式 是一种结构型设计模式&#xff0c;它的核心目的是通过共享对象来减少内存消耗&#xff0c;特别是在需要大量相似对象的场景中。Flyweight 模式通过将对象的共享细节与…

服务器网络配置 netplan一个网口配置两个ip(双ip、辅助ip、别名IP别名)

文章目录 问答 问 # This is the network config written by subiquity network:ethernets:enp125s0f0:dhcp4: noaddresses: [192.168.90.180/24]gateway4: 192.168.90.1nameservers:addresses:- 172.0.0.207- 172.0.0.208enp125s0f1:dhcp4: trueenp125s0f2:dhcp4: trueenp125…

响应面法(Response Surface Methodology ,RSM)

响应面法是一种结合统计学和数学建模的实验优化技术&#xff0c;通过有限的实验数据&#xff0c;建立输入变量与输出响应之间的数学模型&#xff0c;找到最优操作条件。 1.RSM定义 RSM通过设计实验、拟合数学模型&#xff08;如多项式方程&#xff09;和分析响应曲面&#xff…

Spring Boot 拦截器:解锁5大实用场景

一、Spring Boot中拦截器是什么 在Spring Boot中&#xff0c;拦截器&#xff08;Interceptor&#xff09;是一种基于AOP&#xff08;面向切面编程&#xff09;思想的组件&#xff0c;用于在请求处理前后插入自定义逻辑&#xff0c;实现权限校验、日志记录、性能监控等非业务功能…

有两个Python脚本都在虚拟环境下运行,怎么打包成一个系统服务,按照顺序启动?

环境&#xff1a; SEMCP searx.webapp python 问题描述&#xff1a; 有两个python脚本都在虚拟环境下运行&#xff0c;怎么打包成一个系统服务&#xff0c;按照顺序启动&#xff1f; 解决方案&#xff1a; 将这两个 Python 脚本打包成有启动顺序的系统服务&#xff0c;最…

Python 脚本执行命令的深度探索:方法、示例与最佳实践

在现代软件开发过程中&#xff0c;Python 脚本常常需要与其他工具和命令进行交互&#xff0c;以实现自动化任务、跨工具数据处理等功能。Python 提供了多种方式来执行外部命令&#xff0c;并获取其输出&#xff0c;重定向到文件&#xff0c;而不是直接在终端中显示。这种能力使…

PotPlayer 4K 本地万能影音播放器

今日分享一款来自吾爱论坛大佬分享的啥都能播的的本地播放器&#xff0c;不管是不管是普通视频、4K超清、蓝光3D&#xff0c;还是冷门格式&#xff0c;它基本都能搞定。而且运行流畅不卡顿&#xff0c;电脑配置低也能靠硬件加速&#xff0c;让你根本停不下来。 自带解码器&…

2025年电工杯A题第一版本Q1-Q4详细思路求解+代码运行

A题 光伏电站发电功率日前预测问题 问题背景 光伏发电是通过半导体材料的光电效应&#xff0c;将太阳能直接转化为电能的技术。光伏电站是由众多光伏发电单元组成的规模化发电设施。 光伏电站的发电功率主要由光伏板表面接收到的太阳辐射总量决定&#xff0c;不同季节太阳光…

基于阿里云DashScope API构建智能对话指南

背景 公司想对接AI智能体&#xff0c;用于客服系统&#xff0c;经过调研和实施&#xff0c;觉得DashScope 符合需求。 阿里云推出的DashScope灵积模型服务为开发者提供了便捷高效的大模型接入方案。本文将详细介绍如何基于DashScope API构建一个功能完善的智能对话系统&#x…

九州未来十三载:开源赋能 智启未来

2012年&#xff0c;九州未来以“开源赋能云边变革”为使命&#xff0c;开启中国开放云边基础架构服务的探索之路。十三载坚守深耕&#xff0c;我们始终以开源为翼&#xff0c;以算力为基&#xff0c;在科技浪潮中砥砺前行&#xff0c;见证并推动着AI时代的算力变革。 坚守初心丨…

2025年AI搜索引擎发展洞察:技术革新与市场变革

引言&#xff1a;AI搜索的崛起与市场格局重塑 2024-2025年&#xff0c;AI搜索市场迎来了前所未有的变革期。随着DeepSeek-R1等先进大语言模型的推出&#xff0c;传统搜索引擎、AI原生搜索平台以及各类内容平台纷纷加速智能化转型&#xff0c;推动搜索技术从基础信息检索向深度…

dify调用Streamable HTTP MCP应用

一、概述 上一篇文章&#xff0c;介绍了使用python开发Streamable HTTP MCP应用&#xff0c;链接&#xff1a;https://www.cnblogs.com/xiao987334176/p/18872195 接下来介绍dify如何调用MCP 二、插件 安装插件 需要安装2个插件&#xff0c;分别是&#xff1a;Agent 策略(支持 …

HCIP实验五

一、实验拓扑图&#xff1a; 二、实验需求分析&#xff1a; 1. PreVal策略&#xff1a;要求确保R4通过R2到达192.168.10.0/24 &#xff0c;需在R4上针对去往该网段路由配置PreVal策略&#xff0c;为经R2的路径赋予更高优先值&#xff0c;影响本地路由表选路。 2. AS Path策略…

vivado fpga程序固化

一般下载到fpga上的程序在掉电之后就会丢失&#xff0c;如果想要掉电之后程序不丢失&#xff0c;就需要将比特流文件固化到板载的flash上。 以下以我的7a100t开发板为例&#xff0c;介绍程序固化的流程 点击OK就可以下载了。 一个奇怪的问题 有一次我的一个工程固化之后&…

OpenCV CUDA模块图像特征检测与描述------图像中快速检测特征点类cv::cuda::FastFeatureDetector

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 cv::cuda::FastFeatureDetector 是 OpenCV 的 CUDA 加速模块中的一部分&#xff0c;用于在图像中快速检测特征点。FAST&#xff08;Features fro…

SpringMVC(结合源码浅析工作流程)

SpringMVC 概念 Spring MVC 是基于前端控制器&#xff08;Front Controller&#xff09;设计模式的 Web 框架&#xff0c;在 Web 应用中指一个统一的入口&#xff0c;用来接收所有客户端请求&#xff0c;并统一进行分发、处理。在 SpringMVC 中&#xff0c;前端控制器就是 Di…

学习STC51单片机13(芯片为STC89C52RC)

我去&#xff0c;兄弟们我们今天来学习一个牛逼 的硬件&#xff0c;它叫超声波测距模块HC—SR04 硬件&#xff1a;HC—SR04 哎&#xff0c;想当初最想要玩的就是这个模块&#xff0c;科技感十足&#xff0c;那现在就让我们玩玩吧 超声波测距传感器 原理就是说需要给Trig 10u…

Claude 4 系列 Opus 4 与 Sonnet 4正式发布:Claude 4新特性都有哪些?

随着 Claude 4 系列&#xff08;Opus 4 与 Sonnet 4&#xff09;的正式发布&#xff0c;Anthropic 把自家大模型从“会聊天”推进到“能当自主代理”──不仅推理更深、上下文更长&#xff0c;还内置代码执行、多模态理解、工具调用等一揽子全新能力&#xff1b;同时&#xff0…

深度“求索”:DeepSeek+Dify构建个人知识库

目录 前言 环境部署 安装Docker 安装Dify 配置Dify 部署知识库 创建应用 前言 在当今数字化信息爆炸的时代&#xff0c;数据隐私和个性化知识管理成为企业和个人关注的焦点。Dify&#xff0c;作为一款备受瞩目的开源 AI 应用开发平台&#xff0c;为用户提供了完整的私有…

基于R语言的空间异质性数据分析技术

在自然和社会科学领域&#xff0c;存在大量与地理或空间相关的数据&#xff0c;这些数据通常具有显著的空间异质性。传统的统计学方法在处理这类数据时往往力不从心。基于R语言的一系列空间异质性数据分析方法&#xff0c;如地理加权回归&#xff08;GWR&#xff09;、地理加权…