作者: madroid
回顾
根据 App 行为的不同,我们对其进行分离/分层并确定其职责,每层之间的通讯交互采用响应式方式。
App 有三层结构,分别为 UI Layer、Domain Layer、Data Layer,其依赖关系是单向的,上层可以依赖下层,下层却不能反过来依赖上层。大致如下,其中 Domain Layer 是可选层:
△ App Arch Layer Design
每层的主要职责分别为:
1、UI Layer: 使用 UI 元素展示 App 中数据
- 将底层数据处理成容易被 UI elements 使用的 Uistate 数据;
- 根据 UiState 绘制对应的 UI elements;
- 响应用户的操作事件,根据需求对其进行分发;
2、Domain Layer: 封装通用的业务逻辑
- 封装复杂的业务逻辑,避免出现大型类;
- 封装多 ViewModel 通用的业务逻辑,避免代码重复;
3、Data Layer: 封装统一的数据来源,提供单一可信来源
- 定义不同的 DataSource 来封装 Framework 及三方 SDK 的 API;
- 定义 Repository 来整合相同业务的不同数据类型的 DataSource;
每层依赖关系是单向的,UI Layer 可以依赖 Domain Layer,但是 Domain Layer 却不能依赖 UI Layer。这种依赖方式可以使用简单的函数传递依赖事件,但是却不能处理结果的回调,即 UiState 的更新。想要处理结果的回调每层之间就可以采用数据驱动/响应式的方式来交互了。这种方式也被称为是单向数据流的方式,即 UI 事件从 UI 层流向数据层,UiState 从数据层流向 UI 层。
关于 UI Layer、Domain Layer、Data Layer 中更多详细内容可以查看官方文档应用架构指南:
https://developer.android.google.cn/jetpack/guide
与 MVI 的关系?
MVI 的全称是 Model-View-Intent,这里的 Intent 并不是指 Android 中的 Intent 类,而是表示一种意图,可以简单理解为对用户 Event 的一种抽象。其交互图大致如下:
MVI 并不像 MVC、MVP、MVVM 一样,不论是 Controller、Presenter 还是 ViewModel 都是 View 与 Model 之间的桥接类,负责这两者之间的通信与交互 (虽然 MVC 可以跨过 Controller 直接进行交互)。而 Intent 并没有类似的职责,仅仅是约束了 View 的事件通过类似枚举的方式定义,这种方式更像是前端框架中的 Flux 或者是 Redux,更多内容可以查看 Reclaim the reactivity of your state management, say no to imperative MVI,实现 MVI 的主流框架有: Orbit、Mavericks、Uniflow-kt、Mobius。
- Reclaim the reActivity of your state management, say no to imperative MVI
https://zhuinden.medium.com/reclaim-the-reactivity-of-your-state-management-say-no-to-imperative-mvi-3b23ca6b8537
- Orbit
https://github.com/orbit-mvi/orbit-mvi/blob/06e9f759a87e7192767baeebc682fc92369a7eff/orbit-core/src/commonMain/kotlin/org/orbitmvi/orbit/internal/RealContainer.kt#L74-L75
- Mavericks
https://github.com/airbnb/mavericks/blob/e8a631a19fc1b044da3ddff358712e129dc487a6/mvrx/src/main/kotlin/com/airbnb/mvrx/CoroutinesStateStore.kt#L57-L59
- Uniflow-kt
https://github.com/uniflow-kt/uniflow-kt/blob/a1fdbeb733a0b550a162227be3b1e03d03197023/uniflow-core/src/main/kotlin/IO/uniflow/core/flow/ActionReducer.kt#L28-L32
- Mobius
https://github.com/spotify/mobius
有的 MVI 在实现还需要借助 ViewModel,仅仅是把 View 的事件定义成的对应的密封类。目的仅仅是为了强制实现单向数据流的方式,根据之前介绍实现单向数据流的方式还是比较简单的,上层只能依赖下层实现,下层的处理结果通过 LiveData、Flow 方式更新。
那再来聊一下 MVC、MVP、MVVM 与 Android 官方的推荐的 MAD Arch 之间的关系。其实经常提到的 MVVM 与 Android 官方的架构还是有本质区别的。MVX (对 MVC、MVP、MVVM 的统称) 的架构方式对 Model 这一层提到的非常少,留下的印象可能就是除了 VX 之外剩下的就是 Model 的部分。但是这部分在整个 App 的架构中也是非常重要的。我们还是有大量的业务逻辑是在 Model 层处理的。
而 Android 官方的架构中却包含了这部分的描述,新增了 Data Layer 与 Domain Layer。所以总结下来就是 MVX 处理的仅仅是 UI Layer 中的问题,描述的是状态管理的部分;官方文档中描述的确是整个 App 的架构,是一种包含的关系。
如何处理线程?
无论是在哪一层都要确保其在主线程安全的,即在主线程调用不会阻塞主线程或者是抛出异常。那应该是在哪一层进行处理呐?其可选项有 ViewModel、UseCase、Repository、DataSource,只要在任何一层处理耗时操作都可以确保其是主线程安全的。这里建议采用 "就近原则",即谁产生数据谁就保持数据的安全性。
Data Layer 中 DataSource 是 "产生" 数据的地方,在这里直接切换到对应的子线程是可以的,代码大致如下:
class NewsRemoteDataSource( private val newsApi: NewsApi, private val ioDispatcher: CoroutineDispatcher) { /** * 在 IO 线程中,获取网络数据,在主线程调用是安全的 */ suspend fun fetchLatestNews(): List<ArticleHeadline> = withContext(ioDispatcher) { // 将耗时操作移动到 IO 线程中 newsApi.fetchLatestNews() }}
如果 Repository 中需要整合很多的 DataSource 中的数据,在 Repository 中切换到对应的子线程也是可以的,这样可以减少频繁的线程调度。
同时也需要考虑响应业务的生命周期情况,如果当前业务跟随这页面进行的,那么使用 viewModelScope 或者是 lifecycleScope 即可;如果其业务是跟随 App 的什么周期的,那么则需要使用整个 App 生命周期的 CoroutineScope;如果在 App 被终止后,仍然希望可以执行任务,那么可以考虑使用 WorkManager:
https://developer.android.google.cn/topic/libraries/architecture/workmanager
如何处理实体类 (Entity)?
各层之间的 Entity 根据其职责定义会有所不同,可以根据具体的使用场景自定义 Entity。如云端返回的 Entity 与数据库需要存储的 Entity 可能并不相同,使用相同的 Entity 会导致代码的可维护性下降,而且没有必要暴露过多的细节。如下:
@Entity(tableName = "user")data class RemoteUser( @PrimaryKey @SerializedName("user_id") val userId: String, val username: String, @Ignore val token: String, @Ignore val inventory: RemoteInventory, @Ignore val profile: RemoteProfile,)
这种场景下,我们就可以针对云端返回数据与数据库存储数据分别定义不同的 Entity,如下:
// 云端数据 Entitydata class RemoteUser( @SerializedName("user_id") val userId: String, val username: String, val token: String, val inventory: RemoteInventory, val profile: RemoteProfile,)// 数据库 Entity@Entity(tableName = "user")data class UserEntity( @PrimaryKey val userId: String, val username: String,)
对于不同页面直接传递数据的场景 (Intent),建议定义单独的 Entity,因为传递数据的大小是有限的。定义大致如下:
@Parcelizedata class Inventory( val id: UUID, val type: String): Parcelable
对于 UI Layer 中的实体定义,要根据其业务类型进行细分,切记不要将一页面中的所有的 UiState 都定义在同一个 Entity 中。因为汇总型的定义在相关字段的更新频率不一致的时候会导致频繁的 UI element 重复绘制,同时不可变的 Entity 的字段增加也会导致不必要的内存开销。如果一个 UiState 中有超过 5 个状态,那就需要回过来看下 UiState 是否可以进行拆分了。
UiState 中经常遇到的一个场景就是添加 Loading 状态,这种情况添加封装统一的 Wrapper 类进行处理,如下:
sealed interface UiStateWrapper { object Loading : UiStateWrapper class Success<T>(val uiState: T) : UiStateWrapper class Failure(val exception: Throwable) : UiStateWrapper}
这种处理方式,并不需要在 UiState Entity 新增一个 isLoading 字段,保持 UiState 的 "纯洁性",同时也可以在 UI elements 中对 UiStateWrapper 做统一的处理,不必每个 UiState 中都出 Loading 的状态,当然,这是在 Loading 处理逻辑相同的前提下的。
整体而言,根据不同职责定义不同的 Entity 会让我们的代码逻辑相对合理,但是会增加一定的工作量以及会对要使用何种 Entity 产生混淆。所以还是需要根据自己的项目及团队情况决定是否需要精细化管理 Entity,大型团队建议采用这种方式。
如何组织代码?
代码建议按照业务模块方式进行组织,而非功能进行组织。大致如下:
# DO- Project - feature1 - ui - domain - data - feature2 - ui - domain - data - feature3
不要使用如下的方式:
# DO NOT- Project - ui - feature1 - feature2 - feature3 - domain - feature1 - feature2 - feature3 - data
采用 Feature 方式组织代码的优势大致有以下几点:
- 我们大概率都是在已有的项目中开发,而历史的项目中或多或少存在着一些历史技术债务,我们可以在开发特性的时候引入新的技术,这样不会对旧的目录结构产生过多影响;
- 后续可以很方便的对该特性进行改造,比如可以把这个文件夹移到一个单独的 module 中进行模块化相关的改造;
- 这方式在大型项目中的优势会更加明显;
速记手册
整理了一些关键知识点,可以保存图片定期回顾。
官方材料
文章中的内容基本上都是参考官方文档以及 Youtube 上的 mad – arch 系列。都看到这里了建议您到官方文档中的 pathawy 地址中获取下现代 Android 应用架构徽章,只要阅读完下面的文档以及完成对应测试即可。
- Youtube
https://www.youtube.com/watch?v=TPWmfJq16rA&list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX&ab_channel=AndroidDevelopers
- pathawy
https://developer.android.google.cn/courses/pathways/android-architecture
- ui-layer
https://developer.android.google.cn/jetpack/guide/ui-layer
- ui-layer/events
https://developer.android.google.cn/jetpack/guide/ui-layer/events
- domain-layer
https://developer.android.google.cn/jetpack/guide/domain-layer
- data-layer
https://developer.android.google.cn/jetpack/guide/data-layer
- youtube playlist
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8GZWCbUm3tBXKeqIi3rcVX
- Modern Android App Architecture quiz
https://developer.android.google.cn/courses/quizzes/android-architecture/architecture-layers?continue=https://developer.android.com/courses/pathways/android-architecture#quiz-/courses/quizzes/android-architecture/architecture-layers
最后
今年的 Google I/O 发布了一个最新的官方示例 Now in Android,这个示例的完整度比之前的 JetNews、Sunflower 要高,后面也将基于这个仓库做进一步的说明解析,从一个完整项目的角度来看 Android 新推出的架构指南。
- Now in Android
https://github.com/android/nowinandroid
这里也分享一些珍藏资源,从面试简历模板到大厂面经汇总,从大厂内部技术资料到互联网高薪必读书单,以及Android面试核心知识点(844页)和Android面试题合集2022年最新版(354页)等等,这些资料整理给大家,希望踩过的坑不要再踩,遭遇的技术瓶颈一次性消灭。
如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!
部分内容展示如下
01.Android必备底层技术:
- Java序列化:Serializable原理、Parcelable接口原理、Json、XML
- 注解、泛型与反射:自定义注解、注解的使用、泛型擦除机制、泛型边界、Java方法与Arm指令、Method反射源码、invoke方法执行原理
- 虚拟机:JVM垃圾回收器机制、JVM内存分配策略、Android虚拟机与JVM底层区别、虚拟机底层Odex本地指令缓存机制、虚拟机如何分别加载class与object、虚拟机类加载模型
- 并发:Java线程本质讲解、线程原理、线程通信、UnSafe类、线程池
- 编译时技术:OOP面向切面之AspectJ、字节码手术刀JavaSSit实战、字节码插桩技术(ASM)实战
- 动态代理:动态代理实现原理、动态代理在虚拟机中运行时动态拼接Class字节码分析、ProxyGenerator生成字节码流程
- 高级数据结构与算法:HashMap源码、ArrayList源码、排序算法
- Java IO:Java IO体系、IO文件操作
02.Framework:
- Binder:Linux内存基础、Binder四层源码分析、Binder机制、Binder进程通信原理
- Handler:Loop消息泵机制、Message解析
- Zygote:init进程与Zygote进程、Zygote启动流程、Socket通信模式、APP启动过程
- AMS:ActivityThread源码分析、AMS与ActivityThread通信原理、Activity启动机制
- PMS:PMS源码、APK安装过程分析、PMS对安装包的解析原理
- WMS:PhoneWindow实例化流程、DecorView创建过程、ViewRootImpl渲染机制
03.Android常用组件:
- Activty:Activity管理栈与Activity的启动模式、Activity生命周期源码分析
- Fragment:Fragment生命周期深入详解、Fragment事务管理机制详解、性能优化相关方案
- Service:Service启动模式分析、Service管理与通信方案、Service生命周期底层详解
04.高级UI:
- UI绘制原理:setContentView()方法下到底做了什么、AppCompatActivity与Activity的区别、UI测量、布局、绘制的底层执行流程
- 插件换肤:LayoutInflater加载布局分析、Android资源的加载机制、Resource与AssetManager
- 事件分发机制原理:事件执行U形链与L形链、事件拦截原理
- 属性动画:VSYNC刷新机制、ObjectAnimator与ValueAnimator源码讲解、Android属性动画:插值器与估值器
- RecycleView:布局管理器LayoutManager详解、回收池设计思想、适配器模式原理
- 高阶贝塞尔曲线
05.Jetpack:
- Lifecycle:Lifecycle源码、Lifecycle高阶应用
- ViewModel:ViewModel源码、ViewModel应用技巧
- LiveData:LiveData源码
- Navigation:Navigation源码
- Room:Room源码、Room LiveData监听数据库数据变更刷新页面原理
- WorkManager内核
- Pagging原理
- DataBinding:单向绑定、双向绑定、如何与RecyclerView的配合使用、底层原理
06.性能优化:
- 启动优化:系统启动原理、Trace工具分析启动卡顿、类重排机制、资源文件重排机制
- 内存优化
- UI渲染优化:UI层级规范及对UI加载的影响、UI卡顿原因及修复、UI绘制、布局、测量原因以及处理方案
- 卡顿优化:造成卡顿的原因分析、内存抖动与GC回收、回收算法
- 耗电优化
- 崩溃优化:项目崩溃异常捕获、优雅的异常处理方案、如何避免异常弹框
- 安全优化:APP加固实现(防反编译,dex加固)、https防抓包机制(数据传输加载,客户端服务器端双向加密校验)
- 网络优化:serializable原理、parcelable接口原理、http与https原理详解、protbuffer网络IO详解、gzip压缩方案
- 大图加载优化:Glide巨图加载机制原理分析、大图多级缓存实现方案
- 多线程并发优化
- 储存优化:Android文件系统-sdcard与内存存储、Shared Preference原理、MMAP内存映射
- 安装包优化:shrinkResources去除无用资源、合理设置多语言、webp实现图片瘦身、合理配置armable-v7的so库、Lint检查工具实践
如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!
07.音视频:
- C/C :数据类型、数组、内存布局、指针、函数、预处理器、结构体、共用体、容器、类型转换、异常、文件流操作、线程
- H.265/H.265:音视频格式封装原理、编码原理、视频流H264的组装原理切片NAL单元、视频流H264码流分析、切片与宏快,运动矢量、信源编码器、高频滤波、帧间拆分与帧内预测、CTU,PU TU编码结构、DSP芯片解码流程、MediaPlayer与DSP芯片交互机制、投屏架构、MediaProjection与MeidiaCodec交互机制、H265码流交换
- MediaCodec:dsp芯片、编解码器的生命周期、解码器中输入队列与解析队列设计思想、MediaCodec中平缓解码解析、MediaExtractor 多路复用、MediaMuxer合成器、MediaFormat格式
- 音视频剪辑:视频剪辑、音频剪辑、音频合成、音谱显示、视频倒放
- 音视频直播:硬编码、软编码、native实现rtmp推流、摄像头预览帧编码NV21转YUV、视频画面封装拼接Packet包、音频流数据拼接Packet包、RtmpDump实时同步发送音视频数据、MediaProjection、Medicodec编码H264码流、rtmp推流
- OpenGL与音视频解码:OpenGL绘制流程、矩阵、Opencv详解、人脸识别效果实现
- OpenGL特效:CPU与GPU运行机制详解、世界坐标,布局坐标,与FBO坐标系、图像镜像与旋转处理、人脸定位与关键点定位、大眼效果、贴纸效果、美颜效果
- FFmpeg万能播放器:FFmpeg结构体、声音播放原理、Surface的渲染、像素绘制原理与对齐机制、音视频同步原理、视频播放器整体架构
- Webrtc音视频通话:WebRtc服务端环境搭建与Webrtc编译、1v1视频通话实现方案、群聊视频通话实现思路、多对多视频会议实现、1V1音视频通话实现
08.开源框架原理:
- Okhttp
- Retrofit
- RxJava
- Glide
- Hilt
- Dagger2
- EventBus
- 组件化、插件化、热修复等
09.Gradle:
- Groovy语法
- Gradle Android插件配置
- Gradle实践等
10.kotlin:
- Kotlin语法
- 扩展使用
- 进阶使用
- 实践等
11.Flutter:
- Dart语法
- UI
- 进阶使用
- 优化
- 实践等
12.鸿蒙:
- Ability组件
- 分布式任务
- 事件总线
- 鸿蒙线程
- UI自定义控件等
如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!
Android路漫漫,共勉!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。