项目初始的时候,一般都是使用一个分层架构,接入各种框架,然后就写业务代码。但如果项目慢慢变大,那就会出现很多项目管理的问题,诸如:
1.代码复用与抽象问题
2.编译速度问题
3.版本迭代速度问题
所以组件化、模块化、动态化、插件化、跨平台等各种技术就会被提及被重视。
组件化
我们已经度过了洪荒时期的 Android
开发,因为官方已经给出了非常优秀的 Jetpack
组件库,所以我们目前更多的是制作业务型的组件与抽象,去解决某一类场景或者大家都可能用到而写起来又繁琐又容易出错的场景。
写组件也不是一件容易的事情。
首先,开发组件得意识到某些场景是可以抽象的,如果抱着代码能跑起来就行了的态度去编程,那基本不可能想到去抽象的,也是最容易陷入代码陷阱无法自拔的。即使是最简单的抽取,很多时候也是宁愿不厌其烦的复制粘贴,也不愿意去抽取一个公共方法。然后等别人开发出了组件后,除了感叹真香,也就没什么其它感悟了。
其次,开发组件得设计出好用而简洁的 API
,很多童鞋,除了数据实体类和各种 Manager
类,基本不会再定义新的数据结构了,就更别提设计接口供其它人使用了。
然后,开发组件得想好组件的配置,提升组件的灵活性,产品最喜欢复用以前的逻辑而再加上一点点的不同,如果不能够配置,那组件的局限性就大打折扣了。
最后,开发组件还得做好边缘情况的处理、异常的处理。组件会被各种人以各种姿势调用,所以各种想不到的场景都有可能出现,这也考验开发写代码对细节的把控。
所以我一直重视的是写组件的能力,是需要不断的训练的,也是需要不断的阅读优秀的组件库去掌握的。面试必问 Okhttp
的拦截器,可以你有真正在自己的组件里使用过类似的东西吗?
最举个例子? 假设现在我们要用 AudioRecord
写一个录制功能。最开始那可能直接在 Activity
里开撸了:
private var isRecording = false; private suspend fun startRecording() { audioRecord = AudioRecord(...) val data = ByteArray(bufferSize) audioRecord.startRecording() isRecording = true File(applicationContext.filesDir, "recording.pcm").outputStream().use {outputStream -> while (isRecording) { val bytesRead = audioRecord.read(data, 0, bufferSize) if (bytesRead > 0) { outputStream.write(data, 0, bytesRead) } } } audioRecord.stop() audioRecord.release() }
这样写下来,流程一通,然后就觉得大功告成了。
然后产品逻辑就变更了:要把音频数据实时上传到服务器。
改:把写文件的部分换成网络传输
过一会儿,老板又找来了,在没网络的时候录制就失败了,这不行,要在无网络的时候录制到本地。
改:判断下网络环境,加 if
else
。
过一会儿,老板又找来了,我开始有网络,但是进入电梯后录制就失败了,所以一定要保存一份到文件中,如果有网络,那也往网络写一份。
改:从 if
else
逻辑换成双写逻辑。
...
只针对上面的场景,整体流程是相当稳定的,只是写入的目标地不一样,那为何不抽象一个 AudioSink
的接口来表示写入的存储目标?
interface AudioSink { suspend fun write(buf: ByteArray, offset: Int, len: Int) fun close() }
那整个流程就稳定为:
private suspend fun startRecording(sink: AudioSink) { audioRecord = AudioRecord(...) val data = ByteArray(bufferSize) audioRecord.startRecording() isRecording = true while (isRecording) { val bytesRead = audioRecord.read(data, 0, bufferSize) if (bytesRead > 0) { sink.write(data, 0, bytesRead) } } audioRecord.stop() audioRecord.release() sink.close() }
如果要写文件,写个实现:
class NetworkAudioSink(api): AudioSink { ... }
如果要写网络,写个实现:
class NetworkAudioSink(api): AudioSink { ... }
如果要判断本地与网络,就写个代理:
class DelegateAudioSink( val origin: AudioSink ): AudioSink { ... }
如果要同时写本地与网络,就是双写或多写:
class MultiAudioSink( val list: List<AudioSink> ): AudioSink { ... }
这些类一写,那具体逻辑就是入口传参的变更了。当然实际情况应该考虑各种异常情况。如果你足够有经验,那在一开始就能把这些场景考虑到,并且把其用处和局限性同步给产品,帮助产品一开始做出最优选择。这才是所谓的经验。
如果回头来看上面的例子,发现都是很熟悉的东西,说是设计模式也行。如果将整个抽象做好,如果从音频切换到视频,那是否这一块可以完美复用?那是不是就可以更早的完成需求?——然后就可以更早的被裁掉~
组件化,就是对业务场景、功能场景的抽象。这是最需要大家去投入时间去做、去训练的。
模块化
一些复杂的组件,本身就可以构成一个个的模块,然后单独出来维护。但在谈论模块的时候,更多的是谈论业务模块。就是把不同业务放在不同的模块下。这对于巨型 App
, 其意义当然是有的,毕竟可能是一个团队维护某一个模块。而对于小 App
, 那意义就不是特别大了。
因为模块化会带来非常棘手的问题:模块之间的通信问题。Arouter
、TheRouter
都很大程度上是为了解决模块间的通信问题。而简单的模块化一般是考虑基础模块的抽取,但基础模块的定义总是模糊的,不经意间就抽取出一个巨无霸的基础模块。就跟很多喜欢用 Base
类的同学,最后就一个巨大的 Base
类。
所以一般小的 App
, 搞模块化是大可不必的。巨型 App
是存在编译速度、多团队开发的问题,所以各大公司都会出品和分享自己的多模块化实践。小公司就容易跟风把工程搞得巨复杂,这就没意思了。
例如大公司拆分多模块,然后用 aar
包来加速构建速度,相对应的有私有 Maven
仓库,自动打包脚本,搞得很是复杂,对他们而言是很有必要。而对于小公司,其投入产出比,还不如花点钱,买台好一点的电脑来得香。
动态化、插件化
对于动态化和插件化,我只能感叹下,写出这些框架的大佬们,技术是真的强,都是真正的黑客。大概也只有国内的巨型 App
和奇怪的市场审核逻辑才能造就出这些需求。
有时间的话,看看这些库的源码也是很不错的,很涨见识。重要的还是看各大框架遇到的问题场景,以及是如何探索解决方案,最终是如何解决的。
不过问题始终是维护问题,随着 Android
版本的迭代,总归是需要有新的改动或者需要寻求新的方案,但国内的框架和库的成功往往和推动这个项目的人有关,没有一个稳定维护的机制,很容易受到人员变动的影响。
最后
再复杂的东西,也是由一个个的零件慢慢组装起来的。我们要有蓝图,然后为实现这个蓝图而拧螺丝。拧得多了,也许就有机会拧更大的螺丝。我在 emo
组件库上拧了 20 个螺丝,拧得多了,也算是一个不小的库了。不过目前我源码已经转 private
了,后面拧的螺丝,只想留给小团体孤芳自赏了。用行动证明一下,国内的开发者的开源库是多么的不稳定。