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
) в действиях, которые используют переходы действий. Просто скопируйте весь класс в ваш проект.
Вам также понадобятся статические методы createDrawableBitmap(Drawable)
а также createViewBitmap(View, Matrix, RectF)
из следующего класса. Они используются LeakFreeSupportSharedElementCallback
учебный класс.
После того как вы получили 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) {}
}
Итак, в основном мое исправление заключается в следующем:
- Проверьте, выполняются ли переходы, связанные с DecorView. Если нет, немедленно удалите DecorView.
- Если да, добавьте
TransitionListener
ко всем переходам, относящимся к DecorView. Когда каждый переход заканчивается, эти слушатели проверяют, были ли они последним завершением перехода, и, если да, они удаляют DecorView. Такой подход делает DecorView доступным для гоночных переходов, но гарантирует, что в конце он будет удален.
Я не подтвердил, решает ли это сбой, связанный с изменением ориентации, но я осторожно оптимистичен, что это так.