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()
    }

以上代碼被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) }
        }
    }

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM