8

具有共享元素动画和自定义输入动画会导致活动泄漏。

知道可能是什么原因吗?

09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * com.feeln.android.activity.MovieDetailActivity has leaked: 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﹕ * references android.app.ActivityThread.mActivities 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.util.ArrayMap.mArray 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references array java.lang.Object[].[1] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.app.ActivityThread$ActivityClientRecord.activity 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references com.feeln.android.activity.MovieDetailActivity.mActivityTransitionState 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.app.ActivityTransitionState.mEnterTransitionCoordinator 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.app.EnterTransitionCoordinator.mEnterViewsTransition 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.transition.TransitionSet.mParent 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.transition.TransitionSet.mListeners 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references java.util.ArrayList.array 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references array java.lang.Object[].[1] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.transition.TransitionManager$MultiListener$1.val$runningTransitions (anonymous class extends android.transition.Transition$TransitionListenerAdapter) 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references android.util.ArrayMap.mArray 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references array java.lang.Object[].[2] 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * references com.android.internal.policy.impl.PhoneWindow$DecorView.mContext 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * leaks com.feeln.android.activity.MovieDetailActivity instance 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ [ 09-21 16:19:31.007 28269:31066 D/LeakCanary ] * Reference Key: af2b6234-297e-4bab-96e9-02f1c4bca171 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * Device: LGE google Nexus 5 hammerhead 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * Android Version: 5.1.1 API: 22 LeakCanary: 1.3.1 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ * Durations: watch=6785ms, gc=262ms, heap dump=8553ms, analysis=33741ms 09-21 16:19:31.007 28269-31066/com.sample.android D/LeakCanary﹕ [ 09-21 16:19:31.007 28269:31066 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;
    }
};
4

3 回答 3

17

泄漏的情况在于TransitionManager.sRunningTransitions每个都DecorView添加并且从不删除。DecorView有他Activity的链接Context。因为sRunningTransitions是静态字段,所以它有永久的引用链,Activity永远不会被 GC 收集。

我不知道为什么需要 TransitionManager.sRunningTransitions,但是如果你从中删除Activity's DecorView,你的问题就会得到解决。跟随代码是例子,怎么做。在您的活动课程中:

@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();
    }
}
于 2015-12-31T06:57:10.243 回答
6

@Delargo 的解决方案对我不起作用。但是,我在 Android 问题跟踪器上偶然发现了这个最终对我有用的解决方案。

这个想法是在使用活动转换的活动中使用以下类(恰当地命名为LeakFreeSupportSharedElementCallback,从 子类化)。SharedElementCallback只需将整个类复制到您的项目中。

  1. LeakFreeSupportSharedElementCallback

您还需要静态方法createDrawableBitmap(Drawable)createViewBitmap(View, Matrix, RectF)以下类。这些由LeakFreeSupportSharedElementCallback班级使用。

  1. 转换工具

完成LeakFreeSupportSharedElementCallback类设置后,将以下内容添加到使用活动转换框架的活动中:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        setEnterSharedElementCallback(new LeakFreeSupportSharedElementCallback());
        setExitSharedElementCallback(new LeakFreeSupportSharedElementCallback());
}

在过渡动画之后,GC 释放了该内存。

于 2016-05-03T18:17:12.480 回答
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)

TransitionListener发生这种情况是因为在引擎盖下有一个TransitionManager尝试通过使用 DecorView 作为键来访问正在运行的转换列表。但是由于 hack 移除了 DecorView 并且此转换过程的某些部分是异步的,加上侦听器不期望空答案,有时它会导致这里崩溃:

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 可用于赛车转换,但确保最终它将被删除。

现在,我没有确认这是否能解决与方向变化相关的崩溃问题,但我对此持谨慎乐观的态度。

于 2020-04-23T09:25:58.053 回答