前言
說到 Android 啓動優化,你通常會想到什麼呢?java
Android 多線程異步加載android
Android 首頁懶加載git
對,這是兩種很常見的優化手段,可是若是讓你主導這件事情,你會如何開始呢?程序員
梳理現有的業務,哪些是必定要在啓動初始化的,哪些是沒必要要的github
須要在啓動初始化的,哪些是能夠在主線程初始化的,哪些是能夠在子線程初始化的web
當咱們把任務丟到子線程初始化,這時候,咱們又會遇到兩個問題。面試
在首頁,咱們須要用到這個庫,若是直接使用,這個庫可能尚未初始化,這時候直接調用該庫,會發生異常,你要怎麼解決算法
當咱們的任務相互依賴時,好比 A 依賴於 B, C 也依賴於 B,要怎麼解決這種依賴關係。緩存
這些你有想過嘛。答案都在這幾篇文章裏面了,這裏我就不展開講了。有疑問的能夠一塊兒探討探討。微信
Android 啓動優化(二) - 拓撲排序的原理以及解題思路
Android 啓動優化(三)- AnchorTask 開源了
Android 啓動優化(四)- AnchorTask 是怎麼實現的
Android 啓動優化(五)- AnchorTask 1.0.0 版本正式發佈了
接下來,咱們來講一下佈局優化相關的。
佈局優化的現狀與發展趨勢
耗時緣由
衆所周知,佈局加載一直是耗時的重災區。特別是啓動階段,做爲第一個 View 加載,更是耗時。
而佈局加載之因此耗時,有兩個緣由。
讀取 xml 文件,這是一個 IO 操做。
解析 xml 對象,反射建立 View
一些很常見的作法是
減小布局嵌套層數,減小過分繪製
空界面,錯誤界面等界面進行懶加載
那除了這些作法,咱們還有哪些手段能夠優化呢?
解決方案
異步加載
採用代碼的方式編寫佈局
異步加載
google 好久以前提供了 AsyncLayoutInflater,異步加載的方案,不過這種方式有蠻多坑的,下文會介紹
採用代碼的方式編寫佈局
代碼編寫的方式編寫佈局,咱們可能想到使用 java 聲明佈局,對於稍微複雜一點的佈局,這種方式是不可取的,存在維護性查,修改困難等問題。爲了解決這個問題,github 上面誕生了一系列優秀的開源庫。
litho: https://github.com/facebook/litho
X2C: https://github.com/iReaderAndroid/X2C
爲了即保留xml的優勢,又解決它帶來的性能問題,咱們開發了X2C方案。即在編譯生成APK期間,將須要翻譯的layout翻譯生成對應的java文件,這樣對於開發人員來講寫佈局仍是寫原來的xml,但對於程序來講,運行時加載的是對應的java文件.
咱們採用APT(Annotation Processor Tool)+ JavaPoet技術來完成編譯期間【註解】->【解註解】->【翻譯xml】->【生成java】整個流程的操做。
這兩個開源庫在大型的項目基本不會使用,不過他們的價值是值得確定的,核心思想頗有意義。
xml 佈局加載耗時的問題, google 也想改善這種現狀,最近 Compose beta 發佈了,他是採用聲明式 UI 的方式來編寫佈局,避免了 xml 帶來的耗時。同時,還支持佈局實時預覽。這個應該是之後的發展趨勢。
compose-samples: https://github.com/android/compose-samples
小結
上面講了佈局優化的現狀與發展趨勢,接下來咱們一塊兒來看一下,有哪些佈局優化手段,能夠應用到項目中的。
漸進式加載
異步加載
compose 聲明式 UI
漸進式加載
什麼是漸進式加載
漸進式加載,簡單來講,就是一部分一部分加載,當前幀加載完成以後,再去加載下一幀。
一種極致的作法是,加載 xml 文件,就想加載一個空白的 xml,佈局所有使用 ViewStub 標籤進行懶加載。
這樣設計的好處是能夠減緩同一時刻,加載 View 帶來的壓力,一般的作法是咱們先加載核心部分的 View,再逐步去加載其餘 View。
有人可能會這樣問了,這樣的設計很雞肋,有什麼用呢?
確實,在高端機上面做用不明顯,甚至可能看不出來,可是在中低端機上面,帶來的效果仍是很明顯的。在咱們項目當中,複雜的頁面首幀耗時約能夠減小 30%。
優勢:適配成本低,在中低端機上面效果明顯。
缺點:仍是須要在主線程讀取 xml 文件
核心僞代碼
1start(){
2 loadA(){
3 loadB(){
4 loadC()
5 }
6 }
7}
上面的這種寫法,是能夠的,可是這種作法,有一個很明顯的缺點,就是會形成回調嵌套層數過多。固然,咱們也可使用 RxJava 來解決這種問題。可是,若是項目中沒用 Rxjava,引用進來,會形成包 size 增長。
一個簡單的作法就是使用隊列的思想,將全部的 ViewStubTask 添加到隊列當中,噹噹前的 ViewStubTask 加載完成,才加載下一個,這樣能夠避免回調嵌套層數過多的問題。
改造以後的代碼見
1val decorView = this.window.decorView
2ViewStubTaskManager.instance(decorView)
3 .addTask(ViewStubTaskContent(decorView))
4 .addTask(ViewStubTaskTitle(decorView))
5 .addTask(ViewStubTaskBottom(decorView))
6 .start()
1class ViewStubTaskManager private constructor(val decorView: View) : Runnable {
2
3 private var iViewStubTask: IViewStubTask? = null
4
5 companion object {
6
7 const val TAG = "ViewStubTaskManager"
8
9 @JvmStatic
10 fun instance(decorView: View): ViewStubTaskManager {
11 return ViewStubTaskManager(decorView)
12 }
13 }
14
15 private val queue: MutableList<ViewStubTask> = CopyOnWriteArrayList()
16 private val list: MutableList<ViewStubTask> = CopyOnWriteArrayList()
17
18
19 fun setCallBack(iViewStubTask: IViewStubTask?): ViewStubTaskManager {
20 this.iViewStubTask = iViewStubTask
21 return this
22 }
23
24 fun addTask(viewStubTasks: List<ViewStubTask>): ViewStubTaskManager {
25 queue.addAll(viewStubTasks)
26 list.addAll(viewStubTasks)
27 return this
28 }
29
30 fun addTask(viewStubTask: ViewStubTask): ViewStubTaskManager {
31 queue.add(viewStubTask)
32 list.add(viewStubTask)
33 return this
34 }
35
36
37 fun start() {
38 if (isEmpty()) {
39 return
40 }
41 iViewStubTask?.beforeTaskExecute()
42 // 指定 decorView 繪製下一幀的時候會回調裏面的 runnable
43 ViewCompat.postOnAnimation(decorView, this)
44 }
45
46 fun stop() {
47 queue.clear()
48 list.clear()
49 decorView.removeCallbacks(null)
50 }
51
52 private fun isEmpty() = queue.isEmpty() || queue.size == 0
53
54 override fun run() {
55 if (!isEmpty()) {
56 // 當隊列不爲空的時候,先加載當前 viewStubTask
57 val viewStubTask = queue.removeAt(0)
58 viewStubTask.inflate()
59 iViewStubTask?.onTaskExecute(viewStubTask)
60 // 加載完成以後,再 postOnAnimation 加載下一個
61 ViewCompat.postOnAnimation(decorView, this)
62 } else {
63 iViewStubTask?.afterTaskExecute()
64 }
65
66 }
67
68 fun notifyOnDetach() {
69 list.forEach {
70 it.onDetach()
71 }
72 list.clear()
73 }
74
75 fun notifyOnDataReady() {
76 list.forEach {
77 it.onDataReady()
78 }
79 }
80
81}
82
83interface IViewStubTask {
84
85 fun beforeTaskExecute()
86
87 fun onTaskExecute(viewStubTask: ViewStubTask)
88
89 fun afterTaskExecute()
90
91
92}
源碼地址:https://github.com/gdutxiaoxu/AnchorTask,核心代碼主要在 ViewStubTask
,ViewStubTaskManager
, 有興趣的能夠看看
異步加載
異步加載,簡單來講,就是在子線程建立 View。在實際應用中,咱們一般會先預加載 View,經常使用的方案有:
在合適的時候,啓動子線程 inflate layout。而後取的時候,直接去緩存裏面查找 View 是否已經建立好了,是的話,直接使用緩存。不然,等待子線程 inlfate 完成。
AsyncLayoutInflater
官方提供了一個類,能夠來進行異步的inflate,可是有兩個缺點:
每次都要現場new一個出來
異步加載的view只能經過callback回調才能得到(死穴)
所以,咱們能夠仿造官方的 AsyncLayoutInflater 進行改造。核心代碼在 AsyncInflateManager。主要介紹兩個方法。
asyncInflate
方法,在子線程 inflateView,並將加載結果存放到 mInflateMap 裏面。
1 @UiThread
2fun asyncInflate(
3 context: Context,
4 vararg items: AsyncInflateItem?
5 ) {
6 items.forEach { item ->
7 if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
8 return
9 }
10 mInflateMap[item.inflateKey] = item
11 onAsyncInflateReady(item)
12 inflateWithThreadPool(context, item)
13 }
14
15 }
getInflatedView
方法,用來得到異步inflate出來的view,核心思想以下
先從緩存結果裏面拿 View,拿到了view直接返回
沒拿到view,可是子線程在inflate中,等待返回
若是還沒開始inflate,由UI線程進行inflate
1 /**
2 * 用來得到異步inflate出來的view
3 *
4 * @param context
5 * @param layoutResId 須要拿的layoutId
6 * @param parent container
7 * @param inflateKey 每個View會對應一個inflateKey,由於可能許多地方用的同一個 layout,可是須要inflate多個,用InflateKey進行區分
8 * @param inflater 外部傳進來的inflater,外面若是有inflater,傳進來,用來進行可能的SyncInflate,
9 * @return 最後inflate出來的view
10 */
11 @UiThread
12 fun getInflatedView(
13 context: Context?,
14 layoutResId: Int,
15 parent: ViewGroup?,
16 inflateKey: String?,
17 inflater: LayoutInflater
18 ): View {
19 if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {
20 val item = mInflateMap[inflateKey]
21 val latch = mInflateLatchMap[inflateKey]
22 if (item != null) {
23 val resultView = item.inflatedView
24 if (resultView != null) {
25 //拿到了view直接返回
26 removeInflateKey(item)
27 replaceContextForView(resultView, context)
28 Log.i(TAG, "getInflatedView from cache: inflateKey is $inflateKey")
29 return resultView
30 }
31
32 if (item.isInflating() && latch != null) {
33 //沒拿到view,可是在inflate中,等待返回
34 try {
35 latch.await()
36 } catch (e: InterruptedException) {
37 Log.e(TAG, e.message, e)
38 }
39 removeInflateKey(item)
40 if (resultView != null) {
41 Log.i(TAG, "getInflatedView from OtherThread: inflateKey is $inflateKey")
42 replaceContextForView(resultView, context)
43 return resultView
44 }
45 }
46
47 //若是還沒開始inflate,則設置爲false,UI線程進行inflate
48 item.setCancelled(true)
49 }
50 }
51 Log.i(TAG, "getInflatedView from UI: inflateKey is $inflateKey")
52 //拿異步inflate的View失敗,UI線程inflate
53 return inflater.inflate(layoutResId, parent, false)
54 }
簡單 Demo 示範
第一步:選擇在合適的時機調用 AsyncUtils#asyncInflate
方法預加載 View,
1object AsyncUtils {
2
3 fun asyncInflate(context: Context) {
4 val asyncInflateItem =
5 AsyncInflateItem(
6 LAUNCH_FRAGMENT_MAIN,
7 R.layout.fragment_asny,
8 null,
9 null
10 )
11 AsyncInflateManager.instance.asyncInflate(context, asyncInflateItem)
12 }
13
14 fun isHomeFragmentOpen() =
15 getSP("async_config").getBoolean("home_fragment_switch", true)
16}
第二步:在獲取 View 的時候,先去緩存裏面查找 View
1 override fun onCreateView(
2 inflater: LayoutInflater, container: ViewGroup?,
3 savedInstanceState: Bundle?
4 ): View? {
5 // Inflate the layout for this fragment
6 val startTime = System.currentTimeMillis()
7 val homeFragmentOpen = AsyncUtils.isHomeFragmentOpen()
8 val inflatedView: View
9
10 inflatedView = AsyncInflateManager.instance.getInflatedView(
11 context,
12 R.layout.fragment_asny,
13 container,
14 LAUNCH_FRAGMENT_MAIN,
15 inflater
16 )
17
18 Log.i(
19 TAG,
20 "onCreateView: homeFragmentOpen is $homeFragmentOpen, timeInstance is ${System.currentTimeMillis() - startTime}, ${inflatedView.context}"
21 )
22 return inflatedView
23// return inflater.inflate(R.layout.fragment_asny, container, false)
24 }
優缺點
優勢:
能夠大大減小 View 建立的時間,使用這種方案以後,獲取 View 的時候基本在 10ms 以內的。
缺點
因爲 View 是提早建立的,而且會存在在一個 map,須要根據本身的業務場景將 View 從 map 中移除,否則會發生內存泄露
View 若是緩存起來,記得在合適的時候重置 view 的狀態,否則有時候會發生奇奇怪怪的現象。
總結
參考文章:https://juejin.cn/post/6844903924965572615
View 的漸進式加載,在 JectPack compose 沒有推廣以後,推薦使用這種方案,適配成本低
View 的異步加載方案,雖然效果顯著,可是適配成本也高,沒搞好,容易發生內存泄露
JectPack compose 聲明式 UI,基本是將來的趨勢,有興趣的能夠提早了解一下他。
這篇文章,加上一些 Demo,足足花了我幾個晚上的時間,以爲不錯的話能夠關注一下個人微信公衆號程序員徐公,小弟在此感謝各位大佬們。主要分享
Android 開發相關的知識,包括 java,kotlin, Android 技術
面試相關的東西,包括常見的面試題目,面試經驗分享
算法相關的知識,好比怎麼學習算法,leetcode 常見算法總結
一些時事點評,主要是關於互聯網的,好比小米高管屌絲事件,拼多多女員工猝死事件等
源碼地址:https://github.com/gdutxiaoxu/AnchorTask
本文分享自微信公衆號 - 徐公碼字(stormjun94)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。