
前言
說到 Android 啟動優化,你一般會想到什么呢?
- Android 多執行緒異步加載
- Android 首頁懶加載
對,這是兩種很常見的優化手段,但是如果讓你主導這件事情,你會如何開始呢?
- 梳理現有的業務,哪些是一定要在啟動初始化的,哪些是不必要的
- 需要在啟動初始化的,哪些是可以在主執行緒初始化的,哪些是可以在子執行緒初始化的
當我們把任務丟到子執行緒初始化,這時候,我們又會遇到兩個問題,
- 在首頁,我們需要用到這個庫,如果直接使用,這個庫可能還沒有初始化,這時候直接呼叫該庫,會發生例外,你要怎么解決
- 當我們的任務相互依賴時,比如 A 依賴于 B, C 也依賴于 B,要怎么解決這種依賴關系,
這些你有想過嘛,答案都在這幾篇文章里面了,這里我就不展開講了,有疑問的可以一起探討探討,我的微信公眾號程式員徐公
Android 啟動優化(一) - 有向無環圖
Android 啟動優化(二) - 拓撲排序的原理以及解題思路
Android 啟動優化(三)- AnchorTask 開源了
Android 啟動優化(四)- AnchorTask 是怎么實作的
Android 啟動優化(五)- AnchorTask 1.0.0 版本正式發布了
接下來,我們來說一下布局優化相關的,
布局優化的現狀與發展趨勢
耗時原因
眾所周知,布局加載一直是耗時的重災區,特別是啟動階段,作為第一個 View 加載,更是耗時,
而布局加載之所以耗時,有兩個原因,
- 讀取 xml 檔案,這是一個 IO 操作,
- 決議 xml 物件,反射創建 View
一些很常見的做法是
- 減少布局嵌套層數,減少過度繪制
- 空界面,錯誤界面等界面進行懶加載
那除了這些做法,我們還有哪些手段可以優化呢?
解決方案
- 異步加載
- 采用代碼的方式撰寫布局
異步加載
google 很久之前提供了 AsyncLayoutInflater,異步加載的方案,不過這種方式有蠻多坑的,下文會介紹
采用代碼的方式撰寫布局
代碼撰寫的方式撰寫布局,我們可能想到使用 java 宣告布局,對于稍微復雜一點的布局,這種方式是不可取的,存在維護性查,修改困難等問題,為了解決這個問題,github 上面誕生了一系列優秀的開源庫,
litho
X2C
為了即保留xml的優點,又解決它帶來的性能問題,我們開發了X2C方案,即在編譯生成APK期間,將需要翻譯的layout翻譯生成對應的java檔案,這樣對于開發人員來說寫布局還是寫原來的xml,但對于程式來說,運行時加載的是對應的java檔案.
我們采用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間【注解】->【解注解】->【翻譯xml】->【生成java】整個流程的操作,
這兩個開源庫在大型的專案基本不會使用,不過他們的價值是值得肯定的,核心思想很有意義,
xml 布局加載耗時的問題, google 也想改善這種現狀,最近 Compose beta 發布了,他是采用宣告式 UI 的方式來撰寫布局,避免了 xml 帶來的耗時,同時,還支持布局實時預覽,這個應該是以后的發展趨勢,
compose-samples
小結
上面講了布局優化的現狀與發展趨勢,接下來我們一起來看一下,有哪些布局優化手段,可以應用到專案中的,
- 漸進式加載
- 異步加載
- compose 宣告式 UI
漸進式加載
什么是漸進式加載
漸進式加載,簡單來說,就是一部分一部分加載,當前幀加載完成之后,再去加載下一幀,
一種極致的做法是,加載 xml 檔案,就想加載一個空白的 xml,布局全部使用 ViewStub 標簽進行懶加載,
這樣設計的好處是可以級訓同一時刻,加載 View 帶來的壓力,通常的做法是我們先加載核心部分的 View,再逐步去加載其他 View,
有人可能會這樣問了,這樣的設計很雞肋,有什么用呢?
確實,在高端機上面作用不明顯,甚至可能看不出來,但是在中低端機上面,帶來的效果還是很明顯的,在我們專案當中,復雜的頁面首幀耗時約可以減少 30%,
優點:適配成本低,在中低端機上面效果明顯,
缺點:還是需要在主執行緒讀取 xml 檔案
核心偽代碼
start(){
loadA(){
loadB(){
loadC()
}
}
}
上面的這種寫法,是可以的,但是這種做法,有一個很明顯的缺點,就是會造成回呼嵌套層數過多,當然,我們也可以使用 RxJava 來解決這種問題,但是,如果專案中沒用 Rxjava,參考進來,會造成包 size 增加,
一個簡單的做法就是使用佇列的思想,將所有的 ViewStubTask 添加到佇列當中,當當前的 ViewStubTask 加載完成,才加載下一個,這樣可以避免回呼嵌套層數過多的問題,
改造之后的代碼見
val decorView = this.window.decorView
ViewStubTaskManager.instance(decorView)
.addTask(ViewStubTaskContent(decorView))
.addTask(ViewStubTaskTitle(decorView))
.addTask(ViewStubTaskBottom(decorView))
.start()
class ViewStubTaskManager private constructor(val decorView: View) : Runnable {
private var iViewStubTask: IViewStubTask? = null
companion object {
const val TAG = "ViewStubTaskManager"
@JvmStatic
fun instance(decorView: View): ViewStubTaskManager {
return ViewStubTaskManager(decorView)
}
}
private val queue: MutableList<ViewStubTask> = CopyOnWriteArrayList()
private val list: MutableList<ViewStubTask> = CopyOnWriteArrayList()
fun setCallBack(iViewStubTask: IViewStubTask?): ViewStubTaskManager {
this.iViewStubTask = iViewStubTask
return this
}
fun addTask(viewStubTasks: List<ViewStubTask>): ViewStubTaskManager {
queue.addAll(viewStubTasks)
list.addAll(viewStubTasks)
return this
}
fun addTask(viewStubTask: ViewStubTask): ViewStubTaskManager {
queue.add(viewStubTask)
list.add(viewStubTask)
return this
}
fun start() {
if (isEmpty()) {
return
}
iViewStubTask?.beforeTaskExecute()
// 指定 decorView 繪制下一幀的時候會回呼里面的 runnable
ViewCompat.postOnAnimation(decorView, this)
}
fun stop() {
queue.clear()
list.clear()
decorView.removeCallbacks(null)
}
private fun isEmpty() = queue.isEmpty() || queue.size == 0
override fun run() {
if (!isEmpty()) {
// 當佇列不為空的時候,先加載當前 viewStubTask
val viewStubTask = queue.removeAt(0)
viewStubTask.inflate()
iViewStubTask?.onTaskExecute(viewStubTask)
// 加載完成之后,再 postOnAnimation 加載下一個
ViewCompat.postOnAnimation(decorView, this)
} else {
iViewStubTask?.afterTaskExecute()
}
}
fun notifyOnDetach() {
list.forEach {
it.onDetach()
}
list.clear()
}
fun notifyOnDataReady() {
list.forEach {
it.onDataReady()
}
}
}
interface IViewStubTask {
fun beforeTaskExecute()
fun onTaskExecute(viewStubTask: ViewStubTask)
fun afterTaskExecute()
}
原始碼地址:github.com/gdutxiaoxu/… ViewStubTask,ViewStubTaskManager**, 有興趣的可以看看
異步加載
異步加載,簡單來說,就是在子執行緒創建 View,在實際應用中,我們通常會先預加載 View,常用的方案有:
- 在合適的時候,啟動子執行緒 inflate layout,然后取的時候,直接去快取里面查找 View 是否已經創建好了,是的話,直接使用快取,否則,等待子執行緒 inlfate 完成,
AsyncLayoutInflater
官方提供了一個類,可以來進行異步的inflate,但是有兩個缺點:
- 每次都要現場new一個出來
- 異步加載的view只能通過callback回呼才能獲得(死穴)
因此,我們可以仿造官方的 AsyncLayoutInflater 進行改造,核心代碼在 AsyncInflateManager,主要介紹兩個方法,
asyncInflate 方法,在子執行緒 inflateView,并將加載結果存放到 mInflateMap 里面,
@UiThread
fun asyncInflate(
context: Context,
vararg items: AsyncInflateItem?
) {
items.forEach { item ->
if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
return
}
mInflateMap[item.inflateKey] = item
onAsyncInflateReady(item)
inflateWithThreadPool(context, item)
}
}
getInflatedView 方法,用來獲得異步inflate出來的view,核心思想如下
- 先從快取結果里面拿 View,拿到了view直接回傳
- 沒拿到view,但是子執行緒在inflate中,等待回傳
- 如果還沒開始inflate,由UI執行緒進行inflate
/**
* 用來獲得異步inflate出來的view
*
* @param context
* @param layoutResId 需要拿的layoutId
* @param parent container
* @param inflateKey 每一個View會對應一個inflateKey,因為可能許多地方用的同一個 layout,但是需要inflate多個,用InflateKey進行區分
* @param inflater 外部傳進來的inflater,外面如果有inflater,傳進來,用來進行可能的SyncInflate,
* @return 最后inflate出來的view
*/
@UiThread
fun getInflatedView(
context: Context?,
layoutResId: Int,
parent: ViewGroup?,
inflateKey: String?,
inflater: LayoutInflater
): View {
if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {
val item = mInflateMap[inflateKey]
val latch = mInflateLatchMap[inflateKey]
if (item != null) {
val resultView = item.inflatedView
if (resultView != null) {
//拿到了view直接回傳
removeInflateKey(item)
replaceContextForView(resultView, context)
Log.i(TAG, "getInflatedView from cache: inflateKey is $inflateKey")
return resultView
}
if (item.isInflating() && latch != null) {
//沒拿到view,但是在inflate中,等待回傳
try {
latch.await()
} catch (e: InterruptedException) {
Log.e(TAG, e.message, e)
}
removeInflateKey(item)
if (resultView != null) {
Log.i(TAG, "getInflatedView from OtherThread: inflateKey is $inflateKey")
replaceContextForView(resultView, context)
return resultView
}
}
//如果還沒開始inflate,則設定為false,UI執行緒進行inflate
item.setCancelled(true)
}
}
Log.i(TAG, "getInflatedView from UI: inflateKey is $inflateKey")
//拿異步inflate的View失敗,UI執行緒inflate
return inflater.inflate(layoutResId, parent, false)
}
簡單 Demo 示范
第一步:選擇在合適的時機呼叫 AsyncUtils#asyncInflate 方法預加載 View,
object AsyncUtils {
fun asyncInflate(context: Context) {
val asyncInflateItem =
AsyncInflateItem(
LAUNCH_FRAGMENT_MAIN,
R.layout.fragment_asny,
null,
null
)
AsyncInflateManager.instance.asyncInflate(context, asyncInflateItem)
}
fun isHomeFragmentOpen() =
getSP("async_config").getBoolean("home_fragment_switch", true)
}
第二步:在獲取 View 的時候,先去快取里面查找 View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val startTime = System.currentTimeMillis()
val homeFragmentOpen = AsyncUtils.isHomeFragmentOpen()
val inflatedView: View
inflatedView = AsyncInflateManager.instance.getInflatedView(
context,
R.layout.fragment_asny,
container,
LAUNCH_FRAGMENT_MAIN,
inflater
)
Log.i(
TAG,
"onCreateView: homeFragmentOpen is $homeFragmentOpen, timeInstance is ${System.currentTimeMillis() - startTime}, ${inflatedView.context}"
)
return inflatedView
// return inflater.inflate(R.layout.fragment_asny, container, false)
}
優缺點
優點:
可以大大減少 View 創建的時間,使用這種方案之后,獲取 View 的時候基本在 10ms 之內的,
缺點
- 由于 View 是提前創建的,并且會存在在一個 map,需要根據自己的業務場景將 View 從 map 中移除,不然會發生記憶體泄露
- View 如果快取起來,記得在合適的時候重置 view 的狀態,不然有時候會發生奇奇怪怪的現象,
總結
參考文章:Android - 一種新奇的冷啟動速度優化思路(Fragment極度懶加載 + Layout子執行緒預加載)
- View 的漸進式加載,在 JectPack compose 沒有推廣之后,推薦使用這種方案,適配成本低
- View 的異步加載方案,雖然效果顯著,但是適配成本也高,沒搞好,容易發生記憶體泄露
- JectPack compose 宣告式 UI,基本是未來的趨勢,有興趣的可以提前了解一下他,
Talk is cheap. Show me the code
原始碼地址:github.com/gdutxiaoxu/…
找到我
這篇文章,加上一些 Demo,足足花了我幾個晚上的時間,我是站在巨人的肩膀上成長起來的,同樣,我也希望成為你們的巨人,覺得不錯的話可以關注一下我的微信公眾號程式員徐公,在此感謝各位大佬們,主要分享
- Android 開發相關知識:包括 java,kotlin, Android 技術,
- 面試相關分享:包括常見的面試題目,大廠面試真題、面試經驗套路分享,
- 演算法相關學習筆記:比如怎么學習演算法,leetcode 常見演算法總結,跟大家一起學習演算法,
- 時事點評:主要是關于互聯網的,比如小米高管屌絲事件,拼多多女員工猝死事件等
希望我們可以成為朋友,成長路上的忠實伙伴!

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/273762.html
標籤:其他
上一篇:[設計模式C++]工廠模式
下一篇:【ArcGIS Engine二次開發】入門基礎(2):ArcGIS開發方式(VBA、DLL、Add-in、Engine)對比
