Android的布局優(yōu)化有哪些

本篇內(nèi)容主要講解“Android的布局優(yōu)化有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“Android的布局優(yōu)化有哪些”吧!

永仁ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18980820575(備注:SSL證書合作)期待與您的合作!

布局優(yōu)化的現(xiàn)狀與發(fā)展趨勢

耗時原因

眾所周知,布局加載一直是耗時的重災(zāi)區(qū)。特別是啟動階段,作為第一個 View 加載,更是耗時。

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

  1. 讀取 xml 文件,這是一個 IO 操作。

  2. 解析 xml 對象,反射創(chuàng)建 View

一些很常見的做法是

  1. 減少布局嵌套層數(shù),減少過度繪制

  2. 空界面,錯誤界面等界面進(jìn)行懶加載

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

解決方案

  1. 異步加載

  2. 采用代碼的方式編寫布局

異步加載

google 很久之前提供了 AsyncLayoutInflater,異步加載的方案,不過這種方式有蠻多坑的,下文會介紹

采用代碼的方式編寫布局

代碼編寫的方式編寫布局,我們可能想到使用 java 聲明布局,對于稍微復(fù)雜一點(diǎn)的布局,這種方式是不可取的,存在維護(hù)性查,修改困難等問題。為了解決這個問題,github 上面誕生了一系列優(yōu)秀的開源庫。

litho: https://github.com/facebook/litho

X2C: https://github.com/iReaderAndroid/X2C

為了即保留xml的優(yōu)點(diǎn),又解決它帶來的性能問題,我們開發(fā)了X2C方案。即在編譯生成APK期間,將需要翻譯的layout翻譯生成對應(yīng)的java文件,這樣對于開發(fā)人員來說寫布局還是寫原來的xml,但對于程序來說,運(yùn)行時加載的是對應(yīng)的java文件.

我們采用APT(Annotation Processor Tool)+ JavaPoet技術(shù)來完成編譯期間【注解】->【解注解】->【翻譯xml】->【生成java】整個流程的操作。

這兩個開源庫在大型的項(xiàng)目基本不會使用,不過他們的價值是值得肯定的,核心思想很有意義

xml 布局加載耗時的問題, google 也想改善這種現(xiàn)狀,最近 Compose beta 發(fā)布了,他是采用聲明式 UI 的方式來編寫布局,避免了 xml 帶來的耗時。同時,還支持布局實(shí)時預(yù)覽。這個應(yīng)該是以后的發(fā)展趨勢。

compose-samples: https://github.com/android/compose-samples

小結(jié)

上面講了布局優(yōu)化的現(xiàn)狀與發(fā)展趨勢,接下來我們一起來看一下,有哪些布局優(yōu)化手段,可以應(yīng)用到項(xiàng)目中的。

  1. 漸進(jìn)式加載

  2. 異步加載

  3. compose 聲明式 UI

漸進(jìn)式加載

什么是漸進(jìn)式加載

漸進(jìn)式加載,簡單來說,就是一部分一部分加載,當(dāng)前幀加載完成之后,再去加載下一幀。

一種極致的做法是,加載 xml 文件,就想加載一個空白的 xml,布局全部使用 ViewStub 標(biāo)簽進(jìn)行懶加載。

這樣設(shè)計的好處是可以減緩?fù)粫r刻,加載 View 帶來的壓力,通常的做法是我們先加載核心部分的 View,再逐步去加載其他 View。

有人可能會這樣問了,這樣的設(shè)計很雞肋,有什么用呢?

確實(shí),在高端機(jī)上面作用不明顯,甚至可能看不出來,但是在中低端機(jī)上面,帶來的效果還是很明顯的。在我們項(xiàng)目當(dāng)中,復(fù)雜的頁面首幀耗時約可以減少 30%。

優(yōu)點(diǎn):適配成本低,在中低端機(jī)上面效果明顯。

缺點(diǎn):還是需要在主線程讀取 xml 文件

核心偽代碼

1start(){
2    loadA(){
3        loadB(){
4            loadC()
5        }
6    }
7}

上面的這種寫法,是可以的,但是這種做法,有一個很明顯的缺點(diǎn),就是會造成回調(diào)嵌套層數(shù)過多。當(dāng)然,我們也可以使用 RxJava 來解決這種問題。但是,如果項(xiàng)目中沒用 Rxjava,引用進(jìn)來,會造成包 size 增加。

一個簡單的做法就是使用隊(duì)列的思想,將所有的 ViewStubTask 添加到隊(duì)列當(dāng)中,當(dāng)當(dāng)前的 ViewStubTask 加載完成,才加載下一個,這樣可以避免回調(diào)嵌套層數(shù)過多的問題。

改造之后的代碼見

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 繪制下一幀的時候會回調(diào)里面的 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            // 當(dāng)隊(duì)列不為空的時候,先加載當(dāng)前 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,核心代碼主要在 ViewStubTaskViewStubTaskManager, 有興趣的可以看看

異步加載

異步加載,簡單來說,就是在子線程創(chuàng)建 View。在實(shí)際應(yīng)用中,我們通常會先預(yù)加載 View,常用的方案有:

  1. 在合適的時候,啟動子線程 inflate layout。然后取的時候,直接去緩存里面查找 View 是否已經(jīng)創(chuàng)建好了,是的話,直接使用緩存。否則,等待子線程 inlfate 完成。

AsyncLayoutInflater

官方提供了一個類,可以來進(jìn)行異步的inflate,但是有兩個缺點(diǎn):

  1. 每次都要現(xiàn)場new一個出來

  2. 異步加載的view只能通過callback回調(diào)才能獲得(死穴)

因此,我們可以仿造官方的 AsyncLayoutInflater 進(jìn)行改造。核心代碼在 AsyncInflateManager。主要介紹兩個方法。

asyncInflate 方法,在子線程 inflateView,并將加載結(jié)果存放到 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,核心思想如下

  • 先從緩存結(jié)果里面拿 View,拿到了view直接返回

  • 沒拿到view,但是子線程在inflate中,等待返回

  • 如果還沒開始inflate,由UI線程進(jìn)行inflate

 1    /**
2     * 用來獲得異步inflate出來的view
3     *
4     * @param context
5     * @param layoutResId 需要拿的layoutId
6     * @param parent      container
7     * @param inflateKey  每一個View會對應(yīng)一個inflateKey,因?yàn)榭赡茉S多地方用的同一個 layout,但是需要inflate多個,用InflateKey進(jìn)行區(qū)分
8     * @param inflater    外部傳進(jìn)來的inflater,外面如果有inflater,傳進(jìn)來,用來進(jìn)行可能的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,則設(shè)置為false,UI線程進(jìn)行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 示范

第一步:選擇在合適的時機(jī)調(diào)用 AsyncUtils#asyncInflate 方法預(yù)加載 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    }

優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

可以大大減少 View 創(chuàng)建的時間,使用這種方案之后,獲取 View 的時候基本在 10ms 之內(nèi)的。

缺點(diǎn)

  1. 由于 View 是提前創(chuàng)建的,并且會存在在一個 map,需要根據(jù)自己的業(yè)務(wù)場景將 View 從 map 中移除,不然會發(fā)生內(nèi)存泄露

  2. View 如果緩存起來,記得在合適的時候重置 view 的狀態(tài),不然有時候會發(fā)生奇奇怪怪的現(xiàn)象。

到此,相信大家對“Android的布局優(yōu)化有哪些”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

文章名稱:Android的布局優(yōu)化有哪些
標(biāo)題鏈接:http://bm7419.com/article4/igdgie.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設(shè)計公司、網(wǎng)站營銷、App開發(fā)移動網(wǎng)站建設(shè)、定制開發(fā)網(wǎng)站策劃

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

搜索引擎優(yōu)化