前言
关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。
本文不会具体去讲什么是MVC、MVP、MVVM
,但我描述的点应该都是这些模式的基石
,从本质上讲明白为什么
这样做,这样做的好处
是什么,有了这些底层思想
的支持再去看对应的架构模式
,相信会让你有一种焕然一新的感觉。
知识储备:需掌握Java面向对象、六大设计原则
,如果不理解也无妨,我尽量将用到的设计原则加以详细描述
目录
1. 模块化的意义何在?
- 1.1 基本概念以及底层思想
- 1.2 我们要基于哪些特性去做模块化划分?
- 1.3 Android如何做分层处理?
- 1.4 Data Mapper或许是解药
- 1.5 无处安放的业务逻辑
2. 合理分层是给 数据驱动UI 做铺垫
- 2.1 什么是 控制反转?
- 2.2 什么是数据驱动UI?
- 2.3 为什么说数据驱动UI底层思想是控制反转?
- 2.4 为什么引入Diff?
3. 为什么我建议使用 函数式编程
- 3.1 什么是 函数式编程?
- 3.2 Android视图开发可以借鉴函数式编程思想
一、模块化的意义何在?
1.基本概念以及底层思想
所有的模块化都是为了满足单一设计原则 (字面意思理解即可),一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性
在软件工程的背景下,改动就会有出错的可能,不要说"我注意一点就不会出错"
这种话,因为人不是机器。我们能做的就是尽可能让模块更加单一,职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。
所以模块化核心思想即:单一设计原则
2.我们要基于哪些特性去做模块化划分?
做模块化处理的时候尽量基于两种特性进行功能特性
、业务特性
功能特性
网络、图片加载等等都可称之为功能特性。比如网络:我们可以将网络框架的集成、封装等等写到同一个
模块(module、package等)
当中,这样可以增强可读性(同一目录一目了然)
、降低误操作
概率,方便于维护也更加安全。同时也可将模块托管至远程如maven库,可供多个项目使用,进一步提升复用性
业务特性
业务特性字面意思理解即可,就是我们常常编写的业务,需要以业务的特性进行模块划分
为什么说业务特性
优先级要高于功能特性
?
举个例子如下图:
相信很多人见过或者正在使用这种分包方式,在业务层把所有的Adapter
、Presenter
、Activity
等等都放在对应的包中,这种方式合理吗?先说答案不合理
,首先这已经是在业务层,我们做的所有事情其实都在为业务层服务,所以业务的优先级应该是最高的,我们应当优先根据业务特性将对应的类放入到同一个包中。
功能模块
核心是功能,应当以功能
进行模块划分。业务模块
核心是业务,应当优先以业务
进行模块划分,其次再以功能
进行模块划分。
3.Android如何做分层处理?
前端开发其实就是做数据搬运,再展示到视图中。数据
与视图
是两个不同的概念,为了提高复用性以及可维护性,我们应当根据单一设计原则
我们应当将二者进行分层处理,所以无论是MVC
、MVP
还是MVVM
最核心的点都是将数据
与视图
进行分层。
绊脚石:
代码语言:javascript复制通常来讲,我们通过网络请求拿到数据结构都是后端定义的,这也就意味着视图层不得不直接使用后端定义的字段,一旦后端进行业务调整会迫使我们前端从
数据层-->视图层
都会进行对应的改动,如下伪代码所示:
//原始逻辑
数据层
Model{
title
}
UI层
View{
textView = model.title
}
//后端调整后
数据层
Model{
title
prefix
}
UI层
View{
textView = model.prefix model.title
}
起初我们的textView
显示的是model
中的title
,但后端调整后我们需要在model
中加一个prefix
字段,同时textView
显示内容也要做一次字符串拼接。视图层因为数据层的改动而被动做了修改。既然做了分层我们想要的肯定是视图、数据互不干扰,如何解决?往下看...
4.Data Mapper或许是解药
Data Mapper
是后端常用的一个概念,一般情况下他们是不会直接使用数据库里面的字段,而是加一个Data Mapper(数据映射)
将数据库表转按需换成Java Bean
,这样做的好处也很明显,表结构甭管怎么折腾都不会影响到业务层代码。
对于前端我觉得可以适当引入Data Mapper
,将后端数据转换成本地模型
,本地模型只与设计图对应,将后端业务
与视图
完全隔离。这也就解决了 1.3 面临的问题,具体方式如下:
数据层
Model{
title
prefix
}
本地模型(与设计图一一对应)
LocalModel{
//将后端模型转换为本地模型
title = model.prefix model.title
}
UI层
View{
textView = localModel.title
}
LocalModel
相当于一个中间层,通过适配器模式
将数据层与视图层做隔离。
前端引入Data Mapper
后可以脱离后端进行开发,只要需求明确就可以做视图
层的开发,完全不需要担心后端返回什么结构
、字段
。并且这种做法是一劳永逸的,比如后端需要对某些字段做调整,我们可以不暇思索直奔数据层
,涉及到的调整100%不会影响到视图层
注意点:
当下有一部分公司为了将前后端分离更彻底,由前端开发人员提供
Java Bean(相当于LocalModel)
的结构,好处也很明显,更多的业务内聚到后端,很大程度提升了业务的灵活性,毕竟App发一次版成本还是比较大的。面对这种情况我们其实没必要再编写Data Mapper
。所以任何架构设计都要结合实际情况,适合自己的才是最好的。
5.无处安放的业务逻辑
关于业务逻辑
其实是一个很笼统的概念,甚至可以将任意一行代码称之为业务逻辑
,如此宽泛的概念我们该如何去理解?我先大致将它分为两个方面:
- 界面交互逻辑:视图层的交互逻辑,比如手势控制、吸顶悬浮等等都是根据业务需要实现的,所以严格来说这部分也属于业务逻辑。但这部分
业务逻辑
一般在视图层实现。 - 数据逻辑:这部分是大家常说的业务逻辑,属于强业务逻辑,比如根据不同用户类型获取不同数据、展示不同界面,加上Data Mapper一系列操作其实就是给后端兜底,帮他们补全剩余逻辑而已。为了方便大家理解下文我将
数据逻辑
统称为业务逻辑
。
前面我们说到,Android开发应该具备数据层
跟视图层
,那业务逻辑放在哪一层比较合适呢?比如MVVM
模式下大家都说将业务逻辑
放到ViewModel
处理,这么说也没有太大的问题,但如果一个界面足够复杂那对应的ViewModel
代码可能会有成百上千行,看起来会很臃肿可读性也非常差。最重要的一点这些业务很难编写单元测试用例
。
关于业务逻辑我建议单独写一个use case
处理。
use case
通常放在ViewModeler
与数据层
之间,业务逻辑以及Data Mapper
都应该放在use case
中,每一个行为对应一个use case
。这样就解决了ViewModeler
臃肿的问题,同时更方便编写测试用例。
注意点:
好的设计都是特定场景解决特定问题,过度设计不仅解决不了任何问题反而会增加开发成本。以我目前经验来看Android开发至少一半的场景都很简单:
请求-->拿数据-->渲染视图
最多再加个Data Mapper
,流程很单一并且后期改动的可能也不太大,这种情况就没必要写一个use case,Data Mapper
扔到数据层即可。
二、合理分层是给 数据驱动UI 做铺垫
先说结论:数据驱动UI的本质是控制反转
1.什么是 控制反转?
控制
即对程序流程的控制,一般由我们开发者承担,此过程为控制
。但开发者是人所以不可避免出现错误,此时可以将角色做一个反转
由成熟的框架负责整个流程,程序员只需要在框架预留的扩展点上,添加跟自己的业务代码,就可以利用框架来驱动整个程序流程的执行,此过程为反转
。
控制反转
概念和设计原则中的依赖倒置
很相似,只是少了一个依赖抽象
。
打个比方:
现有一个
HTTP请求
的需求,如果想自己维护HTTT链接
、自己管理TCP Socket
、自己处理HTTP缓存
.....就是整个HTTP协议
全部自己封装,先不说这个工程能不能靠个人实现,就算实现也是漏洞百出,此时可以换个思路:通过OkHttp
去实现,OkHttp
是一个成熟的框架用它基本上不会出错。个人封装HTTP协议
到使用OkHttp框架
,这个过程在控制
HTTP的角色上发生了一个反转
,个人--->成熟的框架OkHttp
即控制反转,好处也很明显,框架出错的概率远低于个人。
2.什么是数据驱动UI?
通俗一点说就是当数据改变时对应的UI也要跟着变,反过来说当需要改变UI只需要改变对应的数据即可。现在比较流行的UI框架如Flutter
、Compose
、Vue
其本质都是基于函数式编程实现数据驱动UI,它们共同的目的都是为了解决数据,UI一致性问题。
在当前的Android中可以使用DataBinding
实现同样的效果,以Jetpack MVVM
为例:ViewModel
从Repository
拿到数据暂存到ViewModel
对应的ObservableFiled
即可实现数据驱动UI,但前提是从Repository
拿到的数据可以直接用,如果在Activity
或者Adapter
做数据二次处理再notify UI
,已经违背数据驱动UI核心思想。所以想实现数据驱动UI必须要有合理的分层(UI层拿到的数据无需处理,可以直接用)
,Data Mapper
恰好解决这一问题,同时也可规避大量编写BindAdapter
的现状。
DataBinding
并非函数式编程,它只是通过AbstractProcessor
生成中间代码,将数据映射到XML中
3.为什么说数据驱动UI底层思想是控制反转?
当前Android生态能实现数据绑定UI的框架只有两个:DataBinding、Compose(暂不讨论)
在引入DataBinding之前渲染一条数据通常需要两步,如下:
代码语言:javascript复制var title = "iOS"
fun setTitle(){
//第一步更改数据源
title = "Android"
//第二个更改UI
textView = title
}
共需要两步更改数据源、更改UI,数据源
跟UI
有一个忘记修改便会出现BUG,千万不要说:“两个我都不会忘记修改
”,当面临复杂的逻辑以及十几个甚至几十个的数据源很难保证不出错。这种问题可以通过DataBinding
解决,只需更改对应的ObservableFiled
UI便会同步修改,控制
UI状态也从个人反转
到的DataBinding
,个人疏忽的事情DataBinding
可不会。
所以说数据驱动UI底层思想是控制反转
4.为什么引入Diff?
引入diff
之前:
RecyclerView
想要实现动态删除、添加、更新需要分别手动更新数据和UI,这样在中间插了一道
并且分别更新数据和UI已经违背了前面所说的数据驱动UI
,而我们想要的是不管删除、添加或者更新只有一个入口,只要改变数据源就会驱动UI做更新,想要满足这一原则只能改变数据源后对RecyclerView
做全部刷新,但这样会造成性能问题,复杂的界面会感到明显的卡顿。
引入diff
之后:
Diff
算法通过对oldItem
和newItem
做差异化比对,会自动更新改变的item
,同时支持删除、添加的动画效果,这一特性解决了RecyclerView
需要实现数据驱动UI
的性能问题
三、为什么我建议使用 函数式编程
1.什么是 函数式编程?
- 一个入口,一个出口。
- 不在函数链内部执行与运算本身无关的操作
- 不在函数链内部使用外部变量(实际上这一条很难遵守,可以适当突破)
说的通俗点就是给定一个初始值,经过函数链的运行会得到一个目标值,运算的过程中外部没有插手的权限,同时不做与本身无关的操作,从根本上解决了不可预期错误的产生。
举个例子:
//Kotlin代码
listOf(10, 20).map {
it 1
}.forEach {
Log.i("list", "$it")
}
上面这种链式编程就是标准的函数式编程,输入到输出之间开发者根本没有插手的机会(即Log.i(..)
之前开发者没有权限处理list),所以整个流程是100%
安全的,RxJava
、Flow
、链式高阶函数
都是标准的函数式编程,它们从规范
层面解决数据安全问题。所以我建议在Kotlin
中 碰到数据处理尽量使用链式高阶函数(RxJava、Kotlin Flow亦然)
。
2.Android视图开发可以借鉴函数式编程思想
Android视图开发大都遵循如下流程:请求-->处理数据-->渲染UI,这一流程可以借鉴函数式编程,将请求作为入口,渲染做为出口,在这个流程中尽量不做与当前行为无关的事(这也要求ViewModel
,Repository
中的函数要符合单一原则)。这样说有点笼统,下面举个反例:
View{
//刷新
fun refresh(){
ViewModel.load(true)
}
//加载更多
fun loadMore(){
ViewModel.load(false)
}
}
ViewModel{
//加载数据
load(isRefresh){
if (isRefresh){
//刷新
}else{
//加载更多
}
}
}
View
层有刷新、加载更多两种行为,load(isRefresh)
一个入口,两个出口。面临的问题很明显,修改刷新
或加载更多
都会对对方产生影响,违反开闭原则
中的闭(对修改关闭:行为没变不准修改源代码)
,导致存在不可预期的问题产生。可以借鉴函数式编程
思想对其进行改进,将ViewModel
的load
函数拆分成refresh
和loadMore
,这样刷新
和加载更多
两种行为、两个入口、两个出口互不干涉,通过函数的衔接形成两条独立的业务链条。
函数式编程可以约束我们写出规范的代码,面对不能使用函数式编程的场景,我们可以尝试自我约束往函数式编程方向靠拢,大致也能实现相同的效果。
综上所述
- 合理的分层可以提升复用性、降低模块间耦合性
- Data Mapper 可以让视图层脱离于后端进行开发
- 复杂的业务逻辑应该写到use case中
- 数据驱动UI的本质是控制反转
- 通过函数式编程可以写出更加安全的代码
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
目前我们要做的就是选好自己细分领域深入研究,对基础原理性内容深入理解,尽快向高级水平靠近并达到。当然也要了解新技术,拥有开放的心态去学习一些新技术,不要一开始出来一个新技术就转入学习,这样会导致啥都会一点啥都不会,当然新技术是有一个发展过程的,不会立马流行和大量应用等它稍微成熟点。
接下来分享的系统学习资源以详解各大互联网公司的 Android 常见面试题为主线,从面试的角度带你介绍必备知识点,以及该知识点在项目中的实际应用。
总共分为6个部分:
- Java 基础(★★)
- Java 高级(★★)
- Android 基础(★★★)
- Android 高级(★★★)
- Android 项目(★★★)
- 项目面试常见问题(★★★)
一、Java 基础(★★)
面向对象思想 多态 异常处理 数据类型 Java 的 IO 集合 Java 多线程
Java 高级(★★)
Java 中的反射 Java 中的动态代理 Java 中的设计模式&回收机制 Java 的类加载器
Android 基础(★★★)
Android 基本常识 Activity Service BroadCastReceiver ContentProvider&数据库
Android 中的布局 ListView JNI & NDK Android 中的网络访问 Intent Fragment
Android 高级(★★★)
Android 性能优化 Android 屏幕适配 AIDL 自定义控件 Android 中的事件处理 Android 签名 Android 中的动画 网络协议 其他
项目面试常见问题(★★★)
- 开发周期
- 项目中遇到的难题
- 项目中最大的收获
- 项目是如何上线的
- 项目是如何盈利的
- 绘制项目架构图
- 项目开发流程
- 你在项目中的角色
- 你负责项目中的哪些模块
- 讲讲你负责模块的具体实现
- 项目中都用到了哪些第三发框架
- 有没有自己写过框架
- 业余时间你是如何提高自己(学习)的
- 有没有自己的技术 blog
- 你的职业规划
- 为什么离职
- 为什么选择我们公司
- 说说你们项目的亮点和不足
- 你们的项目是如何保持风格一致的
- 项目架构是如何搭建的
- 屏幕适配是如何解决的
- 都看过哪些源码
- 项目版本是如何升级的
- 用的什么版本控制工具
- 你能独立开发吗
- App 跟服务器是如何交互的
- 需求文档写过吗
- 接口文档写过吗
- 云服务器都用过哪些
- 第三方平台都用过哪些
简历 社招解答 经典HR面试解析
以上是整理总结的Android中高级面试遇到的真题解析,希望对大家有帮助;同时很多人经常也会遇到很多关于简历制作,职业困惑、HR经典面试问题回答等有关面试的问题。同样我也搜集整理了全套简历制作、金三银四社招困惑、HR面试等问题解析,有疑问,可以提供专业的解答。
对于Android开发的朋友来说应该是最全面最完整的面试资料,为了更好地整理每个模块,我参考了很多网上的优质博文和项目,力求不漏掉每一个知识点。很多朋友靠着这些内容进行复习,拿到了BATJ等大厂的offer,这个资料也已经帮助了很多的安卓开发者,希望也能帮助到你。