為什麼要有執行緒(Thread)
為什麼要有執行緒呢,先看一下底下的代碼。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
for (i in 1..100){
println("Thomas : "+i)
Thread.sleep(1000)
}
}
}
執行此app時,Log 視窗每秒吐出一個訊息。但手機的畫面會整個卡住,過了100秒之後,才會出現畫面。所以這種寫法絕對是不良的,但偏偏初學者就是一定會這麼寫。
上面會出現畫面卡住的原因,是因為在列印1, 2, 3…時,都是 UI主執行緒在工作,等100個值都印出後,UI主執行緒才會繼續更新畫面。這種寫法在某些手機上會出現ANR錯誤(Android Non Response)。
說的更明白一點,就是不能讓UI主執行緒執行過於耗時的工作。
Thread及Runnable
要解決上述的問題,需要先產生一個Runnable物件,並將要執行的工作寫在run()方法之中。
接下來再產生一個新的執行緒(雇用一個員工),將上述的工作(Runnable)丟給它,再使用start()方法命令這位員工開始工作。不過要注意一點,並不是下達start()後員工就會馬上工作,至少需等個幾奈秒到幾微秒的時間才會開始運作。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val job=Runnable(){//要執行的工作
for(i in 1..100){
println("Thomas : %d, %s".format(i, Thread.currentThread().name))
Thread.sleep(1000)
}
}
val thread=Thread(job)//產生一個執行緒,並將工作丟進去
thread.start()//一定要用start()啟動執行緒
}
}
精簡寫法
底下藍色的部份,Runnable採用匿名物件(Anonymous)的方式傳入Thread, 而Thread亦是匿名物件。所以整体的寫法,就變的相當的精簡。
package com.asuscomm.mahaljsp.handlertest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//原始的方法val job=Runnable{for(i in 1..100){println("Thomas : 第一個員工 : %d, %s".format(i, Thread.currentThread().name))Thread.sleep(1000)}}val thread=Thread(job)//產生一個執行緒,並將工作丟進去thread.start()//一定要用start()啟動執行緒
//精簡寫法
Thread{//匿名物件Anonymous
for(i in 1..100){
println("Thomas : 第二個員工 : %d, %s".format(i, Thread.currentThread().name))
Thread.sleep(1000)
}
}.start()
}
}
UI與執行緒
首先在畫面上佈局二個TextView, 名稱分別為 txt1及txt2, 再佈局一顆Button, 程式碼如下
fun btnClick(view: View){
Thread{
for(i in 1..100000){
txt1.text=i.toString()
Thread.sleep(10)
}
}.start()
Thread{
for(i in 1..100000){
txt2.text=i.toString()
Thread.sleep(1)
}
}.start()
}
當按下Button後,畫面上的二個TexeView就會一直跑,但跑到一半,就會出現
The specified message queue synchronization barrier token has not been posted or has already been removed. 意思是說要列印的訊息還沒被執行就已經移除了。另外也可能會出現
Only the original thread that created a view hierarchy can touch its views. 意思是只有原先建立UI的執行緒才可以更改UI。這還真的是太靠北了,不就是讓TextView定時的變更裏面的值而以嗎,怎麼問題一大堆。
請熟記一個原則 : UI主執行緒不可執行過於耗時的工作,而新執行緒不可變動UI。
所以上面會出錯,是因為使用新的執行緒去變更TextView的 text。要解決這問題,有二種方法,第一個為runOnUiThread,第二個為Handler
runOnUiThread
當新執行緒執行到某一個時間點,想要改變UI的內容時,就要調到runOnUiThread方法,此方法要塞入一個Runnable物件,如下第一種寫法是正統的方式,而藍色的部份則為精簡寫法。
Thread{//匿名物件Anonymous
for(i in 1..100000){
runOnUiThread(Runnable {
txt1.text = i.toString()
})
Thread.sleep(1000)
}
}.start()
//底下為精簡寫法
Thread{//匿名物件Anonymous
for(i in 1..100000){
runOnUiThread {
txt2.text = i.toString()
}
Thread.sleep(100)
}
}.start()
runOnUiThread 是跟系統說,等UI主執行緒有空時,才執行Runnable物件裏的東西。這個方法的好處是簡單易寫,但缺點是UI有空才執行。那如果沒空呢,可能會跳過某些訊息喔。
比如新執行緒印出 1, 但此時UI沒空,而新執行緒再度印出2,3,4。到了4時,UI才有空。好了,這時UI就把4取出放入TextView內。那麼,只要我們眼睛夠快,就可以看到TextView由1直接變成4,而2跟3都不會顯示出來。
下面的測試,讓新執行緒高速執行而不睡覺。而顯示的功能由UI主執行緒來執行,順便將值也記錄在ls 的ArrayList中。結果新執行緒跑完了1000次後,UI主執行緒只記錄到61個值,也就是說,只有61個值有被顯示出來。
val ls=ArrayList<Int>()
Thread{//匿名物件Anonymous
for(i in 1..1000){
runOnUiThread(Runnable {
txt1.text = i.toString()
ls.add(i)
})
}
println("Thomas : ls size : "+ ls.size)
}.start()
結果 : 61
如果只是為了顯示畫面,因為眼睛跟不上,所以就算有漏掉畫面也沒關係。但如果UI主執行緒還雜夾著其他的運算時,就千萬別用這種方式。
Handler
第二種方式為Handler。首先產生一個Handler(Looper, Handler.Callback),Handler.Callback裏面撰寫著當Handler接收到訊息時要執行的工作。
至於是那一個執行緒執行,則由Looper決定。在本例中,因為Handler是宣告在 MainActivity 裏,所以Looper.myLooper() 就是 MainActivity 的執行緒,也就是UI主執行緒。
在新執行緒中,使用 handler.sendMessage 或 handler.sendEmptyMessage 將訊息送出。
class MainActivity : AppCompatActivity() {
val ls=ArrayList<Int>()
val handler=Handler(
Looper.myLooper()!!, Handler.Callback{
val bundle=it.data
val i=bundle.getInt("value")
txt1.text=i.toString()
ls.add(i)
println("Thomas : ls size " + ls.size)
false
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun btnClick(view: View){
Thread{//匿名物件Anonymous
for(i in 1..1000){
val bundle=Bundle()
bundle.putInt("value",i)
val msg=Message()
msg.data=bundle
handler.sendMessage(msg)
}
}.start()
}
}
當handler在新執行緒中調用 sendMessage() 時,其實是新執行緒將 “訊息包” 送入(push)到等後的駐列(Queue)中。所以就算目前UI執行緒沒空執行,訊息都一直被保存著。
等到 UI 有空時,UI主執行緒從等後駐列中將訊息 POP 出來慢慢消化執行。所以使用Handler可以確保每一個訊息都會被執行,而不會有漏掉的情形,這種方式才是首選。
HandlerThread
HandlerThrad本身就是Thread的子類別,也就是說HandlerThread就是一個執行緒,但HandlerThread本身多了個 looper的物件變數可以供外界使用。
先回想一下Thread的用途。假如有一個很耗時的工作要完成,就不能使用UI主執行緒,而是需要產生一個新的執行緒,然後把要執行的工作丟給Thread。
那如果目前我們還不知道要作什麼工作呢?? 或者是說,想在某條件下才作A工作,另一條件下要作B工作,而且 A 跟 B 二個工作都必需由同一個執行緒執行完成,這時HandlerThread 的功能就出現了,先看一下底下的代碼
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val thread1=HandlerThread("thread1")
thread1.start()
val handler1=Handler(thread1.looper)
handler1.post(Runnable{
for(i in 1..100){
println("Thomas : %d, %s".format(i, Thread.currentThread().name))
Thread.sleep(1000)
}
})
}
}
首先產生一個HandlerThread的物件 thread1, 然後使用 start()讓thread1開始執行。只是目前還沒有任何工作,所以它會一直進入idle狀態。
接下來把要作的工作寫在Runnable物件中,再使用 thread1.post()將工作丟給thread1, 即會開始運作。
如果在第一份工作還沒作完,外面又丟了另一份工作給thread1,此時thread1 會先把第一份工作作完,才會開始執行第二份工作。