Android 启动优化(六)- 深入理解布局优化

在这里插入图片描述

前言

说到 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

已标记关键词 清除标记
相关推荐
CruiseYoung提供的带有详细书签的电子书籍目录 http://blog.csdn.net/fksec/article/details/7888251 Android应用开发揭秘 基本信息 作者: 杨丰盛 出版社:机械工业出版社 ISBN:9787111291954 上架时间:2010-7-29 出版日期:2011 年5月 开本:16开 页码:515 版次:1-8 编辑推荐   国内首本基于Android 2.0的经典著作,5大专业社区一致鼎力推荐! 内容简介   国内第一本基于android 2.0的经典著作,5大专业社区联袂推荐,权威性毋庸置疑!    本书内容全面,不仅详细讲解了android框架、android组件、用户界面开发、游戏开发、数据存储、多媒体开发和网络开发等基础知识,而且还深入阐述了传感器、语音识别、桌面组件 开发、android游戏引擎设计、android应用优化、opengl等高级知识,最重要的是还全面介绍了如何利用原生的c/c++(ndk)和python、lua等脚本语言(android scripting environment) 来开发android应用;本书实战性强,书中的每个知识点都有配精心设计的示例,尤为值得一提的是,它还以迭代的方式重现了各种常用的android应用和经典android游戏的开发全过程,既可 以以它们为范例进行实战演练,又可以将它们直接应用到实际开发中去。    windows操作系统的诞生成就了微软的霸主地位,也造就了pc时代的繁荣。然而,以android和iphone手机为代表的智能移动设备的发明却敲响了pc时代的丧钟!移动互联网时代(3g时代 )已经来临,谁会成为这些移动设备上的主宰?毫无疑问,它就是android——pc时代的windows!    移动互联网还是一个新生的婴儿,各种移动设备上的操作系统群雄争霸!与symbian、iphone os、windows mobile相比,android有着天生的优势——完全开放和免费,对广大开发者和 手机厂商而言,这是何等的诱人!此外,在google和以其为首的android手机联盟的大力支持和推广下,android不仅得到了全球开发者社区的关注,而且一大批世界一流的手机厂商都已经或 准备采用android。    拥抱android开发,拥抱移动开发的未来!    ·android开发与传统的j2me开发有何相似与不同?    ·如何通过shared preferences、files、network和sqlite等方式高效实现android数据的存储?又如何通过content providers轻松地实现android数据的共享?    ·如何使用open core、mediaplayer、mediarecorder方便快速地开发出包含音频和视频等流媒体的丰富多媒体应用?    ·如何利用android 2.0中新增的蓝牙特性开发包含蓝牙功能的应用?又如何使用蓝牙api来完善应用的网络功能?    ·如何解决android网络通信中的乱码问题?    ·在android中如何使用语音服务和 google map api?android如何访问摄像头、传感器等硬件的api?    ·如何进行widget开发?如何用各种android组件来打造漂亮的ui界面?    ·android如何解析xml数据?又如何提高解析速度和减少对内存、cpu资源的消耗?    ·如何使用opengl es在android平台上开发出绚丽的3d应用?在android平台上如何更好地设计和实现游戏引擎?    ·如何对android应用进行优化?如何进行程序性能测试?如何实现ui、zipalign和图片优化?    ·如何通过ndk利用c、c++以及通过ase利用python等脚本语言开发android应用? 作译者 杨丰盛,国内Android领域的先驱者和布道者,资深Android开发工程师,在Android应用开发方面有丰富的实战经验。精通Java、C、C++等语言,专注于移动通信软件开发,在机顶盒软件 开发和MTK平台软件开发方面有非常深厚的积累。他对Android的源代码进行了长达一年的系统学习和研究,对Android系统的架构设计和实现原理有非常深入理解和认识,理论功底也十分深 厚。国内著名IT技术网站 51CTO推荐技术专家,曾多次接受《程序员》杂志采访并为其撰稿,同时他还多次应邀在国内的移动开发者大会和技术沙龙中开展讲座,深受欢迎。他还是畅销书作 家,撰写的《Android应用开发揭秘》一书是目前Android领域口碑最好、销量也最好的一本书之一,这本书自上市以来已经重印9次,而
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页