Android開發(fā)SavedState?Jetpack狀態(tài)保存利器
背景
在我們android開發(fā)中,如果需要actiivty/fragment等有狀態(tài)的控件保存當(dāng)前狀態(tài),由系統(tǒng)進行數(shù)據(jù)保存的恢復(fù)的時候
比如正常的暗黑模式切換/后臺時低內(nèi)存系統(tǒng)回收等等,都需要我們對當(dāng)前的用戶數(shù)據(jù)進行保存,不然下次重新恢復(fù)的時候,就會出現(xiàn)丟失數(shù)據(jù)的情況,給用戶造成不太好的體驗
一般都會重寫onSaveInstanceState方法進行,在里面的Bundle對象進行數(shù)據(jù)的寫入,然后會在onCreate階段或者onRestoreInstanceState階段進行數(shù)據(jù)的恢復(fù)!這套生命周期框架一直是深深嵌入在我們的開發(fā)習(xí)慣中!
但是隨著項目的迭代,也隨著業(yè)務(wù)的發(fā)展,我們可以發(fā)現(xiàn),在頁面復(fù)雜度提高的同時,在onSaveInstanceState里面需要保存的數(shù)據(jù)也是越來越多這就帶來了幾個問題,僅舉例
- onSaveInstanceState存儲數(shù)據(jù)復(fù)雜,可能多人迭代就存在重復(fù)存取的現(xiàn)象,造成代碼結(jié)構(gòu)問題與隱藏bug的風(fēng)險
- 不可復(fù)用,比如activity1需要存儲的數(shù)據(jù),剛好activity2也需要存儲,這個時候就只能寫兩份代碼,以此類推
- 沒有統(tǒng)一的管理層,即數(shù)據(jù)的維護可能需要團隊的代碼規(guī)范
SavedState的登場
為了解決這些歷史android的設(shè)計問題,也為了更方便廣大開發(fā)者進行更好的代碼結(jié)構(gòu)解耦設(shè)計,所以google大哥在jetpack庫中,推出了SavedState,它的定位是在Android開發(fā)中,編寫可插入組件,以在進程終止時保存界面狀態(tài),并在進程重啟時恢復(fù)界面狀態(tài)。
雖然Savedstate推出來一段時間了,但是卻一直處在“默默無聞”的狀態(tài),可能的原因有很多,比如日常開發(fā)很少接觸狀態(tài)保存,大部分app中真正需要保存狀態(tài)的其實是很少一部分,很多app甚至是不寫狀態(tài)保存邏輯的,被系統(tǒng)回收就回收掉了,重建就是了(即使丟失了)。
還有就是學(xué)習(xí)資料比較少,現(xiàn)在基本找不到SavedState的相關(guān)資料(區(qū)別于我們viewmodel常用的SavesStateHandle)。
官方的demo例子也沒有,所以Savedstate很長一段時間都是處于雪藏的狀態(tài)。但是!為了讓我們的app擁有更加好的體驗,同時也提高我們的技術(shù)視野,學(xué)習(xí)Savedstate還是很有必要的,不然官方也不會白白將其加入jetpack系列。
理解SavedState
用法
深入理解之前呢,我們必須要會用不是嘛!我們下面來看一下例子
在Activity onCreate方法中執(zhí)行以下代碼
注意:savedStateRegistry.isRestored在onCreate之后就會變成true,
也就是說,我們必須在ComponentActivity的onCreate走完之后才能用,
原理看下邊的解析
if(savedStateRegistry.isRestored){ val consumeRestoredStateForKey = savedStateRegistry.consumeRestoredStateForKey("test") val test = consumeRestoredStateForKey?.getInt("test") Log.i("hello","tag test is $test") } savedStateRegistry.registerSavedStateProvider("test",DataProvider())
class DataProvider: SavedStateRegistry.SavedStateProvider { override fun saveState(): Bundle { return Bundle().apply { this.putInt("test",1) } } }
如果對上訴程序不理解,不要緊,我們會接下來講解!運行上訴程序,在app一開始啟動的時候,log輸出就是null,這個時候我們退到后臺(可以設(shè)置不保留活動),再次打開的時候,可以看到activity被回收重建,這個時候log輸出的卻是1,這里就驗證了SavedState具有保存數(shù)據(jù)的能力。
在demo中savedStateRegistry其實是ComponentActivity的一個對象,我們接下里分析一下,SavedState的概念組成
SavedState組成概念
SavedState庫中,有以下幾個概念
SavedStateRegistryOwner | SavedStateRegistryController | SavedStateRegistry | SavedStateProvider |
---|---|---|---|
保存狀態(tài)擁有者,用于提供聲明周期的同步操作 | 控制器,相當(dāng)于一個連接角色,用于連接SavedStateRegistryOwner與SavedStateRegistry | 數(shù)據(jù)管理者,用于提供對存儲數(shù)據(jù)的相關(guān)操作 | 數(shù)據(jù)提供者,用于提供存儲的數(shù)據(jù) |
我們再來看一下依賴關(guān)系,可以看到這個是單向依賴
看到這里,我們就對SavedState有了個初步的認(rèn)識,這個時候就可以再回到demo了,首先我們的Activity是繼承了ComponentActivity,而ComponentActivity其實是實現(xiàn)了SavedStateRegistryOwner接口的
所以我們的數(shù)據(jù)存儲過程發(fā)起者都是由該activity的生命周期決定,而ComponentActivity里面擁有一個SavedStateRegistryController對象
我們可以通過SavedStateRegistryController對象,獲取到SavedStateRegistry
構(gòu)成已經(jīng)很清楚了,我們再來解釋一下上面的用法,其實分為兩步:
- savedStateRegistry.registerSavedStateProvider,通過savedStateRegistry的registerSavedStateProvider方法注冊一個數(shù)據(jù)保存集合,第一個參數(shù)就是自定義的key,第二個參數(shù)為實現(xiàn)SavedStateProvider接口的類,即DataProvider
- savedStateRegistry.consumeRestoredStateForKey可以獲取當(dāng)前的存儲的數(shù)據(jù)集合,參數(shù)為我們自定義的key,如果當(dāng)前存在key對應(yīng)的數(shù)據(jù),就返回具體存儲的數(shù)據(jù)(發(fā)生在重組時),如果不存在就返回null(發(fā)生在首次進入的時候)。值得注意的一個點是,如果我們沒有調(diào)用unregisterSavedStateProvider(刪除對應(yīng)key的存儲數(shù)據(jù))方法,那么除了首次進入之外,每次系統(tǒng)回收都會幫我們恢復(fù)在registerSavedStateProvider創(chuàng)建時的數(shù)據(jù)集合。
到這里,用法其實就很明確了,通過以上的分層,我們就可以把原本耦合在onSaveInstanceState的數(shù)據(jù)存儲邏輯,變成了一個個SavedStateProvider,方便了后期數(shù)據(jù)的管理與復(fù)用,即像插件一樣我們隨時可以替換具體的邏輯而不影響業(yè)務(wù)本身。
原理探究
我們來看一下registerSavedStateProvider究竟做了些什么
@MainThread public void registerSavedStateProvider(@NonNull String key, @NonNull SavedStateProvider provider) { SavedStateProvider previous = mComponents.putIfAbsent(key, provider); if (previous != null) { throw new IllegalArgumentException("SavedStateProvider with the given key is" + " already registered"); } }
private SafeIterableMap<String, SavedStateProvider> mComponents = new SafeIterableMap<>();
可以看到,其實就是在mComponents里面放了一個SavedStateProvider對象,當(dāng)前,如果我們之前存放過了,再次存放就會拋出異常,而mComponents,其實就是一個map對象。
那么我們SavedStateRegistry什么時候才觸發(fā)這個存儲邏輯呢?其實在performSave方法里面
@MainThread void performSave(@NonNull Bundle outBundle) { Bundle components = new Bundle(); if (mRestoredState != null) { components.putAll(mRestoredState); } for (Iterator<Map.Entry<String, SavedStateProvider>> it = mComponents.iteratorWithAdditions(); it.hasNext(); ) { Map.Entry<String, SavedStateProvider> entry1 = it.next(); // 觸發(fā)了SavedStateProvider的saveState方法 components.putBundle(entry1.getKey(), entry1.getValue().saveState()); } outBundle.putBundle(SAVED_COMPONENTS_KEY, components); }
這里有個有趣的點,它接受一個外來的Bundle,在外來的Bundle里面,再次存了一個子Bundle,而這個子Bundle,其實就是我們上文的mComponents的數(shù)據(jù),而對應(yīng)的key是一個常量
private static final String SAVED_COMPONENTS_KEY = "androidx.lifecycle.BundlableSavedStateRegistry.key";
存放的子Bundle通過遍歷的方式,觸發(fā)了SavedStateProvider的saveState方法,獲取到我們實際上想要保存的數(shù)據(jù)。 那么是誰調(diào)用了SavedStateRegistry的performSave方法呢?當(dāng)然就是 SavedStateRegistryController啦,我們說過它其實是個中間角色,大部分操作需要經(jīng)過它才能調(diào)用到SavedStateRegistry
@MainThread public void performSave(@NonNull Bundle outBundle) { mRegistry.performSave(outBundle); }
而SavedStateRegistryController的performSave方法,真的被調(diào)用起來,就是在ComponenntActivity中
@CallSuper @Override protected void onSaveInstanceState(@NonNull Bundle outState) { Lifecycle lifecycle = getLifecycle(); if (lifecycle instanceof LifecycleRegistry) { ((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED); } super.onSaveInstanceState(outState); 被調(diào)用 mSavedStateRegistryController.performSave(outState); mActivityResultRegistry.onSaveInstanceState(outState); }
到這里我們應(yīng)該豁然開朗了,其實還是換湯不換藥,都是在onSaveInstanceState里面進行的邏輯保存,只不過這一層被SavedState給封裝起來了,同時加入到了androidx中,也算是官方想要重新完善onSaveInstanceState架構(gòu)的體現(xiàn)。
看到這里了,我們也就能解釋存放在SavedStateProvider的數(shù)據(jù),其實就存放在了onSaveInstanceState的Bundle中,key為SAVED_COMPONENTS_KEY的子Bundle中。
我們再來看一下consumeRestoredStateForKey,取數(shù)據(jù)的邏輯
@MainThread @Nullable public Bundle consumeRestoredStateForKey(@NonNull String key) { if (!mRestored) { throw new IllegalStateException("You can consumeRestoredStateForKey " + "only after super.onCreate of corresponding component"); } if (mRestoredState != null) { Bundle result = mRestoredState.getBundle(key); mRestoredState.remove(key); if (mRestoredState.isEmpty()) { mRestoredState = null; } return result; } return null; }
可以看到,只有mRestored為true的時候,我們才能真正進入到取數(shù)的邏輯,否則拋出異常,因為我們能夠獲取到數(shù)據(jù)的前提是,系統(tǒng)幫我們把數(shù)據(jù)進行恢復(fù)之后!而mRestored被賦值的時候,其實就在performRestore中
@SuppressWarnings("WeakerAccess") @MainThread void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState) { if (mRestored) { throw new IllegalStateException("SavedStateRegistry was already restored."); } if (savedState != null) { mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY); } .... mRestored = true; }
有了上面存數(shù)據(jù)的邏輯,我們很容易知道,performRestore的調(diào)用者,最終肯定是由實現(xiàn)了 SavedStateRegistryOwner接口的ComponentActivity發(fā)起調(diào)用
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { // Restore the Saved State first so that it is available to // OnContextAvailableListener instances //這里就是恢復(fù)數(shù)據(jù)來源 mSavedStateRegistryController.performRestore(savedInstanceState); mContextAwareHelper.dispatchOnContextAvailable(this); super.onCreate(savedInstanceState); mActivityResultRegistry.onRestoreInstanceState(savedInstanceState); ReportFragment.injectIfNeededIn(this); if (mContentLayoutId != 0) { setContentView(mContentLayoutId); } }
最后我們再給出具體的流程圖,存數(shù)據(jù)和取數(shù)據(jù)的邏輯:
最后
通過SavedState,我們也能夠看到j(luò)etpack官方希望改變歷史android架構(gòu)的決心,也想要提供更加便捷優(yōu)雅的方式提供給開發(fā)者。看到這里了,也希望我們在日常開發(fā)中,可以運用到SavedState進行數(shù)據(jù)保存恢復(fù),別讓它繼續(xù)雪藏啦!畢竟連依賴都不需要導(dǎo)入呢 ! 更多關(guān)于SavedState Jetpack狀態(tài)保存的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android基礎(chǔ)之使用Fragment適應(yīng)不同屏幕和分辨率(分享)
以下是對Fragment的使用進行了詳細(xì)的分析介紹,需要的朋友可以過來參考下2013-07-07Kotlin擴展函數(shù)及實現(xiàn)機制的深入探索
擴展函數(shù)與擴展屬性的神奇之處在于,可以在不修改原來類的條件下,使用函數(shù)和屬性,表現(xiàn)得就像是屬于這個類的一樣。下面這篇文章主要給大家介紹了關(guān)于Kotlin擴展函數(shù)及實現(xiàn)機制的相關(guān)資料,需要的朋友可以參考下2018-06-06Android編程實現(xiàn)禁止?fàn)顟B(tài)欄下拉的方法詳解
這篇文章主要介紹了Android編程實現(xiàn)禁止?fàn)顟B(tài)欄下拉的方法,結(jié)合實例形式詳細(xì)分析了Android狀態(tài)欄操作相關(guān)的函數(shù)、屬性調(diào)用及權(quán)限控制設(shè)置技巧,需要的朋友可以參考下2017-08-08Android原生ViewPager控件實現(xiàn)卡片翻動效果
這篇文章主要為大家詳細(xì)介紹了Android原生ViewPager控件實現(xiàn)卡片翻動效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07Android編程開發(fā)之TextView單擊鏈接彈出Activity的方法
這篇文章主要介紹了Android編程開發(fā)之TextView單擊鏈接彈出Activity的方法,涉及Android中TextView控件的相關(guān)操作技巧,需要的朋友可以參考下2016-01-01Android?Jetpack結(jié)構(gòu)運用Compose實現(xiàn)微博長按點贊彩虹效果
Compose在動畫方面下足了功夫,提供了豐富的API。但也正由于API種類繁多,如果想一氣兒學(xué)下來,最終可能會消化不良,導(dǎo)致似懂非懂。結(jié)合例子學(xué)習(xí)是一個不錯的方法,本文就帶大家邊學(xué)邊做,通過實現(xiàn)一個微博長按點贊的動畫效果,學(xué)習(xí)了解Compose動畫的常見思路和開發(fā)技巧2022-07-07基于Android ContentProvider的總結(jié)詳解
本篇文章是對Android ContentProvider進行了詳細(xì)的總結(jié)與分析,需要的朋友參考下2013-05-05