使用动态屏幕时如何防止内存泄漏?

5

我正在创建一个应用程序,其中有显示数据给用户的屏幕。 每个Screen都有自己的数据和布局,因此它有一种方法来返回代表用于填充它的布局的int,然后将此View传递给函数以查找特定视图并使用数据填充它。

生命周期如下: MainPresenter:

screen.getNextScreen ->
screen.getLayout -> 
view = inflateScreen ->
screen.populateScreen(view) ->
(wait for time elappsed or click) -> repeat

这些Screens也需要在SettingsActivity中启用或禁用。

因此,我创建了一个单例的ScreenProvider,它只会初始化一次,并返回该列表。

public class ScreenProvider {

    private List<Screen> screens;

    private static ScreenProvider instance = new ScreenProvider();

    public static ScreenProvider getInstance(){
        return instance;
    }

    private ScreenProvider() {
        screens = new ArrayList<>();

        screens.add(new Welcome());
        screens.add(new CompoundScreen());
        screens.add(new Times());
        screens.add(new Messages());
        screens.add(new Weekly());
    }

    public List<Screen> getScreenList() {
        return Lists.newArrayList(screens);
    }
}

看起来当应用运行时间过长时会因为内存泄漏而崩溃或关闭,所以我添加了leakcanary,并提供以下报告示例:

MainActivity has leaked:
D: * static ScreenProvider.!(instance)!
D: *  ScreenProvider.!(screens)!
D: *  ArrayList.!(array)!
D: *  array Object[].!([0])!
D: *  CompoundScreen.!(disposable)!
D: *  LambdaObserver.!(onNext)!
D: *  -$$Lambda$Screen$67KdQ1jl3VSjSvoRred5JqLGY5Q.!(f$1)!
D: *  AppCompatTextView.mContext
D: *  MainActivity

这只是一个例子,但几乎每个屏幕都有这样的泄漏。 LeakCanary报告显示TextView具有以下内容:D: | mAttachInfo = null,因此我认为这不是问题所在。 此外,每个屏幕都有一个onHide()方法来清除可处理对象,在当前Screen隐藏和MainActivity.onStop()中被调用。
如何解决这个泄漏问题? 我不应该为屏幕使用单例吗? 如果不是,我该如何从其他活动访问屏幕列表?
** 编辑 ** 添加一些每个屏幕都要重写的主要方法Screen
public abstract int getLayout();

public boolean shouldShow()

public void populateData(View view)

public void onHide()

public abstract int getScreenIndex();

public boolean shouldCacheView()

public int getDuration()

https://android.jlelse.eu/9-ways-to-avoid-memory-leaks-in-android-b6d81648e35e - Chaudhary Amar
你为什么要在MainActivity中管理所有的屏幕呢?这听起来一点也不好。如果你继续添加屏幕,会怎样结束呢?我建议使用不同的片段和每个屏幕的Presenter,具有自己的生命周期、业务逻辑等。 - David Miguel
屏幕是一个旨在提供视图或者仅仅是一个视图的接口或包装器?同样的问题也适用于欢迎界面和其他界面。 - Fco P.
@DavidMiguel 是一个只有一个FrameLayout的Activity,它会附加一个视图,然后将其删除并附加另一个视图。该视图是在MainPresenter中填充的。我认为当一个视图被删除时,它会被垃圾回收,除了一些我标记为缓存的视图。@FcoP. 这是一个包装器。它有一个方法来获取视图并填充数据。我将编辑以添加Screen代码示例。 - Ari M
@DavidMiguel 我认为Fragment的生命周期过于复杂了。 - Ari M
显示剩余2条评论
1个回答

0

好的。从你所说的和所展示的看来,似乎你将一些生成的视图实例保留在单例中。不要这样做。每个视图都需要一个上下文来创建,可以通过代码或膨胀(基本上是基于XML和反射的工厂方法)访问应用程序和系统资源,并保持对该上下文的引用,只要它们存在。在你的情况下,这意味着保留到你生成视图的活动的引用。通常,在视图和活动方面,以下是GC方面的情况:

GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference!
GC: Okay... and besides MainActivity, Does anybody else need this View class?
-Nobody answers-
GC: It does not matter my friend, you are being collected as well. Come with me.
And they both go.

在你的情况下:

GC: Hey! Does anybody need this... MainActivity class?
View: I do! I do! I have a reference! and MainActivity references me as well.
GC: Okay... and besides MainActivity, Does anybody else need this View class?
ScreenProvider: I do.
GC: Okay, keep moving View, and take MainActivity with you. Let me know when you folks are done so I can collect you.

And thus the leak.

为了将一个视图从一个活动传递到另一个活动,您需要删除对先前活动的引用(即mContext字段)。由于没有API可以做到这一点,您需要使用反射。然后又出现了另一个问题:每个UI元素都是View的子类。布局、小部件等等。因此,要么您保留对XML文件中每个部分的引用,以便通过反射删除上下文,要么遍历视图的子列表,删除上下文,并继续进行,直到没有更多的子视图,在任何级别上。之后,您需要以同样的方式设置对新活动的引用。这听起来像是一个巨大的黑客,因为它确实是这样的,而且事情很可能会在某个层面上出现问题。毕竟,上下文代表了视图存在的环境和状态。
对于您的情况,更好的解决方案是从单例中删除视图引用,并仅使用它来保留给定视图的状态/配置的表示形式。创建一个支持回调的方法(或类似方法),在后台填充视图并在返回该视图之前执行必要的配置。如果您仍然想保留所有屏幕的单个存储库,那么将其添加到活动类作为成员,以便它与活动一起被收集。
作为一个附带说明,你的情况建议你使用单个活动,然后只需切换由“屏幕”组成的“主屏幕”,或者根据情况在屏幕之间切换。这样做更合理,风险也较小。
最后,引用我自己的话:记住 Android 战斗俱乐部的第一条规则

嗨,抱歉回复晚了,办公室已经关门了。你能解释一下更好的解决方案吗? - Ari M

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