使用 Kotlin 重寫 AOSP 日歷應(yīng)用
兩年前,Android 開源項(xiàng)目 (AOSP) 應(yīng)用團(tuán)隊(duì)開始使用 Kotlin 替代 Java 重構(gòu) AOSP 應(yīng)用。之所以重構(gòu)主要有兩個原因: 一是確保 AOSP 應(yīng)用能夠遵循 Android 最佳實(shí)踐,另外則是提供優(yōu)先使用 Kotlin 進(jìn)行應(yīng)用開發(fā)的良好范例。Kotlin 之所以具有強(qiáng)大的吸引力,原因之一是其簡潔的語法,很多情況下用 Kotlin 編寫的代碼塊的代碼數(shù)量相比于功能相同的 Java 代碼塊要更少一些。此外,Kotlin 這種具有豐富表現(xiàn)力的編程語言還具有其他各種優(yōu)點(diǎn),例如:
- 空安全: 這一概念可以說是根植于 Kotlin 之中,從而幫助避免破壞性的空指針異常;
- 并發(fā): 正如 Google I/O 2019 中關(guān)于 Android 的描述,結(jié)構(gòu)化并發(fā) (structured concurrency) 能夠允許使用協(xié)程簡化后臺的任務(wù)管理;
- 兼容 Java: 尤其是在這次的重構(gòu)項(xiàng)目中,Kotlin 與 Java 語言的兼容性能夠讓我們一個文件一個文件地進(jìn)行 Kotlin 轉(zhuǎn)換。
AOSP 團(tuán)隊(duì)在去年夏天發(fā)表了一篇文章,詳細(xì)介紹了 AOSP 桌面時鐘應(yīng)用的轉(zhuǎn)換過程。而今年,我們將 AOSP 日歷應(yīng)用從 Java 轉(zhuǎn)換成了 Kotlin。在這次轉(zhuǎn)換之前,應(yīng)用的代碼行數(shù)超過 18,000 行,在轉(zhuǎn)換后代碼庫減少了約 300 行。在這次的轉(zhuǎn)換中,我們沿襲了同 AOSP 桌面時鐘轉(zhuǎn)換過程中類似的技術(shù),充分利用了 Kotlin 與 Java 語言的互操作性,對代碼文件一一進(jìn)行了轉(zhuǎn)換,并在過程中使用獨(dú)立的構(gòu)建目標(biāo)將 Java 代碼文件替換為對應(yīng)的 Kotlin 代碼文件。因?yàn)閳F(tuán)隊(duì)中有兩個人在進(jìn)行此項(xiàng)工作,所以我們在 Android.bp 文件中為每個人創(chuàng)建了一個 exclude_srcs 屬性,這樣兩個人就可以在減少代碼合并沖突的前提下,都能夠同時進(jìn)行重構(gòu)并推送代碼。此外,這樣還能允許我們進(jìn)行增量測試,快速定位錯誤出現(xiàn)在哪些文件。
在轉(zhuǎn)換任意給定的文件時,我們一開始先使用 Android Studio Kotlin 插件中提供的從 Java 到 Kotlin 的自動轉(zhuǎn)換工具。雖然該插件成功幫助我們轉(zhuǎn)換了大部份的代碼,但是還是會遇到一些問題,需要開發(fā)者手動解決。需要手動更改的部分,我們將會在本文接下來的章節(jié)中列出。
在將每個文件轉(zhuǎn)換為 Kotlin 之后,我們手動測試了日歷應(yīng)用的 UI 界面,運(yùn)行了單元測試,并運(yùn)行了 Compatibility Test Suite (CTS) 的子集來進(jìn)行功能驗(yàn)證,以確保不需要再進(jìn)行任何的回歸測試。
自動轉(zhuǎn)換之后的步驟
上面提到,在使用自動轉(zhuǎn)換工具之后,有一些反復(fù)出現(xiàn)的問題需要手動定位解決。在 AOSP 桌面時鐘文章中,詳細(xì)介紹了其中遇到的一些問題以及解決方法。如下列出了一些在進(jìn)行 AOSP 日歷轉(zhuǎn)換過程中遇到的問題。
用 open 關(guān)鍵詞標(biāo)記父類
我們遇到的問題之一是 Kotlin 父類和子類之間的相互調(diào)用。在 Kotlin 中,要將一個類標(biāo)記為可繼承,必須得在類的聲明中添加 open 關(guān)鍵字,對于父類中被子類覆蓋的方法也要這樣做。但是在 Java 中的繼承是不需要使用到 open 關(guān)鍵字的。由于 Kotlin 和 Java 能夠相互調(diào)用,這個問題直到大部分代碼文件轉(zhuǎn)換到了 Kotlin 才出現(xiàn)。
例如,在下面的代碼片段中,聲明了一個繼承于 SimpleWeeksAdapter 的類:
- class MonthByWeekAdapter(context: Context?, params:
- HashMap<String?, Int?>) : SimpleWeeksAdapter(context as Context, params) {//方法體}
由于代碼文件的轉(zhuǎn)換過程是一次一個文件進(jìn)行的,即使是完全將 SimpleWeeksAdapter.kt 文件轉(zhuǎn)換成 Kotlin,也不會在其類的聲明中出現(xiàn) open 關(guān)鍵詞,這樣就會導(dǎo)致一個錯誤。所以之后需要手動進(jìn)行 open 關(guān)鍵詞的添加,以便讓 SimpleWeeksAdapter 類可以被繼承。這個特殊的類聲明如下所示:
- open class SimpleWeeksAdapter(context: Context, params: HashMap?) {//方法體}
override 修飾符
同樣地,子類中覆蓋父類的方法也必須使用 override 修飾符來進(jìn)行標(biāo)記。在 Java 中,這是通過 @Override 注解來實(shí)現(xiàn)的。然而,雖然在 Java 中有相應(yīng)的注解實(shí)現(xiàn)版本,但是自動轉(zhuǎn)換過程中并沒有為 Kotlin 方法聲明中添加 override 修飾符。解決的辦法是在所有適當(dāng)?shù)牡胤绞謩犹砑? override 修飾符。
覆寫父類中的屬性
在重構(gòu)過程中,我們還遇到了一個屬性覆寫的異常問題,當(dāng)一個子類聲明了一個變量,而在父類中存在一個非私有的同名變量時,我們需要添加一個 override 修飾符。然而,即使子類的變量同父類變量的類型不同,也仍然要添加 override 修飾符。在某些情況下,添加 override 仍不能解決問題,尤其是當(dāng)子類的類型完全不同的時候。事實(shí)上,如果類型不匹配,在子類的變量前添加 override 修飾符,并在父類的變量前添加 open 關(guān)鍵字,會導(dǎo)致一個錯誤:
- type of *property name* doesn’t match the type of the overridden var-property
這個報錯很讓人疑惑,因?yàn)樵?Java 中,以下代碼可以正常編譯:
- public class Parent {
- int num = 0;
- }
- class Child extends Parent {
- String num = "num";
- }
而在 Kotlin 中相應(yīng)的代碼就會報上面提到的錯誤:
- class Parent {
- var num: Int = 0
- }
- class Child : Parent() {
- var num: String = "num"
- }
這個問題很有意思,目前我們通過在子類中對變量重命名來規(guī)避了這個沖突。上面的 Java 代碼會被 Android Studio 目前提供的代碼轉(zhuǎn)換器轉(zhuǎn)換為有問題的 Kotlin 代碼,這甚至被報告為是一個 bug 了。
import 語句
在我們轉(zhuǎn)換的所有文件中,自動轉(zhuǎn)換工具都傾向于將 Java 代碼中的所有 import 語句截斷為 Kotlin 文件中的第一行。最開始這導(dǎo)致了一些很讓人抓狂的錯誤,編譯器會在整個代碼中報 "unknown references" 的錯誤。在意識到這個問題后,我們開始手動地將 Java 中的 import 語句粘貼到 Kotlin 代碼文件中,并單獨(dú)對其進(jìn)行轉(zhuǎn)換。
暴露成員變量
默認(rèn)情況下,Kotlin 會自動地為類中的實(shí)例變量生成 getter 和 setter 方法。然而,有些時候我們希望一個變量僅僅只是一個簡單的 Java 成員變量,這可以通過使用 @JvmField 注解來實(shí)現(xiàn)。
@JvmField 注解的作用是 "指示 Kotlin 編譯器不要為這個屬性生成 getter 和 setter 方法,并將其作為一個成員變量允許其被公開訪問"。這個注解在 CalendarData 類中特別有用,它包含了兩個 static final 變量。通過對使用 val 聲明的只讀變量使用 @JvmField 注解,我們確保了這些變量可以作為成員變量被其他類訪問,從而實(shí)現(xiàn)了 Java 和 Kotlin 之間的兼容性。
對象中的靜態(tài)方法
在 Kotlin 對象中定義的函數(shù)必須使用 @JvmStatic 進(jìn)行標(biāo)記,以允許在 Java 代碼中通過方法名,而非實(shí)例化來對它們進(jìn)行調(diào)用。也就是說,這個注解使其具有了類似 Java 的方法行為,即能夠通過類名調(diào)用方法。根據(jù) Kotlin 的文檔,"編譯器會為對象的外部類生成一個靜態(tài)方法,而對于對象本身會生成一個實(shí)例方法。"我們在 Utils 文件中遇到了這個問題,當(dāng)完成轉(zhuǎn)換后,Java 類就變成了 Kotlin 對象。隨后,所有在對象中定義的方法都必須使用 @JvmStatic 標(biāo)記,這樣就允許在其他文件中使用 Utils.method() 這樣的語法來進(jìn)行調(diào)用。值得一提的是,在類名和方法名之間使用 .INSTANCE (即 Utils.INSTANCE.method()) 也是一種選擇,但是這不太符合常見的 Java 語法,需要改變所有對 Java 靜態(tài)方法的調(diào)用。
性能評估分析
所有的基準(zhǔn)測試都是在一臺 96 核、176 GiB 內(nèi)存的機(jī)器上進(jìn)行的。本項(xiàng)目中分析用到的主要指標(biāo)有所減少的代碼行數(shù)、目標(biāo) APK 的文件大小、構(gòu)建時間和首屏從啟動到顯示的時間。在對上述每個因素進(jìn)行分析的同時,我們還收集了每個參數(shù)的數(shù)據(jù)并以表格的方式進(jìn)行了展示。
減少的代碼行數(shù)
從 Java 完全轉(zhuǎn)換到 Kotlin 后,代碼行數(shù)從 18,004 減少到了 17,729。這比原來的 Java 代碼量減少了大約 1.5%。雖然減少的代碼量并不可觀,但對于一些大型應(yīng)用來說,這種轉(zhuǎn)換對于減少代碼行數(shù)的效果可能更為顯著,可參閱 AOSP 桌面時鐘文中所舉的例子。
目標(biāo) APK 大小
使用 Kotlin 編寫的應(yīng)用 APK 大小是 2.7 MB,而使用 Java 編寫的應(yīng)用 APK 大小是 2.6 MB。可以說這個差異基本可以忽略不計(jì)了,由于包含了一些額外的 Kotlin 庫,所以 APK 體積上的增加,實(shí)際上是可以預(yù)期的。這種大小的增加可以通過使用 Proguard 或 R8 來進(jìn)行優(yōu)化。
編譯時間
Kotlin 和 Java 應(yīng)用的構(gòu)建時間是通過取 10 次從零進(jìn)行完整構(gòu)建的時間的平均值來計(jì)算的 (不包含異常值),Kotlin 應(yīng)用的平均構(gòu)建時間為 13 分 27 秒,而 Java 應(yīng)用的平均構(gòu)建時間為 12 分 6 秒。據(jù)一些資料 (如 "Java 和 Kotlin 的區(qū)別" 以及 "Kotlin 和 Java 在編譯時間上的對比") 顯示,Kotlin 的編譯時間事實(shí)上比 Java 要更耗時,特別是對于從零開始的構(gòu)建。一些分析斷言,Java 的編譯速度會快 10-15%,又有一些分析稱這一數(shù)據(jù)為 15-20%。拿我們的例子進(jìn)行從零開始完整構(gòu)建所花費(fèi)的時間來說,Java 的編譯速度比 Kotlin 快 11.2%,盡管這個微小的差異并不在上述范圍內(nèi),但這有可能是因?yàn)?AOSP 日歷是一個相對較小的應(yīng)用,僅有 43 個類。盡管從零開始的完整構(gòu)建比較慢,但是 Kotlin 仍然在其他方面占有優(yōu)勢,這些優(yōu)勢更應(yīng)該被考慮到。例如,Kotlin 相對于 Java,更簡潔的語法通常可以保證較少的代碼量,這使得 Kotlin 代碼庫更易維護(hù)。此外,由于 Kotlin 是一種更為安全有效的編程語言,我們可以認(rèn)為完整構(gòu)建時間較慢的問題可以忽略不計(jì)。
首屏顯示的時間
我們使用了這種方法來測試應(yīng)用從啟動到完全顯示首屏所需要的時間,經(jīng)過 10 次試驗(yàn)后我們發(fā)現(xiàn),使用 Kotlin 應(yīng)用的平均時間約為 197.7 毫秒,而 Java 的則為 194.9 毫秒。這些測試都是在 Pixel 3a XL 設(shè)備上進(jìn)行的。從這個測試結(jié)果可以得出結(jié)論,與 Kotlin 應(yīng)用相比,Java 應(yīng)用可能具有微小的優(yōu)勢;然而,由于平均時間非常接近,這個差異幾乎可以忽略不計(jì)。因此,可以說 AOSP 日歷應(yīng)用轉(zhuǎn)換到 Kotlin,并沒有對應(yīng)用的初始啟動時間產(chǎn)生負(fù)面影響。
結(jié)論
將 AOSP 日歷應(yīng)用轉(zhuǎn)換為 Kotlin 大約花了 1.5 個月 (6 周) 的時間,由 2 名實(shí)習(xí)生負(fù)責(zé)該項(xiàng)目的實(shí)施。一旦我們對代碼庫更加熟悉并更加善于解決反復(fù)出現(xiàn)的編譯時、運(yùn)行時和語法問題時,效率肯定會變得更高。總的來說,這個特殊的項(xiàng)目成功地展示了 Kotlin 如何影響現(xiàn)有的 Android 應(yīng)用,并在對 AOSP 應(yīng)用進(jìn)行轉(zhuǎn)換的路途中邁出了堅(jiān)實(shí)的一步。