關于LiveData可能引發(fā)的內存泄漏及優(yōu)化

關于LiveData可能引發(fā)的內存泄漏及優(yōu)化

隨著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 {
        //......
    }

可以發(fā)現這是一個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);
        }
    }

這個引用會一直持續(xù)到LifeCycleOwner的onDestroy回調觸發(fā)后才會將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來觀察一個對象的狀態(tài)變化,那么如果這個對象是在LifeCycleOwner銷毀前被垃圾回收了,那么這個對象的LiveData卻不會被回收,而依然被LifeCycleOwner所引用著,那么此時便會發(fā)生內存泄漏.

解決方法

LiveData作為一個如此優(yōu)秀的輕量級數據觀察組件,只能放在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<T> extends 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線程優(yōu)化

以上解決了內存泄漏的問題,但是實際使用上依然還有個問題,便是線程安全問題.LiveData本身并沒有做線程安全相關的操作,其默認是只能在主線程中使用的,畢竟在大部分方法里都加了assertMainThread方法

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

只要你沒在主線程中調用就會拋異常.

在后臺線程里我們也能通過postValue來發(fā)送消息,但是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可能會導致的時序問題,可以先判斷是否在主線程,如果在則直接執(zhí)行,如果不在則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一個任務去執(zhí)行setValue的操作,并將此后臺線程掛起,當任務切到主線程執(zhí)行完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嗎,f**k!

原因是這樣的,用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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末孵睬,一起剝皮案震驚了整個濱河市之剧,隨后出現的幾起案子,更是在濱河造成了極大的恐慌采盒,老刑警劉巖唧龄,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兼砖,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機讽挟,發(fā)現死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門然走,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人戏挡,你說我怎么就攤上這事芍瑞。” “怎么了褐墅?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵拆檬,是天一觀的道長。 經常有香客問我妥凳,道長竟贯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任逝钥,我火速辦了婚禮屑那,結果婚禮上,老公的妹妹穿的比我還像新娘艘款。我一直安慰自己持际,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布哗咆。 她就那樣靜靜地躺著蜘欲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晌柬。 梳的紋絲不亂的頭發(fā)上姥份,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音年碘,去河邊找鬼澈歉。 笑死,一個胖子當著我的面吹牛屿衅,可吹牛的內容都是我干的埃难。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼傲诵,長吁一口氣:“原來是場噩夢啊……” “哼凯砍!你這毒婦竟也來了?” 一聲冷哼從身側響起拴竹,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤悟衩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栓拜,有當地人在樹林里發(fā)現了一具尸體座泳,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡惠昔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了挑势。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镇防。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖潮饱,靈堂內的尸體忽然破棺而出来氧,到底是詐尸還是另有隱情,我是刑警寧澤香拉,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布啦扬,位于F島的核電站,受9級特大地震影響凫碌,放射性物質發(fā)生泄漏扑毡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一盛险、第九天 我趴在偏房一處隱蔽的房頂上張望瞄摊。 院中可真熱鬧,春花似錦苦掘、人聲如沸换帜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膜赃。三九已至,卻和暖如春揉忘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背端铛。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工泣矛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人禾蚕。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓您朽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親换淆。 傳聞我的和親對象是個殘疾皇子哗总,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容