如何避免内存泄漏:请学会这两种思维

883 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

本篇文章主要是介绍业务开发过程中,通过案例分析引入避免内存泄漏的几种思维,借助这些思维写出更加安全稳定的代码,希望能给你带来帮助。

避免长生命周期对象持有短生命周期对象

业务开发中碰到过这样的一个场景,通过Application.registerActivityLifecycleCallbacks()方法注入ActivityLifecycleCallbacks监听所有Activity的创建,并在onActivityResumed(Activity)方法中将参数Activity保存到Application中,这样就能够实现在任何地方获取当前正在显示的界面Activity实例,代码如下:

class MainApplication : Application() {

    lateinit var mCurrentActivity: Activity

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityResumed(activity: Activity) {
                mCurrentActivity = activity
            }
        })
    }

比如根据网络响应结果弹出一个弹窗,不可能在每个界面Activity中都做一个弹窗的响应处理,这个时候就需要利用这个mCurrentActivity进行Dialog的显示。

这样就会产生一个问题,Applciation的生命周期是比Activity生命周期长的,而一个短周期对象被一个长周期对象就有概率产生内存泄漏的风险,所以业务开发中要避免这样做。

注册监听回调同时要注意反注册

当前有这么一个业务场景:有一个网络监听工具类,界面可以选择性的向这个工具类中添加回调监听,从而获取当前的网络状态及变化。

  1. 定义一个网络监听回调类:
interface NetworkCallback {
    /**
     * 断网
     */
    fun netDown(): Unit

    /**
     * 连上网络
     */
    fun connected(): Unit

    /**
     * 网络较差
     */
    fun connectWorse(): Unit
}
  1. 定义一个网络监听回调的集合管理类,进行回调监听的添加移除、网络状况的通知分发:
object NetworkMonitor {
    private var mCallbacks: CopyOnWriteArrayList<NetworkCallback> = CopyOnWriteArrayList()

    //注册监听
    fun register(callback: NetworkCallback) {
        mCallbacks.add(callback)
    }
    
    //有网分发
    fun dispatchConnected() {
        mCallbacks.forEach {
            it.connected()
        }
    }
}
  1. 某个界面MainActivity2添加网络回调监听:
class MainActivity2 : AppCompatActivity() {

    private lateinit var mBinding: ActivityMain2Binding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMain2Binding.inflate(layoutInflater)
        setContentView(mBinding.root)

        NetworkMonitor.register(object: NetworkCallback {
            override fun netDown() {
            }

            override fun connected() {
            }

            override fun connectWorse() {
            }
        })
    }
}

这样看起来有毛病吗,有很大的毛病,总所周知:

匿名内部类默认会持有外部类的引用。在当前的这个案例中,通过NetworkMonitor添加的NetworkCallback实现类会默认持有外部类MainActivity2,这样NetworkMonitor这个全局网络管理类就间接持有了MainActivity2的引用,如果没有反注册监听回调,那么MainActivity2就无法正常销毁了,同样也出现了上面的长生命周期对象持有了短生命周期对象的引用,这样就直接引发了内存泄漏。

当前这个案例中该怎么解决这个问题呢,有两种方式:

  1. 增加一个反注册方法unregister(),当界面销毁时移除该回调对象:
//移除监听
fun unregister(callback: NetworkCallback) {
    mCallbacks.remove(callback)
}

这种需要我们每次比如都要在onDestroyed()中手动调用unregister()方法移除监听,忘了就巴比Q了。

  1. 借助LifecycleEventObserver绑定界面生命周期,实现自动移除:
object NetworkMonitor {
    private var mCallbacks: CopyOnWriteArrayList<NetworkCallback> = CopyOnWriteArrayList()

    //注册监听
    fun register(activity: LifecycleOwner, callback: NetworkCallback) {
        mCallbacks.add(callback)
        val callbackWrapper = NetworkCallbackWrapper(callback)
        activity.lifecycle.addObserver(callbackWrapper)
    }
    
    //有网分发
    fun dispatchConnected() {
        mCallbacks.forEach {
            it.connected()
        }
    }

    //网络监听回调包装类
    class NetworkCallbackWrapper(private val callback: NetworkCallback): LifecycleEventObserver {

        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                source.lifecycle.removeObserver(this)
                mCallbacks.remove(callback)
            }
        }

    }
}

这里的业务代码只是列出了一部分,主要是为了讲解这个案例,其他的情况暂不进行考虑。

可以看到我增加了一个NetworkCallbackWrapper包装类,包装网络监听回调类NetworkCallback,并实现了LifecycleEventObserver监听界面生命周期,通过改造后的register()方法传入LifecycleOwner添加到界面的生命周期监听集合中,这样当监听到界面销毁时,就移除监听,避免内存泄漏。

有没有感觉这种方式非常的眼熟哈,LiveData的源码中对于观察者的处理就是如此,感兴趣的可以去看下LiveDataobserve()方法以及观察者包装类LifecycleBoundObserver

所以我们可以得出结论,设计一个管理类提供注册监听的时候,一定要提高反注册的方法提供给业务方。

总结

上面介绍的这两种案例是业务开发中非常常见的,所以在进行这种场景开发中一定需要谨记这两种思维:

  1. 避免长生命周期对象持有短生命周期对象;

  2. 注册监听回调同时要注意反注册;

同时要善用内存泄漏监听库LeakCanaryDebug环境下对应用测试。