关于LiveData可能引发的内存泄漏及优化

郭霖 2021-11-25 21:17



/   今日科技快讯   /


近日,在国务院网站求助总理的45岁程序员火上了热搜——“自己精通各种技术体系,却连个面试机会都没有”,央媒回应、网友热议,看似偶然事件的背后,折射的是赤裸裸的现实问题。


/   作者简介   /


明天就是周六啦,提前祝大家周末愉快!


本篇文章来自忆_析风的投稿,文章主要分析了LiveData可能存在的内存泄漏,相信会对大家有所帮助。同时也感谢作者贡献的精彩文章!


忆_析风博客地址:

https://www.jianshu.com/u/9e88b3207a5e


/   前言   /


随着MVVM的流行,LiveData便成了Android数据重要的存储和观察组件.


一般我们会将LiveData和ViewModel结合使用,LiveData作为ViewModel的成员.


LiveData相比较于一般的观察者组件,其好用的地方是它在observe后不需要手动的解除订阅,它会根据订阅者的生命周期自动进行解除.


/   泄漏分析   /


这看起来很完美,不过我们深入源码看看订阅过程.


    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        //......
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        //......
        owner.getLifecycle().addObserver(wrapper);
    }


这里将LiveData的订阅者observer装封到了LifecycleBoundObserver中,然后又将LifecycleBoundObserver实例作为LifeCycleOwner的生命周期观察者.


查看LifecycleBoundObserver的实现


    class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        //......
    }


可以发现这是一个Java的内部类,也就是说这个内部类的实例会持有LiveData的引用,
而LifecycleBoundObserver作为观察者被添加到了LifecycleOwner的生命周期观察者里.


这就是说LifeCycleOwner实际上会持有LiveData的引用.


查找LiveData中解除引用的代码如下


    @MainThread
    public void removeObserver(@NonNull final Observer<? super T> observer) {
        //......
        removed.detachObserver();
        //......
    }

    class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        //......
        @Override
        public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            //......
        }
        //......
        @Override
        void detachObserver() {
            mOwner.getLifecycle().removeObserver(this);
        }
    }


这个引用会一直持续到LifeCycleOwner的onDestroy回调触发后才会将LiveData的引用从LifeCycleOwner中解除.


综上,通过observer方式注册的LiveData,其只能在LifeCycleOwner被销毁后才可能会被回收.


这在ViewModel上使用倒是没问题的,毕竟ViewModel的生命周期和Activity/Fragment基本是同步的.


除非在使用Fragment的ViewModel中的LiveData.observe时传了Activity作为LifeCycleOwner,如果是这样便会导致Fragment销毁后,Activity仍然持有此Fragment关联的ViewModel中的LiveData引用.


但是LiveData作为一个轻量级又特别好用的数据观察组件,只放在ViewModel中多少有点不甘心不是.


但是当我们脱离ViewModel使用LiveData时,比如我们想通过LiveData来观察一个对象的状态变化,那么如果这个对象是在LifeCycleOwner销毁前被垃圾回收了,那么这个对象的LiveData却不会被回收,而依然被LifeCycleOwner所引用着,那么此时便会发生内存泄漏.


/   解决方法   /


LiveData作为一个如此优秀的轻量级数据观察组件,只能放在ViewModel中使用未免太可惜.


那么有什么办法能让LiveData能在其它对象中使用呢?


办法是有的,比如我们在名为TestTask的对象中添加了一个LiveData,如果我们希望在对象被回收的时候LiveData也会被跟着被回收,那么我们可以在TestTask的finalize方法中添加代码让LifeCycleOwner移除对LiveData的引用.


但是这种方法多少有点麻烦,好吧不是有点麻烦而是特别麻烦.


不过这方法能给我们提供一个新的思路,就是我们用一个容器来存放LiveData,然后再通过容器的finalize来解除LifeCycleOwner对LiveData的引用,在TestTask中使用这个容器来访问LiveData.


通过这种方式,当TestTask被回收时,其中的容器也将会被回收,当容器回收时解除LifeCycleOwner对LiveData的引用,这样LiveData本身便能正常被销毁.


嗯,这种方式可行.不过还是略微有点麻烦,毕竟还有一个容器不是.


要解决这个容器也简单,实际上我们只需要使用装饰模式,让这个容器继承于LiveData,并且在其中添加一个实际持有数据的LiveData,然后所有事情都甩给真实持有数据的LiveData的去干,外层LiveData只需要处理finalize时解除LifeCycleOwner对LiveData的引用即可.


将这个装饰者命名为SafeLiveData大致代码如下


public class SafeLiveData<Textends MutableLiveData<T{

    private final MutableLiveData<T> realLiveData;

    private final List<WeakReference<Observer<? super T>>> observerReferenceList = new ArrayList();

    public SafeLiveData(MutableLiveData<T> liveData) {
        super();
        realLiveData = liveData;
    }

    public SafeLiveData() {
        super();
        realLiveData = new MutableLiveData<T>();
    }

    //......

    @MainThread
    private void recordObserver(@NonNull Observer<? super T> observer) {

        //add observer weakreference to observerReferenceList
        //......

    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        runInMainThread(() -> {
            int size = observerReferenceList.size();
            for (int i = 0; i < size; i++) {
                Observer<? super T> observer = observerReferenceList.get(i).get();
                if (observer != null) {
                    realLiveData.removeObserver(observer);
                }
            }
        });
    }

    @Override
    public void observe(@NonNull @NotNull LifecycleOwner owner, @NonNull @NotNull Observer<? super T> observer) {
        runInMainThread(() -> {
            recordObserver(observer);
            realLiveData.observe(owner, observer);
        });
    }

    @Override
    public void observeForever(@NonNull @NotNull Observer<? super T> observer) {
        runInMainThread(() -> {
            recordObserver(observer);
            realLiveData.observeForever(observer);
        });
    }

    //......

}


如此便可完美解决LiveData的内存泄漏的问题.


/   线程优化   /


以上解决了内存泄漏的问题,但是实际使用上依然还有个问题,便是线程安全问题.


LiveData本身并没有做线程安全相关的操作,其默认是只能在主线程中使用的,毕竟在大部分方法里都加了assertMainThread方法


    private static void assertMainThread(String methodName) {
        if (!ArchTaskExecutor.getInstance().isMainThread()) {
            throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
                    + " thread");
        }
    }


只要你没在主线程中调用就会抛异常.


在后台线程里我们也能通过postValue来发送消息,但是LiveData只有这一个方式是切了线程的,其它方法比如observe,在后台线程调用直接crash.


还有我们可能会有这样的操作


class TestTask {

    val infoLiveData = MutableLiveData<Int>().apply { value = 0 }

}


类初始化便会调用setValue方法,如果类初始化并不在主线程就会崩溃;


异或这样的操作


object Manager {

    init {
        Setting.settingLiveData.observeForever{

        }
    }

}

object Setting {

    val settingLiveData = MutableLiveData<Boolean>()

}


如果Manager初次调用不在主线程也会导致崩溃.


那么如何解决这个问题呢?


实际上也很简单对于大部分方法我们只需要在装饰的LiveData的方法中加入Handler.Post对操作进行线程切换,为了避免不必要的Post可能会导致的时序问题,可以先判断是否在主线程,如果在则直接执行,如果不在则Post.


    private static void runInMainThread(Runnable runnable) {
        boolean inMainThread = isInMainThread();
        if (inMainThread) {
            runnable.run();
        } else {
            getHandler().post(runnable);
        }
    }


对于大部分方法都能通过这种方式来实现,但是有一个方法例外就是setValue

setValue是不能Post的,假设我们使用Post来setValue,那么在setValue后我们将不能马上使用这个值,当获取这个值时,这个值可能是空值,这种问题是致命的.


那么如何解决这个问题呢?


我们可以通过wait和notify的方式.如果是一个后台线程调用setValue,则我们可以先Post一个任务去执行setValue的操作,并将此后台线程挂起,当任务切到主线程执行完setValue后,我们再将这个线程唤醒.


实现方法如下


    public static <T> void setValueSync(MutableLiveData<T> liveData, T value) {
        if (liveData == null) {
            return;
        }
        if (isInMainThread()) {
            liveData.setValue(value);
            return;
        }
        Runnable runnable = () -> {
            synchronized (liveData) {
                try {
                    liveData.setValue(value);
                } finally {
                    liveData.notify();
                }
            }
        };
        synchronized (liveData) {
            getHandler().post(runnable);
            try {
                liveData.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


这样也算是完美解决这个问题了,如果后台线程觉得setValue需要等待太慢,也可以使用postValue来传值.


吐槽


为啥上面代码都是Java呢?你好low了,现在不会Kotlin都没人要了兄弟!


??? 是我不想用Kotlin吗


原因是这样的,用Kotlin重写LiveData的setValue和getValue会导致无法访问属性value来交互,这样的体验将是非常差的,而且Google查了半天也没查到解决办法.如果有大佬有解决办法!跪求告知!!!


然后Kotlin对象没有wait和notify,只能用ReentrantLock和Condition实现.


如果用ReentrantLock和Condition来实现同步的setValue则需要很多额外的对象和维护开销,较为繁琐.


这语法糖里有毒......


/   使用SafeLiveData   /


如果你觉得上面所说也能解决你的痛点,不如试试这个SafeLiveData


依赖


    allprojects {
        repositories {
            //...
            maven { url 'https://www.jitpack.io' }
        }
    }


    dependencies {
            def lastSafeLiveDataVersion = "1.1.1" //replace lastSafeLiveDataVersion
            implementation "com.github.dqh147258:SafeLiveData:$lastSafeLiveDataVersion"
    }


使用的话很简单将MutableLiveData替换成SafeLiveData即可


例如


    val infoLiveData = MutableLiveData<Int>()


替换成


    val infoLiveData = SafeLiveData<Int>()


如果你不想破坏自己的LiveData逻辑实现,这是个装饰器,想保留自己LiveData的特性也简单,作为构造参数传入即可


    val yourLiveData = MediatorLiveData<Int>()
    val infoLiveData = SafeLiveData<Int>(yourLiveData)


源码地址:

https://github.com/dqh147258/SafeLiveData



推荐阅读:

我的新书,《第一行代码 第3版》已出版!

如何更好地使用Kotlin语法糖封装工具类

Activity Result API详解,放弃startActivityForResult吧


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注

推荐阅读