Android View组织形式之-跨页面异步加载

2,658 阅读8分钟

前沿

上一篇 Android 业务逻辑应该如何写(第二篇)已经从大方向介绍巨量app的View是如何组织的,其实就两点【拆分】和 【组合】,其实现方式可能多种多样。并且在文的末尾粗略写了下View的优化(预加载、懒加载等),本文将从多个角度介绍大厂是如何进行View相关的优化的,并着重一点介绍如何在子线程中加载view,并跨页面去复用。

传统经典的View相关优化方式

我们在掘金或者百度上搜索【View性能优化】、【布局优化】等字眼,会搜索出很多方法,这些方法都是切实可行的,并且都是我们要在业务开发中需要注意的

  • 减少View布局层级
  • 使用merge、ViewStub等标签
  • 移除不必要的background
  • 避免onDraw中执行大量&&耗时操作(自定义view才需要注意,一般情况下不会遇到)

稍微先进的方式-AsyncLayoutInflater

说它先进不是因为它实现的多么完美,是思想还是比较大胆的。 其使用方式很简单,但似乎没看见有人用过,感觉很鸡肋

class TestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) { view, _, _ ->
            setContentView(view)
        }
    }
}

其他View优化

  • X2C框架 github.com/iReaderAndr…
  • 利用 Kotlin DSL 写布局
  • 通过写自定义ViewGroup(这里的自定义ViewGroup不是对原生View的封装或组合,而是通过重onMeature 和onLayout对原生view进行测量和摆放,完全摆脱LinearLyaout、ConstraintLayout等原生的ViewGroup,以达到极致的原生性能)

优秀的APP都做过哪些

主要还是以字节系的app为例子,举几个典型的优化处理

动画降级

AlYYBHPxba.jpg

  • 第一张是抖音推荐feed页面右下角的【音乐转盘】
  • 第二张是抖音推荐feed页面【点赞】按钮
  • 第三张是抖音feed页面的活动挂件 (第三张其实不是抖音页面,因为挂件一般只有活动时会有,大家就假装是吧)

这三处都有动画

【音乐转盘在不停的转动】、【点击点赞按钮时,会有动画特效】、【挂件本身有动画或者倒计时的转圈】

有同学可能会有疑问,这三处都是很正常的需求,view层级也不会很深,还能做什么优化呢,还需要做优化吗~?

要知道对于巨量的app来说,用户群体是非常广的,同时用户的手机性能也是参差不齐,中低端机器占比还不在少数,为了让这些用户可以享受更加优质的体验,我们必须做些什么,这也是动力所在

针对这些动画,我们可以对动画做降级处理。

  • 针对【音乐转盘】,在中低端机上我们可以降低动画的帧率,比如在高端机上8秒转一圈。那么在中低端机上,我们可以采取各种策略,比如10秒转一圈,20秒转一圈等等。在更加低端的手机上,甚至可以让【音乐转盘】不转圈,毕竟这并不是核心场景。
  • 针对【点赞】按钮,在正常的情况,用户点击【点赞】按钮,会出现漂亮的一个lottie动画。那么在中低端机上可以取消lottie动画,直接展示红心UI。或者通过自定义view用原生的方法去实现动画。
  • 同样的,feed页面中的活动挂件上一般都有倒计时或者lottie动画,我们也可以对其进行降级处理。让倒计时的间隔大一些,比如2秒刷新一次等;控制lottie动画的频率,让lottie动画也“慢”一点。

这些动画降级是实实在在线上运行的。虽然成本大了一点,但性能优化嘛,本来就是追求极致~

需要再次强调的是这些优化并不是针对用户全量开启的,重点是分机型,分中高低端机。

View的延迟加载

抖音feed流滑动的很流畅,背后其实是做了很多事情,包括对ViewPager的滑动优化、播放器提前预渲染、以及视频流的格式选择等等。

另外还有一些View的延迟加载。针对优先级不高的View不必着急去加载,等到视频起播后在去加载。再比如对于广告的卡片,广告的卡片很多都是在视频播放3、5秒后才开始展现的,那就很不必要去马上加载了。注意这里的延迟加载并不是简单的 View.VISIBLEView.GONE 之间的转换,而是 inflate级别的

当然还有其他View相关的优化:RecycleViewViewPager相关的滑动优化,View的加载和复用的优化。受篇幅和本文的主题影响,就先不发散了,开始对【View的加载和复用】进行着重介绍

View跨页面异步加载

这里依旧是着重介绍原理,当然具体实现demo也会在其他时间给出

这里有两个点:1 是异步,2是跨页面

  • 异步比较好理解,就像 AsyncLayoutInflater 一样,开启一个子线程去 inflate View
  • 跨页面似乎就有点难懂了,也就是我们在ActivityA页面加载(这里只进行inflate操作,不会进行addView操作)了一个或者多个View(ViewGroup),当打开ActivityB页面时,不用去重新加载了,直接拿ActivityA加载好的View去addView就好了。

异步的好处不用多讲,跨页面呢,如果实现了跨页面,就好比在app生命周期内有了一个全局的View缓存池,这里的全局是相对RecycleView中的View缓存池而言的。RecycleView中的View缓存池只能自己用,而我们的全局View缓存池可以给任何页面使用,当然也可以给RecycleView使用。

实现步骤以及需要解决的问题

  • 第一步先解决异步问题

异步比较简单,就是开启一个子线程去inflate,不过有一个问题可能需要注意,也就是handler-looper相关的问题,这是子线程和主线程在Android层次上最明显的区别。如果有一个自定义view的构造方法中出现new Handler类似的使用,将会发生什么呢。

  1. 会crash,因为默认情况下子线程是没有looper的
    throw new RuntimeException(
        "Can't create handler inside thread " + Thread.currentThread()
                + " that has not called Looper.prepare()");
}

我们可以使用 HandlerThread 创建线程

  1. 即使用了HandlerThread 创建线程,也会出现一点问题。因为在Android中使用 new Handler,我们都预期会在主线程去处理消息,但是我们现在在子线程中,该Handler将会绑定子线程的looper~
    如何解决呢?直接反射修改子线程的looper为主线程的looper就好了。

差不多到这里异步化的问题解决了

  • 第二步跨页面的实现
  1. 创建全局的View缓存池

假设我们已经加载好了一个View,将给其他页面使用,那么首要的问题就是:其它想要使用该view的页面,如何去得到它呢? 就是要有一个全局的View缓存池,把每次异步加载好的view放进去,其他地方想要直接取就好了。

  1. 第二个问题就是View-Context问题

因为是跨页面使用了,View被加载时的Context和其他页面的 Context是不一样的。这个其实比较好解决,用的时候全量替换View的Context即可。

其实到这里这个功能算是完成了,这也是主要的思路吧。但是还不够精细化。还有很多问题

  • 全局的View缓存池会不会导致内存爆增,造成内存问题?

  • 什么的View需要被异步加载并给其他页面使用呢?(这个问题是最重要的,总不能所有的View都这样处理吧)

  • 既然属于预加载的一种,那在什么情况下或者说什么时机去触发这个预加载呢?

  • 还有就是如何提高这些View的复用率,或者说命中率。比如B页面想要去缓存池里获取View,但是一看没有,那命中率自然就低了。再比如,有一个View一直留在缓存池里面,没有被其他任何页面使用,是不是也算是一种浪费,白加载了。

  • 再然后就是,再后面的业务需求中,如何判断这种预加载的行为对业务是有收益的。毕竟终极目前依然是提高用户体验,进而转化为业务收益。

  • 再最后,框架接入了,又如何在后续的版本中去优化呢,需要优化哪些呢。要不要配合服务端,算法等多方去进一步提升View的命中率呢?

等等这些。其实工作才刚刚开始~

TODO

文中其实也留了一些尾巴,比如 【RecycleViewViewPager相关的滑动优化】【通过写自定义ViewGroup优化】等(留点尾巴也算为了下一篇能够继续)