Android开发Android开发Android 技术开发

Android 架构设计与挑选

2022-06-10  本文已影响0人  丘卡皮

前言

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

由于架构并非空穴来风,每一种设计都有其存在依据。唯有高频痛点熟稔于心,才能技术选型事半功倍。所以今天我们一起探寻 “架构演化” 来龙去脉,相信阅读后你会豁然开朗。

文章目录一览

原生架构

原始图形化架构

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

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 实例 …… ??? 那 DataBinding 不 bind 个寂寞,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 样板代码亦烟消云散。

读写分离 LiveData

而 Jetpack LiveData,通过 protected + mutable 设计,实现单向数据流,从而

解决 “高频痛点 4:消息分发一致性问题”。

所谓单向数据流,即无论请求从何处发起,观察者收到都是从 “唯一可信源” 内部鉴权后统一推送的 “只读消息” ,如此可避免 “多页面、多观察者” 获取 “过时、不实、不一致” 消息。

注:对此如无体会,可参见《吃透 LiveData 本质,享用可靠消息鉴权机制》解析,本文不作累述。

半路杀出 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 根据官方描述,可分别用于 “末流推数据” 及 “页面间通信” 场景。但粘性设定使 LiveData 更倾向于前者场景,在后者场景中易发生不符预期 “数据倒灌” 问题。

为此我们从头梳理 “消息分发” 背景来龙去脉,得出以下结论:

项目语言 DataBinding 可变 State 状态托管和保存恢复 单向数据流
Java 必用 ObservableField Jetpack ViewModel
Kotlin 可不用 可无 Jetpack ViewModel

也即,DataBinding 项目,页面旋屏重建后,DataBinding 可从 ViewModel 拿取绑定的 ObservableField 重新渲染。粘性设定可有可无。

在非 DataBinding 项目,由于现如今 “单向数据流” 结构,末流逻辑皆是 LiveData Observer 回调中完成,因而须 LiveData 粘性设定,自动推送最后一次数据。

由此可见,粘性设定确有其适用场景。但,毕竟是 “mutable 系” 框架先驱,消息鉴权天赋在此,不善用岂不可惜?

于是我们考虑屏蔽其 “粘性设定”,专用于 “应用内 - 页面间 - 生命周期安全 - 来源可靠 - 只读一致” 消息分发,并开源至 Github:UnPeekLiveData 集思广益。

期间 “腾讯音乐” 小伙伴贡献过 v5 版重构代码,用于月活过亿 “生产环境” 痛点治理。

严格模式 DataBinding

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

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

前后通吃 Kotlin Flow

通常 Flow 可用于领域层、数据层,实现复杂数据变换与传递。

2021 官方考虑 Kotlin Flow + Lifecycle.repeatOnLifecycle 取代 LiveData。眼见 “末流推数据” 场景被平替,跟风抹杀 “消息分发场景可行性” 亦不绝于耳,诸如 “LiveData 设计之初就不是为这个用”。对此其实大可不必。

鉴于 Kotlin Flow “生产者消费者” 队列设计,可用于诸如 “618 抢单” 等暴力测试场景。 Java 下 mutable 系唯 LiveData 可用,且常规操作 LiveData 足矣。

消除样板 MVI

显然 Kotlin 开发者还可再进一步,于 “表现层” 和 “领域层” 使用 MVI 设计。

MVI 基于 sealed class 加持,可集中接收本页面 events 并分流具体 event。如此从 “唯一可信源” 角度看,“mutable 先驱” 被缩减为唯一实例,从而 mutable/immutable 样板代码缩减为一,不再百忙出错。

不过,样板代码手写出错,其实易解决,例如 Java + MVVM 开发者完全可通过 “自动化工具” 生成样板代码,最简单办法即是在 Android Studio 中写个 main 函数,循环拼装输出代码。

此外 “单向数据流” 未能杜绝 “末端逻辑” 隐患,易在 “逻辑闭环误被打破” 情况下,引发 “请求响应递归循环”。过去两年读者群有过数起类似事故讨论,MVI 同有概率遭遇此问题。

所以,MVI 使用见仁见智,Java 开发者建议 Jetpack + 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:消息分发一致性问题

消息分发难追溯、过时、不一致,

mutable + 唯一可信源 “单向数据流” 解决,

但 mutable 滋生大量样板代码,于是局部 MVI,

但 “单向数据流” 难杜绝请求响应 “无限循环” 隐患,所以,天下无完美架构,唯有高频痛点熟稔于心,不断死磕精进,集思广益,迭代特定场景最优解。

上一篇下一篇

猜你喜欢

热点阅读