消息机制,对于 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 成功了。真的是这样吗?我自己也不相信,赶紧再验证一遍。这次我在布局文件中添加一个 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 ! 这里推荐一篇文章,
说了这么多,其实子线程是不能直接更新 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 的几种用法。
来源: http://www.cnblogs.com/joy99/p/6121280.html