1. 程式人生 > >Kotlin入門(30)多執行緒互動

Kotlin入門(30)多執行緒互動

Android開發時常會遇到一些耗時的業務場景,比如後臺批量處理資料、訪問後端伺服器介面等等,此時為了保證介面互動的及時響應,必須通過執行緒單獨執行這些耗時任務。簡單的執行緒可使用Thread類來啟動,無論Java還是Kotlin都一樣,該方式首先要宣告一個自定義執行緒類,對應的Java程式碼如下所示:

    private class PlayThread extends Thread {
        @Override
        public void run() {
            //此處省略具體的執行緒內部程式碼
        }
    }

自定義執行緒的Kotlin程式碼與Java大同小異,具體見下:

    private inner class PlayThread : Thread() {
        override fun run() {
            //此處省略具體的執行緒內部程式碼
        }
    }

執行緒類宣告完畢,接著要啟動執行緒處理任務,在Java中呼叫一行程式碼“new PlayThread().start();”即可,至於Kotlin則更簡單了,只要“PlayThread().start()”就行。如此看來,Java的執行緒處理程式碼跟Kotlin差不了多少,沒發覺Kotlin比Java有什麼優勢。倘使這樣,真是小瞧了Kotlin,它身懷多項絕技,單單是匿名函式這招,之前在介紹任務Runnabe時便領教過了,執行緒Thread同樣也能運用匿名函式化繁為簡。注意到自定義執行緒類均需由Thread派生而來,然後必須且僅需重寫run方法,所以像類繼承、函式過載這些程式碼都是走過場,完全沒必要每次都依樣畫葫蘆,編譯器真正關心的是run方法內部的具體程式碼。於是,藉助於匿名函式,Kotlin的執行緒執行程式碼可以簡寫成下面這般:

    Thread {
        //此處省略具體的執行緒內部程式碼
    }.start()

以上程式碼段看似無理,實則有規,不但指明這是個執行緒,而且命令啟動該執行緒,可謂是簡潔明瞭。執行緒程式碼在執行過程中,通常還要根據實際情況來更新介面,以達到動態重新整理的效果。可是Android規定了只有主執行緒才能操作介面控制元件,分執行緒是無法直接呼叫控制元件物件的,只能通過Android提供的處理器Handler才能間接操縱控制元件。這意味著,要想讓分執行緒持續重新整理介面,仍需完成傳統Android開發的下面幾項工作:
1、宣告一個自定義的處理器類Handler,並重寫該類的handleMessage方法,根據不同的訊息型別進行相應的控制元件操作;
2、執行緒內部針對各種執行狀況,呼叫處理器物件的sendEmptyMessage或者sendMessage方法,傳送事先約定好的訊息型別;
舉個具體的業務例子,現在有一個新聞版塊,每隔兩秒在介面上滾動播報新聞,其中便聯合運用了執行緒和處理器,先由執行緒根據情況發出訊息指令,再由處理器按照訊息指令輪播新聞。詳細的業務程式碼示例如下:

class MessageActivity : AppCompatActivity() {
    private var bPlay = false
    private val BEGIN = 0 //開始播放新聞
    private val SCROLL = 1 //持續滾動新聞
    private val END = 2 //結束播放新聞
    private val news = arrayOf("北斗三號衛星發射成功,定位精度媲美GPS", "美國賭城拉斯維加斯發生重大槍擊事件", "日本在越南承建的跨海大橋未建完已下沉", "南水北調功在當代,近億人喝上長江水", "德國外長要求中國尊重“一個歐洲”政策")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_message)
        tv_message.gravity = Gravity.LEFT or Gravity.BOTTOM
        tv_message.setLines(8)
        tv_message.maxLines = 8
        tv_message.movementMethod = ScrollingMovementMethod()
        btn_start_message.setOnClickListener {
            if (!bPlay) {
                bPlay = true
                //執行緒第一種寫法的呼叫方式,通過具體的執行緒類進行構造。
                //注意每個執行緒例項只能啟動一次,不能重複啟動。
                //若要多次執行該執行緒的任務,則需每次都構造新的執行緒例項。
                //PlayThread().start()
                //執行緒的第二種寫法,採用匿名類的形式。第二種寫法無需顯式構造
                Thread {
                    //傳送“開始播放新聞”的訊息型別
                    handler.sendEmptyMessage(BEGIN)
                    while (bPlay) {
                        //休眠兩秒,模擬獲取突發新聞的網路延遲
                        Thread.sleep(2000)
                        val message = Message.obtain()
                        message.what = SCROLL
                        message.obj = news[(Math.random() * 30 % 5).toInt()]
                        //傳送“持續滾動新聞”的訊息型別
                        handler.sendMessage(message)
                    }
                    bPlay = true
                    Thread.sleep(2000)
                    //傳送“結束播放新聞”的訊息型別
                    handler.sendEmptyMessage(END)
                    bPlay = false
                }.start()
            }
        }
        btn_stop_message.setOnClickListener { bPlay = false }
    }

    //自定義的處理器類,區分三種訊息型別,給tv_message顯示不同的文字內容
    private val handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            val desc = tv_message.text.toString()
            tv_message.text = when (msg.what) {
                BEGIN -> "$desc\n${DateUtil.nowTime} 下面開始播放新聞"
                SCROLL -> "$desc\n${DateUtil.nowTime} ${msg.obj}"
                else -> "$desc\n${DateUtil.nowTime} 新聞播放結束,謝謝觀看"
            }
        }
    }

}

通過執行緒加上處理器固然可以實現滾動播放的功能,可是想必大家也看到了,這種互動方式依舊很突兀,還有好幾個難以克服的缺點:
1、自定義的處理器仍然存在類繼承和函式過載的冗餘寫法;
2、每次操作介面都得經過傳送訊息、接收訊息兩道工序,繁瑣且拖沓;
3、執行緒和處理器均需在指定的Activity程式碼中宣告,無法在別處重用;
有鑑於此,Android早已提供了非同步任務AsyncTask這個模版類,專門用於耗時任務的分執行緒處理。然而AsyncTask的用法著實不簡單,首先它是個模板類,初學者瞅著模板就發慌;其次它區分了好幾種執行狀態,包括未執行、正在執行、取消執行、執行結束等等,一堆的概念叫人頭痛;再次為了各種狀況都能與介面互動,又得定義事件監聽器及其事件處理方法;末了還得在Activity程式碼中實現監聽器的相應方法,才能正常呼叫定義好的AsyncTask類。
初步看了下自定義AsyncTask要做的事情,直讓人倒吸一口冷氣,看起來很高深的樣子,確實每個Android開發者剛接觸AsyncTask之時都費了不少腦細胞。為了說明AsyncTask是多麼的與眾不同,下面來個非同步載入書籍任務的完整Java程式碼,溫習一下那些年虐過開發者的AsyncTask:

//模板類的第一個引數表示外部呼叫execute方法的輸入引數型別,第二個引數表示執行過程中與介面互動的資料型別,第三個引數表示執行結束後返回的輸出引數型別
public class ProgressAsyncTask extends AsyncTask<String, Integer, String> {
    private String mBook;
    //建構函式,初始化資料
    public ProgressAsyncTask(String title) {
        super();
        mBook = title;
    }

    //在後臺執行的任務程式碼,注意此處不可與介面互動
    @Override
    protected String doInBackground(String... params) {
        int ratio = 0;
        for (; ratio <= 100; ratio += 5) {
            // 睡眠200毫秒模擬網路通訊處理
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //重新整理進度,該函式會觸發呼叫onProgressUpdate方法
            publishProgress(ratio);
        }
        return params[0];
    }

    //在任務開始前呼叫,即先於doInBackground執行
    @Override
    protected void onPreExecute() {
        mListener.onBegin(mBook);
    }

    //重新整理進度時呼叫,由publishProgress函式觸發
    @Override
    protected void onProgressUpdate(Integer... values) {
        mListener.onUpdate(mBook, values[0], 0);
    }

    //在任務結束後呼叫,即後於doInBackground執行
    @Override
    protected void onPostExecute(String result) {
        mListener.onFinish(result);
    }

    //在任務取消時呼叫
    @Override
    protected void onCancelled(String result) {
        mListener.onCancel(result);
    }

    //宣告監聽器物件
    private OnProgressListener mListener;
    public void setOnProgressListener(OnProgressListener listener) {
        mListener = listener;
    }

    //定義該任務的事件監聽器及其事件處理方法
    public static interface OnProgressListener {
        public abstract void onFinish(String result);
        public abstract void onCancel(String result);
        public abstract void onUpdate(String request, int progress, int sub_progress);
        public abstract void onBegin(String request);
    }}

見識過了AsyncTask的驚濤駭浪,不禁喟嘆開發者的心靈有多麼地強大。多執行緒是如此的令人望而卻步,直到Kotlin與Anko的搭檔出現,因為它倆線上程方面帶來了革命性的思維,即程式設計理應是面向產品,而非面向機器。對於分執行緒與介面之間的互動問題,它倆給出了堪稱完美的解決方案,所有的執行緒處理邏輯都被歸結為兩點:其一是如何標識這種牽涉介面互動的分執行緒,該點由關鍵字“doAsync”闡明;其二是如何在分執行緒中傳遞訊息給主執行緒,該點由關鍵字“uiThread”界定。有了這兩個關鍵字,分執行緒的編碼異乎尋常地簡單,即使加上Activity的響應程式碼也只有以下寥寥數行:

    //圓圈進度對話方塊
    private fun dialogCircle(book: String) {
        dialog = indeterminateProgressDialog("${book}頁面載入中……", "稍等")
        doAsync {
            // 睡眠200毫秒模擬網路通訊處理
            for (ratio in 0..20) Thread.sleep(200)
            //處理完成,回到主執行緒在介面上顯示書籍載入結果
            uiThread { finishLoad(book) }
        }
    }

    private fun finishLoad(book: String) {
        tv_async.text = "您要閱讀的《$book》已經載入完畢"
        if (dialog.isShowing) dialog.dismiss()
    }

以上程式碼被doAsyc括號圈起來的程式碼段,就是分執行緒要執行的全部程式碼;至於uiThread括號圈起來的程式碼,則為通知主執行緒要完成的工作。倘若在分執行緒執行過程中,要不斷重新整理當前進度,也只需在待重新整理的地方新增一行uiThread便成,下面是添加了進度重新整理的程式碼例子:

    //長條進度對話方塊
    private fun dialogBar(book: String) {
        dialog = progressDialog("${book}頁面載入中……", "稍等")
        doAsync {
            for (ratio in 0..20) {
                Thread.sleep(200)
                //處理過程中,實時通知主執行緒當前的處理進度
                uiThread { dialog.progress = ratio*100/20 }
            }
            uiThread { finishLoad(book) }
        }
    }

點此檢視Kotlin入門教程的完整目錄


__________________________________________________________________________
開啟微信掃一掃下面的二維碼,或者直接搜尋公眾號“老歐說安卓”新增關注,更快更方便地閱讀技術乾貨。