Android開發時常會遇到一些耗時的業務場景,比如後臺批量處理數據、訪問後端伺服器介面等等,此時為了保證界面交互的及時響應,必須通過線程單獨運行這些耗時任務。簡單的線程可使用Thread類來啟動,無論Java還是Kotlin都一樣,該方式首先要聲明一個自定義線程類,對應的Java代碼如下所示: 自 ...
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() }
以上代碼被doAsync括弧圈起來的代碼段,就是分線程要執行的全部代碼;至於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) } } }