MAD,现代安卓开发技术:Android 领域开发方式的重大变革~
Posted TechMerger
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MAD,现代安卓开发技术:Android 领域开发方式的重大变革~相关的知识,希望对你有一定的参考价值。
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 提供了三个版本供开发者灵活选择。
版本 | 说明 |
---|---|
Stable Release | 稳定发行版,最新版为 Arctic Fox|2020.3.1 |
Release candidate | 即将发布的下一代版本,可以提前体验新特性和优化,最新版为 Bunblebee|2021.1.1 |
Canary | 试验版本,不稳定但可以试用领先的实验功能,最新版为 Chipmunk|2021.2.1 |
接下来介绍 AS 其中几个好用的特性。
2.1 Database Inspector
Database Inspector
可以实时查看 Jetpack Room
框架生成的数据库文件,同时也支持实时编辑和部署到设备当中。相较之前需要的 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
等
5.Jetpack
Jetpack
单词的本意是火箭人,框架的 Logo 也可以看出来是个绑着火箭的 Android。Google 用它命名,含义非常明显,希望这些框架能够成为 Android 开发的助推器:助力 App 开发,体验飞速提升。
Jetpack 分为架构、UI、基础功能和特定功能等几个方面,其中架构板块是全新设计的,涵盖了 Google 花费大量精力开发的系列框架,是本章节着力讲解的方面。
架构以外的部分实际上是 AOSP
本身的一些组件进行优化之后集成到了Jetpack 体系内而已,这里不再提及。
- 架构:全新设计,框架的核心
- 以外:AOSP 本身组件的重新设计
- UI
- 基础功能
- 特定功能
Jetpack 具备如下的优势供我们在实现某块功能的时候收腰选择:
- 提供 Android 平台的最佳实践
- 消除样板代码
- 不同版本、厂商上达到设备一致性的框架表现
- Google 官方稳定的指导、维护和持续升级
如果对 Jetpack 的背景由来感兴趣的朋友可以看我之前写的一篇文章:「从Preference组件的更迭看Jetpack的前世今生」。下面,我们选取 Jetpack 中几个典型的框架来了解和学习下它具体的优势。
5.1 View Binding
通常的话绑定布局里的 View 实例有哪些办法?又有哪些缺点?
通常做法 | 缺点 |
---|---|
findViewById() | NPE 风险、大量的绑定代码、类型转换危险 |
@ButterKnife | NPE 风险、额外的注解代码、不适用于多模块项目(APT 工具解析 Library 受限) |
KAE 插件 | NPE 风险、操作其他布局的风险、Kotlin 语言独占、已经废弃 |
AS 现在默认采用 ViewBinding
框架帮我们绑定 View。
来简单了解一下它的用法:
<!--result_profile.xml-->
<LinearLayout ... >
<TextView android:id="@+id/name" />
</LinearLayout>
ViewBinding 框架初始化之后,无需额外的绑定处理,即可直接操作 View 实例。
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle)
super.onCreate(savedInstanceState)
val binding = ResultProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.name.text = "Hello world"
原理比较简单:编译器将生成布局同名的绑定类文件,然后在初始化的时候将布局里的 Root View 和其他预设了 ID 的 View 实例缓存起来。事实上无论是上面的注解,插件还是这个框架,其本质上都是通过 findViewById 实现的 View 绑定,只是进行了封装。
ViewBinding 框架能改善通常做法的缺陷,但也并非完美。特殊情况下仍需使用通常做法,比如操作布局以外的系统 View 实例 ContentView,ActionBar 等。
优势 | 局限 |
---|---|
Null 安全:预设 ID 的 View 才会被缓存,否则无法通过 ViewBinding 使用,在编译阶段就阻止了 NPE 的可能 | 绑定布局以外的 View 仍需借助 findViewById |
类型安全:ViewBinding 缓存 View 实例的时候已经处理了匹配的类型 | 依赖配置采用不同布局仍需处理 Null(比如横竖屏的布局不同) |
代码简洁:无需绑定的样板代码 | |
布局专属:不混乱、布局文件为单位的专属类 |
5.2 Data Binding
一般来说,将数据反映到 UI 上需要经过如下步骤:
- 创建 UI 布局
- 绑定布局中 View 实例
- 数据逐一更新到 View 的对应属性
而 DataBinding
框架可以免去上面的步骤 2 和 3。它需要我们在步骤 1 的布局当中就声明好数据和 UI 的关系,比如文本内容的数据来源、是否可见的逻辑条件等。
<layout ...>
<data>
<import type="android.view.View"/>
<variable
name="viewModel" type="com.example.splash.ViewModel" />
</data>
<LinearLayout ...>
<TextView
...
android:text="@viewModel.userName"
android:visibility="@viewModel.age >= 18 ? View.VISIBLE : View.GONE"/>
</LinearLayout>
</layout>
上述 DataBinding 布局展示的是当 ViewModel 的 age 属性大于 18 岁才显示文本,而文本内容来自于 ViewModel 的 userName 属性。
val binding = ResultProfileBinding.inflate(layoutInflater)
binding.viewModel = viewModel
Activity 中无需绑定和手动更新 View,像 ViewBinding 一样初始化之后指定数据来源即可,后续的 UI 展示和刷新将被自动触发。DataBinding 还有诸多妙用,大家可自行了解。
5.3 Lifecycle
监听 Activity 的生命周期并作出相应处理是 App 开发的重中之重,通常有如下两种思路。
通常思路 | 具体 | 缺点 |
---|---|---|
基础 | 直接覆写 Activity 对应的生命周期函数 | 繁琐、高耦合 |
进阶 | 利用 Application#registerLifecycleCallback 统一管理 | 回调固定、需要区分各 Activity、逻辑侵入到 Application |
而 Lifecycle
框架则可以高效管理生命周期。
使用 Lifecycle 框架需要先定义一个生命周期的观察者 LifecycleObserver
,给生命周期相关处理添加上 OnLifecycleEvent
注解,并指定对应的生命状态。比如 onCreate
的时候执行初始化,onStart
的时候开始连接,onPause
的时候断开连接。
class MyLifecycleObserver(
private val lifecycle: Lifecycle
) : LifecycleObserver
...
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun init()
enabled = checkStatus()
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start()
if (enabled)
connect()
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun stop()
if (connected)
disconnect()
然后在对应的 Activity 里添加观察:
class MainActivity : AppCompatActivity()
override fun onCreate(savedInstanceState: Bundle)
...
MyLifecycleObserver(lifecycle).also lifecycle.addObserver(it)
Lifecycle 的简单例子可以看出生命周期的管理变得很清晰,同时能和 Activity 的代码解耦。
继续看上面的小例子:假使初始化操作 init() 是异步耗时操作怎么办?
init 异步的话,onStart 状态回调的时候 init 可能没有执行完毕,这时候 start 的连接处理 connect 可能被跳过。这时候 Lifecycle 提供的 State
机制就可以派上用场了。
使用很简单,在异步初始化回调的时候再次执行一下开始链接的处理,但需要加上 STARTED
的 State 条件。这样既可以保证 onStart 时跳过连接之后能手动执行连接,还能保证只有在 Activity 处于 STARTED 及以后的状态下才执行连接。
class MyLifecycleObserver(...) : LifecycleObserver
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun init()
checkStatus result ->
if (resultMAD,现代安卓开发技术:Android 领域开发方式的重大变革~
现代 Android 开发的三大更新 | 2022 Android 开发者峰会