Android消息机制基本原理和使用
在Android开发过程中,我们常常遇到子线程更新UI的需求,例如在子线程进行耗时较长的下载,等下载完成之后,再去更新UI,提示用户下载完成,直接在子线程里更新UI,会得到报错提示:Only the original thread that created a view hierarchy can touch its views。
Android老手知道这是怎么回事,并且知道解决方案,新手只能去网上找答案,网上的答案告诉我们报错是因为子线程不能直接更新UI线程,也就是主线程的控件,必须通过Android消息机制来更新。略微遗憾的是,网上的答案要么仅仅是罗列了可用方案的代码片段,不知道背后的原理是什么,要么是陷入源码分析的细节中,看完之后的感觉作者早就走远了,我们还在源代码细节中晕头转向。
为此,决定自己写一篇,如果能用通俗语言说明白,表明自己也会了。
要回答这个问题,可以反过来想一想,如果子线程能够访问UI线程会怎样?Android的UI线程是非线程安全,非线程安全是***不对数据进行加锁保护,多线程访问数据时,导致出现数据不一致或者数据污染情况***,例如两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑上的背景颜色属性为B,因此假如子线程能直接修改UI控件的话,会导致出现不可预期结果,那么UI线程为什么不设置成线程安全呢?要设置成线程安全需要加锁,即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用。但这样做会降低执行效率,如果一个子线程长时间占据着,其他线程只能干等,而UI界面直接面对用户,是Android的门面,首先要保证响应快,越快越好,因此UI线程只能是非线程安全。
既然是非线程安全,又不想让子线程随便修改,只能禁止子线程直接访问UI线程的控件。
既然子线程不能直接访问UI,那怎么实现更新UI呢?这就用到了Android消息机制。
3.Android消息机制什么是Android消息机制?说白了是跨线程传递信息机制,注意这里是跨线程,不是跨进程,Android里跨进程通信使用Binder,跨线程传递消息使用消息机制,为什么跨线程通信不使用Binder?
我们知道不同进程内存空间是隔离的,而一个进程里不同线程共享内存空间,用日常生活来理解就是,不同进程好比是不同的房子,一个进程是一间房子,而一个进程里包含不同线程,这些线程就像是不同的人,例如你,你父母,你老婆孩子,这些家庭成员同在一个房子里,也就是共享内存空间。
两个进程两间房子,相互之间通信使用电话沟通,而同在一个房间里的不同线程还使用电话沟通效率降低了,这也是为什么不同线程之间不用Binder的原因。
你可能会问,既然家庭成员同在一间房里时,直接面对面喊话不就行了吗,为什么还设计消息机制来沟通,不还是降低效率了吗?这又回到上节里说的,如果直接喊话就响应,让子线程直接访问UI线程,会导致混乱,这可以由日常生活的例子来理解,假设由你掌控遥控器,你老婆想看电影频道,你小孩想看动画频道,你父母想看戏剧频道,如果他们同时提需求,你到底是按哪个频道?为了解决这个混乱问题,你可以设计一个消息机制来应付。
你可以在桌面上放着一个传票叉,谁想看什么节目就把需求写在便笺,然后插到传票叉上,这样有个先后顺序,如果叉上有便笺,你取出来,看是写了什么,例如老婆便笺写着看10分钟电影,她先把便笺插到叉上,小朋友便笺写着看动画1分钟,也插到叉上,这样你先取出小朋友的便笺,然后给他看一分钟动画,一分钟后,再取出你老婆的便笺,换台到电影频道。这样虽然效率不高,但保证顺序不乱,当然传票叉是后来居上机制,这么设计容易挨打。
Android消息机制也是类似操作。
我们先来看Android消息机制里的各个角色名称。
Message
用于传递消息的载体,对应于上述例子的便笺;
MessageQueue
消息队列,对应上述例子的传票叉,家人看哪个频道的需求写到便笺上,然后插到传票叉上,形成消息队列。其实我们可以发现,正因为传票叉的存在,便笺才有先后顺序,你处理起来才不会乱,如果家人把便笺散放在桌子上,那和直接喊话没什么区别。同理,正是MessageQueue的存在,才解决了UI线程能够挨个处理每个子线程的而不错乱的问题。需要注意的是,从传票叉取出便笺的过程只能由你自己完成,其他人代劳还是会引发混乱,同理,消息队列也只能由接收消息的线程来处理;
Looper
有部电影英文名是《Looper》,只看英文名可能觉得这电影没什么知名度,说中文名称你应该就想起这部电影,这是由囧瑟夫主演的《环形使者》。那这部电影和Android消息机制里的Looper有啥关系?电影中杀手要干掉的目标是未来的自己,陷入死循环,消息机制里的Looper,是为了令程序进入无限循环,在这个循环里,不断检查MessageQueue是否有消息进来,如果有消息,则读取出来,并传递到Handler的handleMessage()方法中。注意,每个线程中只会有一个Looper对象。用遥控器的例子来理解Looper的话,Looper操作你进入不停检查传票叉是否有便笺插进来的状态;
Handler
在遥控器的例子中,你代表主线程,你的家人代表子线程,他们只需要在便笺上写上需求,再自己插到传票叉上,你取出来,这就模拟了消息的跨线程传递。但在程序中,需要使用Handler类来完成往传票叉上插便笺的过程,也就是子线程给主线程传递消息。在主线程中构建Handler类实例,子线程再调用这个Handler类的sendMessage()方法即可,为什么主线程构建的Handler类实例子线程能够使用?因为它们在同一个进程里,同一个进程内不同线程共享资源的,主线程创建的实例子线程当然可以访问到。说到这我们还可以这样理解Handler,Handler相当于主线程分发给不同线程的专用传话筒,子线程可以通过这个传话筒联络主线程。
我们可以使用一个示意图将消息机制里的几个角色的作用展示出来,如图所示。
其中,环形使者Looper启动线程进入无限循环,不停查看MessageQueue是否有消息进来,有消息进来使用Handler取出,线程1实例化出Handler,给线程2引用,线程2有消息发送的话,则使用Handler的sendMessage方法,发送消息,发送的消息进入MessageQueue里。
看了消息机制的基本原理,现在来看一下如何使用。
首先实例化一个Handler实例,如代码所示,我们看到,里面还重写了handleMessage方法,顾名思义,handleMessage是处理消息的意思,在上面介绍Handler时,只说了Handler是子线程传递消息的渠道,其实忘记说了取出消息也是通过Handler。Handler取到消息后,根据消息的类型做相应的动作,这里只是简单更新一下TextView的文字。另外,这里我们看不到Looper,MessageQueue,这些是幕后英雄,在分析源码时会涉及到。
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
//完成主界面更新,拿到数据
String data = (String)msg.obj;
tshow.setText(data);
break;
default:
break;
}
}
};
子线程调用Handler实例发送消息
private void UpdateTextView() {
new Thread(new Runnable(){
@Override
public void run() {
Message msg =new Message();
msg.obj = "子线程更新数据";//可以是基本类型,可以是对象,可以是List、map等;
//发送消息
mHandler.sendMessage(msg);
}
}).start();
}
然后,然后没有然后了,使用消息机制就这么简单,可以看出,因为使用简单,开发者使用消息机制完成子线程更新主线程其实没增加多少工作量。对于小白而言,虽然使用简单,但了解其背后的原理还是有必要的,不然不知道为什么这么使用,我开始接触消息机制时,只知道拷贝他人的代码,不知道原理,导致每次用到的时候,都是拷贝代码片段,然后替换成自己的变量,因为自己写不知道从哪里入手。
本文只是简单理解消息机制的各个角色,以及它们之间如何配合,下一篇博客,我们分析消息机制的源代码。
最后附上Activity的源代码。
package com.test.threaddemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView tshow;
private Button button;
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
//完成主界面更新,拿到数据
String data = (String)msg.obj;
tshow.setText(data);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tshow=findViewById(R.id.tvshow);
button=findViewById(R.id.bClick);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UpdateTextView();
}
});
}
@Override
protected void onResume() {
super.onResume();
}
private void UpdateTextView() {
new Thread(new Runnable(){
@Override
public void run() {
Message msg =new Message();
msg.obj = "子线程更新数据";//可以是基本类型,可以是对象,可以是List、map等;
//发送消息
mHandler.sendMessage(msg);
}
}).start();
}
}
作者:风行南方
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341