SharedElement и пользовательский EnterTransition вызывают утечку памяти

Наличие общей анимации элемента, а также пользовательской анимации ввода приводит к утечке активности.

Есть идеи, что может быть причиной?

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

2 ответа

Случай утечки лежит в TransitionManager.sRunningTransitions где каждый DecorView добавляет и никогда не удаляет. DecorView имеет ссылку на его Activity"s 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();
    }
}

Решение @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 после анимации перехода.

Решение Сергея Василенко в тандеме с решением Фами, кажется, работает для меня лучше всего, но первое действительно представляет аварию, о которой упоминал Младен Раконжак:

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 в качестве ключа. Но поскольку взлом удаляет 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 доступным для гоночных переходов, но гарантирует, что в конце он будет удален.

Я не подтвердил, решает ли это сбой, связанный с изменением ориентации, но я осторожно оптимистичен, что это так.

Другие вопросы по тегам