安卓开发简直就是低效的代名词,以前ui写起来麻烦,现在ui写起来简单了,但是整个系统的复杂度令人难以忍受。想要做安卓开发,不仅要懂业务逻辑,还要成为半个 Gradle 专家、半个系统适配专家和半个架构师,外加还要忍受垃圾的就业市场
活全家的android studio和gradle
android studio和gradle更新了5年,几乎没有任何实用的功能变化,唯一能体会到的就是cpu和内存越吃越多,电脑风扇狂转是家常便饭,慢的一批的同时还特别难用。
第一次Gradle Sync或者下载依赖,简直是一场灾难。这个过程不仅慢,而且经常因为网络问题(尤其是“墙”的存在)失败。Gradle版本号需要和agp、ide版本对应才能正常编译成功,这是人能想出来的设计吗?几乎99%从github拉下来的开源安卓项目都无法正常编译。
你以为你只是在配置几行依赖?不,你实际上是在用一种领域特定语言(DSL)进行编程。你需要理解它的生命周期、Task 依赖图(DAG)、Project 和 Task 对象、闭包(Closure)等一大堆概念。为了改一个简单的编译参数,你可能需要去读 Gradle 官方长篇大论的文档。
你以为你在编程?错了,Gradle 的错误栈又长又臭,真正的错误原因往往埋藏在几百行无关紧要的日志里。很多错误信息极其通用,完全不告诉你到底是哪个依赖冲突了,或者哪个文件有问题。哪有现代编程语言的栈提示这么垃圾
另外就是编译,尽管 Google 推出了各种优化手段,但在中大型项目中,一次全量编译或 Clean Build 耗时几分钟甚至十几分钟仍是常态。理论上,增量编译应该很快,但实际上,它非常脆弱。稍微修改一下常量、资源文件、或者某个模块的公共 API,就可能导致大范围的重新编译,WTF?你永远不知道下一次按下“Run”按钮是 10 秒还是 10 分钟。
实在不行咱们还是抄一抄前端工具链吧,虽然经常有人吐槽前端天天造轮子,但是好歹人家从ide插件、包管理器、工具链到开发都是越来越好用,速度越来越快、操作越来越简单。我实在看不出来安卓开发哪里有进步。
无尽的 API 废弃与迁移成本
天天鼓捣那破b设计模式,MVC、MVP、MVVM 、 MVI……七八年时间换了四五种“最佳实践”,没事发个博客告诉你:太好啦,我们又有新的设计模式可用啦!
然而Google对“废弃”有着特殊的执念,“最佳实践”的寿命最多就两年,很多曾经被官方大力推广的 API 、库和技术架构,短时间内就被废弃。比如MVVM模式,早期谷歌推荐每个UI状态都用 ObservableField
,用了不到半年又推荐用 LiveData
,又用了一年多,最佳实践又变成了通过 StateFlow
来管理。开发者内心真的是一万个草尼玛奔腾。
典型的例子:
AsyncTask
,Loader
,startActivityForResult()
->ActivityResultLauncher
IntentService
->JobIntentService
->WorkManager
ObservableField
->LiveData
->StateFlow
Paging
库连续经历三次大改,完全和最初不一样了
这种非功能性的开发复杂度,与业务逻辑无关,纯粹由平台和生态引起,开发者需要花费大量时间去进行“防御性重构”,不是为了实现新功能,也不是为了优化性能,仅仅是为了让应用在过去和未来的安卓版本上还能正常工作。这种工作毫无成就感,纯属消耗
Activity/Fragment的销毁重建机制
喜欢我mutableStateOf
、rememberSaveable
、SavedStateHandle
、StateFlow
、SharedFlow
、 ViewModel
、DataBinding
、LiveData
、IntentService + Receiver
、Paging (V1, V2, V3)
吗?
如果你是一名前端开发,可能很难想象,仅仅旋转一下屏幕就会导致安卓UI数据错乱/应用崩溃。仅仅为了一个按钮上的文本正常显示,你就需要学习这么多东西
罪魁祸首就是Activity/Fragment的销毁重建机制,安卓应用的各个组件生命周期不同,系统可能会在不同的时候回收组件的内存,所有的ui状态都需要开发者手动维护以对抗系统。
- Compose UI 内部的
mutableStateOf
/rememberSaveable
(UI 瞬时状态) - ViewModel 中的
StateFlow
(屏幕/业务逻辑状态) - ViewModel 中的
SavedStateHandle
(可跨进程重建的状态) Paging
库的PagingData
(列表分页状态)DataStore
(用户偏好设置状态)Room
数据库 (持久化的业务实体状态)
开发者需要像一个水管工,用 collectAsState
、repeatOnLifecycle
、map
、combine
等各种管道和阀门,将这些来自不同源头、生命周期各异的“状态水流”正确地引导到 UI 上,同时还要处理好线程、生命周期和进程重建等一系列复杂问题。即使是一个简单的操作,样板代码也非常多,心智负担越来越高。
context地狱
绝大部分的安卓api都要传Context
,然而这东西有一大堆子类,你只能在特定的地方获取他,并且在特定的地方传入特定的context,这东西还容易产生内存泄漏,有时候子函数想要调用,只能一级一级传下去。大量的、本应由框架透明处理的复杂性,直接暴露给了应用层开发者,迫使开发者花费巨量精力去进行防御性编程
- 在 Activity 里,用
this
还是getApplicationContext()
? - 在 Fragment 里,用
getContext()
还是requireContext()
还是getActivity()
? - 在自定义 View 里,用
getContext()
? - 在工具类里,需要外部传入一个
Context
,如何传不会内存泄漏?
这就像在写JS时,window
对象有好几个版本。一个是‘全局版’,一个是‘页面版’,还有一个是‘组件版’。每次调用一个API前,都必须先决定是传入 window
对象还是一个代表当前 <div
> 的特殊对象,并且选错了就会导致内存泄漏或页面崩溃一样,这无疑是荒谬且极易出错的。
页面导航不如前端一根
在 Jetpack Navigation Component 出现之前,Android 的页面导航和栈管理堪称一团乱麻。开发者需要手动管理 FragmentTransaction
、处理各种 launchMode
、Intent Flag
,极其容易出错。获取navhost、navcontroller,不利用依赖注入的情况下也很麻烦,完全没有前端框架的useRoute方便
有API,但不保证可用
官方提供了WorkManager
、Foreground Service
、AlarmManager
、Boardcast
等多种方式来实现后台任务,然而哪怕你用了官方推荐的 WorkManager
和前台服务,你的任务还是有可能被系统杀掉,那你出他干嘛???国产系统为了不被骂,被迫给微信qq这些应用在操作系统中开“白名单”;开发者被迫研究各种“黑科技”:悬浮窗保活、无障碍保活、PDD的系统漏洞提权保活等等;最终形成了“后台保活”这个畸形的子领域,与 Google 的政策背道而驰
系统碎片化
虽然 Google 努力在推新版本,但市场上 Android 8、9、10、11、12、13、14 、15同时存在,且占有率都不低。一个新 API,你不仅要用,还要为数亿无法升级的用户写兼容性代码,为每个版本做不同适配,代码中充斥大量的:if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.XXX)
的丑陋程度堪比go的 if err == nil
国内厂商定制(魔改)地狱是比系统版本碎片化更恐怖的存在
- “安全”、“反诈”: 除了原生权限,厂商会增加自己的权限管理,比如后台弹出界面、悬浮窗、自启动等,申请方式五花八门。什么?你说你用谷歌官方api申请不到权限?我们没有文档,你自己靠经验和逆向去猜吧。无法在Google Play上架的流氓应用,却可以堂而皇之地在国内应用商店上架
- API 行为不一致: 同一个标准 Android API,在不同厂商的 ROM 上可能有截然不同的行为甚至 Bug。比如通知栏样式、文件读写权限、网络请求等,都可能出现意想不到的坑。你不仅要适配 Android 版本,还要适配 MIUI 版本、EMUI 版本。
UI 测试的痛苦
我生平最讨厌两种人:写测试的人和让我写测试的人,特别是安卓的UI测试
ui测试编写起来繁琐、运行起来极慢且极其不稳定,测试经常因为各种奇怪的原因(如动画、网络延迟、模拟器状态)失败,维护成本非常高。一旦涉及到需要 Android Framework 的集成测试或 UI 测试,就必须依赖模拟器或真机,完全没有办法在 CI/CD 环境中高效、稳定地运行测试
依赖注入变成额外的学习成本
因为谷歌的垃圾设计,依赖注入几乎变成标配,注解、组件、作用域,对于新人开发直接一脸懵逼