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入門教程的完整目錄
__________________________________________________________________________
開啟微信掃一掃下面的二維碼,或者直接搜尋公眾號“老歐說安卓”新增關注,更快更方便地閱讀技術乾貨。