Android 更新UI 只能在主線程?

今天中午去吃飯的時候,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 )

發佈了102 篇原創文章 · 獲贊 76 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章