Android的布局优化有哪些

本篇内容主要讲解“Android的布局优化有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Android的布局优化有哪些”吧!

永仁ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18980820575(备注:SSL证书合作)期待与您的合作!

布局优化的现状与发展趋势

耗时原因

众所周知,布局加载一直是耗时的重灾区。特别是启动阶段,作为第一个 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 = CopyOnWriteArrayList()
16    private val list: MutableList = CopyOnWriteArrayList()
17
18
19    fun setCallBack(iViewStubTask: IViewStubTask?): ViewStubTaskManager {
20        this.iViewStubTask = iViewStubTask
21        return this
22    }
23
24    fun addTask(viewStubTasks: List): 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,核心代码主要在 ViewStubTaskViewStubTaskManager, 有兴趣的可以看看

异步加载

异步加载,简单来说,就是在子线程创建 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的布局优化有哪些”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


文章名称:Android的布局优化有哪些
文章路径:http://myzitong.com/article/igdgie.html