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



相关文章
|
1月前
|
ARouter Android开发
Android不同module布局文件重名被覆盖
Android不同module布局文件重名被覆盖
|
3月前
|
移动开发 监控 前端开发
构建高效Android应用:从优化布局到提升性能
【7月更文挑战第60天】在移动开发领域,一个流畅且响应迅速的应用程序是用户留存的关键。针对Android平台,开发者面临的挑战包括多样化的设备兼容性和性能优化。本文将深入探讨如何通过改进布局设计、内存管理和多线程处理来构建高效的Android应用。我们将剖析布局优化的细节,并讨论最新的Android性能提升策略,以帮助开发者创建更快速、更流畅的用户体验。
66 10
|
2月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
65 20
Android经典面试题之图片Bitmap怎么做优化
|
1月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
46 4
|
16天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
1月前
|
ARouter Android开发
Android不同module布局文件重名被覆盖
Android不同module布局文件重名被覆盖
109 0
|
2月前
|
Java Android开发 UED
安卓应用开发中的内存管理优化技巧
在安卓开发的广阔天地里,内存管理是一块让开发者既爱又恨的领域。它如同一位严苛的考官,时刻考验着开发者的智慧与耐心。然而,只要我们掌握了正确的优化技巧,就能够驯服这位考官,让我们的应用在性能和用户体验上更上一层楼。本文将带你走进内存管理的迷宫,用通俗易懂的语言解读那些看似复杂的优化策略,让你的开发之路更加顺畅。
62 2
|
2月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
3月前
|
编解码 Android开发
【Android Studio】使用UI工具绘制,ConstraintLayout 限制性布局,快速上手
本文介绍了Android Studio中使用ConstraintLayout布局的方法,通过创建布局文件、设置控件约束等步骤,快速上手UI设计,并提供了一个TV Launcher界面布局的绘制示例。
57 1
|
2月前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
70 0