溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android的布局優化有哪些

發布時間:2021-10-09 16:22:25 來源:億速云 閱讀:126 作者:iii 欄目:編程語言

本篇內容主要講解“Android的布局優化有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Android的布局優化有哪些”吧!

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

耗時原因

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

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

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

  2. 解析 xml 對象,反射創建 View

一些很常見的做法是

  1. 減少布局嵌套層數,減少過度繪制

  2. 空界面,錯誤界面等界面進行懶加載

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

解決方案

  1. 異步加載

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

異步加載

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

小結

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

  1. 漸進式加載

  2. 異步加載

  3. 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,常用的方案有:

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

AsyncLayoutInflater

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

  1. 每次都要現場new一個出來

  2. 異步加載的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 之內的。

缺點

  1. 由于 View 是提前創建的,并且會存在在一個 map,需要根據自己的業務場景將 View 從 map 中移除,不然會發生內存泄露

  2. View 如果緩存起來,記得在合適的時候重置 view 的狀態,不然有時候會發生奇奇怪怪的現象。

到此,相信大家對“Android的布局優化有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女