Android 诞生已久,其开发方式保持着高频更迭,相较于早期的开发方式已大不相同,尤其是近几年 Google 热切推崇的 MAD 开发技术。其实很多开发者已经有意或无意地正在使用这门技术,借着 2022 开年探讨技术趋势的契机,想要完整地总结 MAD 的愿景、构成、优势以及一些学习建议。
MAD,全称 Modern Android Development:是 Google 针对 Android 平台提出的全新开发技术。旨在指导我们利用官方推出的各项技术来进行高效的 App 开发。有的时候 Google 会将其翻译成现代安卓开发,有的时候又翻译成新式安卓开发,个人觉得前者的翻译虽然激进、倒也贴切。
下面按照 MAD 的构成要点逐步展开,帮助大家快速了解 MAD 的技术理念。如果大家对其中的语言、工具包或框架产生了兴趣,一定要在日后的开发中尝试和掌握。
内容前瞻
【Modern Android Development】讲述 Android 全新开发技术的由来和构成
【Android Studio】演示 Android 官方 IDE 的重要特性
【Android App Bundle】简要普及 Google 推崇的 App 新格式
【Kotlin】解读 Android 首推的开发语言的优点
【Jetpack】讲述 Android 持续更新的重大框架集合,并逐个演示重要框架解决的问题和优势
【Jetpack Compose】带领大家感受 Android 上 UI 开发方式的重大变革
1.Modern Android Development
官方一直在优化 App 的开发体验:从 IDE 到语言再到框架,这些新技术愈发完善也愈发琐碎。提出一个全新的概念来整合这些松散的技术方便介绍和推广,也方便开发者们理解。
MAD 便是提出的全新理念,期望在语言、工具、框架等多个层面提供卓越的开发体验,其愿景和优势:
倾力打造:汇聚 Google 在 Android 行业十余年的前言开发经验
入门简单:提供大量 Demo 和详尽文档,适用于各阶段各规模的项目
迅速起步:提供显著降低样板代码的开发框架 Jetpack 和 UI 工具包 Jetpack Compose
自由选择:框架丰富多样,可与传统语言、原生开发、开源框架自由搭配
统合一致:兼容不同设备的开发框架达到的一致性开发体验
其涵盖的内容:
Android Studio :持续改进的官方 IDE
Android App Bundle :先进的应用打包和分发方式
Kotlin :首推的编程语言
Jetpack :独立于 AOSP 以外,汇集了大量开发框架的开发套件
Jetpack Compose:Android 平台重大变革的 UI 工具包
同时,官方针对 MAD 技术提供了认证考试和技能的计分插件,大家在实践一段时间之后可以体验一下:
MAD 资格认证
Android Studio 的 MAD Skills 计分插件
2.Android Studio
Android Studio 刚推出的初期饱受批评,吃内存、Bug 多、不好用,开发者一度对 Eclipse 恋恋不舍。随着 Google 和开发者的不断协力,AS 愈加稳定、功能愈加强大,大家可以活用 AS 的诸多特性以提高开发效率。和 Chrome 一样,针对不同需求,AS 提供了三个版本供开发者灵活选择。
接下来介绍 AS 其中几个好用的特性。
2.1 Database Inspector
Database Inspector
可以实时查看 JetpackRoom
框架生成的数据库文件,同时也支持实时编辑和部署到设备当中。相较之前需要的 SQLite
命令或者额外导出并借助 DB 工具的方式更为高效和直观。
2.2 Layout / Motion Editor
Layout Editor 拥有诸多优点,不知大家熟练运用了没有:
可以直观地编辑 UI:随意拖动视图控件和更改约束指向
在不同配置(设备、主题、语言、屏幕方向等)下灵活切换预览,免去实机调试
搭配 Tools 标签自由定制 UI,确保只面向调试而不影响实际逻辑。比如:布局中有上下两个控件,上面的默认为 invisible,想确认下上面的控件如果可见的话对整体布局的影响。无需更改控件的 visibility 属性,添加 Tools:visibility=true 即可预览布局的变化
Motion Editor 则是支持 MotionLayout 类型布局的视觉设计编辑器,可让更轻松地创建和预览和调试动画。
Layout Inspector 则可以查看某进程某画面的详细布局,完整展示 View 树的各项属性。在不方便代码调试或剖析其他 App 的情况下非常好用。同时已经支持直接检查 Compose 编写的 UI 布局了,喜极而泣。
2.3 Realtime Profilers
AS 的 Realtime Profilers 工具可以帮助我们在如下四个方面监测和发现问题,有的时候在没有其他 App 代码的情况下通过 Memory Profilers 还可以查看其内部的实例和变量细节。
CPU:性能剖析器检查 CPU 活动,切换到 Frames 视图还可以界面卡顿追踪
Memory:识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配以定位内存方面的问题
Battery:会监控 CPU、网络无线装置和 GPS 传感器的使用情况,并直观地显示其中每个组件消耗的电量,了解应用在哪里耗用了不必要的电量
Network:显示实时网络活动,包括发送和接收的数据以及当前的连接数。这便于您检查应用传输数据的方式和时间,并适当优化代码
2.4 APK Analyzer
Apk 的下载会耗费网络流量,安装了还会占用存储空间。其体积的大小会对 App 安装和留存产生影响,分析和优化其体积显得尤为必要。
借助 AS 的 APK Analyzer 可以帮助完成如下几项工作:
快速分析 Apk 构成,包括 DEX、Resources 和 Manifest 的 Size 和占比,助力我们优化代码或资源的方向
Diff Apk 以了解版本的前后差异,精准定位体积变大的源头
分析其他 Apk,包括查看大致的资源和分析代码逻辑,进而拆解、Bug 定位
2.5 其他特性
篇幅原因只介绍了少部分特性,其他的还有很多,需要各位自行探索:
性能提升、内嵌到 AS 界面内的的 Fast Emulator
实时预览和编辑 Compose 布局,并支持直接交互的 Compose Preview
针对 Jetpack WorkManager 的 Background Task Inspector
。。。
相比之下,Google 官方的这篇「Android Studio 新特性详解」介绍得更新、更全,大家可以一看。
3.Android App Bundle
android app bundle 是一种发布格式,其中包含您应用的所有经过编译的代码和资源,它会将 APK 生成及签名交由 Google Play 来完成。
这个新格式对面向海外市场的 3rd Party App 影响较大,对面向国内市场的 App 影响不大。但作为未来的构建格式,了解和适配是迟早的事。
其针对目标设备优化 Apk 的构建,比如只预设对应架构的 so文件、图片和语言资源。得以压缩体积,进而提升安装成功率并减少卸载量
支持便捷创建 Instant App,可以免安装、直接启动、体验试用
满足模块化应用开发,提升大型项目的编译速度和开发效率
Google 对 .aab 格式非常重视,也极力推广:从去年也就是 2021 年 8 月起,规定新的 App 必须采用该格式才能在 Google Play 上架。
fun 神的「AAB 扶正!APK 将退出历史舞台」文章针对 AAB 技术有完整的说明,可以进一步了解。
4.Kotlin
A modern programming language that makes developers happier.
Kotlin是 大名鼎鼎的 JetBrains 公司于 2011 年开发的面向 JVM 的新语言,对于 Android 开发者来说,选择 Kotlin 开发 App 有如下理由:
Google IO 2019 宣布 Kotlin 成为了官方认定的 Android 平台首选编程语言,这意味着会得到 Google 巨佬在 Android 端的鼎力支持以实现超越 Java 的优秀编程体验
通过 KMM(Kotlin Multiplatform Mobile)实现跨移动端的支持
Server-side,天然支持后端开发
通过 Kotlin/JS 编译成 JavaScript,支持前端开发
和 Java 几乎同等的编译速度,增量编译下性能甚至超越 Java
4.1 Kotlin 在 Android上优秀的编程体验
Kotlin 代码简洁、可读性高:缩减了大量样板代码,以缩短编写和阅读代码的时间
可与 Java 互相调用,灵活搭配
容易上手,尤其是熟悉 Java 的 Android 开发者
代码安全,编译器严格检查代码错误
专属的协程机制,大大简化异步编程
提供了大量 Android 专属的 KTX 扩展
唯一支持 Android 全新 UI 编程方式 Compose 的开发语言
很多知名 App 都已经采用 Kotlin 进行开发,比如 Evernote、Twiiter、Pocket、WeChat 等。
下面我们选取 Kotlin 的几个典型特性,结合代码简单介绍下其优势。
4.2 简化函数声明
Kotlin 语法的简洁体现在很多地方,就比如函数声明的简化。
如下是一个包含条件语句的 Java 函数的写法:
String generateAnswerString(int count, int countThreshold) { if (count > countThreshold) { return "I have the answer."; } else { return "The answer eludes me."; } }
Java 支持三元运算符可以进一步简化。
String generateAnswerString(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }
Kotlin 的语法并不支持三元运算符,但可以做到同等的简化效果:
fun generateAnswerString(count: Int, countThreshold: Int): String { return if (count > countThreshold) "I have the answer." else "The answer eludes me." }
它同时还可以省略大括号和 return 关键字,采用赋值形式进一步简化。这样子的写法已经很接近于语言的日常表达,高级~
fun generateAnswerString(count: Int, countThreshold: Int): String = if (count > countThreshold) "I have the answer." else "The answer eludes me."
反编译 Class 之后发现其实际上仍采用的三元运算符的写法,这种语法糖会体现在 Kotlin 的很多地方😅。
public final String generateAnswerString2(int count, int countThreshold) { return count > countThreshold ? "I have the answer." : "The answer eludes me."; }
4.3 高阶函数
介绍高阶函数之前,我们先看一个向函数内传入回调接口的例子。
一般来说,需要先定义一个回调接口,调用函数传入接口实现的实例,函数进行一些处理之后执行回调,借助Lambda 表达式可以对接口的实现进行简化。
interface Mapper { int map(String input); } class Temp { void main() { stringMapper("Android", input -> input.length() + 2); } int stringMapper(String input, Mapper mapper) { // Do something ... return mapper.map(input); } }
Kotlin 则无需定义接口,直接将匿名回调函数作为参数传入即可。(匿名函数是最后一个参数的话,方法体可单独拎出,增加可读性)
这种接受函数作为参数或返回值的函数称之为高阶函数,非常方便。
class Temp { fun main() { stringMapper("Android") {input -> input.length + 2} } fun stringMapper(input: String, mapper: (String) -> Int): Int { // Do something ... return mapper(input) } }
事实上这也是语法糖,编译器会预设默认接口来帮忙实现高阶函数。
4.4 Null 安全
可以说 Null 安全是 Kotlin 语言的一大特色。试想一下 Java 传统的 Null 处理无非是在调用之前加上空判断或卫语句,这种写法既繁琐,更容易遗漏。
void function(Bean bean) { // Null check if (bean != null) { bean.doSometh(); } // 或者卫语句 if (bean == null) { return; } bean.doSometh(); }
而 Kotlin 要求变量在定义的时候需要声明是否可为空:带上 ? 即表示可能为空,反之不为空。作为参数传递给函数的话也要保持是否为空的类型一致,否则无法通过编译。
比如下面的 functionA() 调用 functionB() 将导致编译失败,但 functionB() 的参数在声明的时候没有添加 ? 即为非空类型,那么函数内可直接使用该参数,没有 NPE 的风险。
fun functionA() { var bean: Bean? = null functionB(bean) } fun functionB(bean: Bean) { bean.doSometh() }
为了通过编译,可以将变量 bean 声明中的 ? 去掉, 并赋上正常的值。
但很多时候变量的值是不可控的,我们无法保证它不为空。那么为了通过编译,还可以选择将参数 bean 添加上 ? 的声明。这个时候函数内不就不可直接使用该参数了,需要做明确的 Null 处理,比如:
在使用之前也加上 ? 的限定,表示该参数不为空的情况下才触发调用
在使用之前加上 !! 的限定也可以,但表示无论参数是否为空的情况下都触发调用,这种强制的调用即会告知开发者此处有 NPE 的风险
fun functionB(bean: Bean?) { // bean.doSometh() // 仍然直接调用将导致编译失败 // 不为空才调用 bean?.doSometh() // 或强制调用,开发者已知 NPE 风险 bean!!.doSometh() }
总结起来将很好理解:
- 参数为非空类型,传递的实例也必须不为空
- 参数为可空类型,内部的调用必须明确地 Null 处理
反编译一段 Null 处理后可以看到,非空类型本质上是利用 @NotNull
的注解,可空类型调用前的 ? 则是手动的 null 判断。
public final int stringMapper(@NotNull String str, @NotNull Function1 mapper) { ... return ((Number)mapper.invoke(str)).intValue(); } private final void function(String bean) { if (bean != null) { boolean var3 = false; Double.parseDouble(bean); } }
4.5 协程 Coroutines
介绍 Coroutines
之前,先来回顾下 Java 或 Android 如何进行线程间通信?有何痛点?
比如:AsyncTask、Handler、HandlerThread、IntentService、RxJava、LiveData 等。它们都有复杂易错、不简洁、回调冗余的痛点。
比如一个请求网络登录的简单场景:我们需要新建线程去请求,然后将结果通过 Handler 或 RxJava 回传给主线程,其中的登录请求必须明确写在非 UI 线程中。
void login(String username, String token) { String jsonBody = "{ username: \"$username\", token: \"$token\"}"; Executors.newSingleThreadExecutor().execute(() -> { Result result; try { result = makeLoginRequest(jsonBody); } catch (IOException e) { result = new Result(e); } Result finalResult = result; new Handler(Looper.getMainLooper()).post(() -> updateUI(finalResult)); }); } Result makeLoginRequest(String jsonBody) throws IOException { URL url = new URL("https://example.com/login"); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestMethod("POST"); ... httpURLConnection.connect(); int code = httpURLConnection.getResponseCode(); if (code == 200) { // Handle input stream ... return new Result(bean); } else { return new Result(code); } }
Kotlin 的 Coroutines 则是以顺序的编码方式实现异步操作、同时不阻塞调用线程的简化并发处理的设计模式。
其具备如下的异步编程优势:
挂起线程不阻塞原线程
支持取消
通过 KTX 扩展对 Jetpack 组件更好支持
采用协程实现异步处理的将变得清晰、简洁,同时因为指定耗时逻辑运行在工作线程的缘故,无需管理线程切换可直接更新 UI。
fun login(username: String, token: String) { val jsonBody = "{ username: \"\$username\", token: \"\$token\"}" GlobalScope.launch(Dispatchers.Main) { val result = try { makeLoginRequest(jsonBody) } catch(e: Exception) { Result(e) } updateUI(result) } } @Throws(IOException::class) suspend fun makeLoginRequest(jsonBody: String): Result { val url = URL("https://example.com/login") var result: Result withContext(Dispatchers.IO) { val httpURLConnection = url.openConnection() as HttpURLConnection httpURLConnection.run { requestMethod = "POST" ... } httpURLConnection.connect() val code = httpURLConnection.responseCode result = if (code == 200) { Result(bean) } else { Result(code) } } return result }
4.6 KTX
KTX
是专门为 Android 库设计的 Kotlin 扩展程序,以提供简洁易用的 Kotlin 代码。
比如使用 SharedPreferences
写入数据的话,我们会这么编码:
void updatePref(SharedPreferences sharedPreferences, boolean value) { sharedPreferences .edit() .putBoolean("key", value) .apply(); }
引入 KTX 扩展函数之后将变得更加简洁。
fun updatePref(sharedPreferences: SharedPreferences, value: Boolean) { sharedPreferences.edit { putBoolean("key", value) }
这只是 KTX 扩展的冰山一角,还有大量好用的扩展以及 Kotlin 的优势值得大家学习和实践,比如:
- 大大简洁语法的
let
,also
等扩展函数 - 节省内存开销的
inline
函数 - 灵活丰富的
DSL
特性 - 异步获取数据的
Flow
等