IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android——深入理解handler机制 -> 正文阅读

[移动开发]Android——深入理解handler机制

个人博客:haichenyi.com。感谢关注

1. 目录

前言

??Android程序是一个以消息驱动的程序,页面的跟新,Activity生命周期的变化,点击事件等等都与消息息息相关。

简单总结

??简单的理解Handler发送消息的流程:Handler发送消息(message)到MessageQueue,然后,Looper通过loop()方法循环从MessageQueue里面读取消息。然后,发送给对应的target(Handler)。

简单的理解send消息图
我们带着问题来理解这个流程,最后,我们在重新总结一下。辣么问题就来了:

  1. handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?
  2. handler发送的消息过程是什么样子的?
  3. Looper怎么读取消息的?
  4. handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?
  5. 怎么提升消息的优先级?
  6. 我们项目里面可能会用到的Looper.prepare(),Looper.loop(),这是做什么操作?

??我们结合源码一起来看一下这些问题:

Looper的区别:MainLooper和普通Looper

第一个问题,handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?我们都知道

Handler handler = new Handler(Looper.getMainLooper())

通过这个Looper.getMainLooper()方式得到得Handler,可以改变UI,其他的不行,这是为什么呢?我们都知道,UI线程才能改变UI。

ps:app的启动入口是在ActivityThread的main方法。

捡一些 (我看的懂的),呸,是主要的,跟我们聊的这个相关的位置贴出来,源码如下:

public static void main(String[] args) {
        ...
        //loop调用了一个准备MainLooper方法(按照方法的名字意思翻译的)
        Looper.prepareMainLooper();

        ...
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        ...
        //Looper调用loop()方法进入了循环模式
        Looper.loop();
        //如果走到这里,那就没有进入循环模式,就抛出异常了,异常字面意思很好理解,主线程的loop意外的退出了
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注释应该写的比较清楚了吧?这里我想说的是,main启动的时候,这个线程就是UI线程,这个是系统给规定的,只有在这个线程里面才能改变UI。

我们再来看看这个Looper.prepareMainLooper()里面做了什么操作

    @Deprecated
    public static void prepareMainLooper() {
        //调用prepare方法,传了一个false的Boolean值
        prepare(false);
        //锁
        synchronized (Looper.class) {
            //sMainLooper不等于null,就抛异常
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            //等于null,就把myLooper()方法的返回值赋值给sMainLooper
            sMainLooper = myLooper();
        }
    }
    
    //prepare,一个Boolean类型的参数,看名字意思应该是:是否允许退出
    private static void prepare(boolean quitAllowed) {
        //sThreadLocal.get()值不等于null,就抛异常
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //sThreadLocal.get()值等于null,就new一个Looper,放到sThreadLocal中
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
    //Looper的构造方法,我们此时主线程new的时候传的是false
    private Looper(boolean quitAllowed) {
        //新建了一个消息队列,MsgQueue,并且把这个boolean传进去了,赋值给mQueue变量
        mQueue = new MessageQueue(quitAllowed);
        //把当前线程赋值给了mThread变量
        mThread = Thread.currentThread();
    }
    //消息队列的构造方法,Boolean类型的参数,到这里就应该知道了,表示这个线程是否允许退出,true:允许退出。false:不允许退出
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        //nativeInit():native方法,不知道是啥,应该是一些需要的初始化
        mPtr = nativeInit();
    }
    //sThreadLocal.get()的值返回回去
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

注释都有了,最开始调用的prepare(false)方法,看完上面的注释,我大致串一下。

ps:说一下这个sThreadLocal变量,我也不知道怎么说,反正就是它的类型是:ThreadLocal,它就是一个普通的类,然后泛型是Looper,所以,这个类就是用来放Looper的。大致这么理解

我们再说回这个方法,主要就是,

  1. 先判断这个变量是不是空的,如果不是空的,就抛异常了,因为Looper是不允许我们自己手动创建的。
  2. 如果是空,就创建一个Looper,放进sThreadLocal变量里面;
  3. 然后,创建Looper的时候,顺道就创建了MessageQueue。主线程创建的MessageQueue是不允许主动退出的,如果消息队列退出了,退出app了。
  4. 并且,Looper的mQueue,mThread也都赋值好了,一个是消息队列,一个是当前线程(这两个变量用的也比较多)。

prepare()方法,到这里就说完了,我们再看剩下的代码,往上面翻一下,再看一下。

ps:sMainLooper变量,类型就是Looper

剩下的代码就是一个锁方法,

  1. 判断sMainLooper是不是不等于null,如果,不等于null就抛出了异常
  2. 如果等于null,就把上面创建的looper,赋值给sMainLooper

?我们Looper.getMainLooper()获取的Looper就是这个sMainLooper,也就是我们当前线程(UI线程)的Looper,我们只有绑定了这个looper的handler才能改变UI,因为,这个handler是在给UI线程传递消息。

??为什么不等于null就抛出异常了呢?因为sMainLooper只在系统的时候创建,不能在其他的时候创建,如果,在其他的时候创建,说明系统启动的时候没有创建Looper,那么,主线程就没法通信,这是有问题的。

第一个问题我说明白了吧?Looper.getMainLooper()获取到的是主线程的Looper,跟它绑定的handler能改变UI,没有跟它绑定的hanler都不能改变UI

handler发送的消息过程

第二个问题,handler发送的消息过程是什么样子的?

说到这里,我们先聊一下Message类

public final class Message implements Parcelable {
    public int what;
    public int arg1;
    public int arg2;
    public Object obj;
    ...
    public long when;
    @UnsupportedAppUsage
    /*package*/ Handler target;
    ...
    @UnsupportedAppUsage
    /*package*/ Message next;
    /** @hide */
    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;

??写代码这么长时间,我们发了那么多消息,是不是都没有仔细看看Message的成员变量?看看上面这几个变量。

  1. what,arg1,arg2,obj可能我们用的比较多。
  2. 这个long 类型的when,很重要,是消息放在队列哪个位置的重要依据。是放在队头?还是队尾?(重点
  3. Handler类型的target变量,我们之前没注意过吧?字面意思:目标。目标handler(重点
  4. 下面还有两个Message类型的变量,一个next,一个sPool;next字面意思:下一条消息。pool:水池。类型又是Message;那么,sPool:池子的消息
  5. Object类型的sPoolSync:异步池子。根据经验来看,碰到过很多这种Object类型的东西,大部分都是加锁用的。synchronized(sPoolSync),一般都是这样用
  6. int类型的两个变量,sPoolSize值是0,再就是MAX_POOL_SIZE,值是50。字面的意思就是池子的大小是0,池子的最大值是50.

??什么池子啊,什么最大值啊。我相信很多人跟我的反应都是一样的,线程池,复用。所以这里就是消息池,消息能复用,消息池最大的消息个数是50个,异步。

??延申到这里,引出我想问的第一个问题,消息的创建,消息创建的两种方式:一种是new出来,一种是obtain的方式,它有一系列的重载方法。

//第一种:new的方式
Message msg1 = new Message();
msg1.what = 1;
msg1.arg1 = 20;
handler.sendMessage(msg1);

//第二种:obtain的方式
Message msg = Message.obtain();
msg.what =1;
msg1.arg1 = 20;
handler.sendMessage(msg);

第一种没啥好说的,我们看第二种:Message.obtain()

public static Message obtain() {
        //加锁
        synchronized (sPoolSync) {
            //判断sPool是否为空
            if (sPool != null) {
                //sPool不为空,就复用sPool msg对象
                Message m = sPool;
                //然后,把m的next赋值给sPool
                sPool = m.next;
                //然后,把已经去出去的msg的额next置空
                m.next = null;
                m.flags = 0; // clear in-use flag
                //这时候,消息池已经去出去了一条消息,消息池大小就减一
                sPoolSize--;
                return m;
            }
        }
        //sPool为空,就new一个msg对象
        return new Message();
    }

简单的理解就是,不需要重新创建消息,从消息池里面取出一条消息,赋值给我们需要创建的msg对象。

这里为什么要加锁?什么情况下需要加锁?当然是防止并发呀,handler可以随时随地的发消息,所以,为了防止并发,加锁。

问题来了,这个sPool是什么时候赋值的呢?我们创建消息的时候没有赋值。创建的时候没有赋值,我们在Message类里面,检索sPool对象,我们找到如下方法:

@UnsupportedAppUsage
    void recycleUnchecked() {
        //重置一些列的成员变量
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;
        //就是这里,加锁
        synchronized (sPoolSync) {
            //当前消息池子是否小于限制的最大值
            if (sPoolSize < MAX_POOL_SIZE) {
                //把sPool赋值给next
                next = sPool;
                //sPool赋值现在的消息
                sPool = this;
                //消息池子大小加1
                sPoolSize++;
            }
        }
    }

看这个方法名就应该能猜到,消息回收的时候调用的。所以,在消息回收的时候,就把这条消息重置,把这条回收的消息赋值给sPool,这里就是赋值的位置。在消息回收的时候赋值。

所以,只要你并发量不大,你每次都是obtain创建消息,基本上都是复用的,不会重新创建消息。

消息说完了,跑题了,跑题了,言归正传

handler发送消息的流程

欢迎来到走进科学之Android消息发送流程,我们来一步一步的剖析这条消息是怎么一步一步放进消息队列的。

Message msg = Message.obtain();
msg.what =1;
msg1.arg1 = 20;
handler.sendMessage(msg);

我们来看这个sendMessage的源码。

public final boolean sendMessage(@NonNull Message msg) {
        //是不是眼熟,没错,它实际上调用的就是我们延时消息的方法,只不过,这个延时的时间是0
        return sendMessageDelayed(msg, 0);
}

//我们再来看看这个sendMessageDelayed方法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        //这里有个判断时间,小于0,就赋值给0,所以,发送延时消息的时候时间传递负数,会立马接收到消息,知道是为什么了吧?
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        //这里又调用的sendMessageAtTime方法
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

//这里我想说的是SystemClock.uptimeMillis():表示系统开机时间
//我们再来看看这个sendMessageAtTime方法

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        //这里有一个消息队列的判断,这个消息队列是在Handler创建的时候赋值的。
        //可以点进去看一下。Hander构造方法传递一个Looper,Looper构造方里面创建了msgQueue,就是这个。
        MessageQueue queue = mQueue;
        //如果是空,就抛异常,因为消息队列都没有,循环啥?
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        //这里又调用了enqueueMessage
        return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        //这里,我们前面说消息的时候,说很重要,就是在handler发送消息的这里赋值,这个值也是后面Looper发送给哪个handler的依据。
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        //这个是判断你的这个消息是不是异步,提升消息优先级的位置。同步消息,同步屏障,异步消息。
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        //这里就开始进入到消息队列了
        return queue.enqueueMessage(msg, uptimeMillis);
    }

??msg.target是后面Looper拿到这条消息之后,发送的目的地,不然,Looper怎么知道要发送给谁(handler)?

??提升消息优先级的位置。同步消息,同步屏障,异步消息。也是比较重要,后面再细唠。

MessageQueue怎么把这条消息放进队列的

到这里handler的发送就完了,MessageQueue怎么把这条消息放进去的呢?方法如下:

boolean enqueueMessage(Message msg, long when) {
        //判断有没有目标handler,如果没有,直接就抛异常,都没有这个目的地,我最后取出这条消息,我发给谁?所以,直接就抛异常
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        //加锁,不加锁,如果很多消息同时需要加紧队列就会出问题
        synchronized (this) {
            //判断这条消息是否正在使用,如果正在使用,那也抛异常。为什么消息会正在使用呢?
            //我们前面说了obtain方式消息是复用的,发送消息会并发,所以,是吧?
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
            //如果,当前msgQueue正在退出,把消息回收了。
            //比方说,你新建线程请求网络,网络请求完,线程一般就会死掉了,线程都没有了,MsgQueue当然要退出了。
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            
            //走到这里,消息就准备放进消息队列了,就是放在那里的问题
            //给消息加个标记,表示消息正在使用。跟前面那个判断正好对应
            msg.markInUse();
            //这个时间,还记不记得?系统开机时间+你延时的时间
            msg.when = when;
            //把消息队列的当前消息赋值给p
            Message p = mMessages;
            //是否需要唤醒线程,唤醒跟休眠都是native方法。
            boolean needWake;
            //当前消息是空,说明当前消息队列没有消息,就直接把我们传递的这条消息加进队列
            //我加进来的这条消息的执行时间是0,时间是不会有负数的,如果传进来是负数,都被改成0了,所以,我加的这条消息应该是最先执行的,所以,要加进队列
            //加进来的这条消息的执行时间小于当前线程的执行时间,我加进来的这条消息执行的时间,比你当前消息队列循环的时间小,说明,我要在它的前面执行,要加进队列
            //上面这个时间小的问题,你可以理解成,消息队列循环的时间是延时10秒处理的,我新进的这条消息是要延时5秒,所以,要放在它的前面
            if (p == null || when == 0 || when < p.when) {
               //走到这里,说明新加消息需要放在队首 //我新加的消息放进来了之后,要把当前消息的后面,也就是我新加消息的next
                //因为,我新加的消息要在它的前面执行
                msg.next = p;
                //然后,把我新加消息赋值给当前消息变量
                mMessages = msg;
                needWake = mBlocked;
            } else {
         //走到这个else里面,就说明当前消息不需要放到队首,就循环判断看它要被放在哪 
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //进入死循环
                for (;;) {
                    prev = p;
                    p = p.next;
                    //如果当前msg的下一个消息是空,表示没有消息了,for循环就要中止了,需要把新加消息放进来了
                    //如果当前消息的下一条消息的执行时间在新加的执行时间的后面,说明,新加消息要在这条消息的前面执行。所以,for循环就要中止了,需要把新加消息放进来了
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //把当前消息放在新加消息的后面
                msg.next = p; 
                //把新加消息,放在当前执行消息的后面。此时,消息就插件队列了
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            //是否需要唤醒消息队列开始循环获取消息,是native层面做的事情。
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

总结下来就是:三个条件

  1. 根据当前队列是否空闲(p == null)
  2. 当前消息执行的时间when(when == 0)
  3. 当前队列执行的消息是否需要在新增消息的后面执行(when < p.when)

??来判断当前消息是否需要插到队首,只要满足上面的任意一个条件,就需要放进队首;否则,for循环判断当前消息需要放到消息队列的哪个位置。需要插队的话就记得把队列中后面的消息放到当前消息的后面。

再重复一遍,这个时间是SystemClock.uptimeMillis() + delayMillis,系统开机时间+你传递的延时时间。

到这里,消息就被插件消息队列了。代码基本上每行都有注释,一遍没有看懂的话就多看几遍。

Looper读取消息:loop()

消息已经放进队列了,第二个问题就结束了,接下来就是第三个问题:Looper怎么读取消息的?

Looper是通过loop()方法循环读取消息的。代码如下:

代码比较多,我把无关的(看不懂的)都去掉了

public static void loop() {
        //获取当前线程的looper
        final Looper me = myLooper();
        //如果,等于null,就抛异常,看异常的消息就应该看的出来,说没有在当前线程调用Looper.prepare()方法
        //Looper.prepare()这个方法就是创建Looper的
        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(); 
            //如果,没有消息了,就退出循环,进入休眠状态
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            
            ...
            
            //获取观察者模式的对象
            final Observer observer = sObserver;

            ...
            
            Object token = null;
            if (observer != null) {
                //这里应该是这个观察者对象发送了一个消息正在分发的消息
                token = observer.messageDispatchStarting();
            }
            
            ...
            
            try {
            //msg.target:是不是很眼熟?就是需要接收这条消息的handler //通过这个handler调用dispatchMessage方法,发送消息 msg.target.dispatchMessage(msg);
                if (observer != null) {
                   //然后,观察者发送一个消息分发完成的消息 observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) {
                   //如果出现了异常,这个观察者就发送一个消息分发异常的消息 observer.dispatchingThrewException(token, msg, exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            
            ...
            
            //眼熟不?就是前面说的,消息回收,重复利用,就是在消息分发完成之后触发
            msg.recycleUnchecked();
        }
    }

我们先看一下这个handler的dispatchMessage方法:

public void dispatchMessage(@NonNull Message msg) {
        //这个Message的callback是什么时候赋值的呢?就是创建Message的时候,可以回过头去看一下
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //这个方法,眼熟吗?看下面,我们新建handler的时候,不就重写了这个方法吗?
            handleMessage(msg);
        }
    }
    
    Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    };

到此,消息的发送,入队,取消息,处理,就形成了闭环了。整个流程:

  1. 新建handler,发送消息sendMessage
  2. 此时消息的创建obtain复用模式,后面可能会造成正在使用的异常,所以,需要加锁同步一下
  3. 然后,消息进队,target(目的地的handler)和when(执行的时间系统开机时间+延时时间)
  4. 判断的三个条件,是放进队首(队列中是空的,时间是0,时间在队列消息时间的前面),还是队中(需要循环判断队列中是否还有消息和时间)
  5. 通过loop方法取出来消息,通过这个消息的target发送消息

我们接下来说第四个问题:handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?

我们上面分析完,好像并没有看到这个延时消息的问题啊,Looper的loop方法是,只要queue.next()返回给它消息了,它就直接发送回去了,没有什么延时。

MessageQueue读取消息:next()

重点就在这里queue.next(),读取消息。这里也是提升消息优先级的位置(同步屏障,异步消息)。

@UnsupportedAppUsage
    Message next() {
        
        final long ptr = mPtr;
        //通过注释翻译过来就是:loop已经退出了,或者应用正在尝试重启一个looper,就直接return null
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        //下一次循环的时间,单位是:秒
        int nextPollTimeoutMillis = 0;
        //开始进入死循环去读取消息
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                //不知道啥意思。
                Binder.flushPendingCommands();
            }
            //这是一个native方法,看名字,大概的意思应该就是循环一次,经过nextPollTimeoutMillis长的时间
            //就是底层C/C++经过这么长时间之后,触发一次循环
            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;
                //屏障消息的实质就是创建一条target为null的消息
                //看这里的if条件,正常的消息target不等于null,这里的判断是不会进入的。
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    //再看这里的do...while循环,退出的条件是找到一条不为空的异步消息。
                    //msg.isAsynchronous():异步消息返回true,取反之后就是false,更前面&&,就是false,就退出do..while循环了
                    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.
                        //修改这个下次循环的时间,前面说的native调用的时间,就是根据这个变量判断的。
                        //时间就是消息执行的时间-系统开机时间=延时时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //否则就是一条实时消息,就是正常的赋值流程,返回这条消息给looper,然后发送出去
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    //消息是空,就表示没有更多消息了
                    nextPollTimeoutMillis = -1;
                }
            
            ...
        
                
        }
    }

流程就是:

  1. 先判断looper有没有,系统有没有重启,如果重启了,looper没有,那就直接返回一个null对象,Looper接收到了一个null对象,会直接return
  2. 然后,判断是不是屏障消息(屏障消息消息的target等于null),如果是屏障消息,就进行do…while循环,直到取出一条异步消息为止
  3. 正常的判断消息,是同步消息还是延时消息,同步消息立刻返回,延时消息,提醒底层多长时间之后再调用我

??看到了吗?MessageQueue取消息的流程,通过msg的执行时间与当前系统的开机时间进行比较,延时消息就是判断了延时多长时间之后,告诉底层多长时间之后,你还要调用一次这个取消息的方法。这就是延时消息的实现。

如何提高消息的优先级?同步消息,屏障消息,异步消息

既然说到这里,我们就直接聊一下这个消息的优先级

ps:这里的异步消息,同步消息,并不是说多线程去处理消息。异步消息是有一个属性是true

//同步消息
Message msg = Message.obtain();
//异步消息,调用了一个setAsynchronous并且设置为true
Message msg = Message.obtain();
msg.setAsynchronous(true);

我们平时发消息是下面这个样子的:

Message msg1 = Message.obtain(handler,new Runnable(){

            @Override
            public void run() {
                Log.v("hcy","这是一条延时3秒的消息");
            }
        });
handler.sendMessageDelayed(msg1,3*1000);

Message msg = Message.obtain(handler, new Runnable() {
            @Override
            public void run() {
                Log.v("hcy","这是一条延时5秒的异步消息");
            }
        });
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
            msg.setAsynchronous(true);
        }
handler.sendMessageDelayed(msg,5*1000);
Log.v("hcy","两条消息都发送完了");

上面就是new了两条消息,一条同步消息,一条异步消息,如果没有屏障消息的情况下,同步消息和异步消息是一样的,没啥区别。程序运行完,过三秒钟同步消息回调,再过两秒打印异步消息回调,上面消息的打印结果如下:

2021-11-21 08:53:10.363 29452-29452/com.haichenyi.myapplication V/hcy: 两条消息都发送完了
2021-11-21 08:53:13.366 29452-29452/com.haichenyi.myapplication V/hcy: 这是一条延时3秒的消息
2021-11-21 08:53:15.365 29452-29452/com.haichenyi.myapplication V/hcy: 这是一条延时5秒的异步消息

那么,什么是屏障消息呢?怎么实现呢?我们先说怎么实现的。

Message msg1 = Message.obtain(handler,new Runnable(){

    @Override
    public void run() {
        Log.v("hcy","这是一条延时3秒的消息");
    }
});
handler.sendMessageDelayed(msg1,3*1000);

Message msg = Message.obtain(handler, new Runnable() {
    @Override
    public void run() {
        Log.v("hcy","这是一条延时5秒的异步消息");
        //异步消息处理完移除消息屏障
        try {
            Class<?> msgQueue = Class.forName("android.os.MessageQueue");
            Method removeSyncBarrier = msgQueue.getDeclaredMethod("removeSyncBarrier", int.class);
            removeSyncBarrier.invoke(Looper.myQueue(),token);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
    msg.setAsynchronous(true);
}
handler.sendMessageDelayed(msg,5*1000);
try {
    //启动消息屏障
    Class<?> msgQueue = Class.forName("android.os.MessageQueue");
    Method postSyncBarrier = msgQueue.getDeclaredMethod("postSyncBarrier");
    token = (int) postSyncBarrier.invoke(Looper.myQueue());
}catch (Exception e){
    e.printStackTrace();
}
Log.v("hcy","两条消息都发送完了");

打印结果如下:

2021-11-21 09:05:36.915 29743-29743/com.haichenyi.myapplication V/hcy: 两条消息都发送完了
2021-11-21 09:05:41.922 29743-29743/com.haichenyi.myapplication V/hcy: 这是一条延时5秒的异步消息
2021-11-21 09:05:41.949 29743-29743/com.haichenyi.myapplication V/hcy: 这是一条延时3秒的消息

??代码执行完之后,先是过了5秒回调了异步消息,然后立刻回调了同步消息,为什么呢?因为,同步消息是延时3秒执行呀,异步消息是延时5秒执行,因为加了消息屏障,会把异步消息的优先级提到同步消息的前面,所以,执行完异步消息,同步消息的执行时间早就过了,肯定要立刻执行呀。

说了这么多,那么,这个提升优先级是怎么实现的呢?透过现象去看本质。两段代码的区别,就是通过反射,执行了两个方法postSyncBarrier,removeSyncBarrier。其中还有一个带参数的方法。

//执行消息屏障
try {
    Class<?> msgQueue = Class.forName("android.os.MessageQueue");
    Method postSyncBarrier = msgQueue.getDeclaredMethod("postSyncBarrier");
    token = (int) postSyncBarrier.invoke(Looper.myQueue());
}catch (Exception e){
    e.printStackTrace();
}

//移除消息屏障
try {
    Class<?> msgQueue = Class.forName("android.os.MessageQueue");
    Method removeSyncBarrier = msgQueue.getDeclaredMethod("removeSyncBarrier", int.class);
    removeSyncBarrier.invoke(Looper.myQueue(),token);
}catch (Exception e){
    e.printStackTrace();
}

我们先来看看这个消息屏障的方法:

@UnsupportedAppUsage
@TestApi
//我们反射调用的是这个方法,它最终执行的是一个同样名字的带参的重载方法
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

//最终执行到这里
private int postSyncBarrier(long when) {
//以来还是老规矩,加锁,防止多线程调用
    synchronized (this) {
        //token比较重要,是移除屏障的标记
        final int token = mNextBarrierToken++;
        //常规的msg的创建,msg执行的时间是系统的开机时间
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        //把这个token值赋值给了msg的arg1变量
        msg.arg1 = token;
        //说白了,下面就是链表操作了,不停的移动指针
        //prev:上一条消息变量
        Message prev = null;
        //p:消息。
        //mMessages:这个变量眼熟不?我们前面消息从队列中取的时候就是那个next()方法,
        //在进入for循环里面,判断是否是屏障消息之前,是不是也同样是给一个成员变量赋值,赋值的值也是mMessages。
        Message p = mMessages;
        //执行时间不等于0,前面取的时候那三个条件,有一个时间等于0,就插进消息队首,这里我觉得也可以这样理解
        if (when != 0) {
            //这里是一个while循环,字面理解就是当前消息不等于null,当前消息的执行时间,小于屏障消息的执行时间,就继续循环。直到这两个条件不满足为止
            //结合上下文的意思就是,我执行这个屏障消息的时候,如果发现队列里面还有消息的执行时间在我这个屏障消息的前面,就继续让它先执行。
            while (p != null && p.when <= when) {
                //把当前消息赋值给变量prev
                prev = p;
                //把当前消息的下一条消息,赋值给p变量(当前消息变量)
                p = p.next;
                //继续while循环
            }
        }
        //经过上面的while循环之后,就找准了屏障消息该插入的为止了
        if (prev != null) {
        //如果prev不等于null,表示,消息队列里面还有需要在屏障消息前面执行的消息
        //队列就变成了:prev-屏障消息-当前消息
            msg.next = p;
            prev.next = msg;
        } else {
            //如果prev等于null,就表示消息队列里面没有需要在屏障消息执行前面执行的消息了
            //队列也就变成了:屏障消息-当前消息
            msg.next = p;
            mMessages = msg;
        }
        //返回这个token值。移除屏障消息的时候需要用到
        return token;
    }
}

上面这个执行消息屏障说的很清楚了吧?多看注释,多理解。

我们再来看看这个移除消息屏障

@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {
    //加锁
    synchronized (this) {
        //两个变量赋值
        Message prev = null;
        Message p = mMessages;
        //这个while循环,第一个条件:当前消息不等于null(当前消息队列还有消息)
        //第二个条件:当前消息得目的地不为空(我们屏障消息这里是等于空的,这里应该是为了判断其他地方调用这个方法)
        //然后就是第三个条件,我们传进来的token值,就是上面执行屏障消息时候的返回值,当时赋值给了arg1。这里比较,如果相同,那么,这条消息就是屏障消息
        //第二个条件和第三个条件是或的关系,满足一条就行。
        //屏障消息就要移除,链表的常规移除操作
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
            //其实就是:原来是:上一条消息——屏障消息——下一条消息
            //变成了:上一条消息——下一条消息
        }
        //到这里就移除完了
        //上面while循环完,发现当前消息是空,说明消息队列中没有消息了,直接抛异常
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
        }
        
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        //消息回收
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        //native层的是否需要唤醒服务
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

总结一下这个提升消息优先级的方式就是:把你想发送的消息定义view异步消息发送,光这样还不行,还要发送一条屏障消息,具体流程:

  1. 往消息队列里面插入一条target为null的消息
  2. MessageQueue.next()方法读取的时候,会先判断这条消息是不是屏障消息,如果是,他就会执行do…while循环,直到找到一条异步消息为止。
  3. MessageQueue拿到消息之后,正常的取消息流程
  4. 在你执行完这条异步消息之后,记得要移除屏障消息,不然所有的异步消息都在同步消息前面执行。

其实有个更简单的方法,handler发消息的api都给出来了

//发送消息到队列前面
handler.sendMessageAtFrontOfQueue(msg1);

经过上面的整个流程之后,最后一个问题就比较简单了,自己看一下源码吧,我给出结论:

Looper.prepare():给当前线程创建Looper,MessageQueue的过程,这个MessageQueue是可退出的

Looper.loop():从头到尾都在说loop(),循环读取消息。

handler知识点总结

总结一下handler的东西:整理了一个流程图:
handler知识点图

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-22 12:27:21  更:2021-11-22 12:29:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:57:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码