924a8940736ea7120cfdd3fffcdcfbe3
重学安卓:百闻不如一见的 视图系统 架构全貌

温馨提示:如果这是第一次接触《重学安卓》,可借助 这份在 GitBook 上维护的 “导读” 来快速了解《重学安卓》专栏、获取它的目录、试读内容,以及了解它的最新动态 和 发展状况。

截至目前,专栏已对 体系化文章 做了 810 余次修订,数十位群友告诉我 受专栏的启发 他们也开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创的开源库 和 提供内推机会,订阅后可随时进群交流。

前言

很高兴见到你!

在上一期的《不如我们 从零开始设计一套 视图系统》 中,我们 全网独家地 以 “所见即所得的 触控体验” 为切入点,从零开始构思了一款视图系统,并自恰地解释了当下安卓视图系统中 某些细节之所以如此设计的缘由,相信阅读完这篇文章的小伙伴,对视图系统背景状况的印象 已跑赢了全网 90%的 移动开发者!😉

比如现在我们确知了:当我们谈论视图系统时,我们实际上是在谈论 在冯诺依曼计算机的背景下的 人机交互系统因为一旦脱离了这个背景,就构不成 输入输出设备 的存在,也就谈不上对可编程内容的输入和输出,更不要说营造 符合用户直觉的 实时的 输入输出反馈。

所以,关于视图系统,就算遗忘了大部分细节,通过上一篇文章,我们也至少知道了,从宏观来看,它主要就是包含 彼此独立且交替着的 输入和输出 这两大过程 —— 在此之前,如果我们对此不假思索、和普通用户一样去 感知(觉得 视图系统 “像光一样 具有波粒二象性”)、而不是 思考(知道所谓的 “实时响应” 只是计算机速度过快给人的错觉、实际上每一段触控都包含了 无数次 顺序执行的 输入 - 输出 - 输入 - 输出 ...),那么我们便无法真正理解和驾驭 视图系统 为我们做功。

文章目录一览

  • 前言
  • 为什么要反思 视图系统
  • 作为差强人意的 Android 视图系统
  • 视图系统 为什么要基于 C/S 架构
  • 一睹 视图接口 和 视图服务 的抽象
  • 当下 Android 视图系统 的怪状
  • 综上
  • Note 2020.1.29 加餐:
    • 是让人 过目难忘 的 Android GUI 族谱解析

为什么要反思 视图系统

原因很简单,做客户端开发的,100% 要和视图系统打交道。

尽管许多技术层面的问题,从产品层面来看完全不是问题,

比如 《才不会告诉你,自定义视图我这样学》 一文提到的,尽量用普适的、现成的交互方案,这样用户的学习成本最小,不到万不得已的时候 不要自己去设计自定义视图,否则费力不讨好。

又比如 Android 4.4 ~ 5.1 各种不成熟、各种毛病需要兼容,但从产品角度来看,上架的 App 完全可以考虑 6.0 起步,因为 2019 年还在用 4.4 系统的用户,大概率不会是正经用户

—— 从某位独立开发者分享的数据统计来看,Google Play 上的 4.4 用户 “贡献了” 78% 的差评和近乎为 0 的增值付费

然而唯有 正确地理解 一个系统 所服务的普适需求 乃至 推知 和 确立 该系统的必要构成,才能在关键时刻有条不紊地 界定问题的边界、判定事物的归属(比如你知道了 LayoutInflater 是视图排版的 前置辅助工具,那么在谈论具体的排版流程时,你便不会将其考虑进去,因为你明确地知道 它的边界、它的归属),从而 不做无用功地、毫不费力地迎刃而解

—— 你一定不希望 老板交代你任务时 一脸懵逼、不知从何下手,对吧。

作为差强人意的 Android 视图系统

所以 本来我是打算趁热打铁、接着上一篇 打下的认知基础,来继续深入对 排版过程的反思,

然而在我调研了一圈后发现,网上的文章多是 千篇一律地 贴代码、告诉你 how、how、how,就是不告诉你 why。由此引发了我 “走在路上,脑壳疼” 的难受。

比如,你知道 为什么要 独立地存在 “测量” 的流程吗?是否能够提供案例,来证实这个过程的存在 确实是 某种场景下解决某种问题时的 不可替代?否则视图系统 为什么要存在这样的设计?

通过 LayoutParams 我能拿到对宽高的描述,那么我直接在 “布局” 环节根据描述来计算视图的 Left、Top、Right、Bottom、Height、Width 不也可以?(如我所料,Flutter 下就没有专门的测量流程)

就是这样烧脑的、但在我看来是不可绕过的关键,消耗了我大量的时间来思考和反复论证。😵

所以前几天有位读者私下感叹说,要是互联网上的每篇文章都能像 《这样理解,你也能在 30 秒内讲明白 TCP 三次握手》 一样,能用 "你听得见吗" "我听得见" "我也听得见" 让人秒懂三次握手的本质 该有多好,

实际上在我看来,并不是撰文、教书的人 讲解方式 的问题,而是 设计系统 和 撰写文档 的 通常不是同一个人。

我们常常能在源码中看到精妙的设计,文档却常常只顾 how 而忽略了对 作为重中之重的 如此设计的用意 的介绍。

也即,上游的设计师 出于各种原因 没有出面解释,导致中游的教师或作者 需要花费大量的精力 才有一丝丝的机会正确地理解到。

并且,从生物代谢的角度来说,照本宣科、人云亦云,张口闭口 ACK、SYN(三次握手中的术语),显然是最节能的,尽管这样一来他们自己都蒙在鼓里、不知道自己究竟在讲些什么。

TQLAWSL

于是,在 “现状如此” 的背景下,我决定将本篇文章分两部分讲,上半部分着重于反思各平台通用的 视图系统架构实现 的全貌,下半部分将对 安卓中的局部实现 作有选择地介绍。

在之前的 《你 hold 不住自定义视图,只因缺了这把钥匙》 一文中已提到过:源码的实现 绝非一蹴而就,它是几十年下来不断演化、不断变更的结果,所以我们务必 首先基于反思 来为自己准备一把切入的钥匙,然后在这把钥匙的指引下 有目的地、点到为止地 到源码中去确认,而不是打一开始就盲目地一头栽进去 试图事无巨细地 全盘通吃。

好了 不多说了,下面让我们跟随 深度思考的脚步,从抽象到具体地 将视图系统架构实现的全貌 无痛地过一遍!

视图系统 为什么要基于 C/S 架构

无论是 Android、iOS、还是 MacOS、Windows、Linux,视图系统为了实现 输入和输出的完美协调,必然需要基于 C/S 架构。

为什么呢?

首先,排版结果的渲染 和 屏幕事件的接收,是通用的、且 使用频率极高,所以它应当在处于 支持高效率运行的、规避数据被篡改或破坏 的环境下运作,

根据大学时期我们学过的《操作系统》课程,我们能够得知,进程通常存在两大类:一类是用户进程,一类是系统进程。显然 独立存在 的系统进程 能够为上述需求提供理想的环境:运行时无须申请任何权限、运行效率更高、且规避了内部数据惨遭外部篡改 而导致的运行结果不可预期的风险

由此我们的视图系统 便不得不 分为两部分 来运行,一部分是上述提到的,常驻 在系统进程中的 视图服务,另一部分就是 运行在 用户自己开启的用户进程中的 视图接口(客户端),客户端的 排版结果 和 事件分发,都是通过这个“接口”去向 视图服务 传递和接收的。

除了上述提到的 运行时高效、运行时安全,一分为二的设计的另一个好处在于 实现了效率和节省资源的平衡:让频繁使用的底层服务 常驻、让随机使用的客户端 随叫随到、用完即走(关掉 App 的进程,进程申请的这部分资源 也就归还给系统了)。

所以 至此我们确立了:视图系统需要 “一分为二” 的设计 ——

  • 让使用频率极高的视图服务 常驻系统进程,以减少频繁创建进程导致的开销,以及由于是在系统进程中运行,因而效率更高、更安全。

  • 与此同时,让客户端 在用户进程中生死存亡,从而达到即用即走、节省系统资源的目的。

正是因为这个需要,造成了 视图接口 和 视图服务 分别处于不同的进程,从而有 IPC(Inter-Process Communication,进程间通信)的需要。而该场景下效率最高的 IPC 通信方式是 RPC(Remote Procedure Call,远程过程调用):一种 C/S 架构的实现,因而,视图系统需要基于 C/S 架构。

简单地翻看一下安卓源码,如我所料,Stub 和 Session 等字样,几乎实锤了 安卓的确是用 RPC 方式来完成 该场景下的 IPC。

对 RPC 不熟悉的同学请 不要慌,可以自行搜索资料 或查阅相关书籍(例如 CSAPP)。

我们千万不要 死记硬背 RPC 的源码实现,这里分享一下 我看的好的 来自大学教材的小结。比起 源码的具体实现,对存在缘由的 抽象介绍 其实 一目了然 而 易于理解

top Created with Sketch.