前言
在Google I/O 2017中箕戳,Google 宣布 Kotlin 成為 Android 官方開(kāi)發(fā)語(yǔ)言。
在Google I/O 2019中国撵,Google 宣布陵吸,Kotlin 編程語(yǔ)言現(xiàn)在是 Android 應(yīng)用程序開(kāi)發(fā)人員的首選語(yǔ)言,“Android 的開(kāi)發(fā)將越來(lái)越以 Kotlin 為先卸留∽咴剑” 許多新的 Jetpack API 和特性將首先在 Kotlin 中提供。
Kotlin將成為Android開(kāi)發(fā)程序員的必修課耻瑟。
本文既不是全面系統(tǒng)的學(xué)習(xí)手冊(cè),也不是對(duì)Kotlin的簡(jiǎn)單介紹赏酥,而是講解作為一個(gè)Android程序員喳整,想要真正快速進(jìn)入Kotlin開(kāi)發(fā),所必須學(xué)習(xí)掌握的知識(shí)要點(diǎn)裸扶,也可以當(dāng)作是入門(mén)速查手冊(cè)框都。
如果你是一名安卓開(kāi)發(fā)的Java程序員,想快速上手轉(zhuǎn)Kotlin開(kāi)發(fā)呵晨,那么本文就是為你而準(zhǔn)備的了魏保。
如果你對(duì)Kotlin已經(jīng)有一定的了解熬尺,只想直接查看如何不使用接口回調(diào)而得到異步函數(shù)執(zhí)行的結(jié)果,請(qǐng)直接跳到 協(xié)程實(shí)戰(zhàn) 一節(jié)谓罗。
Kotlin的特點(diǎn)
簡(jiǎn)潔: 大大減少樣板代碼的數(shù)量粱哼。
安全: 避免空指針異常等錯(cuò)誤。
互操作性: 充分利用 JVM檩咱、Android 的現(xiàn)有庫(kù)揭措。可以跟Java類互相訪問(wèn)刻蚯,幾乎沒(méi)有橋接成本绊含。
環(huán)境準(zhǔn)備
安裝 Kotlin 插件
Android Studio 從 3.0(preview)版本開(kāi)始將內(nèi)置 Kotlin 插件。
如果你的環(huán)境還沒(méi)有安裝Kotlin炊汹,那么打開(kāi) Settings ( Mac 為 Preferences) 面板躬充,在右側(cè)找到 Plugins 選項(xiàng) ,搜索框輸入 "Kotlin" 查找讨便,點(diǎn)擊 Search in repositories(倉(cāng)庫(kù)中搜索)麻裳,然后安裝即可,安裝完成之后需要重啟 Android Studio器钟。
工程設(shè)置
新建工程津坑,在工程向?qū)е羞x擇Kotlin語(yǔ)言,然后就可以直接開(kāi)始Kotlin開(kāi)發(fā)了傲霸。
如果你想在現(xiàn)有Java工程中添加Kotlin支持疆瑰,也將非常簡(jiǎn)單,步驟如下:
- 選擇Project視圖
- 打開(kāi)工程外層build.gradle昙啄,在buildscript中添加:
在dependencies中添加:ext.kotlin_version = '1.3.31'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- 打開(kāi)app里面的build.gradle穆役,在文件頭部中添加:
在dependencies中添加:apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions'
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
如圖所示,Kotlin環(huán)境便成功集成進(jìn)工程了梳凛。
Java工程集成Kotlin后耿币,安裝包將增大大約200~500K。
代碼示范
Kotlin的最大優(yōu)勢(shì)就是減少樣板代碼量韧拒,現(xiàn)在先來(lái)直觀感受一下如何直接訪問(wèn)layout xml中的元素:
- 新建一個(gè)Kotlin工程后淹接;
- 在content_main.xml添加一個(gè)TextView控件,并將控件id修改為textView叛溢;
- 在MainActivity中塑悼,添加如下代碼:
textView.setText("Test Kotlin")
注意以上代碼,跟Java代碼差不太多楷掉,Kotlin的每行代碼結(jié)束不必使用分號(hào)厢蒜,將幾行代碼寫(xiě)到一行中則需要用分號(hào)分隔開(kāi)。
在Android Studio IDE中,會(huì)被提示該行代碼錯(cuò)誤斑鸦,使用Alt+Enter愕贡,將自動(dòng)添加:
import kotlinx.android.synthetic.main.content_main.*
然后剛才的代碼就可以編譯運(yùn)行了。
不再需要使用 findViewById(R.id.xxx)巷屿,感覺(jué)是不是輕松多了固以。如果你使用了xml中未定義的id來(lái)訪問(wèn),將無(wú)法通過(guò)編譯攒庵。
單方法接口只需要寫(xiě)一個(gè)閉包就行了:
textView.setOnClickListener {
Log.d("KT", "onClick")
}
textView.setOnClickListener { view ->
Log.d("KT", "onClick:${view.toString()}") // 字符串模板中可以放任意表達(dá)式
}
textView.setOnClickListener { view, tag -> // 這里只是示范多參數(shù)嘴纺,由于接口并沒(méi)有tag參數(shù),編譯將出錯(cuò)
Log.d("KT", "onClick:$view $tag")
}
代碼轉(zhuǎn)換
- Kotlin插件提供將Java代碼轉(zhuǎn)換為Kotlin代碼的功能浓冒,只需要在.Java文件上點(diǎn)右鍵栽渴,彈出菜單中選擇"Convert Java File to Kotlin File"即可(據(jù)說(shuō)某些復(fù)雜代碼偶爾會(huì)出現(xiàn)問(wèn)題)
- 也可以將Java代碼復(fù)制到剪貼板,在Kotlin文件中粘貼稳懒,將彈出對(duì)話框闲擦,詢問(wèn)是否將要粘貼的代碼轉(zhuǎn)換為Kotlin代碼
- Kotlin文件擴(kuò)展名為.kt
語(yǔ)法基礎(chǔ)
變量聲明
- var - 聲明變量
- val - 聲明常量
val name: String = "Tom"
var a: Int = 10
a = 100
數(shù)據(jù)類型可以省略,Kotlin會(huì)自動(dòng)識(shí)別類型
val name = "Tom"
var a = 10
基本數(shù)據(jù)類型
var f: Float = 0.1f
var d: Double = 3.14
var n: Int = 1
var l: Long = 1000L
var s: Short = 0xFF
var b: Byte = 0b1101
var r: Boolean = false
類型轉(zhuǎn)換
Java中的顏色:
int color = 0xFFFF00FF;
粘貼到Kotlin會(huì)被轉(zhuǎn)換為:
val color = -0xff01
如果不轉(zhuǎn)換:
var color: Int = 0xFFFF00FF // 無(wú)法編譯
可以使用強(qiáng)制轉(zhuǎn)換
var color = 0xFFFF00FF.toInt() // 等同于Java:int color = 0xFFFF00FF;
注意Java的強(qiáng)制轉(zhuǎn)換 (int)a场梆,在Kotlin中需要使用 a.toInt() 墅冷。
Java中的 TextView v = (TextView)aView;
Kotlin中需要這樣:
var v: TextView = aView as TextView
函數(shù)
fun outputString(text: String) {
println("$text")
}
fun getNumber(): Int {
return 0
}
fun getDate(): Date {
return Date() // 創(chuàng)建對(duì)象無(wú)需使用 new 關(guān)鍵字
}
fun equal(a: Int, b: Int): Boolean {
return if (a == b) true else false
}
fun compareFloat(a: Float, b: Float): Int {
return if (a - b > 0.001) 1 else (if (a - b < -0.001) -1 else 0)
}
函數(shù)必須使用 fun 關(guān)鍵字來(lái)修飾。
如果函數(shù)需要返回值或油,則在括號(hào)后面添加冒號(hào)寞忿,然后聲明返回值類型。
Kotlin沒(méi)有三元操作符顶岸,可以使用if/else來(lái)替代腔彰。
命名參數(shù)
fun createStudent(name: String? = null, age: Int = 0, title: String? = null) {
println("createStudent name:$name age:$age title:$title")
}
createStudent("Tom", 10)
createStudent(age = 10, name = "Tom") // 跟上一行代碼等效
函數(shù)需要使用fun關(guān)鍵字修飾,函數(shù)的參數(shù)聲明里面辖佣,不可以使用var來(lái)修飾霹抛,因?yàn)镵otlin不允許函數(shù)體內(nèi)修改參數(shù)的值。
有默認(rèn)值的參數(shù)卷谈,在調(diào)用的時(shí)候可以省略杯拐,使用命名參數(shù)方式來(lái)調(diào)用函數(shù)時(shí),參數(shù)順序可以任意調(diào)整世蔗。因此函數(shù)不需要多態(tài)端逼,編寫(xiě)一份函數(shù)就夠靈活使用了。
空指針處理
上面的示例代碼中參數(shù)類型出現(xiàn)了問(wèn)號(hào)?凸郑,問(wèn)號(hào)的作用是告訴編譯器裳食,該變量可以為空。
指針變量默認(rèn)不可以為空
var name: String = null // 編譯器將報(bào)錯(cuò)
var title: String? = null // 類型后面加上問(wèn)號(hào)芙沥,就可以通過(guò)編譯了
var textView: TextView? = null
fun setColor(textView: TextView, color: Int) {
textView.setTextColor(color)
}
函數(shù)參數(shù)默認(rèn)不允許為空,因此這里不會(huì)出現(xiàn)空指針崩潰,無(wú)需做空指針檢測(cè)
fun setColor(textView: TextView?, color: Int) {
textView.setTextColor(color) // 由于name可以為空而昨,不能訪問(wèn)其屬性救氯,因此編譯會(huì)報(bào)錯(cuò)
}
fun setColor(textView: TextView?, color: Int) {
textView?.setTextColor(color) // 添加問(wèn)號(hào)?,告訴編譯器處理為空的情況
}
指針變量加上問(wèn)號(hào)再訪問(wèn)屬性方法歌憨,其實(shí)就是編譯器幫我們添加了if/else代碼着憨,如果textView為空,就不會(huì)去訪問(wèn)textView的setTextColor方法务嫡,從而避免空指針崩潰甲抖。因此有人說(shuō),kotlin不再會(huì)出現(xiàn)空指針崩潰心铃。
當(dāng)然了准谚,有時(shí)候,可能你就是希望當(dāng)指針為空時(shí)去扣,拋出異常柱衔,而不要掩蓋問(wèn)題原因,那么你可以這樣:
fun setColor(textView: TextView?, color: Int) {
textView!!.setTextColor(color) // 添加!!愉棱,如果textView為空唆铐,將拋出空指針異常
}
!! 的作用是告訴編譯器,這個(gè)指針不允許為空奔滑,如果遇到空指針艾岂,則直接拋出異常。
類
class AdwordsView(context: Context, name: String?) : FrameLayout(context) {
private val TAG = "AdwordsView"
var textView: TextView
private var _button: Button
var name: String? = null // 可以為空的變量必須聲明時(shí)賦初值
set(text) {
field = text
}
get() {
return field
}
init {
this.name = name
_button = Button(context)
textView = TextView(context) // 不能為空的變量必須在初始化函數(shù)中賦初值
textView.setTextColor(0xFFFF0000.toInt())
}
// 第二個(gè)構(gòu)造函數(shù)
constructor(context: Context) : this(context, null) {
println("$TAG constructor")
}
}
主構(gòu)造函數(shù)已經(jīng)內(nèi)聯(lián)到class聲明中了朋其,它會(huì)自動(dòng)調(diào)用init方法王浴,init方法不能顯示調(diào)用。第二個(gè)構(gòu)造函數(shù)其實(shí)有些多余令宿,因?yàn)橹鳂?gòu)造函數(shù)的name參數(shù)其實(shí)可以寫(xiě)成 name: String? = null叼耙,那么第二個(gè)構(gòu)造函數(shù)就沒(méi)有存在的價(jià)值了,當(dāng)然一旦寫(xiě)了粒没,它就會(huì)優(yōu)先筛婉,當(dāng)我們調(diào)用如下代碼
var ad = AdwordsView(context) // 分配對(duì)象時(shí),不需要new關(guān)鍵字
該行代碼會(huì)執(zhí)行第二個(gè)構(gòu)造函數(shù)癞松,刪除第二個(gè)構(gòu)造函數(shù)則會(huì)執(zhí)行主構(gòu)造函數(shù)爽撒。當(dāng)參數(shù)很多的時(shí)候,多個(gè)構(gòu)造函數(shù)會(huì)有一定的用處响蓉。
- 繼承關(guān)系直接使用冒號(hào)即可硕勿,不可以多重繼承
- 構(gòu)造函數(shù)一般不編寫(xiě)具體代碼,初始化工作應(yīng)該全部放在init函數(shù)去做
- 類成員變量和函數(shù)默認(rèn)是public的枫甲,需要隱藏則添加private關(guān)鍵字
- setter和getter方法源武,不必顯示編寫(xiě)扼褪,編譯器會(huì)自動(dòng)生成
- 初始化函數(shù)和構(gòu)造函數(shù)不需要 fun 關(guān)鍵字
- 一個(gè)文件可以定義多個(gè)類
class AdwordsView(context: Context, name: String?)
等同于
class AdwordsView constructor(context: Context, name: String?)
主構(gòu)造函數(shù)的constructor可以省略。
既然Kotlin的就是為了減少樣板代碼量的粱栖,那么實(shí)現(xiàn)多重接口的意義就不大话浇,回調(diào)都可以使用代碼block來(lái)實(shí)現(xiàn)。保留多重接口的支持更多可能是為了Java代碼能直接轉(zhuǎn)換為kotlin代碼吧闹究,畢竟kotlin的代碼形式基本可以全部兼容Java代碼幔崖。
接口
interface IAdwords {
fun showAdwords()
}
class AdwordsView(context: Context, name: String? = null)
: FrameLayout(context)
, IAdwords {
override fun showAdwords() {
println("showAdwords")
}
}
- 如果要實(shí)現(xiàn)多重接口,則繼續(xù)在后面添加渣淤,用逗號(hào)隔開(kāi)赏寇,最多只能有一個(gè)class,其它必須是interface
- 重載接口中的方法必須使用 override 關(guān)鍵字
回調(diào)接口
private var _adwordCallback: IAdwords? = null
fun setAdwordsCallback(adwordCallback: IAdwords?) {
_adwordCallback = adwordCallback
}
fun onEvent() {
_adwordCallback?.showAdwords()
}
這種方式的回調(diào)接口价认,基本上跟Java是個(gè)相似的嗅定。
lambda
Kotlin提供lambda表達(dá)式語(yǔ)法來(lái)精簡(jiǎn)代碼量,當(dāng)回調(diào)接口只有一個(gè)方法的時(shí)候刻伊,就可以使用這種方式來(lái)簡(jiǎn)化代碼:
class Loader {
private var _onLoadErrorCallback: ((errCode: Int, errMsg: String) -> Unit)? = null
fun setOnLoadErrorCallback(listener: ((errCode: Int, errMsg: String) -> Unit)) {
_onLoadErrorCallback = listener
}
fun onErrorEvent() {
_onLoadErrorCallback?.invoke(501, "Error(501)")
}
}
var load: Loader()
load.setOnLoadErrorCallback { errCode, errMsg ->
println("TAG onLoadError errCode: ${errCode} errMsg: ${errMsg}")
}
當(dāng)我們?cè)贙otlin中調(diào)用Java的點(diǎn)擊事件監(jiān)聽(tīng)的時(shí)候露戒,就使用了lambda的方式:
fab.setOnClickListener { view ->
println("TAG onClick:$view")
}
既然Kotlin的就是為了減少樣板代碼量,那么就不應(yīng)該再定義有很多方法的接口捶箱,盡量將接口拆分到多個(gè)接口智什,一個(gè)接口只有一個(gè)方法。這樣接口的實(shí)現(xiàn)方就再也不會(huì)出現(xiàn)很多空方法的窘境了丁屎,再加上使用協(xié)程來(lái)處理異步耗時(shí)操作荠锭,不再異步中定義回調(diào)接口,Kotlin的優(yōu)勢(shì)才能得到最大發(fā)揮晨川。
擴(kuò)展
fun TextView.textLanguage(): String {
return if (TextUtils.isEmpty(this.text)) "English" else "[null]"
}
println("${textView.textLanguage()}")
- 使用擴(kuò)展可以為現(xiàn)有類添加新的方法
- 擴(kuò)展函數(shù)內(nèi)证九,可以通過(guò) this 訪問(wèn)其屬性及方法,但不能訪問(wèn)私有屬性和方法
容器
數(shù)組
var nameList = arrayOf("Tom", "Mike", "Steven") // 返回Array對(duì)象
nameList.reverse()
//nameList.add(); // 長(zhǎng)度不可變
for (name in nameList) {
Log.w(TAG, "name: ${name}")
}
Array共虑,數(shù)組愧怜,長(zhǎng)度不可變,但內(nèi)容可以修改妈拌,例如如下:
var list = arrayOf(1, "Mike", 3.14, 0)
list.set(0, 0.1)
list[3] = "100"
for (value in list) {
Log.w(TAG, "value: ${value}")
}
數(shù)組的遍歷方法:
val array = Array<Int>(5) { index -> 0 } // full, verbose syntax
val arr = Array(5) { 0 }
for (i in 0..5-1) {
Log.w(TAG, "array[${i}]: ${array[i]}")
}
for (i in arr.indices) {
Log.w(TAG, "arr[${i}]: ${arr[i]}")
}
ArrayList拥坛,可變,內(nèi)容和長(zhǎng)度都可以修改:
var arrList = ArrayList<Int>(5)
for (i in 10 downTo 0 step 2) {
arrList.add(i)
}
for ((index, value) in arrList.withIndex()) {
println("arrList[$index] is $value")
}
}
字典
val map = mapOf("Tom" to 20, "Mike" to 18, "Steven" to 19) // key to value
println(map)
println(map["Mike"])
println(map["George"])
輸出結(jié)果:
{Tom=20, Mike=18, Steven=19}
18
null
mapOf 返回一個(gè)Map<K, V>對(duì)象尘分,只讀猜惋,如果要便捷構(gòu)造一個(gè)可修改的Map,請(qǐng)使用HashMap<K, V>:
val hashMap = hashMapOf("Tom" to 20, "Mike" to 18, "Steven" to 19)
hashMap["Steven"] = 15
hashMap.set("George", 13)
println(hashMap)
輸出結(jié)果:
{Mike=18, Tom=20, George=13, Steven=15}
Map的遍歷:
map.forEach {
println("${it.key} ${it.value}")
}
還可以這樣:
for ((key, value) in map){
println("${key} ${value}")
}
循環(huán)控制
for (i in 0..5) { // 5后面也可以添加step
print("${i}")
}
// 輸出:0 1 2 3 4 5
for (i in 10 downTo 0 step 2) {
print("${i}")
}
// 輸出: 10 8 6 4 2 0
頭尾都是閉區(qū)間培愁。
條件控制
沒(méi)有三元操作符著摔,但可以用if/else代替
val c = if (condition) a else b
when(強(qiáng)大的switch替代品)
when (a) {
1 -> {
println("a = 1")
}
2 -> {
println("a = 2")
}
else -> {
println("a = $a")
}
}
不需要break,多個(gè)條件也可以合并到一起:
when (a) {
1, 2, 3 -> {
println("a = $a")
}
else -> {
println("other a = $a")
}
}
類型檢測(cè)
fun testObject(obj: Any) {
// 類型檢測(cè)
if (obj is String) {
println(obj.length) // 自動(dòng)類型轉(zhuǎn)換定续,類型檢測(cè)后不必再寫(xiě)強(qiáng)制轉(zhuǎn)換代碼
} else {
println("1. obj isn't a String : " + obj)
}
if (obj !is String) {
println("2. obj isn't a String : " + obj)
}
if (obj is String && obj.length > 0) {
println("3. obj is a String")
}
}
線程
fun testThread() {
// 方法一:
object : Thread() { // kotlin的object 表達(dá)式創(chuàng)建匿名類谍咆,且重寫(xiě)了run()方法
override fun run() {
syncCopyFile("1.jpg")
}
}.start()
// 方法二:
Thread({ // kotlin中很容易使用lambda表達(dá)式禾锤,隱藏了對(duì)象和方法,直接將代碼block傳給Thread的構(gòu)造函數(shù)
syncCopyFile("2.jpg")
}).start()
// 方法三:
var task = thread(start = false){
syncCopyFile("3.jpg")
}
task.start() // 因?yàn)閷tart參數(shù)傳入了false卧波,因此需要手動(dòng)start
// 方法四:
thread(start = true) { // start參數(shù)默認(rèn)就為true时肿,但還是推薦顯示聲明為true
syncCopyFile("4.jpg")
}
println("testThread End.")
}
@Synchronized fun syncCopyFile(fname: String) {
println(" %%% Copy File[${fname}] in Thread: ${Thread.currentThread()}")
sleep(2000)
println(" %%% Copy File[${fname}] OK.")
}
@Synchronized 為Kotlin的函數(shù)同步鎖注解姑裂,并非關(guān)鍵字论熙。
如果在代碼中使用同步鎖巫糙,那么需要使用 synchronized 函數(shù):
fun syncCopyFile(fname: String) {
synchronized(this) {
sleep(2000)
}
}
如何轉(zhuǎn)到主線程執(zhí)行代碼:
runOnUiThread {
textView.setText("Title")
}
協(xié)程
概念
協(xié)程 - 輕量級(jí)線程
雖然Kotlin中使用線程已經(jīng)很方便了,但還是推薦使用協(xié)程代替線程查坪。
協(xié)程主要是讓原來(lái)要使用“異步+回調(diào)方式”寫(xiě)出來(lái)的復(fù)雜代碼, 簡(jiǎn)化成可以用看似同步的方式寫(xiě)出來(lái)(對(duì)線程的操作進(jìn)一步抽象)。 這樣我們就可以按串行的思維模型去組織原本分散在不同上下文中的代碼邏輯宁炫,而不需要去處理復(fù)雜的狀態(tài)同步問(wèn)題偿曙,基本上也不再需要接口處理代碼了。
先來(lái)看看如下代碼:
fun startCoroutine(name: String) {
println(" ### 1. Coroutine start in ${Thread.currentThread()}")
val c1 = GlobalScope.launch(Dispatchers.Default) {
println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
delay(1000)
println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
}
println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
}
startCoroutine("CO1")
輸出結(jié)果:
### 1. Coroutine start in Thread[main,5,main]
### 4. Coroutine End. in Thread[main,5,main]
*** 2. CO1 launch start in Thread[DefaultDispatcher-worker-1,5,main]
*** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
GlobalScope.launch(Dispatchers.Default) 用于啟動(dòng)協(xié)程羔巢。
從輸出結(jié)果可以看出望忆,啟動(dòng)協(xié)程之前,是在主線程中竿秆,但是協(xié)程啟動(dòng)后启摄,協(xié)程的代碼Block是在子線程中執(zhí)行的。這不是重點(diǎn)幽钢,重點(diǎn)在于delay過(guò)后歉备,協(xié)程的代碼一定是在子線程執(zhí)行的,哪怕launch指定了Unconfined參數(shù)匪燕,協(xié)程一開(kāi)始將在主線程中執(zhí)行蕾羊,但是delay依然不會(huì)阻塞主線程,但它的確可以在指定的時(shí)間過(guò)后返回代碼塊繼續(xù)執(zhí)行后面的代碼帽驯。這就是delay的強(qiáng)大之處龟再,這個(gè)delay是不可以在協(xié)程外部的代碼中調(diào)用的。
協(xié)程調(diào)度器 | 功能描述 |
---|---|
Dispatchers.Default | 運(yùn)行在 Dispatchers.Default 的線程池中 |
Dispatchers.Main | 運(yùn)行在主線程中 |
Dispatchers.IO | 運(yùn)行在 IO 線程中 |
Dispatchers.Unconfined | 運(yùn)行在當(dāng)前線程中 |
PS:之前低版本的那套launch/await 全局函數(shù)已經(jīng)廢棄尼变,新版本必須使用GlobalScope.xxx利凑。
協(xié)程的作用,就是讓開(kāi)發(fā)者感覺(jué)是在多線程中工作一樣享甸,可以異步處理耗時(shí)操作截碴,但實(shí)際上可能并沒(méi)有真正使用線程,而就在同一線程中切換蛉威。協(xié)程的切換是由編譯器來(lái)完成的日丹,因而開(kāi)銷(xiāo)很小,并不依賴系統(tǒng)資源蚯嫌,你可以開(kāi)100000個(gè)協(xié)程哲虾,而無(wú)法啟動(dòng)100000個(gè)線程丙躏。
delay跟線程的sleep很相似,都是延時(shí)一段時(shí)間束凑,但是不同點(diǎn)在于晒旅,delay不會(huì)阻塞當(dāng)前線程,而是掛起協(xié)程本身汪诉,從而將線程資源釋放出來(lái)废恋,供其它協(xié)程使用。
我們所必須要了解的是扒寄,在協(xié)程中鱼鼓,當(dāng)你的耗時(shí)任務(wù)做完之后,你的代碼很可能不在剛才的線程當(dāng)中该编,此時(shí)必須要注意代碼的線程安全問(wèn)題迄本,例如訪問(wèn)UI,你可以使用runOnUiThread { }课竣。
在startCoroutine的結(jié)尾處嘉赎,可以使用c1.join()來(lái)等待協(xié)程結(jié)束,一旦使用join于樟,編譯器便提醒必須添加suspend關(guān)鍵字公条,該函數(shù)也必須在協(xié)程中調(diào)用。
再來(lái)看看修改后的代碼:
suspend fun startCoroutine(name: String) {
println(" ### 1. Coroutine start in ${Thread.currentThread()}")
val c1 = GlobalScope.launch(Dispatchers.Default) {
println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
delay(3000)
println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
}
c1.join()
println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
}
該方法因?yàn)樘砑恿藄uspend關(guān)鍵字隔披,因此只能在協(xié)程中調(diào)用:
GlobalScope.launch(Dispatchers.Main) {
startCoroutine("CO1")
}
輸出結(jié)果如下:
### 1. Coroutine start in Thread[main,5,main]
*** 2. CO1 launch start in Thread[DefaultDispatcher-worker-2,5,main]
*** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
### 4. Coroutine End. in Thread[main,5,main]
可以看到赃份,代碼中的日志順序,是按1奢米、2抓韩、3、4的順序輸出的了鬓长,join函數(shù)會(huì)等待協(xié)程結(jié)束谒拴。由于我指定了startCoroutine在Dispatchers.Main父協(xié)程中運(yùn)行,因此當(dāng)join等待子協(xié)程完成之后涉波,又回到了主線程執(zhí)行英上,這種方式來(lái)更新UI的話,都不再需要使用runOnUiThread了啤覆,很適合用于做動(dòng)畫(huà)苍日。
<span id="jump">協(xié)程實(shí)戰(zhàn)</span>
我們通過(guò)一個(gè)網(wǎng)絡(luò)URL加載Web數(shù)據(jù)的實(shí)例,來(lái)展示協(xié)程對(duì)于異步處理的強(qiáng)大之處窗声。
首先相恃,需要在build.gradle中添加:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
新建一個(gè)UrlDownload類:
class UrlDownload {
// kotlin沒(méi)有static方法,而是要使用伴生對(duì)象來(lái)替代
companion object {
suspend fun asyncDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
download(url)
}.await()
}
fun download(url: String): String {
var urlConn : HttpURLConnection? = null
var strBuffer = StringBuffer()
var inputStream: InputStream? = null
var buffer: BufferedReader? = null
var inputReader: InputStreamReader? = null
try {
urlConn = URL(url).openConnection() as HttpURLConnection
inputStream = urlConn.getInputStream()
inputReader = InputStreamReader(inputStream)
buffer = BufferedReader(inputReader)
do {
var line = buffer.readLine()
strBuffer.append(line)
} while (line != null)
} catch (e: Exception){
e.printStackTrace()
} finally {
inputReader?.close()
buffer?.close()
inputStream?.close()
urlConn?.disconnect()
}
return strBuffer.toString()
}
}
}
fun startDownload() {
var url = "https://m.weibo.cn/"
GlobalScope.launch(Dispatchers.Default) {
var content = UrlDownload.asyncDownload(url) // 這是一個(gè)異步執(zhí)行的耗時(shí)的操作
println(content)
}
}
執(zhí)行以上程序笨觅,在主線程調(diào)用startDownload()函數(shù)拦耐,可以看到控制臺(tái)打印出了網(wǎng)頁(yè)內(nèi)容耕腾。請(qǐng)注意整個(gè)程序沒(méi)有定義任何回調(diào)接口,但結(jié)果的確是在業(yè)務(wù)層打印出來(lái)的杀糯,閱讀代碼就好像是同步執(zhí)行的一樣扫俺,你也可以看的出,以上代碼并不會(huì)阻塞主線程固翰。
- download(url: String)是一個(gè)同步方法狼纬,實(shí)現(xiàn)聯(lián)網(wǎng)返回網(wǎng)頁(yè)數(shù)據(jù)的功能,該方法會(huì)阻塞當(dāng)前線程倦挂,不能在主線程調(diào)用畸颅。
- asyncDownload方法添加了suspend關(guān)鍵字,說(shuō)明該函數(shù)將被掛起并異步執(zhí)行方援,等到異步執(zhí)行完畢才會(huì)返回結(jié)果。
- suspend關(guān)鍵字聲明的函數(shù)涛癌,是一個(gè)掛起函數(shù)犯戏,只能在協(xié)程里面調(diào)用。
- 編譯器將每一個(gè)掛起點(diǎn)的前后作為獨(dú)立的代碼片段拳话,這些代碼片段在需要的時(shí)候才會(huì)執(zhí)行先匪,不會(huì)阻塞當(dāng)前線程,內(nèi)部使用狀態(tài)機(jī)來(lái)保證協(xié)程狀態(tài)的恢復(fù)以及代碼片段的順序執(zhí)行弃衍。
- 執(zhí)行了掛起方法之后呀非,無(wú)法確定是在哪個(gè)線程恢復(fù)執(zhí)行,除非指定了Dispatchers.Main調(diào)度器镜盯。
如果需要一層一層的往上傳遞岸裙,那么將startDownload做個(gè)簡(jiǎn)單改造即可:
suspend fun startDownload(url: String): String? {
return GlobalScope.async(Dispatchers.Default) {
UrlDownload.asyncDownload(url)
}.await()
}
fun appStartDownload() {
var url = "https://m.weibo.cn/"
GlobalScope.launch(Dispatchers.Default) {
var content = startDownload(url)
println(content)
}
}
委托
相信很多同學(xué)都寫(xiě)過(guò)類似如下代碼:
interface IShare {
void shareText(String text);
void shareUrl(String url);
void shareImage(String filePath);
}
class WhatsappShare implements IShare {
public void shareText(String text) {}
public void shareUrl(String url) {}
public void shareImage(String filePath) {}
}
class ShareWrapper {
public void shareText(String text) {
_impl.shareText(text)
}
public void shareUrl(String url) {
_impl.shareText(url)
}
public void shareImage(String filePath) {
_impl.shareText(filePath)
}
}
Kotlin的解決方案:
interface IShare {
fun shareText(text: String)
fun shareUrl(url: String)
fun shareImage(filePath: String)
}
class WhatsappShare : IShare {
override fun shareText(text: String) {}
override fun shareUrl(url: String) {}
override fun shareImage(filePath: String) {}
}
class ShareWrapper(impl: IShare) : IShare by impl {
}
fun main(args: Array<String>) {
val whats = WhatsappShare()
ShareWrapper(whats).shareUrl("http://")
}
一個(gè)by關(guān)鍵字就將接口委托給另一個(gè)對(duì)象處理,而不必編寫(xiě)那些樣板代碼速缆。