V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
m30102
V2EX  ›  Android

问一个关于 kotlin 协程 lifecycleScope 用法和内存泄漏的问题。

  •  
  •   m30102 · 2021-01-07 18:05:43 +08:00 · 9834 次点击
    这是一个创建于 1400 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在随便一个 Activity 上启动此 Activity,然后迅速关闭,leakcanary 就会报内存泄漏。引用链包括了传入的第二个参数 lambda 对象和 Okhttpclient,这里泄漏的原因是什么呢? 一般 retrofit 或者 okhttpclient 对象全局只需要一个就行了吧, 如果还是需要传参和传回调的方式访问网络,该如何正确修改下面的代码呢?

    class TestActivity:AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
            requestBylifecycleCoroutine("https://www.baidu1.com/"){
                Log.w("TestActivityTAG","result:"+it)
            }
        }
    
        val client = OkHttpClient.Builder().build()
        fun requestBylifecycleCoroutine(url: String, callBack: (s: String) -> Unit) {
            lifecycleScope.launch {
                val result = withContext(Dispatchers.IO) {
                    suspendCoroutine<String> { continuation ->
                        val request = Request.Builder().url(url).build()
                        val newCall = client.newCall(request)
                        newCall.enqueue(object : Callback {
                            override fun onFailure(call: Call, e: IOException) {
                                continuation.resume("fail")
                            }
                            override fun onResponse(call: Call, response: Response) {
                                continuation.resume("success")
                            }
                        })
                    }
                }
                callBack(result)
            }
        }
    }
    
    21 条回复    2021-01-13 10:25:26 +08:00
    lianyue13
        1
    lianyue13  
       2021-01-07 18:34:01 +08:00
    协程取消的时候,请求没取消吧。用 suspendCancellableCoroutine,在 cancel 里把请求取消试试
    m30102
        2
    m30102  
    OP
       2021-01-07 21:16:54 +08:00
    @lianyue13 我换了 suspendCancellableCoroutine, 也在 invokeOnCancellation 添加了 call.cancel(). 和之前一样,leakcanary 还是会有 x retained objects,tap to dump heap,不过很快通知就变成了 All retained objects were garbage collected . 这样是为什呢,我还需要担心吗?
    k10ndike
        3
    k10ndike  
       2021-01-08 10:11:57 +08:00
    @m30102 OKHttpClient 对象是 Activity 持有的么?
    hlayk
        4
    hlayk  
       2021-01-08 13:18:42 +08:00
    如果在 viewmodel 中使用对应的 viewModelScope 那么携程 launch 返回的 job 会由 viewmodel 的 onCleared 统一自动 cancel
    你在 activity 中这样使用 lifecycleScope 可以在 onDestory() 将 lifecycleScope.launch {} 返回的对象 job 主动取消下

    [Easy Coroutines in Android: viewModelScope]( https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471)
    m30102
        5
    m30102  
    OP
       2021-01-08 14:43:37 +08:00
    @k10ndike OKHttpClient 对象就算写在其他类中,同样是单例的话, 也会间接持有到 activity
    m30102
        6
    m30102  
    OP
       2021-01-08 14:51:24 +08:00
    @hlayk 如果还需要考虑 onDestory,那么 lifecycleScope 就不用叫 lifefcycleScope 了。实际上 lifefcycleScope 也确实自动 cancel 了,最终的 callBack 没有执行。但是网络请求不一定能成功 cancel,而且回调时间较长,leakcanary 不知为什么显示引用到了 activity 。 如果替换为 viewModelScope 也有效,有时候一个页面就一个网络请求懒得再写一个类直接在 activity 中请求网络,这样貌似 无解?
    k10ndike
        7
    k10ndike  
       2021-01-08 15:08:35 +08:00
    @m30102 呃,Activity 怎么间接持有到的?有测试过吗,创建一个类里面就一个静态的 OkHttpClient 实例
    vanxy
        8
    vanxy  
       2021-01-08 15:49:34 +08:00 via iPad
    你这个等于用了一半的协程,用错了
    应该同步地在协程里调用 okhttp,而不是通过回调 callback 的方式

    直接 newCall.execute() 拿到 response
    m30102
        9
    m30102  
    OP
       2021-01-08 16:58:58 +08:00
    @vanxy 同步的我试了,还是一样。同步的话只是自动取消协程,但是 call.execute()方法开始执行后并不会立即取消。
    m30102
        10
    m30102  
    OP
       2021-01-08 17:00:55 +08:00
    @k10ndike 测试过, static 的 OkHttpClient 最终会通过 activity 中传入的 lambda 回调,引用到 activity
    vanxy
        11
    vanxy  
       2021-01-08 17:22:26 +08:00 via iPad
    @m30102 你需要在 execute 外面包一个 withContext,这样当 activity finish 了,就不会执行接下来的代码了
    sankemao
        12
    sankemao  
       2021-01-08 18:06:37 +08:00
    我觉得应该是 callback 持有了 activity 的引用,可以试试把结果传给 livedata
    m30102
        13
    m30102  
    OP
       2021-01-08 18:59:30 +08:00
    @vanxy 是的,我有用 withContext. 无论是 execute 还是 enqueue ,activity finish 后传的 callback 不会执行,但是 okhttp 的 call 还是会执行的。
    m30102
        14
    m30102  
    OP
       2021-01-08 19:00:48 +08:00
    @sankemao liveData 一般配合 viewmodel 用吧,难道非得 mvp 或者 mvvm 把 activity 完全隔开才行吗。。。。
    sankemao
        15
    sankemao  
       2021-01-08 19:14:53 +08:00 via iPhone
    @m30102 你可以在 ac 里面用 livedata 验证下是否还有泄漏。你也可以用其他办法
    vanxy
        16
    vanxy  
       2021-01-08 21:02:40 +08:00
    @m30102 #13 那是自然地呀,call 是一整段的阻塞式调用。 不止是协程, 实际上没有任何方式可以取消它(除非强制停止线程),协程只是可以帮你在取消 withContext 之后的所有代码执行。

    因为不执行接下来的 callback 了, 所以 activity 也不会内存泄露了。 也不需要关心 execute 是否执行完成
    m30102
        17
    m30102  
    OP
       2021-01-08 21:57:39 +08:00
    @vanxy 我反编译了半天对比了下找到原因了, 是 okhttpclient 的原因,activity 虽然执行 destory 了,但是 okhttpclient 还在执行 call,所以延长了 activity 生命,报泄漏。如果把 okhttpclient 写在其他类中声明 static, 那么 activity 中调用协程方法传的 callBack 必须不能引用 activity 任何成员变量或者 view 等,不然还是会被延长生命,一般传回调就是为了改变 view 等,所以这个是无解的!
    vanxy
        18
    vanxy  
       2021-01-08 22:01:32 +08:00
    @m30102 #17
    哦哦, 因为 kotlin 的 lambda 持有了 activity 的引用。

    所以要把执行的放到 viewmodel 或者 presenter 里,activity 解除与 viewmodel 或者 presenter 之间的引用之后, 就不会造成泄露了
    k10ndike
        19
    k10ndike  
       2021-01-08 23:15:04 +08:00 via Android
    @m30102 你本地的代码应该有其他操作吧,帖子里那样打 Log 应该不会泄露。可以在请求 callback 里调用 livedata,就不用处理生命周期了
    xhpan10
        20
    xhpan10  
       2021-01-12 13:41:35 +08:00
    协程的代码没有 rxjava 的好看
    DiDiz
        21
    DiDiz  
       2021-01-13 10:25:26 +08:00
    @xhpan10 也要封装的,封装好了协程代码就和普通同步代码一样。相比之下 rxjava 就显得很啰嗦了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5692 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 01:52 · PVG 09:52 · LAX 17:52 · JFK 20:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.