zoukankan      html  css  js  c++  java
  • Android开发之漫漫长途 Ⅶ——Android消息机制(Looper Handler MessageQueue Message)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!另外,本系列文章知识可能需要有一定Android开发基础和项目经验的同学才能更好理解,也就是说该系列文章面向的是Android中高级开发工程师。


    前言

    上一篇我们介绍了LeakCanary工具用来分析内存泄漏以及谈了下几种常见内存泄漏的表现和解决方法。本篇内容我们来分析Android的消息机制。我们为什么要介绍Android的消息机制呢,因为Android系统本质上来说就是一个消息驱动的系统。我们在开发中什么时候会用到Handler呢,工作年限较长的开发工程师应该对这个Handler很熟悉了,因为在早期的开发中,无论是网络请求刷新UI还是子线程耗时任务的通知的应用场景都能看到Handler的身影。现在Handler在我们的日常开发中少了一些,因为我们有了RxJava、Retrofit等对Handler进行了很精美的封装。但是理解Android的消息机制对于理解Android系统的运作包括那些开源框架的原理都有很大帮助。

    关于Android的消息机制网上也有好多文章,我本人也看了好多。但是不仅没有让我更清晰明了,反而让我陷入更深的迷惑。本篇的目的在于以一种相对更容易理解的方式来解释。

    我们先来模拟一个场景,在Activity中执行了耗时操作,耗时操作完成之后显示一个Toast。这种应用场景还是比较常见的。我们来模拟代码。

    public class MessageActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_message);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //模拟耗时操作
                        Thread.sleep(3* 60 * 1000);
                        //耗时操作完成之后显示一个通知
                        Toast.makeText(MessageActivity.this,"test",Toast.LENGTH_SHORT).show();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            }).start();
        }
    }
    

    我们来运行上面的代码。呦呵,崩溃了,我们查看日志得到以下信息。

    关于上面的崩溃我们稍后分析。

    ActivityThread

    既然讨论Android 消息机制,如果我们所有的操作都能在一个线程中完成貌似就不需要这个消息处理机制了,,但这又是不现实的,正是因为我们不能在一个线程中把所有的工作(网络请求、耗时操作、更新UI)在一个线程中完成,我们才有了多线程,多线程的互相协作才造就了我们这个Android欣欣向荣的世界。由此我们不得不说到我们Android App中的主线程(UI)线程,关于这个线程的叫法有很多。读者只需要知道不能在这个线程之外的线程直接对UI进行操作就行了。Android 4.0 以上甚至不能在主线程中(UI线程)中进行网络操作。否则的话会报android.os.NetworkOnMainThreadException,这个错误大家应该都见过把。那我们就从这个主线程(UI线程说起)

    public static void main(String[] args) {
        ......
    	//1 创建Looper 和 MessageQueue,本来该线程也是一个普通的线程,但是创建了Looper以及结合后文的Looper.loop()方法,使这个线程成为了Looper线程(读者可以简单的理解为拥有Looper的线程,而这个Looper就是Android消息处理机制的一部分)。
        Looper.prepareMainLooper();
    
      	//2 建立与AMS的通信
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
    
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
    
       	
        ......
    	//3 无限循环
        Looper.loop();
    	//可以看出来主线程也是在无限的循环的,异常退出循环的时候会报错. 
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    

    1 创建Looper 和 MessageQueue

    public final class Looper {
    	
     	......
        public static void prepare() {
            prepare(true);
        }
    	
    	//prepare 函数 
        private static void prepare(boolean quitAllowed) {
    		//判断sThreadLocal.get()是否为空,如果不为空说明已经为该线程设置了Looper,不能重复设置。
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
    		//如果sThreadLocal.get()为空,说明还没有为该线程设置Looper,那么创建Looper并设置
            sThreadLocal.set(new Looper(quitAllowed));
        }
    
       	//ActivityThread 调用Looper.prepareMainLooper();该函数调用prepare(false);
        public static void prepareMainLooper() {
            prepare(false);
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();
            }
        }
    
     
        public static Looper getMainLooper() {
            synchronized (Looper.class) {
                return sMainLooper;
            }
        }
    
    
    	
    
    	......
       
    }
    

    在这里呢有个静态变量sThreadLocal,它的定义如下

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    

    那么我们就得来讲解ThreadLocal这个类:

    线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。这里线程自己的本地存储区域存放是线程自己的Looper。简单的来说就是通过ThreadLocal来进行Looper的统一存储和读取。那么接着来看被ThreadLocal存储的对象Looper的构造函数。

    //Looper的构造函数
    private Looper(boolean quitAllowed) {
    	//这里创建了MessageQueue
        mQueue = new MessageQueue(quitAllowed);
    	
        mThread = Thread.currentThread();
    }
    

    这里创建了MessageQueue为后续的步骤做准备,MessageQueue可以简单理解为一个“队列”(其底层实际上是一个单向链表),之所以是打上引号的“队列”,是因为其并不是严格意义上的队列,而是一个单项链表,使用者可以根据节点的优先级等等插入该链表。链表上的节点就是Message。第①步 整个的结构图如下所示

    2 建立与AMS的通信

    关于这一部分内容必须得对Android Binder知识有相关了解才能更好的理解。我们下一篇就会讲解Android Binder,到时候我们在回来这里。

    3 无限循环

    在上面的工作中我们已经准备好Looper和MessageQueue,下面就有了两个问题,① Message从何而来,② Message如何处理。

    Message

    我们在讨论Message的来源以及如何处理之前,先来看一下Message类

    public class Message{
    	//消息码
    	public int what;
    	//handler
    	Handler target;
    	//下一级节点
    	Message next;
    	//消息发送的时间
    	long when;
    
    }
    

    上面的代码也从侧面证明了我们的MessageQueue是一个由Message组成的单向链表

    我们先来看Message如何处理,至于为什么,当然是保证因为我们的思路不被打断,我们先分析ActivityThread的最后Looper.loop()函数做了什么。

    ② Message如何处理

    我们来到了ActivityThread的最后一步Looper.loop()
    ActivityThread.java

    public static void loop() {
    	//得到Looper
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
    	//得到MessageQueue
        final MessageQueue queue = me.mQueue;
    
        ......
        for (;;) {//无限循环
            Message msg = queue.next(); // 取下一个Message 可能阻塞在这里
            if (msg == null) {
                //如果队列为空直接return直接结束了该方法,即循环结束
                return;
            }
    		......
           
            
            try {
    			//分发message到指定的target handler
                msg.target.dispatchMessage(msg);
                ......
            } finally {
               
            }
            
    		......
        }
    }
    

    主线程由此进入无限循环等待消息,有人看到这里就由疑问了,执行到for循环时,不就“卡死”在这个无限循环内了吗?其他的操作无法得到CPU怎么执行呢?关键点就在于queue.next()方法。
    为了更好的理解这个方法我们先来讲一下关于线程阻塞与唤醒的知识

    线程阻塞

    什么是阻塞呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。结合我们上面的代码。我们的代码运行Message msg = queue.next();这一句时,主线程可能一直阻塞在这里等待消息的到来(它去睡觉去了,也就是说我们的主线程,居然是大部分时间都在睡觉,心真大啊)。

    注:线程阻塞跟线程忙循环轮询是有本质区别的,不要听到线程阻塞就以为是CPU一直在无限循环轮询状态啊。线程阻塞是不占用CPU资源的,但是线程忙循环轮询就不一样了,将几乎占满CPU资源。什么是CPU资源,简单的来说CPU资源就是分配给程序的执行时间。

    线程唤醒

    要想把主线程活动起来一般有两种方式:一种是系统唤醒主线程,并且将点击事件传递给主线程;第二种是其他线程使用Handler向MessageQueue中存放了一条消息,导致loop被唤醒继续执行。在下面的Message从何而来中我们这里使用了hander向MessageQueue中存放了一条消息,导致loop被唤醒继续执行。

    ① Message从何而来

    public class MessageActivity extends AppCompatActivity {
    
        private Handler mHandler= new Handler(){
    		//处理消息
            @Override
            public void handleMessage(Message msg) {
                handleMsg(msg);
            }
    
        };
        private void handleMsg(Message msg) {
            switch (msg.what){
                case 0:
                    Toast.makeText(this,"成功",Toast.LENGTH_SHORT).show();
                    break;
            }
        }
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_message);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //模拟耗时操作
                        Thread.sleep(3*1000);
    					//发送消息
                        mHandler.sendEmptyMessage(0);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            }).start();
        }
    }
    

    我们经常使用上面的代码来做耗时操作,那么这里这里我们的猪脚就出场了,mHandler是Handler的对象。我们来看一下Handler类

    public class Handler { 
    	//handler类有个Looper
     	final Looper mLooper;
    	//handler类有个MessageQueue
        final MessageQueue mQueue;
    	//handler类有个Callback
        final Callback mCallback;
    	
    
    	public Handler() {//我们使用的是这一个
        	this(null, false);
    	}
    
    
        public Handler(Callback callback) {
            this(callback, false);
        }
    
       
        public Handler(Looper looper) {
            this(looper, null, false);
        }
    
      
        public Handler(Looper looper, Callback callback) {
            this(looper, callback, false);
        }
    
        public Handler(boolean async) {
            this(null, async);
        }
    
      
        public Handler(Callback callback, boolean async) {
            //这里获取主线程的Looper,Handler的mLooper指向ThreadLocal内的Looper对象
            mLooper = Looper.myLooper();
            if (mLooper == null) {
                throw new RuntimeException(
                    "Can't create handler inside thread that has not called Looper.prepare()");
            }
    		//这里获取主线程的Looper的MessageQueue,Handler的mQueue指向ThreadLocal内Looper对象内的MessageQueue对象
            mQueue = mLooper.mQueue;
            mCallback = callback;
            mAsynchronous = async;
        }
    
      
        public Handler(Looper looper, Callback callback, boolean async) {
            mLooper = looper;
            mQueue = looper.mQueue;
            mCallback = callback;
            mAsynchronous = async;
        }
    
    	
    
    }
    

    创建Handler 之后就调用 mHandler.sendEmptyMessage(0);发送消息(Handler的发送消息的方式有好多种,但这不是我们的重点),最终调用到Handler enqueueMessage 方法
    Handler.java

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    	设置msg.target 为当前Handler对象
        msg.target = this;
       	......
    	//调用MessageQueue的enqueueMessage()方法
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    我们再来看一下MessageQueue的enqueueMessage()
    MessageQueue.java

    boolean enqueueMessage(Message msg, long when) {
      	......
    
        synchronized (this) {
           ......
            
            msg.when = when;
            Message p = mMessages;
    		//检测当前头指针是否为空(队列为空)或者没有设置when 或者设置的when比头指针的when要前
            if (p == null || when == 0 || when < p.when) {
                //插入队列头部,并且唤醒线程处理msg
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
               // 几种情况要唤醒线程处理消息:1)队列是堵塞的 2)barrier,头部结点无target 3)当前msg是堵塞的
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // 将当前msg插入第一个比其when值大的结点前。
                prev.next = msg;
            }
    
            //调用Native方法进行底层操作,在这里把那个沉睡的主线程唤醒
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    

    我们的Handler在发送消息的时候把自身设置给了msg.target,发送消息并唤醒Looper,Looper被唤醒后便使用queue.next()取出Message,并根据msg.target进行派发。Handler整体过程如下图

    我们再稍微看下Handler的dispatchMessage方法
    Handler.java

    public void dispatchMessage(Message msg) {
    	
        if (msg.callback != null) {//判断有没有为Message设置callback(这里的callback是个Runnable接口,我们在为Message设置callback的时候需要自己实现run方法),如果设置了,那么调用Runnable实例的run方法
            handleCallback(msg);
        } else {
            if (mCallback != null) {//判断Handler的mCallback是否为空(这里的Handler是个Callback接口,我们在为Handler设置mCallback的时候需要自己实现handleMessage方法),如果设置了,那么调用Callback实例的handleMessage方法
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
    		//调用handleMessage方法
            handleMessage(msg);
        }
    }
    

    我们在创建Handler使用的是无法构造函数,并重写了handleMessage方法,所以我们的重写的handleMessage得到调用,弹出了Toast

    本篇总结

    本篇比较详细的介绍了Android的消息机制,不过有一部分内容需要其他的知识作为基础才能更好的理解。不过这不影响我们分析Android的消息机制的整个流程。我们在这里再梳理一下。

    1. 主线程准备Looper和MessageQueue

    2. 创建一个线程(因为下面我们进入死循环了,所以在这之前创建一个线程用来处理,这是个Binder线程)

    3. 主线程进入无限循环等待并处理消息。(这个消息可能是系统本身的消息,也有可能是我们自己的消息。在本例中分析的是我们自己创建的Handler发送的消息。)

    我们再上个整图

    这里呢我们呢是使用Activity的创建作为分析,因为这是Activity的起点。在注释第2步中的代码sendMessage(H.LAUNCH_ACTIVITY, r);与我们例子中 mHandler.sendEmptyMessage(0);并没有什么大的不同。

    现在也是揭晓我们文章开头的那个崩溃的秘密的时候了,相信读者也有答案了。没错,是因为我们在非UI线程中更新了UI,导致了异常。原因是我们在子线程没有Looper啊。你可以做出如下更改就不会有异常了。

    public class MessageActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_message);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //模拟耗时操作
                        Thread.sleep(3* 60 * 1000);
    					
    					//在子线程中更新UI之前,先准备一个Looper,与主线程相同
                        if (Looper.myLooper() != null){
                            Looper.prepare();
                        }
                        //耗时操作完成之后显示一个通知
                        Toast.makeText(MessageActivity.this,"test",Toast.LENGTH_SHORT).show();
    
                        //无限循环
                        Looper.loop();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            }).start();
        }
    }
    

    下篇预告

    好了,我们下一篇介绍Android的Binder,Binder是个大工程哈。。


    此致,敬礼

  • 相关阅读:
    c语言学习指针变量
    SQL字符串函数
    [转]HttpWebRequest使用注意(发生阻塞的解决办法)
    Image.FormFile的锁文件解决
    不联网在win8安装framework3.5
    metro app损坏,修复以及商店速度慢的解决方法
    关于C#使用来电通的来电通内部原理浅析
    winform模拟qq聊天界面的小功能textbox1输入自动跳到textbox2
    C#下的路由器后台登录
    dropbox文件夹路径丢失变回默认文件的解决方法
  • 原文地址:https://www.cnblogs.com/wangle12138/p/8032687.html
Copyright © 2011-2022 走看看