• Android中线程间通信原理分析:Looper,MessageQueue,Handler


    自问自答的两个问题

    在我们去讨论Handler,Looper,MessageQueue的关系之前,我们需要先问两个问题:

    1.这一套东西搞出来是为了解决什么问题呢?

    2.如果让我们来解决这个问题该怎么做?

    以上者两个问题,是我最近总结出来的,在我们学习了解一个新的技术之前,最好是先能回答这两个问题,这样你才能对你正在学习的东西有更深刻的认识。

    第一个问题:google的程序员们搞出这一套东西是为了解决什么问题的?这个问题很显而易见,为了解决线程间通信的问题。我们都知道,Android的UI/View这一套系统是运行在主线程的,并且这个主线程是死循环的,来看看具体的证据吧。

    public final class ActivityThread {
    	public static void main(String[] args) {
    		
    		//...
    		
    		Looper.loop();
    
    		throw new RuntimeException("Main thread loop unexpectedly exited");
    	}
    }
    

    如上面的代码示例所示,ActivityThread.main()方法作为Android程序的入口,里面我省略了一些初始化的操作,然后就执行了一句Looper.loop()方法,就没了,再下一行就抛异常了。

    loop()方法里面实际上就是一个死循环,一直在执行着,不断的从一个MQ(MessageQueue,后面我都缩写成MQ了)去取消息,如果有的话,那么就执行它或者让它的发送者去处理它。

    一般来说,主线程循环中都是执行着一些快速的UI操作,当你有手touch屏幕的时候,系统会产生事件,UI会处理这些事件,这些事件都会在主线程中执行,并快速的响应着UI的变化。如果主线程上发生一些比较耗时的操作,那么它后面的方法就无法得到执行了,那么就会出现卡顿,不流畅。

    因此,Android并不希望你在主线程去做一些耗时的操作,这里对“耗时”二字进行朴素的理解就行了,就是执行起来需要消耗的时间比较多的操作。比如读写文件,小的文件也许很快,但你无法预料文件的大小,再比如访问网络,再比如你需要做一些复杂的计算等等。

    为了不阻碍主线程流畅的执行,我们就必须在需要的时候把耗时的操作放到其他线程上去,当其他线程完成了工作,再给一个通知(或许还带着数据)给到主线程,让主线程去更新UI什么的,当然了,如果你要的耗时操作只是默默无闻的完成就行了,并不需要通知UI,那么你完全不需要给通知给到UI线程。这就是线程间的通信,其他线程做耗时操作,完成了告诉UI线程,让它进行更新。为了解决这个问题,Android系统给我们提供了这样一套方案来解决。

    第二个问题:如果让我们来想一套方案来解决这个线程间通信的问题,该怎么做呢?
    先看看我们现在已经有的东西,我们有一个一直在循环的主线程,它实现起来大概是这个样子:

    public class OurSystem {
    	public static void main(String [] args) {
    		for (;;) {
    			//do something...
    		}
    	}
    }
    

    为什么主线程要一直死循环的执行呢?

    关于这一点,我个人并没有特别透彻的认知,但我猜测,对于有GUI的系统/程序,应该都有一个不断循环的主线程,因为这个GUI程序肯定是要跟人进行交互的,也就是说,需要等待用户的输入,比如触碰屏幕,动动鼠标,敲敲键盘什么的,这些事件肯定是硬件层先获得一个响应/信号,然后会不断的向上封装传递。

    如果说我们一碰屏幕,一碰鼠标,就开启一个新线程去处理UI上的变化,首先,这当然是可以的!UI在什么线程上更新其实都是可以的嘛,并不是说一定要在主线程上更新,这是系统给我设的一个套子。然后,问题也会复杂的多,如果我们快速的点击2下鼠标,那么一瞬间就开启了两个新线程去执行,那么这两个线程的执行顺序呢?两个独立的线程,我们是无法保证说先启动的先执行。

    所以第一个问题就是执行顺序的问题。

    第二个问题就是同步,几个相互独立的线程如果要处理同一个资源,那么造成的结果都是令人困惑,不受控制的。另一方面强行给所有的操作加上同步锁,在效率上也会有问题。

    为了解决顺序执行的问题,非常容易就想到的一种方案是事件队列,各种各样的事件先进入到一个队列中,然后有个东西会不断的从队列中获取,这样第一个事件一定在第二个事件之前被执行,这样就保证了顺序,如果我们把这个“取事件”的步骤放在一个线程中去做,那么也顺便解决了资源同步的问题。

    因此,对于GUI程序会有一个一直循环的(主)线程,可能就是这样来的吧。

    这是一个非常纯净的死循环,我们想要做一些事情的话,就得让它从一个队列里面获取一些事情来做,就像打印机一样。因此我们再编写一个消息队列类,来存放消息。消息队列看起来应该是这样:

    public class OurMessageQueue() {
    	private LinkedList<Message> mQueue = new LinkedList<Message>();
    	
    	// 放进去一条消息
    	public void enQueue() {
    		//...
    	}
    	
    	// 取出一条消息
    	public Message deQueue() {
    		//...
    	}
    	
    	// 判断是否为空队列
    	public boolean isEmpty() {
    		//...
    	}
    }
    

    接下来我们的循环就需要改造成能从消息队列里获取消息,并能够根据消息来做些事情了:

    public class OurSystem {
    	public static void main(String [] args) {
    		
    		// 初始化消息队列
    		OurMessageQueue mq = ...
    	
    		for (;;) {
    			if (!mq.isEmpty()) {
    				Message msg = mq.deQueue();
    				//do something...
    			}
    		}
    	}
    }
    

    现在我们假象一下,我们需要点击一下按钮,然后去下载一个超级大的文件,下载完成后,我们再让主线程显示文件的大小。
    首先,按一下按钮,这个事件应该会被触发到主线程来(具体怎么来的我还尚不清楚,但应该是先从硬件开始,然后插入到消息队列中,主线程的循环就能获取到了),然后主线程开启一个新的异步线程来进行下载,下载完成后再通知主线程来更新,代码看上去是这样的:

    // 脑补的硬件设备……
    public class OurDevice {
    	
    	// 硬件设备可能有一个回调
    	public void onClick() {
    	
    		// 先拿到同一个消息队列,并把我们要做的事情插入队列中
    		OurMessageQueue mq = ...
    		Message msg = Message.newInstance("download a big file");
    		mq.enQueue(msg);
    	}
    }
    

    然后,我们的主线程循环获取到了消息:

    public class OurSystem {
    	public static void main(String [] args) {
    		
    		// 初始化消息队列
    		OurMessageQueue mq = ...
    	
    		for (;;) {
    			if (!mq.isEmpty()) {
    				Message msg = mq.deQueue();
    				
    				// 是一条通知我们下载文件的消息
    				if (msg.isDownloadBigFile()) {
    				
    					// 开启新线程去下载文件
    					new Thread(new Runnable() {
    						void run() {
    							// download a big file, may cast 1 min...
    							// ...
    							// ok, we finished download task.
    							
    							// 获取到同一个消息队列
    							OurMessageQueue mq = ...
    							
    							// 消息入队
    							mq.enQueue(Message.newInstance("finished download"));
    						}
    					}).start();
    				}
    				
    				// 是一条通知我们下载完成的消息
    				if (msg.isFilishedDownload()) {
    					// update UI!
    				}
    			}
    		}
    	}
    }
    

    注意,主线程循环获取到消息的时候,显示对消息进行的判断分类,不同的消息应该有不同的处理。在我们获取到一个下载文件的消息时,开启了一个新的线程去执行,耗时操作与主线程就被隔离到不同的执行流中,当完成后,新线程中用同一个消息队列发送了一个通知下载完成的消息,主线程循环获取到后,里面就可以更新UI。

    这样就是一个我随意脑补的,简单的跨线程通信的方案。

    有如下几点是值得注意的:

    • 主线程是死循环的从消息队列中获取消息。
    • 我们要将消息发送到主线程的消息队列,我们需要通过某种方法能获取到主线程的消息队列对象
    • 消息(Message)的结构应该如何设计呢?

    Android中的线程间通信方案

    Looper

    android.os.Looper from Grepcode

    Android中有一个Looper对象,顾名思义,直译过来就是循环的意思,Looper也确实干了维持循环的事情。
    Looper的代码是非常简单的,去掉注释也就300多行。
    在官方文档的注释中,它推荐我们这样来使用它:

    class LooperThread extends Thread {
    	public Handler mHandler;
    
    	public void run() {
    		Looper.prepare();
    
    		mHandler = new Handler() {
    			public void handleMessage(Message msg) {
    			  // process incoming messages here
    			}
    		};
    
    		Looper.loop();
    	}
    }
    

    先来看看prepare方法干了什么:

    Looper.prepare()

    public static void prepare() {
    	prepare(true);
    }
    
    private static void prepare(boolean quitAllowed) {
    	if (sThreadLocal.get() != null) {
    		throw new RuntimeException("Only one Looper may be created per thread");
    	}
    	sThreadLocal.set(new Looper(quitAllowed));
    }
    

    注意prepare(boolean)方法中,有一个sThreadLocal变量,这个变量有点像一个哈希表,它的key是当前的线程,也就是说,它可以存储一些数据/引用,这些数据/引用是与当前线程是一一对应的,在这里的作用是,它判断一下当前线程是否有Looper这个对象,如果有,那么就报错了,"Only one Looper may be created per thread",一个线程只允许创建一个Looper,如果没有,就new一个新的塞进这个哈希表中。然后它调用了Looper的构造方法。

    Looper的构造方法

    private Looper(boolean quitAllowed) {
    	mQueue = new MessageQueue(quitAllowed);
    	mThread = Thread.currentThread();
    }
    

    Looper的构造方法中,很关键的一句,它new了一个MessageQueue对象,并自己维持了这个MQ的引用。

    此时prepare()方法的工作就结束了,接下来需要调用静态方法loop()来启动循环。

    Looper.loop()

    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
    
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
    
            msg.target.dispatchMessage(msg);
    
            //...
        }
    }
    

    loop()方法,我做了省略,省去了一些不关心的部分。剩下的部分非常的清楚了,首先调用了静态方法myLooper()获取一个Looper对象。

    public static Looper myLooper() {
        return sThreadLocal.get();
    }
    

    myLooper()同样是静态方法,它是直接从这个ThreadLocal中去获取,这个刚刚说过了,它就类似于一个哈希表,key是当前线程,因为刚刚prepare()的时候,已经往里面set了一个Looper,那么此时应该是可以get到的。拿到当前线程的Looper后,接下来,final MessageQueue queue = me.mQueue;拿到与这个Looper对应的MQ,拿到了MQ后,就开启了死循环,对消息队列进行不停的获取,当获取到一个消息后,它调用了Message.target.dispatchMessage()方法来对消息进行处理。

    Looper的代码看完了,我们得到了几个信息:

    • Looper调用静态方法prepare()来进行初始化,一个线程只能创建一个与之对应的LooperLooper初始化的时候会创建一个MQ,因此,有了这样的对应关系,一个线程对应一个Looper,一个Looper对应一个MQ。可以说,它们三个是在一条线上的。
    • Looper调用静态方法loop()开始无限循环的取消息,MQ调用next()方法来获取消息

    MessageQueue

    android.os.MessageQueue from Grepcode

    对于MQ的源码,简单的看一下,构造函数与next()方法就好了。

    MQ的构造方法

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
    

    MQ的构造方法简单的调用了nativeInit()来进行初始化,这是一个jni方法,也就是说,可能是在JNI层维持了它这个消息队列的对象。

    MessageQueue.next()

    Message next() {
        
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
    
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
    
            nativePollOnce(ptr, nextPollTimeoutMillis);
    
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
    		}
    
        }
    }
    

    next()方法的代码有些长,我作了一些省略,请注意到,这个方法也有一个死循环,这样做的效果就是,在Looper的死循环中,调用了next(),而next()这里也在死循环,表面上看起来,方法就阻塞在Looper的死循环中的那一行了,知道next()方法能返回一个Message对象出来。

    简单浏览MQ的代码,我们得到了这些信息:

    • MQ的初始化是交给JNI去做的
    • MQ的next()方法是个死循环,在不停的访问MQ,从中获取消息出来返回给Looper去处理。

    Message

    android.os.Message from Grepcode

    Message对象是MQ中队列的element,也是Handler发送,接收处理的一个对象。对于它,我们需要了解它的几个成员属性即可。

    Message的成员变量可以分为三个部分:

    • 数据部分:它包括what(int), arg1(int), arg2(int), obj(Object), data(Bundle)等,一般用这些来传递数据。
    • 发送者(target):它有一个成员变量叫target,它的类型是Handler的,这个成员变量很重要,它标记了这个Message对象本身是谁发送的,最终也会交给谁去处理。
    • callback:它有一个成员变量叫callback,它的类型是Runnable,可以理解为一个可以被执行的代码片段。

    Handler

    android.os.Handler from Grepcode

    Handler对象是在API层面供给开发者使用最多的一个类,我们主要通过这个类来进行发送消息与处理消息。

    Handler的构造方法(初始化)

    通常我们调用没有参数的构造方法来进行初始化,使用起来大概是这样的:

    Handler mHandler = new Handler() {
        handleMessage(Message msg) {
            //...
        }
    }
    

    没有参数的构造方法最终调用了一个两个参数的构造方法,它的部分源码如下:

    public Handler(Callback callback, boolean async) {
        //...
    	mLooper = Looper.myLooper();
    	if (mLooper == null) {
    		throw new RuntimeException(
    			"Can't create handler inside thread that has not called Looper.prepare()");
    	}
    	mQueue = mLooper.mQueue;
    	mCallback = callback;
    	mAsynchronous = async;
    }
    

    注意到,它对mLooper成员变量进行了赋值,通过Looper.myLooper()方法获取到当前线程对应的Looper对象。上面已经提到过,如果Looper调用过prepare()方法,那么这个线程对应了一个Looper实例,这个Looper实例也对应了一个MQ,它们三者之间是一一对应的关系。

    然后它通过mLooper对象,获取了一个MQ,存在自己的mQueue成员变量中。

    Handler的初始化代码说明了一点,Handler所初始化的地方(所在的线程),就是从将这个线程对应的Looper的引用赋值给Handler,让Handler也持有。

    对于主线程来说,我们在主线程的执行流中,new一个Handler对象,Handler对象都是持有主线程的Looper(也就是Main Looper)对象的。

    同样的,如果我们在一个新线程,不调用Looper.prepare()方法去启动一个Looper,直接new一个Handler对象,那么它就会报错。像这样

    new Thread(new Runnable() {
            @Override
            public void run() {
                //Looper.prepare(); 
    
                //因为Looper没有初始化,所以Looper.myLooper()不能获取到一个Looper对象
                Handler h = new Handler();
                h.sendEmptyMessage(112);
    
            }
         }).start();
    

    以上代码运行后会报错:

    java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
    

    小结Handler的初始化会获取到当前线程的Looper对象,并通过Looper拿到对应的MQ对象,如果当前线程的执行流并没有执行过Looper.prepare(),则无法创建Handler对象。

    Handler.sendMessage()

    sendMessage消息有各种各样的形式或重载,最终会调用到这个方法:

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    	MessageQueue queue = mQueue;
    	if (queue == null) {
    		RuntimeException e = new RuntimeException(
    				this + " sendMessageAtTime() called with no mQueue");
    		Log.w("Looper", e.getMessage(), e);
    		return false;
    	}
    	return enqueueMessage(queue, msg, uptimeMillis);
    }
    

    它又调用了enqueueMessage方法:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    	msg.target = this;
    	if (mAsynchronous) {
    		msg.setAsynchronous(true);
    	}
    	return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    注意到它对Messagetarget属性进行了赋值,这样这条消息就知道自己是被谁发送的了。然后将消息加入到队列中。

    Handler.dispatchMessage()

    Message对象进入了MQ后,很快的会被MQ的next()方法获取到,这样Looper的死循环中就能得到一个Message对象,回顾一下,接下来,就调用了Message.target.dispatchMessage()方法对这条消息进行了处理。

    public void dispatchMessage(Message msg) {
    	if (msg.callback != null) {
    		handleCallback(msg);
    	} else {
    		if (mCallback != null) {
    			if (mCallback.handleMessage(msg)) {
    				return;
    			}
    		}
    		handleMessage(msg);
    	}
    }
    
    private static void handleCallback(Message message) {
    	message.callback.run();
    }
    
    public void handleMessage(Message msg) {
        //这个方法是空实现,让客户端程序员去覆写实现自己的逻辑
    }
    

    dispatchMessage方法有两个分支,如果callbackRunnable)不是null,则直接执行callback.run()方法,如果callbacknull,则将msg作为参数传给handleMessage()方法去处理,这样就是我们常见的处理方法了。

    Message.target与Handler

    特别需要注意Message中的target成员变量,它是指向自己的发送者,这一点意味着什么呢?

    意味着:一个有Looper的线程可以有很多个Handler,这些Handler都是不同的对象,但是它们都可以将Message对象发送到同一个MQ中,Looper不断的从MQ中获取这些消息,并将消息交给它们的发送者去处理。一个MQ是可以对应多个Handler的(多个Handler都可以往同一个MQ中消息入队)。

    下图可以简要的概括下它们之间的关系。
    Looper,MessageQueue,Handler,Message

    总结

    • Looper调用prepare()进行初始化,创建了一个与当前线程对应的Looper对象(通过ThreadLocal实现),并且初始化了一个与当前Looper对应的MessageQueue对象。
    • Looper调用静态方法loop()开始消息循环,通过MessageQueue.next()方法获取Message对象。
    • 当获取到一个Message对象时,让Message的发送者(target)去处理它。
    • Message对象包括数据,发送者(Handler),可执行代码段(Runnable)三个部分组成。
    • Handler可以在一个已经Looper.prepare()的线程中初始化,如果线程没有初始化Looper,创建Handler对象会失败。
    • 一个线程的执行流中可以构造多个Handler对象,它们都往同一个MQ中发消息,消息也只会分发给对应的Handler处理。
    • Handler将消息发送到MQ中,Messagetarget域会引用自己的发送者,Looper从MQ中取出来后,再交给发送这个MessageHandler去处理。
    • Message可以直接添加一个Runnable对象,当这条消息被处理的时候,直接执行Runnable.run()方法。
  • 相关阅读:
    IntelliJ IDEA maven库下载依赖包速度慢的问题
    安装 PHP
    秒杀怎么样才可以防止超卖?基于mysql的事务和锁实现
    MySQL 使用自增ID主键和UUID 作为主键的优劣比较详细过程(从百万到千万表记录测试)
    架构师之路16年精选50篇
    基于ELK和Python搭建简单的监控告警系统
    MySQL5.7 利用keepalived来实现mysql双主高可用方案的详细过程
    在线数据迁移
    jedisLock—redis分布式锁实现
    Intellij IDEA 最新旗舰版注册激活破解(2018亲测,可用)
  • 原文地址:https://www.cnblogs.com/kross/p/5283027.html
Copyright © 2020-2023  润新知