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

简介: 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 版本正式发布了


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


接下来,我们来说一下布局优化相关的。


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


耗时原因


众所周知,布局加载一直是耗时的重灾区。特别是启动阶段,作为第一个 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()
}

源码地址: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 里面。


@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,基本是未来的趋势,有兴趣的可以提前了解一下他。


这篇文章,加上一些 Demo,足足花了我几个晚上的时间,觉得不错的话可以关注一下我的微信公众号程序员徐公,小弟在此感谢各位大佬们。主要分享


  1. Android 开发相关的知识,包括 java,kotlin, Android 技术
  2. 面试相关的东西,包括常见的面试题目,面试经验分享
  3. 算法相关的知识,比如怎么学习算法,leetcode 常见算法总结
  4. 一些时事点评,主要是关于互联网的,比如小米高管屌丝事件,拼多多女员工猝死事件等


源码地址:https://github.com/gdutxiaoxu/AnchorTask



相关文章
|
16小时前
|
缓存 程序员 定位技术
Android Studio 插件,那些被大厂优化的程序员们
Android Studio 插件,那些被大厂优化的程序员们
|
16小时前
|
设计模式 算法 前端开发
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
|
1天前
|
移动开发 监控 Android开发
构建高效安卓应用:Kotlin 协程的实践与优化
【5月更文挑战第16天】 在移动开发领域,性能优化一直是开发者们追求的重要目标。特别是对于安卓平台来说,由于设备多样性和系统资源的限制,如何提升应用的响应性和流畅度成为了一个关键议题。近年来,Kotlin 语言因其简洁、安全和高效的特点,在安卓开发中得到了广泛的应用。其中,Kotlin 协程作为一种轻量级的并发解决方案,为异步编程提供了强大支持,成为提升安卓应用性能的有效手段。本文将深入探讨 Kotlin 协程在安卓开发中的应用实践,以及通过合理设计和使用协程来优化应用性能的策略。
16 8
|
2天前
|
缓存 移动开发 Android开发
Android 应用性能优化实践
【5月更文挑战第15天】 在移动开发领域,应用的性能直接关系到用户体验和产品的市场表现。特别是对于安卓平台,设备的多样性和应用生态环境的复杂性使得性能优化成为开发者的一项重要技能。本文将深入探讨针对安卓应用的性能瓶颈识别、分析方法以及具体的优化策略,旨在为开发者提供一套实用的性能提升解决方案。
|
2天前
|
移动开发 测试技术 Android开发
构建高效Android应用:从优化用户体验到提升性能表现
【5月更文挑战第15天】 在移动开发领域,一个成功的Android应用不仅需要具备吸引用户的功能,更应提供流畅和高效的用户体验。随着技术的不断进步,开发者面临着将先进技术集成到现有架构中以提高应用性能的挑战。本文将深入探讨如何通过最新的Android框架和工具来优化应用性能,包括对UI的响应性、内存管理以及多线程处理等关键方面的改进,旨在帮助开发者构建出更加强大、快速且稳定的Android应用。
|
2天前
|
缓存 Android开发 UED
构建高效Android应用:从优化用户体验到提升性能
【5月更文挑战第15天】 在移动开发领域,构建一个高效的Android应用不仅仅意味着实现功能,还要确保流畅的用户体验和出色的性能。本文将深入探讨如何通过界面优化、代码整洁、资源管理和多线程处理等技术手段来提升Android应用的整体效率。我们将透过实际案例,揭示常见性能瓶颈的成因,并提供相应的解决方案。此外,文章还会涵盖最新的Android Studio工具和Lint检查的使用,帮助开发者早期发现潜在问题。
|
2天前
|
缓存 Java Android开发
Android应用性能优化实战
【5月更文挑战第14天】 在竞争激烈的应用市场中,一个流畅、高效的应用能显著提升用户体验并增强用户黏性。本文深入探讨了针对安卓平台进行应用性能优化的策略与实践,从内存管理到多线程处理,再到布局渲染和网络请求的优化,旨在为开发者提供一套全面的优化工具箱。通过分析常见的性能瓶颈并结合最新的Android技术动态,我们不仅讨论理论,还将分享具体的代码示例和改进方法,帮助开发者在实际应用中实现性能提升。
|
2天前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin协程的实践与优化策略
【5月更文挑战第14天】在移动开发领域,性能优化和资源管理是提升用户体验的关键因素之一。随着Kotlin语言的普及,其异步编程解决方案——协程,已经成为Android开发者手中的强大工具。本文将深入探讨Kotlin协程在Android应用中的实践方法,分析其在处理异步任务时带来的优势,并提出一系列优化策略,帮助开发者构建更加高效、响应迅速的Android应用。通过具体案例分析和性能对比,我们将展示如何充分利用协程来简化代码结构,提高应用性能,并确保用户界面的流畅性。
|
2天前
|
存储 传感器 Android开发
构建高效Android应用:从优化布局到提升性能
【5月更文挑战第13天】 在竞争激烈的移动应用市场中,一个高效的Android应用不仅需要具备直观的用户界面和丰富的功能,还要确保流畅的性能和快速的响应时间。本文将深入探讨如何通过优化布局设计、减少资源消耗以及利用系统提供的API来提升Android应用的性能。我们将分析布局优化的策略,讨论内存使用的常见陷阱,并介绍异步处理和电池寿命的考量。这些技术的综合运用将帮助开发者构建出既美观又高效的Android应用。
|
缓存 Android开发 数据格式
Android ListView性能优化,异步加载图片
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/48184383 ListView性能优...
1158 0