欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android消息机制基本原理和使用

程序员文章站 2022-07-14 10:12:45
...

在Android开发过程中,我们常常遇到子线程更新UI的需求,例如在子线程进行耗时较长的下载,等下载完成之后,再去更新UI,提示用户下载完成,直接在子线程里更新UI,会得到报错提示:Only the original thread that created a view hierarchy can touch its views

Android老手知道这是怎么回事,并且知道解决方案,新手只能去网上找答案,网上的答案告诉我们报错是因为子线程不能直接更新UI线程,也就是主线程的控件,必须通过Android消息机制来更新。略微遗憾的是,网上的答案要么仅仅是罗列了可用方案的代码片段,不知道背后的原理是什么,要么是陷入源码分析的细节中,看完之后的感觉作者早就走远了,我们还在源代码细节中晕头转向。
为此,决定自己写一篇,如果能用通俗语言说明白,表明自己也会了。
Android消息机制基本原理和使用

2.为什么其他线程不能直接访问UI线程?

要回答这个问题,可以反过来想一想,如果子线程能够访问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消息机制也是类似操作。

我们先来看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里。

Android消息机制基本原理和使用

4.Android消息机制使用示例

看了消息机制的基本原理,现在来看一下如何使用。

  1. 首先实例化一个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;
            }
        }

    };
  1. 子线程调用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();
    }
}