为啥异步网络调用的回调方法在活动完成时不会导致内存泄漏?

Posted

技术标签:

【中文标题】为啥异步网络调用的回调方法在活动完成时不会导致内存泄漏?【英文标题】:Why callback method for asynchronous network call can't cause memory leak when activity finished?为什么异步网络调用的回调方法在活动完成时不会导致内存泄漏? 【发布时间】:2018-08-10 09:35:51 【问题描述】:

我们知道匿名内部类可能会导致内存泄漏。但是为什么在异步网络调用时它不起作用。 例如:

OkHttpClient client = new OkHttpClient();

 Request request = new Request.Builder()
                .get()
                .url(url)
                .build();
        client.newCall(request).enqueue(new Callback() 
            @Override
            public void onFailure(Call call, IOException e) 

            
            @Override
            public void onResponse(Call call, Response response) throws IOException 
                if (response.isSuccessful()) 
                // String str = response.body().string();
                // do sth to our View, but those views may be null when activity finished
                

            
        );

我们将在回调方法调用时更改视图的状态,但是当活动完成时这些视图始终为空。为什么这个用于回调的匿名内部类实例不会导致活动泄漏。

【问题讨论】:

【参考方案1】:

是的,你是对的,这就是为什么你可以添加一个

 if (response.isSuccessful()) 
                // String str = response.body().string();
                if(view != null)
                //do sth to view
                
 

【讨论】:

感谢您的回答,我知道如何处理,活动结束后,我们可以取消我们的网络请求或添加此private boolean isActivityFinished(Context context) if (context == null) return true; else if (!(context instanceof Activity)) return false; else return ((Activity) context).isFinishing(); 。但我想知道的是为什么会这样。为什么这个匿名内部类不会导致内存泄漏。 好的,所以我找到了您可能正在寻找的答案:***.com/a/10968689/9429555 如果类是嵌套的而不是静态的,主要的事情是要小心 - 因为其他东西可能会引用它,即使外面的班级被破坏了。 @DimitarDihanov 如果有对内部类实例的引用,则外部类实例还不能被销毁。 这不是最好的方法,因为你只是忽略了回调阶段的内存泄漏。更好的方法是在活动之外的某个地方而不是在活动内部进行调用。就像 Presenter 或 ViewModel 具有与 Activity 不同的生命周期。【参考方案2】:

为什么这个用于回调的匿名内部类实例不会导致活动泄漏

我假设你在这里的意思是它不会导致内存泄漏,但它肯定会,因为你实例化匿名Callback 的范围是Activity

如果您在 android Activity 中实例化一个内部类,然后将此实例的引用传递给某个其他组件,只要该组件是可访问的,那么该内部类的实例也是如此。例如,考虑一下:

class MemorySink 

    static private List<Callback> callbacks = new ArrayList<>();

    public static void doSomething(Callback callback)
        callbacks.add(callback);
    

如果您从某些活动创建了Callbacks 的实例并将它们传递给doSomething(callback),当Activity 之一被销毁时,系统将不再使用该实例,预计垃圾收集器将释放那个例子。但是,如果这里的MemorySink 有一个对Callback 的引用,而Activity 的引用又指向了那个Activity,那么这个Activity 的实例即使在被销毁之后也会保留在内存中。 Bam,内存泄漏。

所以你说你的样本没有导致内存泄漏,我首先建议你试试MemorySink,创建一个简单的Activity“MainActivity”,带有2个按钮,可能还有一些图像来增加内存占用。在onCreate 中以这种方式在第一个按钮上设置监听器:

    findViewById(R.id.firstButton).setOnClickListener(new View.OnClickListener() 
        @Override
        public void onClick(View v) 
            startActivity(new Intent(MainActivity.this, MainActivity.class));
            MemorySink.doSomething(new Callback() 
                @Override
                public void onFailure(Call call, IOException e) 

                

                @Override
                public void onResponse(Call call, Response response) throws IOException 

                
            );
            finish();
        
    );

您刚刚使用Callback 创建了内存泄漏。每次单击MainActivity 中的按钮时,MainActivity 的实例将被销毁并创建一个新实例。但是,MainActivity 的旧实例将保留在内存中。我邀请您多次单击该按钮,然后转储内存(使用 Android Studio 中的 Android Profiler),或使用 LeakCanary。

因此,我们使用来自 OP 的同一类 Callback 创建了内存泄漏。现在,让我们将此方法添加到MemorySink

public static void releaseAll() 
    callbacks.clear();

然后从MainActivity 上的另一个按钮调用它。如果您多次按下第一个按钮(如果您在MainActivity 中有图像则更好),即使您手动触发垃圾收集(Android 配置文件),您也会看到内存使用量上升。然后单击第二个按钮,所有对Callback 的引用都被释放,触发垃圾回收,内存下降。不再有内存泄漏。

所以问题不在于Callback 是否可以造成内存泄漏,它肯定可以。问题是你在哪里传递Callback。在那种情况下,OkHttpClient 不会造成内存泄漏,所以你说,但完全不能保证这总是会发生。在这种情况下,您需要确定OkHttpClient 的实现,以保证它不会产生内存泄漏。

我的建议是始终假设如果您将对 Activity 的引用传递给某个外部类,就会发生内存泄漏。

【讨论】:

谢谢你的回答,我想是的。但是当我指出我同事的代码有这个问题时,他认为不会导致匿名类实例中的那些视图组件的内存泄漏在活动完成时将为空,并得出结论认为活动不会泄漏无论生命周期多长这个匿名类是 .我无法给他一个正确的解释,因为当活动完成时,网络回调导致视图组件的 NullPointException 为 null 确实发生了很多次。 这真的取决于你的代码,你真的在​​Activity中创建回调吗?如果要检查是否发生内存泄漏,可以使用 LeakCanary 库,或者在 android studio 中使用 android profiler 手动检查内存。我的建议是在这些情况下假设更糟的情况,正如我向您展示的那样,这取决于第三方组件的实现是否发生内存泄漏【参考方案3】:

非静态内部类对外部类的实例具有强引用。另一方面,静态内部类对外部类的实例没有强引用。它使用 Wea​​kReference 来引用外部类。 you can learn more here

【讨论】:

您的评论传达的信息好像静态内部类固有地使用弱引用,这是不正确的。尽管您的意思实际上是上述示例中给出的内容 基于 OP 的示例。【参考方案4】:

client.newCall(request).enqueue(new Callback() ...)

在这里,您将一个回调对象传递给改造。这样,您就是在告诉改造程序在通话结束时使用它来回拨给您。

该进程不在当前活动中,它在单独的线程和上下文中(从概念上讲,不是 android 的上下文)。在您的活动被某些东西(如旋转)破坏后,改造服务可能仍然存在并持有对该对象的引用,并且您的活动导致内存泄漏(无法从内存中清除,从而污染内存),因为您的改造需要活动的回调对象。

如果您想修复此内存泄漏,您需要取消调用并删除活动的onStop 中的回调,但这样您会丢失轮换调用。

强烈推荐的更好的做法是不要在 Activity 本身内部执行异步操作,而是在具有与 Activity 不同的生命周期(不会被旋转或...破坏的生命周期)。

【讨论】:

以上是关于为啥异步网络调用的回调方法在活动完成时不会导致内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

从不同线程调用时,WCF Duplex 回调方法永远不会执行

如何在Playground中运行异步回调

如何在 Playground 中运行异步回调

对“XXX::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们

自动挂钩到 Activity 生命周期方法的异步任务库

对“xxx”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。 错误解决一例。(代码片段