Android 机制篇 — 全面解析 Handler 机制(用法篇)

52次阅读

共计 8718 个字符,预计需要花费 22 分钟才能阅读完成。

开篇
引出问题
在 Android 开发中,我们经常会遇到这样一种情况:在 UI 界面上进行某项操作后要执行一段很耗时的代码,比如我们在界面上点击了一个“下载”按钮,那么我们需要执行网络请求,这是一个耗时操作。
为了保证不影响 UI 线程,所以我们会创建一个新的线程去执行我们的耗时的代码。当我们的耗时操作完成时,我们需要更新 UI 界面以告知用户操作完成了。
代码实例
比如,我们看以下代码:
package com.example.marco.handlerdemo;

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 Button.OnClickListener{

private TextView textView = null;
private Button btnDownload = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
btnDownload = findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
}

@Override
public void onClick(View v) {
DownLoadThread downLoadThread = new DownLoadThread();
downLoadThread.start();
}

class DownLoadThread extends Thread {
@Override
public void run() {
try {
System.out.println(“ 开始下载文件 ”);
Thread.sleep(5000);
System.out.println(“ 下载完成 ”);
MainActivity.this.textView.setText(“ 文件下载完成 ”); // 执行后,此处崩溃!!!
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
执行结果
E/AndroidRuntime: FATAL EXCEPTION: Thread-4
Process: com.example.marco.handlerdemo, PID: 14415
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7579)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1200)
at android.view.View.requestLayout(View.java:22156)
at android.view.View.requestLayout(View.java:22156)
at android.view.View.requestLayout(View.java:22156)
at android.view.View.requestLayout(View.java:22156)
at android.view.View.requestLayout(View.java:22156)
at android.view.View.requestLayout(View.java:22156)
at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3172)
at android.view.View.requestLayout(View.java:22156)
at android.widget.TextView.checkForRelayout(TextView.java:8553)
at android.widget.TextView.setText(TextView.java:5416)
at android.widget.TextView.setText(TextView.java:5272)
at android.widget.TextView.setText(TextView.java:5229)
at com.example.marco.handlerdemo.MainActivity$DownLoaderThread.run(MainActivity.java:36) // 错误开始
错误分析
执行之后,我们发现程序崩溃,并且出现了以上的错误:只有创建 View 的原始线程才能更新 View。为什么会出现这个错误,这个错误是什么意思?
出现这样错误的原因是 Android 中的 View 不是线程安全的,在 Android 应用启动时,会自动创建一个线程,即程序的主线程,主线程负责 UI 的展示、UI 事件消息的派发处理等等,因此主线程也叫做 UI 线程,textView 是在 UI 线程中创建的,当我们在 DownloadThread 线程中去更新 UI 线程中创建的 textView 时自然会报上面的错误。
Android 的 UI 控件是非线程安全的,不同的平台提供了不同的解决方案以实现跨线程更新 UI 控件,Android 为了解决这种问题引入了 Handler 机制。
Handler 引入
那么 Handler 到底是什么呢?Handler 是 Android 中引入的一种让开发者参与处理线程中消息循环的机制。
每个 Hanlder 都关联了一个线程,每个线程内部都维护了一个消息队列 MessageQueue,这样 Handler 实际上也就关联了一个消息队列。可以通过 Handler 将 Message 和 Runnable 对象发送到该 Handler 所关联线程的 MessageQueue(消息队列)中,然后该消息队列一直在循环拿出一个 Message,对其进行处理,处理完之后拿出下一个 Message,继续进行处理,周而复始。当创建一个 Handler 的时候,该 Handler 就绑定了当前创建 Hanlder 的线程。从这时起,该 Hanlder 就可以发送 Message 和 Runnable 对象到该 Handler 对应的消息队列中,当从 MessageQueue 取出某个 Message 时,会让 Handler 对其进行处理。
Handler 可以用来在多线程间进行通信,在另一个线程中去更新 UI 线程中的 UI 控件只是 Handler 使用中的一种典型案例,除此之外,Handler 可以做很多其他的事情。每个 Handler 都绑定了一个线程,假设存在两个线程 ThreadA 和 ThreadB,并且 HandlerA 绑定了 ThreadA,在 ThreadB 中的代码执行到某处时,出于某些原因,我们需要让 ThreadA 执行某些代码,此时我们就可以使用 Handler,我们可以在 ThreadB 中向 HandlerA 中加入某些信息以告知 ThreadA 中该做某些处理了。由此可以看出,Handler 是 Thread 的代言人,是多线程之间通信的桥梁,通过 Handler,我们可以在一个线程中控制另一个线程去做某事。
Handler 用法
Handler 提供了两种方式解决前面遇到的问题(在一个新线程中更新主线程中的 UI 控件),一种是通过 post 方法,一种是调用 sendMessage 方法。
post
代码实例
package com.example.marco.handlerdemo;

import android.os.Handler;
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 Button.OnClickListener{

private TextView textView = null;
private Button btnDownload = null;

private Handler uiHandler = new Handler();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
btnDownload = findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println(“MainThread id ” + Thread.currentThread().getId());
}

@Override
public void onClick(View v) {
DownLoadThread downLoadThread = new DownLoadThread();
downLoadThread.start();
}

class DownLoadThread extends Thread {
@Override
public void run() {
try {
System.out.println(“DownloadThread id ” + Thread.currentThread().getId());
System.out.println(“ 开始下载文件 ”);
Thread.sleep(5000);
System.out.println(“ 下载完成 ”);
// MainActivity.this.textView.setText(“ 文件下载完成 ”);
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(“RunnableThread id ” + Thread.currentThread().getId());
MainActivity.this.textView.setText(“ 文件下载完成 ”);
}
};
uiHandler.post(runnable);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
执行结果
2018-09-28 15:18:24.474 15864-15864/com.example.marco.handlerdemo I/System.out: MainThread id 2
2018-09-28 15:18:26.901 15864-15938/com.example.marco.handlerdemo I/System.out: DownloadThread id 2441
2018-09-28 15:18:26.901 15864-15938/com.example.marco.handlerdemo I/System.out: 开始下载文件
2018-09-28 15:18:31.902 15864-15938/com.example.marco.handlerdemo I/System.out: 下载完成
2018-09-28 15:18:31.906 15864-15864/com.example.marco.handlerdemo I/System.out: RunnableThread id 2
通过输出结果可以看出,Runnable 中的代码所执行的线程 ID 与 DownloadThread 的线程 ID 不同,而与主线程的线程 ID 相同,因此我们也由此看出在执行了 Handler.post(Runnable) 这句代码之后,运行 Runnable 代码的线程与 Handler 所绑定的线程是一致的,而与执行 Handler.post(Runnable) 这句代码的线程(DownloadThread)无关。
结果分析
我们在 Activity 中创建了一个 Handler 成员变量 uiHandler,Handler 有个特点,在执行 new Handler() 的时候,默认情况下 Handler 会绑定当前代码执行的线程,我们在主线程中实例化了 uiHandler,所以 uiHandle r 就自动绑定了主线程,即 UI 线程。当我们在 DownloadThread 中执行完耗时代码后,我们将一个 Runnable 对象通过 post 方法传入到了 Handler 中,Handler 会在合适的时候让主线程执行 Runnable 中的代码,这样 Runnable 就在主线程中执行了,从而正确更新了主线程中的 UI。
sendMessage
代码实例
package com.example.marco.handlerdemo;

import android.os.Handler;
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;

public class MainActivity extends AppCompatActivity implements Button.OnClickListener{

private TextView textView = null;
private Button btnDownload = null;

private Handler uiHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
System.out.println(“handleMessage thread id ” + Thread.currentThread().getId());
System.out.println(“msg.arg1:” + msg.arg1);
System.out.println(“msg.arg2:” + msg.arg2);
MainActivity.this.textView.setText(“ 文件下载完成 ”);
break;
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
btnDownload = findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(this);
System.out.println(“MainThread id ” + Thread.currentThread().getId());
}

@Override
public void onClick(View v) {
DownLoadThread downLoadThread = new DownLoadThread();
downLoadThread.start();
}

class DownLoadThread extends Thread {
@Override
public void run() {
try {
System.out.println(“DownloadThread id ” + Thread.currentThread().getId());
System.out.println(“ 开始下载文件 ”);
Thread.sleep(5000);
System.out.println(“ 下载完成 ”);
// 文件下载完成后更新 UI
Message msg = new Message();
msg.what = 1;
msg.arg1 = 123;
msg.arg2 = 456;

// 我们也可以通过给 obj 赋值 Object 类型传递向 Message 传入任意数据
//msg.obj = null;

// MainActivity.this.textView.setText(“ 文件下载完成 ”);
/* Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(“RunnableThread id ” + Thread.currentThread().getId());
MainActivity.this.textView.setText(“ 文件下载完成 ”);
}
};
uiHandler.post(runnable);*/
uiHandler.sendMessage(msg);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
执行结果
2018-09-28 16:16:16.613 19652-19652/? I/System.out: MainThread id 2
2018-09-28 16:16:19.431 19652-19692/com.example.marco.handlerdemo I/System.out: DownloadThread id 2493
2018-09-28 16:16:19.431 19652-19692/com.example.marco.handlerdemo I/System.out: 开始下载文件
2018-09-28 16:16:24.432 19652-19692/com.example.marco.handlerdemo I/System.out: 下载完成
2018-09-28 16:16:24.434 19652-19652/com.example.marco.handlerdemo I/System.out: handleMessage thread id 2
2018-09-28 16:16:24.434 19652-19652/com.example.marco.handlerdemo I/System.out: msg.arg1:123
2018-09-28 16:16:24.435 19652-19652/com.example.marco.handlerdemo I/System.out: msg.arg2:456
结果分析
通过 Message 与 Handler 进行通信的步骤是:

重写 Handler 的 handleMessage 方法,根据 Message 的 what 值进行不同的处理操作;
设置 Message 的 what 值:Message.what 是我们自定义的一个 Message 的识别码,以便于在 Handler 的 handleMessage 方法中根据 wha t 识别出不同的 Message,以便我们做出不同的处理操作;
设置 Message 的所携带的数据,简单数据可以通过两个 int 类型的 field:arg1 和 arg2 来赋值,并可以在 handleMessage 中读取;
如果 Message 需要携带复杂的数据,那么可以设置 Message 的 obj 字段,obj 是 Object 类型,可以赋予任意类型的数据;
我们通过 Handler.sendMessage(Message) 方法将 Message 传入 Handler 中让其在 handleMessage 中对其进行处理;
需要说明的是,如果在 handleMessage 中不需要判断 Message 类型,那么就无须设置 Message 的 what 值;而且让 Message 携带数据也不是必须的,只有在需要的时候才需要让其携带数据;如果确实需要让 Message 携带数据,应该尽量使用 arg1 或 arg2 或两者,能用 arg1 和 arg2 解决的话就不要用 obj,因为用 arg1 和 arg2 更高效。
由上我们可以看出,执行 handleMessage 的线程与创建 Handler 的线程是同一线程,在本示例中都是主线程。执行 handleMessage 的线程与执行 uiHandler.sendMessage(msg) 的线程没有关系。

参考 Blog
  01. https://blog.csdn.net/iisprin…

正文完
 0