Android 启动优化(六)- 深入理解布局优化,android开发手册app

  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 = CopyOnWriteArrayList()

private val list: MutableList = CopyOnWriteArrayList()

fun setCallBack(iViewStubTask: IViewStubTask?): ViewStubTaskManager {

this.iViewStubTask = iViewStubTask

return this

}

fun addTask(viewStubTasks: List): 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

}
te,但是有两个缺点:

  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

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值