Kotlin- 风险高、RxJava- 不老,Android- 原生开发现状分析 (1)
不过,Google 最近宣布,他们将围绕 WorkManager 来统一后台任务调度[3]。这听起来非常棒,我再也不用学习那么多后台调度的知识了,只是,不知道为什么,我好像以前在哪儿听到过这句话……
我们不管将来是否会统一使用 WorkManager,WorkManager 都还存在一些问题,比如可靠性。我不想在本文中解释为什么,但是请你一定要记住,如果你想在应用程序中使用 WorkManager,实现后台作业,一定要去读一下 dontkillmyapp.com 上的所有内容,并且要关注一下与 WorkManager 相关的 Google 问题列表[4]。
Android 的后台作业由于碎片化等原因,导致它们一团糟,而且还很不可靠。
在过去,我一直主张尽可能对数据和其它类型的后台处理进行同步。我或许是 SyncAdapter 最后的粉丝。今天,考虑到可靠性的问题,我建议,尽可能避免后台作业。如果你的老板一直坚持要使用这个功能,请将上面的链接发给他们,告诉他们,后台作业可能会需要数百小时的工作,才能实现,并且它带来的问题也会比它带来的好处多很多。
很多情况下,后台作业的需求是不可避免的。但是在大多数情况下,你都可以不使用它。虽然有时候会给用户带来一些不便,但是这可能是到目前为止的最佳方案。
数据库
在有关 SQLite ORMs 的内容里面,没有什么令人吃惊的内容—— Room 主宰着一切。Room 从 2.2.0 开始,添加增量注解解析支持。不过,请你一定要记住,你应用程序的架构不应该关心你使用的是什么样的 ORM 框架。说到 Room,“架构组件”也只是一个营销术语,并不是一个技术角色。
在 Android ORM 框架中,与 Room 竞争的主要是 SQLDelight。这是一个比 Room 老很多的库。但是据我所知,**在过去的一年里,它几乎被全部重写。****但新版本的 SQLDelight 只针对 Kotlin。**另一方面,SQLDelight 也支持 Kotlin Multiplatform,所以,随着 Kotlin 使用的增多,我预计, SQLDelight 的采用率也会随之增加。
顺便说一下,在 AndroidX 的命名空间下,也有 SQLite 的镜像。我不知道这个会有什么用处,但是如果你在应用程序中直接使用 SQLite,也可以对这个进行深入的研究。
此外,我们也不要忘记非关系型数据库,比如 Realm、Parse、Firebase、ObjectBox 等等 (其中一些,核心还是使用的 SQLite 来实现的),如果我没有记错的话,这些非关系型数据库中,大多数(甚至全部)都具有自动数据同步的功能。在之前有段时间里,它们非常地流行。但是现在,据我所知,已经不再流行了。也就是说,在短期内,我会持续关注非关系型数据库。
去年,我写了一个非常复杂的应用程序,对接了一个 Parse 的服务。这个 App 具有完全的离线支持,服务端本地化,用户指定的系统和语言设定,复杂的多媒体系统等功能。我在 Android 端上使用了 Parse 的 SDK。除了一些小的 WTF 以外,其他体验都非常棒。如果你们公司有很多后台开发人员,或者说你需要实现大量的服务端逻辑,这也许并不是最佳解决方案。但是对于仅仅只需要执行简单的 CRUD 操作的初创公司,这或许是一个不错的选择。
一个警告:如果你打算采用数据库即服务解决方案(如 Firebase),请你一定要关注长期使用的成本和影响。
外部存储
Android 外部存储发生了一个“很有意思”的变动。
如果,你在开发?App 的时候,把 Target API 调整为 29 及以上,之前获取 SD 卡文件的方法,现在都不能使用了,并且不会提示任何异常。现在,你需要使用 Android 存储访问框架来进行更细粒度的文件访问。不幸的是,Android 存储访问框架的工作原理与之前的读取方式完全不同,你可能需要对代码进行大量的重构,来实现新的文件访问和读取。
Google 原本希望在 Android 10 上,对所有的应用程序进行文件访问的限制,推广使用 Android 存储访问框架的方式进行文件访问。但是这个改动引起了社区内的强烈抗议,所以 Google 决定推迟推出这个功能。因此,现在即使你的应用程序在 Target API 29 及以上,也可以设置为在“Legacy”模式下可读取文件。不过,有可能在 Android 的下一个版本,不管你设置的 Target API 为多少,都会限制应用程序使用新的作用域访问模式。
到目前为止,我也还没有更新我应用程序的文件访问方式。但是从互联网上的讨论来看,实现新的文件访问方式是一项很具挑战的任务,虽然你的应用程序在“Legacy”模式下,没有任何异常,但是我建议你最好从现在开始,着手对代码进行重构和测试,以防发生不可控的事情。
Shared Preferences
在几周前,AndroidX 家族增加了一个新的库,这条提交信息写道:
新的库用来替换 SharedPreferences,新库的名称还没有确定下来,这次提交只是为了评审和设计文档(请自行申请设计文档)。
现在还没有什么值得担心的。不过,从长远来看,SharedPreferences 会被废弃掉,取而代之,使用新的库来实现类似的功能。
与 SharedPreferences 不同的是,这个新的库默认情况下使用的是异步的方式。换句话说,如果你需要取某个值,你需要实现回调,通过这个回调才能拿到值。
如果你对这种异步回调的原理感兴趣,你可以去看看 StackOverflow 上的这个回答。Reddit 的一个用户 Tolriq 分享了他们的 App 遇到的一个 SharedPreferences 的 Bug,这个问题影响了万分之一的月活用户。对于一般的应用程序来说,这个问题并没有什么明显的影响。但在一些需要高可靠性的应用上,就显得很不可靠了。举个例子,如果在 Android 汽车上,应用程序的无响应和崩溃会引起驾驶员的注意力被分散,这将有可能导致出现交通意外。
依赖注入
在依赖注入领域,最大的新闻莫过于 Dagger-Android 被弃用,有两点需要强调一下,首先我所说的弃用,不是正式的弃用,因为官方并没有发布声明。其次,Dagger-Android 并不是指整个 Dagger 2 框架,只是指其中相对比较新的部分。详细细节可以看我的另一篇文章[8]。
在 Android 领域也存在其他的依赖注入框架,但是我不认为他们会比 Dagger 更好。值得一提的是,Koin 是一个不错的依赖注入框架,但是我依然觉得它也不会引起多大的潮流。它之所以会被采用,无非是这两个原因,一个是,它拥有比 Dagger 好很多的文档,降低了很大的学习成本。第二个是它基于 Kotlin 进行编写,因为 Kotlin 的热度,给它也带来了不少的关注。到目前为止,Kotlin 的热潮几乎已经全部过去了,所以,我预测,Koin 的关注也将会逐渐减少。
不管这些框架如何发展,手动依赖注入的发展都会很缓慢。
Google 声称:随着应用程序的增大,手动依赖注入的成本会出现指数级增长。但是我并不这么认为,我觉得 Google 既不了解 "指数" 的含义,也没有实际去“衡量”过任何东西。这个申明的内容是错误的,我希望 Google 不再使用这种方式误导社区。
事实上,这种手动依赖注入在后端比较常见(尤其是微服务中,你并不想在每个服务中都添加对注入框架的依赖),也可以正常的工作。在后端,反射被经常用到,所以后端的依赖注入框架并不需要解析编译时的代码。
在 Android 上,解决方案与后端有一些不同,我们几乎不会用反射方案的依赖注入框架,所以就只剩下 Dagger 可以用了。其实,反射虽然会影响性能,但是在大多数项目,都是可以用的。我的意思并不是建议你们使用反射方案的依赖注入框架,这个选择并非是非黑即白的,你需要按照你的要求来进行选择。
无论如何,Android 领域上, Dagger 作为依赖注入框架的现行标准,我们所有人都在使用它。尽管 Google 在宣传上,对 Dagger 的使用成本使用漂亮的绿色图形进行展示,但是 Dagger 使用成本在实际上依然会随着时间增长而快速增长。越来越多的代码,在编译构建的时候需要花费更多的时间;你的开发人员越多,代码编译的次数就越多。当然,所有的开发人员都需要学习如何使用 Dagger , 这本身就是一项很大的成本。
换句话说,**虽然 Dagger 可以减少项目中编写的代码,但是需要花更多的时间去培训新人,在编译上花费更多的时候。**所以,对大型项目来说,使用 Dagger 会更耗时。
在一个大型项目中,编译耗时会逐渐成为生产力的瓶颈。当然, Dagger 也提供了很多优秀新的功能来帮助你优化编译时间,但前提是,你需要知道如何使用这些工具。读到这里,我相信你对手动依赖注入会很感兴趣。
数据绑定
作为一个 Android 开发者,都知道在写布局的时候,会经常调用 findViewById() 这个方法。DataBinding 诞生就是为了取代掉这个模板方法。老实说,在使用 findViewById 的时候,我并没有遇到过任何问题,虽然希望摆脱掉它,但我并不认为使用 DataBinding 就是一个更合理的方式。有一个好消息,很快我们就可以使用 ViewBinding 来摆脱 findViewById 了,也不需要使用 DataBinding。
说句实话,我不相信 DataBinding。对于它想解决的问题来说,这种方案在过于复杂。在使用 DataBinding 的时候,需要把代码逻辑放到 XML 布局中,这听起来很不错,但是经验丰富的开发人员都不会这么做,这个做法也是 Databinding 的另一个缺点。
早在 2016 年 11 月的时候,那个时候 Google 还在大肆宣传 DataBinding。我在 StackOverflow 的回答中作了如下预测:
我可以非常自信地预测:DataBinding 不会成为行业标准。DataBinding 可以带来短期收益,但是从长远来看,它将会使代码变得不可维护。一旦 Databinding 被长期使用,它的缺点就会暴露出来,将来它一定会被废弃掉。
我没有统计过使用 DataBinding 的项目,但是很明显,它没有成为行业标准。我从来没有在自己的项目中使用过它,也很少看到其他开发者使用。据我猜测,当 ViewBinding 逐渐成熟,并且被广泛采用,DataBinding 将会作为一个“传统”框架,大量地被引用到。
状态保存
自从引入 ViewModel 架构组件以来,在 Android 应用程序中,当配置发生更改,保存与恢复状态的逻辑,就变成了一个烂摊子,没有人去管理。虽然这样子说有点过分,但是我觉得,这已经是我最温和的表达方式了。
Gabor Varadi(又叫 Zhuinden)在 Reddit 论坛中描述了 ViewModel 引入带来的问题,我不需要再去写一遍了。再次强调,不推荐使用 onRetainCustomNonConfigurationInstance(),推荐使用 ViewModel。
在帖子的末尾,Gabor 作了一些预测:
你知道吗?Fragment 状态保存的方法已经被弃用了。
在我看来,废弃 Fragment 状态保存的方法是非常好的主意,众所周知, Fragment 的 onAttach() 和 onDetach() 方法就是为了支持状态保存的,现在废弃了状态保存的方法,那这两个方法也可以被废弃掉,并且这样子可以简化 Fragment 的生命周期。我长期以来都建议不保存 Fragments 的状态,忽略掉 onAttach 和 onDetach 方法,和我之前写的处理 Framgent 生命周期方法一致[12]。
尽管有很多理由表明,要废弃掉 Fragment 的状态保存,但是也不可能废弃掉 onRetainCustomNonConfigurationInstance(),这个可不是我说的,是 Jake Wharton 说的。在上面 Gabor 的帖子上,他的回复获得了最多的点赞。虽然我不太赞成 Jake 所说的话,但是我找不到更好的理由去说服自己。这个方法和 ViewModel 后台使用的原理完全一致,完全没有理由废弃掉它。
那我们应该怎么对待这些废弃的方法呢?Google 不管这些方法使用的技术方案和优势,都强制所有的 Andorid 应用迁移到 ViewModel。即使这些方案有可能优于 ViewModel 本身,他们也愿意放弃。听起来有点像是阴谋论吧。
我确实不喜欢保存非配置的状态,并且废弃掉对我没有任何影响,因为我从来都没有使用过它。事实上,大多数应用程序都不需要这些方法,当然,ViewModel 也不需要。我
们需要处理状态改变的方法仅仅只有 onSaveInstanceState(Bundle) 这个方法。这个方法非常简单明了,可以同时处理保存和恢复的逻辑。所以,只要能用这种方式保存状态就可以了,我相信,我不是唯一一个使用这个方法的人。虽然 Google 对 ViewModel 进行了大量的营销宣传,但是对于很多经验丰富的开发者来说, ViewModel 还是太复杂了,我们有更简单有效的方法来处理状态存储。
如果 Google 别有用心,想强制所有项目使用 ViewModel , 那么它还将废弃掉 onSaveInstanceState(Bundle)方法。这听起来有点不可思议,如果将来真的这样发展,那说明我的基础理论是正确的。
考虑到 Android 的内存管理机制,Google 不可能在没有稳定的解决方案之前就废弃掉 onSaveInstanceState(Bundle) 。“幸运的是”,我们已经可以使用 ViewModel 来完成相关工作了。
我想,在一两年内,就能看到我的理论是否正确。
总而言之,如在本节开头所说,Android 状态保存将变成一个烂摊子。两年多前,我曾经写过 ViewModel 架构组件有害的文章[13]时,我就预测 ViewModel 会对保存与恢复状态的一点点造成影响。我所预测的都变成了现实,而且现在的情况比我曾经的预测更糟。
并发
在 Android 并发编程中,一个重要的 API 就是 AsyncTask, 不过它现在已经被弃用掉了。我之前已经写过很详细的文章分析过它了[14],在这里,将不再赘述。
下面我要说的内容,有可能会伤害很多读者,但是,请不要“恨”我。
**RxJava,是一个 Andorid 中常见的多线程框架。但是它现在将逐渐退出历史的舞台。**从 StackOverflow 的趋势图可以看出:
很多开发者对这个说法提出了质疑,他们反驳说这个数据不具有代表性,并且我们可以找到其它的理由来解释图上所发生的事情。他们所说可能是正确的,我个人本身也不是数据科学家。但是从图中我们可以看到,RxJava 与 AsnycTask 有相同的斜率。
**如果你没有时间去学习 RxJava 如何使用,并且你的项目中也没有使用过 RxJava,我建议你不要在你的项目中使用 RxJava。**事实上,我也一直不推荐使用 RxJava ,现在已经有数据支持我的这个观点了。
如果你的项目中使用了 RxJava,你也不用慌张,不需要紧急去重构你的项目。如果你的项目只有你一个人,或者整个项目组成员基本不会变动,保持项目现状就好了。但是你需要记住,**以后要招具有 RxJava 开发经验的人会越来越困难,新招开发人员可能需要学习使用 RxJava。**广泛使用 RxJava 的项目,在以后也会被认为"不酷",就像今天还在使用 AsyncTask 和 Loader 的项目一样。
我知道,很多 RxJava 的开发者都是 RxJava 骨灰级的粉丝,他们花了数周的时间去学习 RxJava,付出巨大的努力才说服队友在项目中使用 RxJava。现在我却在这里说 RxJava 已经过时了。我只能说,这不是我的个人意见,我只是对现有的情况进行分析,并根据我所看到的内容做出预测。我也有可能是错的。两军交战,不斩来使,请大家不要“攻击”我。
在 Kotlin 中,使用协程来实现多并发。最近使用协程实现了一些简单的用例,我发现它复杂、不稳定,甚至还有一些 Bug。
所有的人都在说,协程可以降低并发的复杂度,使用并发变得简单。我从来都不相信这句话,因为我知道并发从根本上来说就是很复杂的。我动手写过一些测试用例过后,据我的经验,我可以很自信的告诉你,协程不能使并变得简单。我认为,协程会增加复杂性,我建议你们谨慎使用他们。
在 Kotlin 中,协程将作为处理多线程的默认方式,如果你已经开始使用 Kotlin 进行开发,那么你应该花点时间,去学习一下,协程的使用。
据我所知,还有一个 Flow 框架,它是基于协程,添加了流运算符。在几个月之前,已经稳定了。所以现在也没啥好评价的。
Kotlin
现在,让我们来讨论一下 Kotlin 在 Android 领域的现状。根据我个人的经验来看,这是一个很敏感的话题,不论我所说的话有多么公正客观,都会有一些粉丝评价我所说的话是“Shit”。但从专业的角度来说,Kotlin 的话题是跳不过去的,所以,我要强调的是,这些内容只是我的个人观点,仅供参考。
在 Android 开发中,使用 Kotlin,会大大地增加你的编译时间。
在我的另一篇文章中,我统计了使用 Kotlin 过后,编译时间的增长情况。结果是,全量编译的情况下,会增加 18%左右的耗时,如果是增量编译,则会增加 8% 左右的耗时。
Uber 和 JetBrains 联合发表了他们有关 Kotlin 对项目编译时间的影响, 在文章中,显示的结果非常的悲观。他们表示,如果你不开启 IDE 中的 annotation processors ,引入 Kotlin 的项目,编译构建的时间大约会增加四倍,如果开启了 annotation processors,编译构建的时间也会增加 50% ~ 100%。
Uber 的结果与 OkHttp 迁移到 Kotlin 后得到的结果是一致的,都是编译时长都增加了 4 倍。
别担心,虽然这个结果让人吃惊,这个也不是你的错,很多人都和你一样,正在使用 Kotlin。这个问题,虽然很重要,但是它并没有引起广泛的关注。我觉得,Google 也在试图解决这个问题。我曾问过 Google 相关的开发者,并进行了深度的交流,他们在这个问题给我的回答是:“这是一个很棘手的问题,我宁愿不做”。
Kotlin 除了会增加编译时间,直到上周,才支持增量注解处理,而 Java ,在 10 个月以前就支持了。
评论