【问题标题】:SharedElement and custom EnterTransition causes memory leakSharedElement 和自定义 EnterTransition 导致内存泄漏
【发布时间】:2015-09-21 14:40:20
【问题描述】:

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

知道可能是什么原因吗?

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;
    }
};

【问题讨论】:

    标签: android memory-leaks android-memory leakcanary


    【解决方案1】:

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

    我不知道为什么需要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();
        }
    }
    

    【讨论】:

    • Android 框架中是否存在关于此的未解决问题?我遇到了同样的问题,但我在其他任何地方都找不到任何提及。
    • @fast3r 这是跟踪器上问题的链接。他们说它将在 Nougat 中修复:code.google.com/p/android/issues/detail?id=170469
    • 唯一更丑的是我现在正在使用这个解决方案和fahmy's solution 解决方案来堵住漏洞,因为两者都解决了不同的问题。伟大的黑客,德拉戈 - 有时“丑陋”是我们必须滚动的方式。
    • 感谢您的回答。现在我收到此错误:尝试在 android.transition.TransitionManager$MultiListener$1.onTransitionEnd 的空对象引用上调用虚拟方法 'boolean java.util.ArrayList.remove(java.lang.Object)'
    • 这个解决方案对我有用,但是当您需要处理方向更改和共享元素事务时,它确实不起作用。不幸的是,它使应用程序因上述 cmets 中提到的崩溃而崩溃......
    【解决方案2】:

    @Delargo 的解决方案对我不起作用。但是,我在 Android 问题跟踪器上偶然发现了 this solution,它终于为我工作了。

    这个想法是在使用活动转换的活动中使用以下类(恰当地命名为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 释放。

    【讨论】:

    • 每个解决方案似乎都修复了不同的泄漏,所以我现在有这个代码,我的应用程序中有workaround by Delargo。垃圾收集器现在工作正常。
    • @RichardLeMesurier 它仍然会产生 OOM
    【解决方案3】:

    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 并且此转换过程的某些部分是异步的,再加上侦听器不期望空答案,因此有时会导致崩溃:

    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 可用于赛车过渡,但确保最终它会被移除。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-05
      • 1970-01-01
      • 2015-07-06
      • 2014-06-07
      相关资源
      最近更新 更多