今天中午去吃飯的時候,zk問了我一個問題,“Android只能在UI線程更新UI 麼”,我的回答是“對”。然後zk讓我回去寫在子線程中更新UI,看會有什麼問題。
一、三個子線程更新UI
下午空閒的時候,就帶着zk的疑問,寫了這個DEMO,代碼如下:
package com.troy.nouithread;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private Button btNew;
private Button btNew2;
private Button btNew3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btNew = (Button)findViewById(R.id.btNew);
btNew2 = (Button)findViewById(R.id.btNew2);
btNew3 = (Button)findViewById(R.id.btNew3);
btNew.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
newThread();
}
});
btNew2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
newThread2();
}
});
btNew3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new TestThread().start();
}
});
}
private void newThread(){
new Thread(new Runnable() {
@Override
public void run() {
WindowManager windowManager = getWindowManager();
TextView textView = new TextView(getApplicationContext());
textView.setText("在工作線程更新UI");
textView.setTextColor(getResources().getColor(R.color.colorPrimary));
windowManager.addView(textView, new WindowManager.LayoutParams());
}
}).start();
}
private void newThread2(){
new Thread(new Runnable() {
@Override
public void run() {
// btNew.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
btNew.setText("選擇比努力重要");
}
}).start();
}
class TestThread extends Thread{
@Override
public void run() {
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
tx.setText("時間煮雨");
tx.setTextColor(getResources().getColor(R.color.white));
tx.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
tx.setGravity(Gravity.CENTER);
WindowManager wm = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
250, 150, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
wm.addView(tx, params);
Looper.loop();
}
}
}
主UI中含有三個Button,其點擊事件分別對應着三個不同的子線程。
btNew點擊事件,報錯信息是:
btNew2點擊事件,報錯信息是:
btNew3點擊事件,正常展示,在子線程中更新了UI。
二、Android UI 線程檢查機制
分析CalledFromWrongThreadException
在事件2中,出現的錯誤信息是:
CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.
線程能否刷新UI的關鍵在於ViewRoot是否屬於該線程。
首先,CalledFromWrongThreadException這個異常是由下面的代碼拋出的:
public final class ViewRootImpl implements ViewParent{
...
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(//這個請求來自於錯誤的線程
"Only the original thread that created a view hierarchy can touch its views.");
//只有最初創建視圖層次結構的線程纔可以接觸到這些視圖。
}
}
...
}
依次調用順序是:
-> android.widget.TextView.setText
-> android.widget.TextView.checkForRelayout
-> android.view.View.invalidate
-> android.view.ViewGroup.invalidateChild
-> android.view.ViewRootImpl.invalidateChildInParent
-> android.view.ViewRootImpl.invalidateChild
-> android.view.ViewRootImpl.checkThread
因此知道,調用btNew.setText()報CalledFromWrongThreadException(這個請求來自於錯誤的線程)的原因是,調用該方法所在的線程與btNew對應的ViewRoot所初始化的線程不是同一個線程。能否更新UI與是否是工作線程、主線程沒有關係。取決於該線程擁有自己的ViewRoot。
而btNew3事件中,通過 Looper.prepare(),在自己的線程中創建了WindowManager並與當前線程對應,因此可以更新UI。
三、在子線程中更新UI的五個策略
1、使用Handler
2、用Activity對象的runOnUiThread方法
3、View.post(Runnable r)
4、Broadcast子線程中發送廣播,主線程中接收廣播並更新UI
5、AsyncTask
參考致謝:
(1)、Android裏子線程真的不能刷新UI嗎?
(2)、爲什麼我們可以在非UI線程中更新UI
(3)、Android子線程中更新UI的幾種方法
(4)、Android View.post(Runnable )