主頁 > 軟體設計 > Android 啟動優化(六)- 深入理解布局優化

Android 啟動優化(六)- 深入理解布局優化

2021-04-08 11:53:10 軟體設計

在這里插入圖片描述

前言

說到 Android 啟動優化,你一般會想到什么呢?

  1. Android 多執行緒異步加載
  2. Android 首頁懶加載

對,這是兩種很常見的優化手段,但是如果讓你主導這件事情,你會如何開始呢?

  1. 梳理現有的業務,哪些是一定要在啟動初始化的,哪些是不必要的
  2. 需要在啟動初始化的,哪些是可以在主執行緒初始化的,哪些是可以在子執行緒初始化的

當我們把任務丟到子執行緒初始化,這時候,我們又會遇到兩個問題,

  1. 在首頁,我們需要用到這個庫,如果直接使用,這個庫可能還沒有初始化,這時候直接呼叫該庫,會發生例外,你要怎么解決
  2. 當我們的任務相互依賴時,比如 A 依賴于 B, C 也依賴于 B,要怎么解決這種依賴關系,

這些你有想過嘛,答案都在這幾篇文章里面了,這里我就不展開講了,有疑問的可以一起探討探討,我的微信公眾號程式員徐公

Android 啟動優化(一) - 有向無環圖

Android 啟動優化(二) - 拓撲排序的原理以及解題思路

Android 啟動優化(三)- AnchorTask 開源了

Android 啟動優化(四)- AnchorTask 是怎么實作的

Android 啟動優化(五)- AnchorTask 1.0.0 版本正式發布了

接下來,我們來說一下布局優化相關的,

布局優化的現狀與發展趨勢

耗時原因

眾所周知,布局加載一直是耗時的重災區,特別是啟動階段,作為第一個 View 加載,更是耗時,

而布局加載之所以耗時,有兩個原因,

  1. 讀取 xml 檔案,這是一個 IO 操作,
  2. 決議 xml 物件,反射創建 View

一些很常見的做法是

  1. 減少布局嵌套層數,減少過度繪制
  2. 空界面,錯誤界面等界面進行懶加載

那除了這些做法,我們還有哪些手段可以優化呢?

解決方案

  1. 異步加載
  2. 采用代碼的方式撰寫布局

異步加載

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

小結

上面講了布局優化的現狀與發展趨勢,接下來我們一起來看一下,有哪些布局優化手段,可以應用到專案中的,

  1. 漸進式加載
  2. 異步加載
  3. 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/… ViewStubTaskViewStubTaskManager**, 有興趣的可以看看

異步加載

異步加載,簡單來說,就是在子執行緒創建 View,在實際應用中,我們通常會先預加載 View,常用的方案有:

  1. 在合適的時候,啟動子執行緒 inflate layout,然后取的時候,直接去快取里面查找 View 是否已經創建好了,是的話,直接使用快取,否則,等待子執行緒 inlfate 完成,

AsyncLayoutInflater

官方提供了一個類,可以來進行異步的inflate,但是有兩個缺點:

  1. 每次都要現場new一個出來
  2. 異步加載的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 之內的,

缺點

  1. 由于 View 是提前創建的,并且會存在在一個 map,需要根據自己的業務場景將 View 從 map 中移除,不然會發生記憶體泄露
  2. View 如果快取起來,記得在合適的時候重置 view 的狀態,不然有時候會發生奇奇怪怪的現象,

總結

參考文章:Android - 一種新奇的冷啟動速度優化思路(Fragment極度懶加載 + Layout子執行緒預加載)

  1. View 的漸進式加載,在 JectPack compose 沒有推廣之后,推薦使用這種方案,適配成本低
  2. View 的異步加載方案,雖然效果顯著,但是適配成本也高,沒搞好,容易發生記憶體泄露
  3. JectPack compose 宣告式 UI,基本是未來的趨勢,有興趣的可以提前了解一下他,

Talk is cheap. Show me the code

原始碼地址:github.com/gdutxiaoxu/…

找到我

這篇文章,加上一些 Demo,足足花了我幾個晚上的時間,我是站在巨人的肩膀上成長起來的,同樣,我也希望成為你們的巨人,覺得不錯的話可以關注一下我的微信公眾號程式員徐公,在此感謝各位大佬們,主要分享

  1. Android 開發相關知識:包括 java,kotlin, Android 技術,
  2. 面試相關分享:包括常見的面試題目,大廠面試真題、面試經驗套路分享,
  3. 演算法相關學習筆記:比如怎么學習演算法,leetcode 常見演算法總結,跟大家一起學習演算法,
  4. 時事點評:主要是關于互聯網的,比如小米高管屌絲事件,拼多多女員工猝死事件等

希望我們可以成為朋友,成長路上的忠實伙伴!
image

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/273762.html

標籤:其他

上一篇:[設計模式C++]工廠模式

下一篇:【ArcGIS Engine二次開發】入門基礎(2):ArcGIS開發方式(VBA、DLL、Add-in、Engine)對比

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more