[译]Android 原生开发的现状,截止到 2019 年 12 月,一招彻底弄懂
在讨论完 AndroidX 之后,就不得不提到 Jetpack 了。 据我所知,Jetpack 最初只是“architecture components”的工具集合,但是后来扩展为包含了 AndroidX 的大多数(甚至所有)API 的工具集合。 因此,到目前为止,我还没有看到 AndroidX 和 Jetpack 之间有任何有意义的区别,市场营销和公关宣传除外。
当您访问[Jetpack](
)的官方网站时,它看起来不像是技术文档,更像是早期 SaaS 初创公司的主页。
看看这些“感言”:
再看看下面这些 app:
如果 Jetpack 申请 2020 年独立 IPO,我不会感到惊讶,因为他们是如此的专注于营销和公关。
不过说真的,这种向自己的生态系统中的开发人员“销售”api 的做法存在一些深层次的问题。比如,为什么有人真的想在搜索中宣传 ViewModel?
总而言之,由于 Jetpack 的大部分内容都是来源于 AndroidX,所以我之前写的有关 AndroidX 的内容在很大程度上也适用于 Jetpack。
下面,我将分别讨论其中一些具体的 API。
Background Work
当应用程序不在前台时让应用也能执行操作是 Android 开发中最常见的场景之一。 在引入 doze 模式、SyncAdapter、GCMNetworkManager、FirebaseJobDispatcher、JobScheduler 以及最近的 WorkManager 之前,您可以通过启动服务(而不是绑定服务)来实现。 这些都是 Google 自己的 API,不过也有很多第三方解决方案,比如 Android-Job。
但是,[Google 最近宣布,他们将通过 WorkManager API 来统一后台任务的调度](
)。 听起来很不错,但是由于某种原因,当我听到这样的声音时,我总有种似曾相识的感觉……
无论最终能否统一,WorkManager 都无法解决在执行后台任务过程中存在的一个最严重的问题:可靠性。 我在这里不做解释,但是请记住,如果您需要在应用程序中执行后台任务,请先阅读[dontkillmyapp.com](
)上的所有信息。 此外,请在 Google 的[issue tracker](
)中阅读并加注星标。
Android 系统的后台任务执行与调度是一团糟,碎片化使得它非常细微且不可靠。
过去,我一直主张尽可能的将数据同步等类似的工作放在后台来执行,我可能是 SyncAdapter 的最后一批粉丝。 但是今天,鉴于可靠性问题,我主张相反的做法:尽可能避免在后台执行操作。 如果您的 PM 坚持使用此功能,请向他们展示以上链接,并向他们解释,后台任务需要花费数百小时的时间来实现,而且带来的麻烦多于收益。
有些时候执行后台任务是不可避免的,但是在大多数情况下,您可以不这样做。即使以给用户带来一些不便为代价,它也可能是最佳选择。
Databases
毫无疑问,Room 在众多 SQLite 的 ORM 框架中占据着主导地位。从 2.2.0 开始,Room 支持增量注解处理。不过请记住,您的应用架构不应过于关心使用了哪种 ORM 框架。因此,作为 architecture components 的一员,Room 只是市场术语,而不是技术角色。
Android 开发中 ORM 框架的主要竞争者是 SQLDelight。 这个库比 Room 还要老,但是据我了解,在过去的一年左右的时间里,它已经被重写了很多。 不幸的是,它现在只针对 Kotlin。 另一方面,SQLDelight 支持 Kotlin 跨平台。 因此,随着 Kotlin 使用率的增加,我预计 SQLDelight 的使用率也会随之增加。
顺便说一下,AndroidX 中有对原生 SQLite 的使用说明,我还不知道该怎么使用。但是如果您想在应用中使用原生 SQLite,那么你或许需要去认真的研究下这个主题。
此外还有许多针对 Android 的非关系型的数据库,例如 Realm,Parse,Firebase,ObjectBox 等(其中有些仍在使用 SQLite)。如果我没记错的话,它们中的大多数(甚至全部)都具有自动数据同步功能。 一段时间以来,这些解决方案比较流行,但据我所知,它们已经不复存在了。但是我并不会马上认为非关系型的数据库不再重要了。
去年,我编写了一个非常复杂的集成了 Parse Server 的 Android 应用。 我使用了 Android 版本的的 Parse SDK,体验都非常好。如果您的公司已经雇用了许多后端开发人员,或者您需要实现许多服务器端逻辑,这可能不是最佳解决方案,但是对于仅在后端执行 CRUD 操作的初创企业和个人来说,这可能会是一种好的选择。
但是我必须提醒的一点是:如果您要采用数据库即服务的解决方案(例如 Firebase),那么请务必了解其长期的成本和影响。
External Storage
关于外部存储的开发,这里有许多有“意思”的事情。
如果您应用的 target sdk 版本等于或者大于 29,[那么你的应用将无法再正常访问手机外部存储上的文件,除了少数几种明显的情况](
)。 相反,您需要使用 SAF 框架(据说),该框架允许用户进行更精细的访问管理。不幸的是,SAF 的工作方式与之前完全不同,因此某些应用程序可能需要进行重大重构。
Google 希望从 Android 10 开始对所有的应用程序都实行这一要求,但它引起了开发者社区的强烈抗议,于是他们决定推迟此功能。 因此,即使您的应用设置 target sdk 版本为 29,它仍可以在“旧版”模式下工作。 但是,无论目标 API 级别是多少,下一版的 Android 系统都将对所有应用的存储访问范围做更加严格的限制。
到目前为止,我还没有使用 SAF 框架,但是从我在互联网上阅读的许多讨论中看来,这可能是一项艰巨的任务。因此,如果您的应用程序还在以“旧版”模式使用外部存储,那么最好立即开始进行重构和测试。
Shared Preferences
几周前,[AndroidX 系列中添加了一个新框架](
)。 它的 commit message 是这么说的:
New library meant to replace SharedPreferences. The name is not final, this is just for implementation review and to support the design doc (feel free to request the design doc privately)[…]
目前我们无需担心,但从长远来看,似乎 SharedPreferences 会被重写,我们需要使用这种新的方法。
SharedPreferences 和这个新框架之间的主要区别在于,[默认情况下后者是异步的](
)。 换句话说,您需要实现一个回调以获取特定键的值,该回调将在以后的某个时间收到通知。
如果您对这种异步通知的机制感到好奇,则可以阅读 StackOverflow 上的这个[答案](
)。 Reddit 用户 Tolriq 在这里分享了他们遇到此 bug 的概率。 在他们的应用中,这个 bug 会影响 1 / 10,000 / SESSIONS_PER_USER_PER_MONTH 的用户。 对于一般的应用程序,这可能微不足道。但是在需要高可靠性的情况下,这可能会引起严重的后果。 例如,在装有 Android Auto 的汽车中,应用程序挂起和随后的崩溃会分散驾驶员的注意力,这可能会导致非常不幸的后果。
Dependency Injection
在依赖注入方面,最大的变化就是 Dagger-Android 的弃用。 这里我想解释两点: 首先,我说的弃用并不是指“正式”弃用,因为它尚未正式弃用。 其次,Dagger-Android 并不是整个 Dagger2 框架,而只是相对较新的功能。 我在这个主题上写了一篇非常详尽的[文章](
),所以我在这里不再重复。
至于其他依赖注入框架,我不认为它们是 Dagger 的真正竞争者。 例如,Koin 也许不错,但我认为它不会吸引很多人。 实际上,我相信它仅由于两个主要原因而得到了初步采用。 第一个是 Dagger 的糟糕文档,Koin 在这方面要比 Dagger 领先 N 光年。 第二个原因是 Koin 是用 Kotlin 编写的,它借着 kotlin 发展的浪潮开始兴起。 到目前为止,这波浪潮已经几乎消逝。
我认为可能会发生的情况是,纯依赖注入的框架(又称为手动依赖注入)会逐渐出现。
现在,谷歌声称[“随着应用程序的不断增大,手动依赖项注入成本呈指数增长”](
)。 我认为,这仅表明他们既不了解“指数”的含义,也没做过任何实际的“测量”。 此声明是完全错误的,我希望 Google 不要以这种方式来误导社区里的开发者了。
事实上,纯依赖注入在后端开发中非常普遍(尤其是在开发微服务的时候,您不想在其中添加对每个服务的框架的依赖),反射也是后端开发中的一个有效的选项。 因此,如果要使用依赖注入框架,他们通常不需要解析编译时代码。
但是,Android 开发的情况有所不同。由于我们不能使用反射式 DI 框架,所以我们使用了 Dagger。事实上,我们可以使用反射式 DI 框架,并且对于大多数项目来说都可以,但是却存在性能问题。我并不是说使用反射式 DI 框架是安全的,但它绝对不是一种非黑即白的方案。无论如何,Dagger 已经是在 Android 开发中使用依赖注入的事实上的标准,我们都使用它。但使用 Dagger 的代价也很明显:
1)应用的代码越多,在构建过程中运行注解处理所花费的时间就越多。
2)应用参与的开发人员越多,他们需要执行的构建次数就越多。
3)所有开发人员都需要学习 Dagger ,这需要很多时间。
换句话说,虽然 Dagger 确实允许您编写更少的代码,但由于它会影响构建时间和所需的培训时间,因此在大型项目上它会花费更多的时间。
在大型项目中,构建时间慢才是真正的问题,并成为主要的生产力瓶颈。 因此,尽管 Dagger 确实提供了非常出色的功能来简化 DI(当然,一旦您知道如何使用它),但我相信我们对纯依赖注入会产生越来越多的兴趣。
DataBinding
开发人员采用 DataBidning 的主要原因之一是不再需要调用 findViewById()了。 老实说,findViewById 确实很冗余,我也不介意摆脱它们。 但是,在我看来,调用 findViewById()带来的小麻烦并不能证明使用 DataBinding 是合理的。 好消息是,很快我们将能够使用另一个新功能[ViewBinding](
)来删除这些 findViewById()的调用。
实际上,我从来都不相信 DataBinding。对于它(应该)解决的问题,我感觉太复杂了。 此外,DataBinding 允许开发人员将逻辑放入 XML 布局中。 经验丰富的开发人员是不会使用这种方法的,因为这增加了项目维护的难度。这是 DataBinding 框架的另一个缺点。
早在 2016 年 11 月,当 DataBinding 正处于大肆宣传的顶峰时,我在 StackOverflow 上的一个[答案](
)中做出了以下预测:
However, there is one prediction I can make with a high degree of confidence: Usage of the Data Binding library will not become an industry standard. I’m confident to say that because the Data Binding library (in its current implementation) provides short-term productivity gains and some kind of architectural guideline, but it will make the code non-maintainable in the long run. Once long-term effects of this library will surface – it will be abandoned.
现在,关于 DataBinding 的使用率,我没有任何统计数据,但是很明显,它并没有成为行业标准。 我自己还从未见过使用 DataBinding 的专业项目,也很少见到在其应用中使用 DataBinding 的开发人员。据我估计,一旦 ViewBinding 成熟并被广泛采用,DataBinding 将会更加流行,并成为“传统”框架。
Preserving State on Configuration Changes
自从引入 ViewModel 之后,在 Android 应用中对于配置更改的处理就变得一团糟。 我知道我的这种说法太苛刻了点,但实际上,这是我可以描述的最温和的表达方式。
对我来说幸运的是,Gabor Varadi(又名 Zhuinden)已经在 Reddit 上的[这篇文章](
)中对这一问题进行了总结,所以我不需要自己做。他的结论就是:不推荐使用 onRetainCustomNonConfigurationInstance(),而推荐使用 ViewModel。有趣的是,在该帖子的结尾,Gabor 做了一些颇具嘲讽味道的预测:
你发现什么了吗? [Retained Fragments 现在已经被弃用了!](
) 。
我认为,弃用 Retained Fragments 实际上是一个好主意。 Fragment 的生命周期里具有 onAttach()和 onDetach()这两个方法的唯一原因就是为了支持 Retained Fragments 的使用。 通过弃用 Retained Fragments,这些方法也可以弃用,并且可以简化 Fragment 的生命周期。 如果您使用[我的方法](
)来处理 Fragment 的生命周期,那么这种弃用就不会让您感到困扰,因为我长期以来一直建议您避免 Retained Fragments,忽略 onAttach()和 onDetach()方法。
尽管有充分的理由要弃用 Retained Fragments,但弃用 onRetainCustomNonConfigurationInstance()却是胡说八道。 这不是我说的,而是 Jake Wharton 说的(您可以在前面提到的 Gabor 在 Reddit 上的帖子下阅读他的原话)。
为什么要做这些变动呢?我只能看到一种解释:Google 决定不管其它技术优势如何,都强制将所有 Android 项目迁移到 ViewModel。 他们愿意弃用所有现有的替代方案以实现其目标,即使这些替代方案实际上优于 ViewModel 本身。
听起来有点阴谋吧? 我同意。 但是,幸运的是,我们可以对此理论有一个简单的检验。
虽然我不喜欢 Preserving State on Configuration Changes,但它不会以任何方式影响我,因为我没有使用它。 实际上,绝大部分应用程序都不需要它。 它们也不需要 ViewModel。正确处理 Configuration Changes 的方式就是在 onSaveInstanceState(Bundle)回调方法里增加处理逻辑。 这是一种更简单,更好的方法,因为它还可以处理保存和恢复流程(也称为进程终止)。 因此,只要我能以这种方式保存状态,就可以了。 尽管 Google 进行了大量的营销和公关工作,但许多经验丰富的开发人员都意识到 ViewModel 太复杂了,并且有更好的方法来保留配置更改的状态。
因此,如果 Google 确实有别有用心,并且想迫使所有项目都使用 ViewModel,那么他们还需要弃用 onSaveInstanceState(Bundle)。 我知道这听起来很疯狂,但这实际上是件好事,因为如果这种疯狂的预测成真,您就会知道基础理论是正确的。
但是,鉴于 Android 的内存管理机制,Google 不能仅在不提供可靠替代方案的情况下就弃用 onSaveInstanceState(Bundle)。 “幸运”的是,[这些变化已经应用在 Vie
wModel 的保存状态模块上了](
)。
我想在一两年内我们就会知道这种做法是否有任何优点。‘
总而言之,正如我在本节开头所说的那样,自 ViewModel 发布以来,Android 中的 Configuration Changes 就成了屎。 两年多以前,当我撰写题为[“Android ViewModel Architecture Component Considered Harmful”](
)的文章时,我预测 ViewModels 将是一种浪费。我的所有预测都是真实的,但不幸的是,事实证明真相比这还糟。
Concurrency
评论