注册

通俗易懂 Android 架构组件发展史

前言


谈到 Android 架构,相信谁都能说上两句。从 MVC,MVP,MVVM,再到时下兴起 MVI,架构设计层出不穷。如何为项目选择合适架构,也成常备课题。


由于架构并非空穴来风,每一种设计都有其存在依据。故今天我们一起探寻 “架构演化” 来龙去脉,相信阅读后你会豁然开朗。


文章目录一览



  • 前言
  • 原生架构

    • 原始图形化架构

      • 高频痛点 1:Null 安全一致性问题


    • 原始工程架构 MVC

      • 高频痛点 2:成员变量爆炸
      • 高频痛点 3:状态管理一致性问题
      • 高频痛点 4:消息分发一致性问题




  • 它山之石

    • 矫枉过正 MVP

      • 反客为主 Presenter
      • 简明易用 三方库


    • 拨乱反正 MVVM

      • 曲高和寡 DataBinding
      • 未卜先知 mBinding




  • 力挽狂澜

    • 官方牵头 Jetpack

      • 一举多得 ViewModel


    • 半路杀出 Kotlin

      • 喜闻乐见 ViewBinding




  • 百花齐放

    • 最佳实践 Jetpack MVVM

      • 屏蔽回推 UnPeekLiveData
      • 消息分发 Dispatcher
      • 严格模式 DataBinding


    • 另起炉灶 Compose


  • 综上

原生架构


原始图形化架构


完整软件服务,通常包含客户端和服务端。


Linux 服务端,开发者通过命令行操作;Android 客户端,面向普通用户,须提供图形化操作。为此,Android 将图形系统设计为,通过客户端 Canvas 绘制图形,并交由 Surface Flinger 渲染。


但正如《过目难忘 Android GUI 关系梳理》所述,复杂图形绘制离不开排版过程,而开发者良莠不齐,如直接暴露 Canvas,易导致开发者误用和产生不可预期错误,


为此 Android 索性基于 “模板方法模式” 设计 View、Drawable 等排版模板,让 UI 开发者可继承标准化模板,配置出诸如 TextView、ImageView、ShapeDrawable 等自定义模板,供业务开发者用。



这样误用 Canvas 问题看似解决,却引入 “高频痛点 1”:View 实例 Null 安全一致性问题。这是 Java 语言项目硬伤,客户端背景下尤明显。



高频痛点 1:Null 安全一致性问题


例如某页面有横竖两布局,竖布局有 TextViewA,横布局无,那么横屏时,findViewbyId 拿到则是 Null 实例,后续 mTextViewA.setText( ) 如未判空处理,即造成 Null 安全问题,



对此不能一味强调 “手动判空”,毕竟一个页面中,控件成员多达十数个,每个控件实例亦遍布数十方法中。疏忽难避免。



那怎办?此时 2008 年,回顾历史,可总结为:“同志们,7 年暗夜已开始,7 年后会有个框架,驾着七彩祥云来救你”。



原始工程架构 MVC


时间来到 2013,以该年问世 Android Studio 为例,


工程结构主要包含 Java 代码和 res 资源。考虑到布局编写预览需求,Android 开发默认基于 XML 声明 Layout,MVC 形态油然而生,



其中 XML 作 View 角色,供 View-Controller 获取实例和控制,


Activity 作 View-Controller 角色,结合 View 和 Model 控制逻辑,


开发者另外封装 DataManager,POJO 等,作 Model 角色,用于数据请求响应,



显而易见,该架构实际仅两层:控制层和数据层,


Activity 越界承担 “领域层” 业务逻辑职责,也因此滋生如下 3 个高频痛点:


高频痛点 2:成员变量爆炸


成员声明,动辄数十行,令人眼花缭乱。接手老项目开发者,最有体会。



高频痛点 3:状态管理一致性问题


View 状态保存和恢复,使用原生 onInstanceStateSave & Restore 机制,开发者容易因 “记得 restore、遗漏 save” 而产生不可预期错误。



高频痛点 4:消息分发一致性问题


由于 Activity 额外承担 “领域层” 职责,乃至消息收发工作也直接在 Activity 内进行,这使消息来源无法保证时效性、一致性,易 “被迫收到” 不可预期推送,滋生千奇百怪问题。


EventBus 等 “缺乏鉴权结构” 框架,皆为该背景下 “消息分发不一致” 帮凶。



“同志们,5 年水深火热已过去,再过 2 年,曙光降临”


好家伙,这是提前拿到剧本。既然如此,这 2 年时间,不如放开手脚,引入它山之石试试(就逝世)。



它山之石


矫枉过正 MVP


这一版对 “现实状况” 判断有偏差。


MVP 规定 Activity 应充当 View,而 Presenter 独吞 “表现层” 逻辑,通过 “契约接口” 与 View、Model 通信,


这使 Activity 职能被严重剥夺,只剩末端通知 View 状态改变,无法全权自治 “表现逻辑”。



反客为主 Presenter


从 Presenter 角度看,似乎遵循 “依赖倒置原则” 和 “最小知道原则”,但从关系界限层面看,Presenter 属 “空降” 角色,一切都其自作主张、暗箱操作,不仅 “未能实质解决” 原 Activity 面临上述 4 大痛点,反因贪婪夺权引入更多烂事。


这也是为何,开发过 MVP 项目,都知有多别扭。


简明易用 三方库


基于其本质 “依赖倒置原则” 和 “最小知道原则”,更建议将其用于 “局部功能设计”,如 “三方库” 设计,使开发者 无需知道内部逻辑,简单配置即可使用



Github:Linkage-RecyclerView


我们维护的 “饿了么二级联动列表” 库,即是基于该模式设计,感兴趣可自行查阅。



拨乱反正 MVVM


经历漫长黑夜,Android 开发引来曙光。


2015 年 Google I/O 大会,DataBinding 框架面世。


该框架可用于解决 “高频痛点1:View 实例 Null 安全一致性问题”,并跟随 MVVM 模式步入开发者视野。


曲高和寡 DataBinding



MVVM 是种约定,双向绑定是 MVVM 特征,但非 DataBinding 本质,故长久以来,开发者对 DataBinding 存在误解,认为使用 DataBinding 即须双向绑定、且在 XML 中调试。



事实并非如此。


DataBinding 是通过 “可观察数据 ObservableField” 在编译时与 XML 中对应 View 实例绑定,这使上文所述 “竖布局有 TextViewA 而横布局无” 情况下,有 TextViewA 即被绑定,无即无绑定,于是无论何种情况,都不至于 findViewById 拿到 Null 实例从而诱发 Null 安全问题。



也即,DataBinding 仅负责通知末端 View 状态改变,仅用于规避 Null 安全问题,不参与视图逻辑。而反向绑定是 “迁就” 这一结构的派生设计,非核心本质。



碍于篇幅限制,如这么说无体会,可参见《从被误解到 “真香” Jeptack DataBinding》解析,本文不再累述。



未卜先知 mBinding


除了本质难理解,DataBinding 也有硬伤,由于隔着一层 BindingAdapter,难获取 View 体系坐标等 getter 属性,乃至 “属性动画” 等框架难兼容。



有说 MotionLayout 可破此局,于多数场景轻松完成动画。


但它也非省油灯,不同时支持 Drag & Click,难实现我们 示例项目 “展开面板” 场景。



于是,DataBinding 做出 “违背祖宗” 决定 —— 允许开发者在 Java 代码中拿到 mBinding 乃至 View 实例 … 如此上一节提到的 “改用 ObservableField 的绑定来消除 Null 安全问题” 的努力前功尽弃。


—— 鉴于 App 页面并非总是 “横竖布局皆有”,于是开发者索性通过 “强制竖屏” 扼杀 View 实例 Null 安全隐患,而调用 mBinding 实例仅用于规避 findViewById 样板代码。



至于为何说 mBinding 使用即 “未卜先知”,因为群众智慧多年后即被应验。



力挽狂澜


官方牵头 Jetpack


时间回到 2017,这年 Google I/O 引入一系列 AAC(Android Architecture Components)


一举多得 ViewModel


其中 Jetpack ViewModel,通过支持 View 实例状态 “托管” 和 “保存恢复”,


一举解决 “高频痛点2:成员变量爆炸” 和 “高频痛点 3:状态管理一致性问题”,


Activity 成员变量表,一下简洁许多。Save & Restore 样板代码亦烟消云散。



半路杀出 Kotlin


并且这时期,Kotlin 被扶持为官方语言,背景发生剧变。


Kotlin 直接从语言层面支持 Null 安全,于是 DataBinding 在 Kotlin 项目式微。


喜闻乐见 ViewBinding


千呼万唤,ViewBinding 问世 2019。


如布局中 View 实例隐含 Null 安全隐患,则编译时 ViewBinding 中间代码为其生成 @Nullable 注解,使 Kotlin 开发过程中,Android Studio 自动提醒 “强制使用 Null 安全符”,由此确保 Null 安全一致。



ViewBinding 于 Kotlin 项目可平替 DataBinding,开发者喜闻乐见 mBinding 使用。


百花齐放


最佳实践 Jetpack MVVM


自 2017 年 AAC 问世,部分原生 Jetpack 架构组件至今仍存在设计隐患,


基于 “架构组件本质即解决一致性问题” 理解,我们于 2019 陆续将 “隐患组件” 改造和开源。


屏蔽回推 UnPeekLiveData


LiveData 是效仿响应式编程 BehaviorSubject 的设计,由于


1.Jetpack 架构示例通常只包含 “表现层” 和 “数据层” 两层,缺乏在 “领域层” 分发数据的工具,


2.LiveData Observer 的设计缺乏边界感,


容易让开发者误当做 “一次性事件分发组件” 来使用,造成订阅时 "自动回推脏数据";


容易让开发者误将同一控件实例放在多个 Observer 回调中 造成恢复状态时 “数据不一致” 等问题(具体可参见《MVI 的存在意义》 关于 “响应式编程漏洞” 的描述)


3.DataBinding ObservableField 组件的 Observer 能限定为 "与控件一对一绑定",更适合承担表现层 BehaviorSubject 工作,


4.LiveData 具备生命周期安全等优势,


因此决定将 LiveData 往领域层 PublishSubject 方向改造,去除其 “自动推送最后一次状态” 的能力,使其专职生命周期安全的数据分发。



具体可参见 Github:UnPeek-LiveData 使用。



消息分发 Dispatcher


由于 LiveData 存在的初衷并非是专业的 “一次性事件分发组件”,改造过的 UnPeekLiveData 也只适用于 “低频次数据分发(例如每秒推送 1 次)” 场景,


因而若想满足 “高频次事件分发” 需求(例如每秒推送 5 次以上),请改用或参考专职 “领域层” 数据分发的 Github:MVI-Dispatcher 组件,该组件内部通过消息队列设计,确保不漏掉每一次推送。




Dispatcher 的存在解决了 “高频痛点 4:消息分发一致性问题”,


也即通过在领域层设立 “专职业务处理和结果回推” 的 Dispatcher,来将业务处理过程中产生的 Event 或 State,以串流的方式统一从 output 出口回传,


由此表现层页面可根据消息的性质,采取 “一致性执行” 或 “交由 BehaviorSubject 托管状态”。


对此具体可参见《解决 MVI 架构实战痛点》 解析。



严格模式 DataBinding


此外我们明确约定 Java 下 DataBinding 使用原则,确保 100% Null 安全。如违背原则,便 Debug 模式下警告,方便开发者留意。



具体可参见 Github:KunMinX-MVVM 使用。



另起炉灶 Compose


回到文章开头 Canvas,为实现 View 实例 Null 安全,先是 DataBinding 框架,但它作为一框架,并不体系自洽,与 “属性动画” 等框架难兼容。


于是出现声明式 UI,通过函数式编程 “纯函数原子性” 解决 Null 安全一致。且体系自洽,动画无兼容问题,学习成本也低于 View 体系。


后续如性能全面跟上、120Hz 无压力,建议直接上手 Compose 开发。



注:关于声明式 UI 函数式编程本质,及纯函数原子性为何能实现 Null 安全一致,详见《一通百通 “声明式 UI” 扫盲干货》,本文不作累述。



综上


高频痛点1:Null 安全一致性问题


客户端,图形化,需 Canvas,


为避免接触 Canvas 导致不可预期错误,原生架构提供 View、Drawable 排版模板。


为解决 Java 下 View 实例 Null 安全一致性问题,引入 DataBinding。


但 DataBinding 仅是一框架,难体系自洽,


于是兵分两路,Kotlin + ViewBinding 或 Kotlin + Compose 取代 DataBinding。


高频痛点2:成员变量爆炸


高频痛点3:状态管理一致性问题


引入 Jetpack ViewModel,实现状态托管和保存恢复。


高频痛点4:消息分发一致性问题


引入 Dispatcher 承担 PublishSubject,实现统一的消息推送。


最后,天下无完美架构,唯有高频痛点熟稔于心,不断死磕精进,集思广益,迭代特定场景最优解。


相关资料


Canvas,View,Drawable,排版模板:《过目难忘 Android GUI 关系梳理》


DataBinding,Null 安全一致,ViewBinding:《从被误解到 “真香” Jetpack DataBinding》


Dispatcher,消息分发,State,Event:《解决 MVI 架构实战痛点》


架构组件解决一致性问题:《耳目一新 Jetpack MVVM 精讲》


Compose,纯函数原子性,Null 安全一致:《一通百通 “声明式 UI” 扫盲干货》


版权声明



Copyright © 2019-present KunMinX 原创版权所有。



如需 转载本文,或引用、借鉴 本文 “引言、思路、结论、配图” 进行二次创作发行,须注明链接出处,否则我们保留追责权利。


本文封面 Android 机器人是在 Google 原创及共享成果基础上再创作而成,遵照知识共享署名 3.0 许可所述条款付诸应用。


作者:KunMinX
链接:https://juejin.cn/post/7106042518457810952
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册