消息機制,對於Android開發者來說,應該是非常熟悉。對於處理有着大量交互的場景,採用消息機制,是再好不過了。有些特殊的場景,比如我們都知道,在Android開發中,子線程不能更新UI,而主線程又不能進行耗時操作,一種常用的處理方法就是,在子線程中進行耗時操作,完成之後發送消息,通知主線程更新UI。或者使用異步任務,異步任務的實質也是對消息機制的封裝。
關於子線程到底能不能更新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!" /> </RelativeLayout>
佈局中只有一個TextView,java代碼如下:
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
運行程序,結果如下:
這說明什麼?從結果看,子線程更新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代碼:
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。結果如下:
怎麼回事?程序崩了。仔細看,你會發現,點擊 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。通過下面一行
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的幾種用法。
轉載自:https://www.cnblogs.com/joy99/p/6121280.html