教你如何使用協程(二)初步啓動Kotlin協程

協程是什麼?

首先kotlin協程是kotlin的擴展庫(kotlinx.coroutines)。

上一篇我們簡單瞭解了線程的概念,線程在Android開發中一般用來做一些複雜耗時的操作,避免耗時操作阻塞主線程而出現ANR的情況,例如IO操作就需要在新的線程中去完成。但是呢,如果一個頁面中使用的線程太多,線程間的切換是很消耗內存資源的,我們都知道線程是由系統去控制調度的,所以線程使用起來比較難於控制。這個時候kotlin的協程就體現出它的優勢了,kotlin協程是運行在線程之上的,它的切換由程序自己來控制,無論是 CPU 的消耗還是內存的消耗都大大降低。所以大家趕緊來擁抱kotlin協程吧_

爲Android項目中引入kotlin協程

  1. 添加依賴

首先要確保你的kotlin版本在1.1以上,我們在Android module中的build.gradle的dependencies中添加如下依賴。

api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'

這裏我使用的kotlin版本爲1.3.50,協程庫版本爲1.3.2

  1. 添加混淆

在混淆代碼中,具有不同類型的字段可以具有相同的名稱,並且AtomicReferenceFieldUpdater可能無法找到正確的字段。要避免在混淆期間按類型進行字段重載,請將其添加到配置中:

-keepclassmembernames class kotlinx.** {
    volatile <fields>;
}

回想一下剛學 Thread 的時候

我相信現在接觸 Kotlin 的開發者絕大多數都有 Java 基礎,我們剛開始學習 Thread 的時候,一定都是這樣乾的:

val thread = object : Thread(){
    override fun run() {
        super.run()
        //do what you want to do.
    }
}
thread.start()

肯定有人忘了調用 start,還特別納悶爲啥我開的線程不啓動呢。說實話,這個線程的 start 的設計其實是很奇怪的,不過我理解設計者們,畢竟當年還有 stop 可以用,結果他們很快發現設計 stop 就是一個錯誤,因爲不安全而在 JDK 1.1 就廢棄,稱得上是最短命的 API 了吧。

既然 stop 是錯誤,那麼總是讓初學者丟掉的 start 是不是也是一個錯誤呢?

哈,有點兒跑題了。我們今天主要說 Kotlin。Kotlin 的設計者就很有想法,他們爲線程提供了一個便捷的方法:

val myThread = thread {
    //do what you want
}

這個 thread 方法有個參數 start 默認爲 true,換句話說,這樣創造出來的線程默認就是啓動的,除非你實在不想讓它馬上投入工作:

val myThread = thread(start = false) {
    //do what you want
}
//later on ...
myThread.start()

這樣看上去自然多了。接口設計就應該讓默認值滿足 80% 的需求嘛。

爲什麼我們要使用協程

上面我們簡單介紹協程的設計巧妙的避開了Thread的弊端,但是協程的作用究竟是什麼呢?爲啥我們的整個項目要直接用協成替換了Thread?它又是如何能夠在如此大的項目中直接扮演Thread這麼重要的角色?
首先來強調一個概念:協程是一個輕量級的線程。

接下來用一個官方的demo,解釋一下協程爲什麼能夠被如此重視:

runBlocking{
	repeat(100_000){//循環100000次
		launch{//開啓一個協程
			delay(1000L)
			print(".")
		}
	}
}

案例很簡單,開啓10萬個協程。等等?啓動10萬個??沒錯!這裏可以很順暢的啓動10萬個!這裏我們想想,如果我們啓動10萬個Thread會是什麼樣子呢?從這點來看,協程的確可以稱的上輕量級。那麼協程的優點僅此而已嗎?不着急,我們一點點來看。

初識協程:

首先我們來瞄一眼協程是長啥樣的, 以下引用(copy)了官網的一個例子:

fun main(args: Array<String>) {
    launch(CommonPool) {
        delay(1000L) 
        println("World!") 
    }
    println("Hello,")
    Thread.sleep(2000L)
}

運行結果: ("Hello,"會立即被打印, 1000毫秒之後, "World!"會被打印)

Hello, 
World!

姑且不管裏面具體的細節, 上面代碼大體的運行流程是這樣的:

  • 主流程:

1、調用系統的launch方法啓動了一個協程, 跟隨的大括號可以看做是協程體.
(其中的CommonPool暫且理解成線程池, 指定了協程在哪裏運行)
2、打印出"Hello,"
3、主線程sleep兩秒
(這裏的sleep只是保持進程存活, 目的是爲了等待協程執行完)

  • 協程流程:

協程延時1秒
打印出"World!"

解釋一下delay方法:
在協程裏delay方法作用等同於線程裏的sleep, 都是休息一段時間, 但不同的是delay不會阻塞當前線程, 而像是設置了一個鬧鐘, 在鬧鐘未響之前, 運行該協程的線程可以被安排做了別的事情, 當鬧鐘響起時, 協程就會恢復運行.

再看協程:

我們可以把協程認爲是一個輕量的線程。像線程一樣,協程同樣可以並行運行,彼此等待並進行通信。協程和線程最大的不同就是,協程很輕量,我們可以創建上千個,並且只消耗很少的性能。線程從開始到保持都要耗費很多資源,而且對現在機器來說上千個線程是一個很嚴峻的挑戰。我們可以通過launch{}方法開啓一個協程,默認情況下協程運行在一個共享的線程池上。線程仍然可以運行在一個基於協程開發的程序中,一個線程可以運行很多個協程,所以我們將不再需要很多的線程。示例如下:

import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) {
    println("Start")

    // Start a coroutine
    launch {
        delay(1000)
        println("Hello")
    }

    Thread.sleep(2000) // wait for 2 seconds
    println("Stop")
}

運行結果:
運行結果

說明:上述代碼中我們開啓了一個協程,一秒後打印hello。我們使用delay()方法,就像使用Thread.sleep()方法,但是delay方法會更好一些,它不會阻塞線程,它只是暫停協程本身。當協程正在等待時,線程返回到池中,並且當等待完成時,協程將在池中的空閒線程上恢復。
如果你想在main函數中使用非阻塞的delay方法,會發生一個編譯錯誤Suspend functions are only allowed to be called from a coroutine or another suspend function,因爲我們沒有在協程中執行,我們將它包裝在runBolcking{}中使用。runBlocking{}會啓動協程並等待協程執行完成


import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) {
   println("Start")

   // Start a coroutine
   launch {
       delay(1000)
       println("Hello1")
   }

runBlocking {
    delay(2000)
    println("Hello2")
}

   Thread.sleep(2000) // wait for 2 seconds
   println("Stop")
}

運行結果

kotlin協程的三種啓動方式

到這裏,已經我們已經看了兩個簡單的協程案例了,這兩個案例我們都是用了launch關鍵字來啓動協程,其實協程有三種通用的啓動方式

  1. runBlocking:T

  2. launch:Job

  3. async/await:Deferred

第一種啓動方式(runBlocking:T)

runBlocking 方法用於啓動一個協程任務,通常只用於啓動最外層的協程,例如線程環境切換到協程環境。
官方解釋

上圖是官方源碼中給出的該方法的解釋,意思就是說runBlocking啓動的協程任務會阻斷當前線程,直到該協程執行結束。

代碼示例:

runBlocking啓動

執行結果:可以清楚的看到先將協程中的任務完成才執行主線程中的邏輯
結果

第二種啓動方式(launch:Job)

我們最常用的用於啓動協程的方式,它最終返回一個Job類型的對象,這個Job類型的對象實際上是一個接口,它包涵了許多我們常用的方法。例如join()啓動一個協程、cancel() 取消一個協程

注⚠️:該方式啓動的協程任務是不會阻塞線程的

代碼示例:

launch方式啓動
執行結果:可以清楚的看到主線程沒有被阻塞

執行結果

第三種啓動方式(async/await:Deferred)

1.async和await是兩個函數,這兩個函數在我們使用過程中一般都是成對出現的。

2.async用於啓動一個異步的協程任務,await用於去得到協程任務結束時返回的結果,結果是通過一個Deferred對象返回的。

代碼示例:
async/await方式啓動

執行結果:可以看到當協程任務執行完畢時可以通過await()拿到返回結果

執行結果
雖然有三種啓動方式,但是大部分情況,我們都是使用Launch這種方式。
協程是可以被取消的和超時控制,可以組合被掛起的函數,協程中運行環境的指定,也就是線程的切換

協程啓動後還可以取消

launch方法有一個返回值, 類型是Job, Job有一個cancel方法, 調用cancel方法可以取消協程, 看一個數羊的例子:

fun main(args: Array<String>) {
    val job = launch(CommonPool) {
        var i = 1
        while(true) {
            println("$i little sheep")
            ++i
            delay(500L)  // 每半秒數一隻, 一秒可以輸兩隻
        }
    }

    Thread.sleep(1000L)  // 在主線程睡眠期間, 協程裏已經數了兩隻羊
    job.cancel()  // 協程才數了兩隻羊, 就被取消了
    Thread.sleep(1000L)
    println("main process finished.")
}

運行結果是:

1 little sheep
2 little sheep
main process finished.

如果不調用cancel, 可以數到4只羊。

在開發過程中通過cancel我們可以及時釋放不必要的資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章