【Android 系统开发】_“核心技术”篇 -- Handler机制(用法)
开篇
引出问题
在 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) 的线程没有关系。