联合学习 Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系

 

1. 首先我们通过一个实例案例来引出一个异常:

(1)布局文件activity_main.xml:

 <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"
tools:context=".MainActivity" > <TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" /> </RelativeLayout>

(2)MainActivity.java:

package com.itheima.testthread;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.EditText;
import android.widget.TextView; public class MainActivity extends Activity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
System.out.println(Thread.currentThread().getName());
final TextView tv = (TextView) findViewById(R.id.tv);
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
tv.setText("哈哈哈"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
} }

(3)布署程序到模拟器上,出现如下效果,程序直接stop;并且直接报出异常错误: 


报错:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

意思:从错误线程调用的异常。谁创建的view,谁才能修改编辑view,只有主线程才可以修改view(所有的View都是主线程创建的)。

上面的报错是ViewRootImpl对UI操作做了验证,这个验证工作是由ViewRootImpl的checkThread方法来完成的,如下所示:

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

总结:上面报这个CalledFromWrongThreadException错误,是因为只有主线程才能修改View更新UI,但是我们这里却自己新建一个线程new Thread()其中run()的内容涉及到更新UI,这是不允许的,所有才会出现这样的错误警告。

但是往往我们编写程序的时候设计多个线程需要控制UI显示,但是我们上面已经说过了UI更新只能交给主线程,为了解决这个矛盾,解放程序员,google开发出来消息机制,用来子线程和主线程通信,子线程要更新UI,就会发送消息数据给主线程,这样主线程就会更新相应的UI界面,从而解决这个矛盾。

(4)系统为什么不允许在子线程中访问UI呢 ?

这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那为什么系统不对UI控件的访问加锁呢? 缺点有两个:

首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,这是因为锁机制会阻塞某些线程的执行。

鉴于这两个缺点,最简单且高效的方法就是采用单线程模型去处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。

2. 消息机制的原理和实现

消息机制实现逻辑图:

  这里handler负责把外面的子线程的消息发到message queue消息队列之中,然后looper会以无限循环的形式查看message queue里面是不是有新消息,如果有的话,looper就把消息发送到handler,交给handler的handlemessage根据信息,更新UI,否则就一直等待。

  Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal不是线程,它的作用是可以在每个线程中存储数据。我们知道,Handler创建的时候会采用当前的线程的Looper来构造消息循环系统,那么Handler内部如何获取当前线程的Looper呢 ? 这就是要使用ThreadLocal了,ThreadLocal可以在不同线程中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。当然需要注意的是,线程默认是没有Looper,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时候就会初始化Looper,这也就是主线程中默认可以使用Handler的原因。

使用消息机制目的:

Android系统的主线程安全的系统,别的线程不可以修改ui线程的界面,如果子线程里面想要更新UI,就必须采用消息机制处理。

使用handler消息处理器编写代码步骤:

(1)在主线程里面声明消息处理器handler;

private Handler  handler = new  Handler()  { };

(2)子线程想要更新UI,利用消息机制

Message  msg  =  new Message();
msg.what 消息类型
msg.obj 具体消息携带的数据
handler.sendMessage(msg);

(3)系统内部有消息队列message queue和消息looper,轮询到消息,交给handler去处理:

(4)重写handler的方法处理消息:

public  void  handleMessage (Message msg)  {
//运行在主线程,更新UI };

接下来我们就修改上面的案例,就更好理解这个消息机制:

MainActivity.java:

 package com.itheima.testthread;

 import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.EditText;
import android.widget.TextView; public class MainActivity extends Activity {
private TextView tv;
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
// 运行在主线程,更新ui
String str = (String) msg.obj;
tv.setText(str);
EditText et; };
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
System.out.println(Thread.currentThread().getName());
tv = (TextView) findViewById(R.id.tv);
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
// tv.setText("哈哈哈"+i);
Message msg = new Message();
msg.obj = "哈哈哈" + i;
handler.sendMessage(msg);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
} }

acitivity_main.xml:

<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"
tools:context=".MainActivity" > <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" /> </RelativeLayout>

3. Hanlder之 --- 传递Message --- 传递Runnable对象

 (1)简介:

  一般来说在工作线程中执行耗时任务,当任务完成时,会返回UI线程,一般是更新UI。这时有两种方法可以达到目的。
  一种是:handler.sendMessage。发一个消息,,它是一个Message,再根据消息,执行相关任务代码。
  另一种是:handler.post(r)。r是要执行的任务代码,r是Runnable对象。意思就是说r的代码实际是在UI线程执行的。可以写更新UI的代码。(工作线程是不能更新UI的)

(2) 传递Message。用于接受子线程发送的数据, 并用此数据配合主线程更新UI。

  在Android中,对于UI的操作通常需要放在主线程中进行操作。如果在子线程中有关于UI的操作,那么就需要把数据消息作为一个Message对象发送到Handler的消息队列中,然后,有Handler中的handlerMessge方法处理传过来的数据信息,并操作UI。当然,Handler对象是在主线程中初始化的,以为它需要绑定在主线程的消息队列中。

类sendMessage(Message msg)方法实现发送消息的操作。在初始化Handler对象时重写的handleMessage方法来接收Messgae并进行相关操作:

 //Handler处理子线程消息代码示例:
public class Activity01 extends Activity
{
//声明ProgressBar对象
private ProgressBar m_ProgressBar;
private ProgressBar m_ProgressBar2;
private Button mButton01;
protected static final int GUI_STOP_NOTIFIER = 0x108;
protected static final int GUI_THREADING_NOTIFIER = 0x109; public int intCounter=0; @Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//设置窗口模式,,因为需要显示进度条在标题栏
requestWindowFeature(Window.FEATURE_PROGRESS);
setProgressBarVisibility(true);
setContentView(R.layout.main);
//取得ProgressBar
m_ProgressBar = (ProgressBar) findViewById(R.id.ProgressBar01);
m_ProgressBar2= (ProgressBar) findViewById(R.id.ProgressBar02);
mButton01 = (Button)findViewById(R.id.Button01);
m_ProgressBar.setIndeterminate(false);
m_ProgressBar2.setIndeterminate(false);
//当按钮按下时开始执行,
mButton01.setOnClickListener(new Button.OnClickListener()
{
@Override
public void onClick(View v)
{
// TODO Auto-generated method stub
//设置ProgressBar为可见状态
m_ProgressBar.setVisibility(View.VISIBLE);
m_ProgressBar2.setVisibility(View.VISIBLE);
//设置ProgressBar的最大值
m_ProgressBar.setMax(100);
//设置ProgressBar当前值
m_ProgressBar.setProgress(0);
m_ProgressBar2.setProgress(0);
//通过线程来改变ProgressBar的值
new Thread(new Runnable() {
public void run()
{
for (int i = 0; i < 10; i++)
{
try
{
intCounter = (i + 1) * 20;
Thread.sleep(1000);
if (i == 4)
{
Message m = new Message();
m.what = Activity01.GUI_STOP_NOTIFIER;
Activity01.this.myMessageHandler.sendMessage(m);
//将message发送到消息队列
break;
}
else
{
Message m = new Message();
m.what = Activity01.GUI_THREADING_NOTIFIER;
Activity01.this.myMessageHandler.sendMessage(m);
//将message发送到消息队列
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}).start();
}
});
} //通过匿名类复写Handler类中的handleMessage方法,用于接收传递到消息队列中的Message,并进行UI操作。
Handler myMessageHandler = new Handler()
{
// @Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
//ProgressBar已经是对大值
case Activity01.GUI_STOP_NOTIFIER:
m_ProgressBar.setVisibility(View.GONE);
m_ProgressBar2.setVisibility(View.GONE);
Thread.currentThread().interrupt();
break;
case Activity01.GUI_THREADING_NOTIFIER:
if (!Thread.currentThread().isInterrupted())
{
// 改变ProgressBar的当前值
m_ProgressBar.setProgress(intCounter);
m_ProgressBar2.setProgress(intCounter);
// 设置标题栏中前景的一个进度条进度值
setProgress(intCounter*100);
// 设置标题栏中后面的一个进度条进度值
setSecondaryProgress(intCounter*100);//
}
break;
}
super.handleMessage(msg);
}
};
}

以上的例子中,子线程只是对进度条的参数进行了变更,并将结果以message形式发送到消息队列中去,子线程的内部并未进行UI操作,而是在重写的Handler的handlerMessage方法中操作了UI界面。

(3)传递Runnable对象。用于通过Handler绑定的消息队列,安排不同操作的执行顺序。

  Handler对象在进行初始化的时候,会默认的自动绑定消息队列。利用类post方法,可以将Runnable对象发送到主线程的消息队列中,按照队列的机制按顺序执行不同的Runnable对象中的run方法。

 public class HandlerActivity extends Activity {
//声明两个按钮控件
private Button startButton = null;
private Button endButton = null; @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//根据控件的ID得到代表控件的对象,并未这两个按钮设置相应的监听器
startButton = (Button)findViewById(R.id.startButton);
startButton.setOnClickListener(new StartButtonListener());
endButton = (Button)findViewById(R.id.endButton);
endButton.setOnClickListener(new EndButtonListener());
} class StartButtonListener implements OnClickListener{
@Override
public void onClick(View v) {
//调用Handler的post方法,将要执行的线程对象添加到队列当中
handler.post(updateThread);
}
} class EndButtonListener implements OnClickListener{
@Override
public void onClick(View v) {
handler.removeCallbacks(updateThread);
}
} //创建一个Handler对象
Handler handler = new Handler();
//将要执行的操作写在线程对象的run方法当中
Runnable updateThread = new Runnable(){
@Override
public void run() {
System.out.println("UpdateThread");
//在run方法内部,执行postDelayed或者是post方法
handler.postDelayed(updateThread, 3000);
}
};
}

程序的运行结果就是每隔3秒钟,就会在控制台打印一行UpdateTread。这是因为实现了Runnable接口的updateThread对象进入了空的消息队列即被立即执行run方法,而在run方法的内部,又在3000ms之后将其再次发送进入消息队列中。

注意:

  post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程。

(4)多线程中传递Runnable对象

  这样对于那些简单的操作,似乎并不会影响。但是对于耗时较长的操作,当它被加入到消息队列中之后执行会占用很长的时间,以至于处于同一线程的其他操作无法继续执行,就会出现“假死”。为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程的上的消息队列中处理传过来的Runnable对象和消息。

具体操作方法如下:

 public class HandlerTest2 extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//打印了当前线程的ID
System.out.println("Activity-->" + Thread.currentThread().getId()); //生成一个HandlerThread对象
HandlerThread handlerThread = new HandlerThread("handler_thread"); //在使用HandlerThread的getLooper()方法之前,必须先调用该类的start(),同时开启一个新线程;
handlerThread.start(); //将由HandlerThread获取的Looper传递给Handler对象,即由处于另外线程的Looper代替handler初始化时默认绑定的消息队列来处理消息。——关键是这句
MyHandler myHandler = new MyHandler(handlerThread.getLooper());
Message msg = myHandler.obtainMessage(); //将msg发送到目标对象,所谓的目标对象,就是生成该msg对象的handler对象
Bundle b = new Bundle();
b.putInt("age", 20);
b.putString("name", "Jhon");
msg.setData(b);
msg.sendToTarget(); //将msg发送到myHandler
} //定义类,这里只是定义MyHandler一个类,并未和新线程相关起来
class MyHandler extends Handler{
public MyHandler(){
}
public MyHandler(Looper looper){
super(looper);
} @Override
public void handleMessage(Message msg) {
Bundle b = msg.getData();
int age = b.getInt("age");
String name = b.getString("name");
System.out.println("age is " + age + ", name is" + name);
System.out.println("Handler--->" + Thread.currentThread().getId());
System.out.println("handlerMessage");
}
}
}

这样,当使用sendMessage方法传递消息或者使用post方法传递Runnable对象时,就会把它们传递到与handler对象绑定的处于另外一个线程的消息队列中,它们将在另外的消息队列中被处理。而主线程还会在发送操作完成时候继续进行,不会影响当前的操作。

这里需要注意,这里用到的多线程并非由Runnable对象开启的,而是ThreadHandler对象开启的。Runnable对象只是作为一个封装了操作的对象被传递,并未产生新线程。

Android(java)学习笔记145:Handler消息机制的原理和实现的更多相关文章

  1. Android:日常学习笔记(9)———探究广播机制

    Android:日常学习笔记(9)———探究广播机制 引入广播机制 Andorid广播机制 广播是任何应用均可接收的消息.系统将针对系统事件(例如:系统启动或设备开始充电时)传递各种广播.通过将 In ...

  2. Android(java)学习笔记202:Handler消息机制的原理和实现

     联合学习 Android 异步消息处理机制 让你深入理解 Looper.Handler.Message三者关系   1. 首先我们通过一个实例案例来引出一个异常: (1)布局文件activity_m ...

  3. Android开发学习之路-Handler消息派发机制源码分析

    注:这里只是说一下sendmessage的一个过程,post就类似的 如果我们需要发送消息,会调用sendMessage方法 public final boolean sendMessage(Mess ...

  4. android菜鸟学习笔记26----Android广播消息及BroadcastReceiver

    1.广播类型: Android中的广播有两种类型:标准广播和有序广播.其中,标准广播是完全异步发送的广播,发出之后,几乎所有的广播接收者都会在同一时刻收到这条广播消息,因而,这种类型的广播消息是不可拦 ...

  5. java学习笔记13--反射机制与动态代理

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note13.html,转载请注明源地址. Java的反射机制 在Java运行时环境中,对于任意 ...

  6. Android全面解析之由浅及深Handler消息机制

    前言 很高兴遇见你~ 欢迎阅读我的文章. 关于Handler的博客可谓是俯拾皆是,而这也是一个老生常谈的话题,可见的他非常基础,也非常重要.但很多的博客,却很少有从入门开始介绍,这在我一开始学习的时候 ...

  7. Android消息传递之Handler消息机制

    前言: 无论是现在所做的项目还是以前的项目中,都会遇见线程之间通信.组件之间通信,目前统一采用EventBus来做处理,在总结学习EventBus之前,觉得还是需要学习总结一下最初的实现方式,也算是不 ...

  8. Android Handler消息机制源码解析

    好记性不如烂笔头,今天来分析一下Handler的源码实现 Handler机制是Android系统的基础,是多线程之间切换的基础.下面我们分析一下Handler的源码实现. Handler消息机制有4个 ...

  9. Android Handler 消息机制原理解析

    前言 做过 Android 开发的童鞋都知道,不能在非主线程修改 UI 控件,因为 Android 规定只能在主线程中访问 UI ,如果在子线程中访问 UI ,那么程序就会抛出异常 android.v ...

随机推荐

  1. sql语句之约束条件

    not null约束,需设置默认值 sex enum('male','female') not null default 'male' unique 约束,值唯一 单列唯一: create table ...

  2. 【网络爬虫】【java】微博爬虫(五):防止爬虫被墙的几个技巧(总结篇)

    爬虫的目的就是大规模地.长时间地获取数据,跟我们正常浏览器获取数据相比,虽然机理相差不大,但总是一个IP去爬网站,大规模集中对服务器访问,时间一长就有可能被拒绝.关于爬虫长时间爬取数据,可能会要求验证 ...

  3. java集合框架之HashCode

    参考http://how2j.cn/k/collection/collection-hashcode/371.html List查找的低效率 假设在List中存放着无重复名称,没有顺序的2000000 ...

  4. 20个Flutter实例视频教程-第07节: 毛玻璃效果制作

    视频地址: https://www.bilibili.com/video/av39709290/?p=7 博客地址: https://jspang.com/post/flutterDemo.html# ...

  5. C#简单的国际化

    1.新建一个资源文件夹,并在资源文件夹新建中英问的资源文件,如图: 2.中英文资源文档添加资源,如图: 3.Program.cs中添加根据系统语言确定中英文,这里默认为英文: using Intern ...

  6. C#项目中一些文件类型说明

    designer.cs  是窗体设计器生成的代码文件,作用是对窗体上的控件做初始化工作 (在函数InitializeComponent()中)VS2003以前都把这部分代码放到窗体的cs文件中,由于这 ...

  7. HDU - 1098 - Ignatius's puzzle - ax+by=c

    http://acm.hdu.edu.cn/showproblem.php?pid=1098 其实一开始猜测只要验证x=1的时候就行了,但是不知道怎么证明. 题解表示用数学归纳法,假设f(x)成立,证 ...

  8. Apple Mach-O Linker Warning 警告解决办法

    此警告解决办法: 项目名字 -> targets -> Build Settings -> search path

  9. opencv3.1 压缩并拼图

    必须有重叠才能拼,压缩越多,拼接越快 #include <opencv2\opencv.hpp> #include <opencv2\stitching.hpp> using ...

  10. 3.Python自我修炼(升仙中....整数,布尔值,字符串,for循环)

    python学习(整数,布尔值,字符串,for循环) 1.整数 ​ 在python3中所有的整数都是int类型. 但在python2中如果数据量比较大. 会使用long类型.但是在python3中不存 ...