SharedElement和自定义EnterTransition导致内存泄漏

9

同时拥有共享元素动画和自定义进入动画会导致Activity泄漏。

你有什么想法是什么原因吗?

09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * com.feeln.android.activity.MovieDetailActivity已经泄露: 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * GC ROOT android.app.ActivityThread$ApplicationThread.this$0 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.app.ActivityThread.mActivities 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.util.ArrayMap.mArray 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了数组 java.lang.Object[].[1] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.app.ActivityThread$ActivityClientRecord.activity 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 com.feeln.android.activity.MovieDetailActivity.mActivityTransitionState 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.app.ActivityTransitionState.mEnterTransitionCoordinator 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.app.EnterTransitionCoordinator.mEnterViewsTransition 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.transition.TransitionSet.mParent 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.transition.TransitionSet.mListeners 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了数组 java.lang.Object[].[1] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.transition.TransitionManager$MultiListener$1.val$runningTransitions (匿名类扩展自android.transition.Transition$TransitionListenerAdapter) 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 android.util.ArrayMap.mArray 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了数组 java.lang.Object[].[2] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 引用了 com.android.internal.policy.impl.PhoneWindow$DecorView.mContext 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 泄露了 com.feeln.android.activity.MovieDetailActivity实例 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ [ 09-21 16:19:31.007 28269:31066 D/LeakCanary ] * 引用密钥: af2b6234-297e-4bab-96e9-02f1c4bca171 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * 设备: LGE google Nexus 5 hammerhead 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * Android版本: 5.1.1 API: 22 LeakCanary: 1.3.1 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ *

要复现此问题,您需要拥有一个大的共享图像动画以及自定义的EnterAnimation和setEnterSharedElementCallback。所有这些都来自支持库。
以下是我设置EnterTransition的方式:
private SharedElementCallback mCallback = new SharedElementCallback() {
    @Override
    public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        {
            if(sharedElements.size()>0)
                getWindow().setEnterTransition(makeEnterTransition(getWindow().getEnterTransition(), getSharedElement(sharedElements)));
        }
    }


    private View getSharedElement(List<View> sharedElements)
    {
        for (final View view : sharedElements)
        {
            if (view instanceof ImageView)
            {
                return view;
            }
        }
        return null;
    }
};
3个回答

19

泄漏问题出在TransitionManager.sRunningTransitions上,每个DecorView都会被添加进去但却从未被移除。而DecorView有指向其所属ActivityContext的链接。由于sRunningTransitions是一个静态字段,它拥有对Activity的永久引用链,这些引用将不会被垃圾回收。

我不知道为什么需要使用TransitionManager.sRunningTransitions,但是如果你将ActivityDecorView从中删除,问题就能够解决。下面的代码提供了如何实现的示例,在你的活动类中:

@Override
protected void onDestroy() {
    super.onDestroy();
    removeActivityFromTransitionManager(Activity activity);
}

private static void removeActivityFromTransitionManager(Activity activity) {
    if (Build.VERSION.SDK_INT < 21) {
        return;
    }
    Class transitionManagerClass = TransitionManager.class;
    try {
        Field runningTransitionsField = transitionManagerClass.getDeclaredField("sRunningTransitions");
            runningTransitionsField.setAccessible(true);
        //noinspection unchecked
        ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>> runningTransitions
                = (ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>)
                runningTransitionsField.get(transitionManagerClass);
        if (runningTransitions.get() == null || runningTransitions.get().get() == null) {
            return;
        }
        ArrayMap map = runningTransitions.get().get();
        View decorView = activity.getWindow().getDecorView();
        if (map.containsKey(decorView)) {
            map.remove(decorView);
        }
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

2
@fast3r 这是一个指向问题跟踪器的链接。他们说这个问题将在Nougat中得到解决:https://code.google.com/p/android/issues/detail?id=170469 - Jarett Millard
1
现在唯一更丑的事情是我正在使用这个解决方案和fahmy的解决方案来填补漏洞,因为两者都修复了不同的问题。很棒的黑客技巧,Delargo - 有时“丑陋”就是我们必须采取的方式。 - Richard Le Mesurier
类android/support/transition/TransitionManager中没有字段sRunningTransitions。 - Mladen Rakonjac
4
谢谢您的提问。现在我遇到了这个错误: 尝试在 null 对象引用上调用虚拟方法 'boolean java.util.ArrayList.remove(java.lang.Object)' 位于 android.transition.TransitionManager$MultiListener$1.onTransitionEnd - Mladen Rakonjac
2
这个解决方案对我有效,但是当你需要处理方向变化和共享元素事务时,它真的不起作用。不幸的是,它会导致应用程序崩溃,并在评论中提到了上述崩溃。 - Alex
显示剩余8条评论

6
@Delargo提供的解决方法对我没有用。然而,在Android问题跟踪器上我偶然发现了这个解决方案,最终解决了我的问题。
这个想法是在使用活动转换的活动中使用以下类(恰当地命名为LeakFreeSupportSharedElementCallback,是从SharedElementCallback子类化的)。只需将整个类复制到您的项目中即可。
  1. LeakFreeSupportSharedElementCallback
您还需要以下类中的静态方法createDrawableBitmap(Drawable)createViewBitmap(View, Matrix, RectF)。这些由LeakFreeSupportSharedElementCallback类使用。
  1. TransitionUtils
在设置好LeakFreeSupportSharedElementCallback类之后,将以下内容添加到使用活动转换框架的活动中:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        setEnterSharedElementCallback(new LeakFreeSupportSharedElementCallback());
        setExitSharedElementCallback(new LeakFreeSupportSharedElementCallback());
}

在过渡动画后,由于GC释放了内存,因此内存得到了释放。

1
每个解决方案似乎都修复了不同的泄漏,所以现在我在我的应用程序中有这段代码和Delargo的解决方法。垃圾收集器现在正常工作。 - Richard Le Mesurier
1
@RichardLeMesurier 它仍然会产生OOM。 - Usman Rana

0
Sergei Vasilenko的解决方案与fahmy的方案似乎对我来说效果最好,但前者确实引入了Mladen Rakonjac提到的崩溃问题。
Attempt to invoke virtual method 'boolean java.util.ArrayList.remove(java.lang.Object)' on a null object reference
android.transition.TransitionManager$MultiListener$1.onTransitionEnd (TransitionManager.java:306)

这是因为在幕后,TransitionManager 中有一个 TransitionListener 试图使用 DecorView 作为键来访问正在运行的转换列表。但由于此 hack 移除了 DecorView 并且某些部分的转换过程是异步的,再加上监听器不期望得到 null 的答案,有时会导致在此处崩溃:
mTransition.addListener(new TransitionListenerAdapter() {
    @Override
    public void onTransitionEnd(Transition transition) {
        ArrayList<Transition> currentTransitions =
                   runningTransitions.get(mSceneRoot); //"mSceneRoot" is basically the DecorView
            currentTransitions.remove(transition); //This line crashes, because "currentTransitions" is null
            transition.removeListener(this);
        }
    });

为了解决这个问题,我对解决方法进行了以下更改:
fun AppCompatActivity.removeActivityFromTransitionManager() {
    if (Build.VERSION.SDK_INT < 21) {
        return;
    }
    val transitionManagerClass: Class<*> = TransitionManager::class.java
    try {
        val runningTransitionsField: Field =
            transitionManagerClass.getDeclaredField("sRunningTransitions")
        runningTransitionsField.isAccessible = true
        @Suppress("UNCHECKED_CAST")
        val runningTransitions: ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>?> =
            runningTransitionsField.get(transitionManagerClass) as ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>?>
        if (runningTransitions.get() == null || runningTransitions.get()?.get() == null) {
            return
        }
        val map: ArrayMap<ViewGroup, ArrayList<Transition>> =
            runningTransitions.get()?.get() as ArrayMap<ViewGroup, ArrayList<Transition>>
        map[window.decorView]?.let { transitionList ->
            transitionList.forEach { transition ->
                //Add a listener to all transitions. The last one to finish will remove the decor view:
                transition.addListener(object : Transition.TransitionListener {
                    override fun onTransitionEnd(transition: Transition) {
                        //When a transition is finished, it gets removed from the transition list
                        // internally right before this callback. Remove the decor view only when
                        // all the transitions related to it are done:
                        if (transitionList.isEmpty()) {
                            map.remove(window.decorView)
                        }
                        transition.removeListener(this)
                    }

                    override fun onTransitionCancel(transition: Transition?) {}
                    override fun onTransitionPause(transition: Transition?) {}
                    override fun onTransitionResume(transition: Transition?) {}
                    override fun onTransitionStart(transition: Transition?) {}
                })
            }
            //If there are no active transitions, just remove the decor view immediately:
            if (transitionList.isEmpty()) {
                map.remove(window.decorView)
            }
        }
    } catch (_: Throwable) {}
}

所以,我的修复程序实际上是这样做的:

  1. 检查是否有与DecorView相关的转换正在运行。如果没有,则立即删除DecorView。
  2. 如果有,将TransitionListener添加到所有与DecorView相关的转换中。当每个转换结束时,这些侦听器会检查它们是否是最后一个完成的转换,如果是,则会删除DecorView。 这种方法使DecorView可用于竞争转换,但确保最终会被删除。

现在,我没有确认这是否解决了与方向更改相关的崩溃问题,但我谨慎地乐观地认为它可以解决。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接