Android子線程更新UI的方法總結

  消息機制,對於Android開發者來講,應該是很是熟悉。對於處理有着大量交互的場景,採用消息機制,是再好不過了。有些特殊的場景,好比咱們都知道,在Android開發中,子線程不能更新UI,而主線程又不能進行耗時操做,一種經常使用的處理方法就是,在子線程中進行耗時操做,完成以後發送消息,通知主線程更新UI。或者使用異步任務,異步任務的實質也是對消息機制的封裝。html

  關於子線程到底能不能更新UI這個問題,以前看到一篇文章頗有趣,讓我對這個問題也有了新的認識,那麼我也來寫個簡單例子測試下,佈局文件以下:java

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.joy.messagetest.MainActivity">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Hello World!" />

</RelativeLayout>

  佈局中只有一個TextView,java代碼以下:android

package com.example.joy.messagetest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private TextView mTvTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mTvTest.setText("子線程能夠更新UI");
            }
        }).start();
    }

    private void initView() {
        mTvTest = (TextView) findViewById(R.id.tv_test);
    }
}

  代碼也很簡單,我開啓子線程,在子線程中,將 TextView 內容設置爲「子線程能夠更新UI」,而在佈局文件中,TextView 的 text 爲「Hello world!」,那麼如今運行程序,可能會出現的結果有三種:安全

  • 程序崩了,拋異常了:說明子線程不能更新UI
  • 程序正常運行,textview 上面顯示「Hello World!」:說明子線程不能更新UI
  • 程序正常運行,textview 上面顯示「子線程能夠更新UI」:說明子線程能夠更新UI

  運行程序,結果以下:app

                      

  這說明什麼?從結果看,子線程更新UI成功了。真的是這樣嗎?我本身也不相信,趕忙再驗證一遍。此次我在佈局文件中添加一個Button,修改後的佈局文件以下:異步

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.joy.messagetest.MainActivity">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Hello World!" />

    <Button
        android:id="@+id/btn_test1"
        android:layout_below="@id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="子線程更新UI測試"/>

</RelativeLayout>

  同時修改java代碼:ide

package com.example.joy.messagetest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mTvTest;
    private Button mBtnTest1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mTvTest.setText("子線程能夠更新UI");
            }
        }).start();
    }

    private void initView() {
        mTvTest = (TextView) findViewById(R.id.tv_test);
        mBtnTest1 = (Button) findViewById(R.id.btn_test1);
        mBtnTest1.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.btn_test1:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTvTest.setText("子線程真的能夠更新UI嗎?");
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

  咱們增長了一個button,點擊button,啓動一個子線程,在子線程中將 textview 的顯示內容改成 「子線程真的能夠更新UI嗎?」。一樣按照前面的分析,咱們再來驗證一下。從新運行程序, textview 顯示 「子線程能夠更新UI」, 而後咱們點擊 button。結果以下:oop

                    

  怎麼回事?程序崩了。仔細看,你會發現,點擊 button 後 textview 的內容實際上是發生了更改的,而後程序崩潰了。查看日誌,拋出以下異常:佈局

AndroidRuntime: FATAL EXCEPTION: Thread-176
                     Process: com.example.joy.messagetest, PID: 11201
                     android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                              at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
                              at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)

  此次終於看到了熟悉的錯誤日誌,只有初始建立視圖的線程才能觸碰這些視圖,也就是說只有主線程才能更新UI。經過下面一行post

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)

咱們能發現點端倪:在 framework/base/core/java/android/view/ViewRootImpl.java 中有一個方法 checkThread ,源碼以下:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

  該異常就是在這裏觸發的。對於這個問題,若是你還想深刻下去探究清楚,能夠跟進去 RTFSC ! 這裏推薦一篇文章,Android中子線程真的不能更新UI嗎?

  說了這麼多,其實子線程是不能直接更新UI的。Android實現View更新有兩組方法,分別是invalidate和postInvalidate。前者在UI線程中使用,後者在非UI線程即子線程中使用。換句話說,在子線程調用 invalidate 方法會致使線程不安全。熟悉View工做原理的人都知道,invalidate 方法會通知 view 當即重繪,刷新界面。做一個假設,如今我用 invalidate 在子線程中刷新界面,同時UI線程也在用 invalidate 刷新界面,這樣會不會致使界面的刷新不能同步?這就是invalidate不能在子線程中使用的緣由。

  可是咱們能夠在子線程執行某段代碼,須要更新UI的時候去通知主線程,讓主線程來更新。如何作呢?常見的方法,除了前面提到的在UI線程建立Handler,在子線程發送消息到UI線程,通知UI線程更新UI,還有 handler.post(Runnable r)、 view.post(Runnable r)、activity.runOnUIThread(Runnable r)等方法。跟進去看源碼,發現其實它們的實現原理都仍是同樣,最終都是經過Handler發送消息來實現的。下面分別用這幾種方法實現一下在子線程更新UI。

  修改後的佈局文件代碼以下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.joy.messagetest.MainActivity">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Hello World!" />

    <Button
        android:id="@+id/btn_test1"
        android:layout_below="@id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="子線程更新UI測試"/>

    <Button
        android:id="@+id/btn_test2"
        android:layout_below="@id/btn_test1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Handler發送消息"/>

    <Button
        android:id="@+id/btn_test3"
        android:layout_below="@id/btn_test2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Handler.Post"/>

    <Button
        android:id="@+id/btn_test4"
        android:layout_below="@id/btn_test3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="View.Post"/>

    <Button
        android:id="@+id/btn_test5"
        android:layout_below="@id/btn_test4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Activity.RunOnUIThread"/>

</RelativeLayout>

   java代碼以下:

package com.example.joy.messagetest;

import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mTvTest;
    private Button mBtnTest1;
    private Button mBtnTest2;
    private Button mBtnTest3;
    private Button mBtnTest4;
    private Button mBtnTest5;


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 100) {
                mTvTest.setText("由Handler發送消息");
            }
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mTvTest.setText("子線程能夠更新UI");
            }
        }).start();
    }

    private void initView() {
        mTvTest = (TextView) findViewById(R.id.tv_test);
        mBtnTest1 = (Button) findViewById(R.id.btn_test1);
        mBtnTest2 = (Button) findViewById(R.id.btn_test2);
        mBtnTest3 = (Button) findViewById(R.id.btn_test3);
        mBtnTest4 = (Button) findViewById(R.id.btn_test4);
        mBtnTest5 = (Button) findViewById(R.id.btn_test5);
        mBtnTest1.setOnClickListener(this);
        mBtnTest2.setOnClickListener(this);
        mBtnTest2.setOnClickListener(this);
        mBtnTest3.setOnClickListener(this);
        mBtnTest4.setOnClickListener(this);
        mBtnTest5.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_test1:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTvTest.setText("子線程真的能夠更新UI嗎?");
                    }
                }).start();
                break;
            case R.id.btn_test2:   //經過發送消息
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mHandler.sendEmptyMessage(100);
                    }
                }).start();
                break;
            case R.id.btn_test3:  //經過Handler.post方法
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mTvTest.setText("handler.post");
                            }
                        });
                    }
                }).start();
                break;
            case R.id.btn_test4:  //經過 view.post方法
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTvTest.post(new Runnable() {
                            @Override
                            public void run() {
                                mTvTest.setText("view.post");
                            }
                        });
                    }
                }).start();
                break;
            case R.id.btn_test5:  //經過 activity 的 runOnUiThread方法
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mTvTest.setText("runOnUIThread");
                            }
                        });
                    }
                }).start();
                break;
            default:
                break;
        }
    }
}

  運行一下效果以下圖:

                    

  以上就是消息機制最多見的應用場景——在子線程通知主線程更新UI的幾種用法。

相關文章
相關標籤/搜索