Android的消息机制--Handler

这篇具有很好参考价值的文章主要介绍了Android的消息机制--Handler。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、四大组件概述

Android的消息机制是由Handler、Message、MessageQueue,Looper四个类支撑,撑起了Android的消息通讯机制,Android是一个消息驱动系统,由这几个类来驱动消息与事件的执行

Handler:

  • 用来发送消息和处理消息
  • 无论使用的post ,还是send,都会执行enqueueMessage 方法,将消息加到队列中
  • 发送的消息并不是立刻得到执行的,所以必须有个地方把它存起来,也就是MessageQueue

MessageQueue

  • 基于单向链表,以触发时间的顺序排放在队列中,链表头部信息被触发的时间是最接近的
  • 队列中的消息怎样让它在必要的时间得到执行,就需要依靠Lopper 

有三种消息类型:

  • BarrierMessage:屏障消息

  • AsyncMessage:异步消息

  • Message:同步消息

Lopper

  • 无限循环驱动器
  • 在它内部有个loop 方法,开启无限循环,从头遍历这个队列,检查满足条件的消息,有就把它取出来进行分发执行,没有或者消息为空时,当前线程就会进入阻塞状态,从而释放掉CPU 的资源占用,当有新消息进来的时候就会唤醒当前线程,从而继续遍历队列中是否有满足条件的消息,所以Looper并不会真的一直无限循环下去

Message

消息

  • long when:该消息被执行的时间戳,这个时间戳是它在队列中排队的唯一依据
  • Message next:消息队列是一条单向链表,每一条消息都会包含下一条消息的引用关系,从而形成单向链表
  • Handler target:代表着该message是由哪一个handler发送的,在消费这条消息时也由这个target来消费
  • 一个线程最多存在一个Lopper
  • 一个Looper对应一个MessageQueue
  • 一个MessageQueue中可以存在多个Message对象
  • 一个MessageQueue 或者一个Looper 对应多个handler

二、消息分发的优先级

  1. Message的回调方法:message.callback.run()  优先级最高
  2. Handler的回调方法:Handler.mCallback.handleMessage(msg)
  3. Handler的默认方法:handler.handleMessage(msg)
       //1.直接在Runnable中处理任务
        handler.post(runable = Runnable {
            //这条消息在消费时,首先回调给Message中的callback,也就是runable对象
        })


        //2.使用Handler.callback来接收处理消息
      val handler = object :Handler(callback{
          //在创建handler的时候是可以传递一个callback 的,在消息分发的时候首先把消息分发给callback 来处理
          return@callback true
      })

        //3.最常用的handler的handlerMessage方法
        

三、疑问点

1.在使用handler的时候并没有指定Looper ,这三个类是怎么关联起来的

在创建实例对象时虽然没有传递Looper对象,但是在构造函数的重载里会调用Looper.myLooper来获取当前线程绑定的Looper对象

2.主线程的Looper是在哪里创建的?

在ActivityThread的main方法中调用Looper.prepareMainLooper()创建了主线程的Looper对象,然后调用loop()开启消息队列循环,所以在主线程中创建Handler不用给他创建Looper

如果一个线程的Looper对象没有调用prepare 方法,它的Looper是为空的

3.Looper.myLooper是如何保证获取到的Looper是当前线程的Looper 对象?

    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));
    }

        无论是主线程的Looper 还是子线程的Looper都只能调用prepare()方法或者prepareMain()方法,在prepare()方法中首先判断在这个当前线程这个Looper对象是否已经被创建过了,如果是,再次调用该方法就会报错。

        这就是一个线程最多只能存在一个Looper 对象的保证

        接下来把Looper 对象保存到了ThreadLocal中,在获取Looper 时也是从ThreadLocal 这个对象中得到的

ThreadLocal:

        是用来存储数据的,使其成为线程的私有局部变量,通过它提供的get、set来访问

        好处:可以在线程的任意地方去访问这个局部变量,不用传来传去

4.如何让子线程拥有消息分发的能力?如何在子线程中弹出Toast?

        默认子线程是没有Looper 对象的,要主动调用Looper.prepare 方法以及 Looper.loop方法,在这两个方法中间就可以弹出Toast 了,在接着给它创建一个handler,在主线程中拿到这个handler对象,就可以处理在子线程中处理主线程发送过来的消息了

        Toast在没有显示出来之前,它还没有被添加到窗口上,对它的操作就不会触发线程的检查
        实现方式跟ActivityThread方式一样
        需要注意的是一旦给子线程执行了这两个方法,要在必要的时候调用Looper.quit 方法,让Looper 退出循环,否则会一直循环下去,这个线程就不会被销毁,不会被回收

四、消息入队

一条消息被插入到MessageQueue时做了哪些事情?

发送一条消息时主要有两种方法,一种是post开头的,一种是send开头的,无论使用的哪种方式发送,都会以Message的形式插入到队列中

对于post,发送一条消息时,都会通过getPostMessage,把runnable对象包装成Message对象,再次调用sendMessageDelayd方法把它加入到队列中

享元设计模式,共用已创建的对象   避免重复创建对象

        getPostMessage方法中在获取Message对象时,使用的是Message.obtain()方法

Message提供了消息复用池的能力,最多会缓存50条Message对象,通过.obtain()方法来获取 Message对象是可以复用的,不需要每次都去创建一个新的 

        采用了链表的形式来管理消息池,链表的插入和删除比ArrayList快

        还提供了消息回收能力,当消息被分发完了之后就会调用recycleUnchecked,将Message的对象进行重置,情况数据,并将其插入到链表的头节点中,它是一个队头复用机制

通过new Message出来的对象会出现大量的临时的Message对象,会导致内存占用率过高

消息入队,按照消息被执行的时间戳when插入队列

  • 无论是post、还是send,最终都会执行enqueueMessage,把消息插入到队列中
  • postSyncBarrier:发送一条屏障消息

新消息插入时按照消息插入的时间插入到队列中

enqueueMessage

Handler.enqueueMessage

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
       ……
        return queue.enqueueMessage(msg, uptimeMillis);
    }

将Message的target赋值成当前的Handler,在下面会对这个target进行判空,空的话直接抛出异常

消息被消费的时候会通过handlerDispatchMessahe方法来完成

MessageQueue.enqueueMessage

  boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            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;
            //mMessage始终是队头消息,拿到队头,这个队列才能执行增删改查的操作
            Message p = mMessages;
            boolean needWake;
            //p == null 说明队头为null,则队列为空
            //when == 0 或者新消息触发的时间戳为0
            // when < p.when 或者消息被触发的时间小于队头消息被触发的时间
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                //满足条件就把新消息插入到队列的头部
                //把新消息的next节点指向原来的头结点,然后将新信息赋值给头消息
                msg.next = p;
                mMessages = msg;
                //如果当前Looper处于休眠状态,则本次插入消息之后需要唤醒
                //mBlocked:是否处于阻塞状态,只有对列为空,队列当中没有可处理消息的时候,线程才会进入阻塞状态,mBlocked才会为true
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                //needWake:要不要唤醒线程,目的是让异步消息尽早的执行
                //mBlocked:当前线程是否处于休眠状态
                //p.target == null:队头消息是否为空,队头消息是异步同步屏障消息
                //msg.isAsynchronous:新消息是异步消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //for循环:找到一个合适的位置来插入这条新消息
                for (;;) {
                    prev = p;
                    p = p.next;
                    //找到合适的位置退出for循环
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //调整链表中节点的指向位置,实现消息插入队列的目的
                //msg:新消息  p:下一条消息  prev:上一条消息
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            //Looper被唤醒
            if (needWake) {
                //线程被唤醒,使用nativeWake native方法
                nativeWake(mPtr);
            }
        }
        return true;
    }

enqueueMessage在插入一条新消息时,主要是检查消息是否都具备target对象,否则消息是无法被处理的,还会选择性的决定唤醒当前的线程,继续轮询队列是否有符合条件的消息拿出来处理

postSyncBarrier 同步屏障消息

message.target == null ,这类消息不会真的执行,起到标记作用

MessageQueqe在遍历消息队列时,如果队头是同步屏障消息,那么会忽略同步消息,优先让异步消息得到执行

一般异步消息和同步屏障消息会一同使用

异步消息 & 同步屏障

使用场景:

  • ViewRootImpl接收屏幕垂直同步信息事件用于驱动UI测绘
  • ActivityThread接收AMS的事件驱动生命周期
  • InputMethodMessage分发软键盘输入事件
  • PhoneWindowManager分发电话页面各种事件

目的:

        让重要的消息尽可能早的得到执行

注意:

  • 开发过程中无法使用,只能系统源码使用
  • 必须使用MessageQueue来发生,Handler是无法发送同步屏障消息的,只能用来发送异步消息和同步消息

MessageQueue#postSyncBarrier

    public int postSyncBarrier() {
        //使用uptimeMillis,意思是:发送了这条消息,在这期间如果设备进入休眠状态,那么消息是不会执行的,设备被唤醒之后才会执行
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
  • currentTimeMillis() 系统当前时间,即日期时间,可以被系统设置修改,如果设置系统时间,时间值也会发生改变
  • uptimeMillis() 自开机后,经过的时间,不包括深度休眠的时间

sendMessageDelay.postDelay 也都使用了这个时间戳

问题:

        如果使用handler发送一条消息,然后让设备进入休眠,也就是先熄屏,然后长时间不操作手机,这条消息会不会得到执行,为什么?

        进入休眠之后,消息是不会被触发的,因为设备休眠之后uptimeMillis是不会被累加的

 private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //从消息池复用,构建新消息体
            final Message msg = Message.obtain();
            msg.markInUse();
            //并没有给target赋值
            //区分是不是同步屏障,就看target是否等于null,等于null,就是同步屏障消息

            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
            //遍历队列所有消息,直到找到一个message.when > msg.when 的消息,决定新消息插入的位置
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            //如果找到了合适的位置则插入
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                //如果没有找到直接放队头
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

  屏障消息在插入队列时是没有主动唤醒线程的,因为屏障消息并不需要得到执行,也不需要唤醒这个线程去轮询它

屏障消息的移除,谁添加的就由谁来移除

比如ViewRootImpl,在接收到垂直同步信号的到达,发送一条异步消息,并发送了一条屏障消息,当接收到异步消息时,ViewRootImpl就会把同步屏障消息从队列中移除

问题:

        ViewRootImpl是如何在UI测绘的工作优先得到执行的?

        发送了同步屏障和异步消息

五、消息分发

Looper#loop()

队列中的消息之所以能得到分发,是由于Looper中的loop方法,会开启一个无限for循环消息的驱动器,在这个无限for循环中会调用MessageQueue的next方法,去获取一个可执行的msg对象

这个next方法可能使得当前线程进入一个阻塞的状态,此时这个方法不会有返回值,下面的分发代码就不会得到执行,所以这个无限for循环并不会一直空轮询下去,

目的:只是不让这个线程退出,因为一个线程任务执行完成,就会自动退出,如果想让它不退出,开启一个while循环等待一段时间,这里也是一样的道理

直到队列中有可处理的消息才会返回

for(;;){
    //取出队列中的消息
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }
    ……
    //分发消息
    msg.target.dispatchMessage(msg);
    ……
    //回收Message
    msg.recycleUnchecked();
}   

拿到消息之后调用msg.target.dispatchMessage(msg) 去分发消息

当消息处理完成之后调用msg.recycleUnchecked()方法回收Message,留着复用

总结:

        loop方法的作用是从队列中去取消息,然后分发,然后回收

MessageQueue#next()

首先也是开启一个for循环,在消息分发时可能存在消息的插入、删除,而且队列是一个单向链表无法确切知道消息插入时是有多少的,当这个next方法找到一个合适的消息时就会退出for循环

    int nextPollTimeoutMillis = 0;
    for (;;) {
        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;
                        //prevMsg不为空说明需要删除的是中间的消息,只需要上一条消息的next指向msg的next,为空说明要删除的这条消息是队头消息
                        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 {
                    // No more messages.
                    //msg对象为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,直到有新消息到达
                    nextPollTimeoutMillis = -1;
                }
    }

通过nativePollOnce这个native方法进行阻塞,传递的nextPollTimeoutMillis,如果这个值是大于0的,就会使得当前线程进入阻塞,并且释放掉对CPU资源的使用权,尽管Looper中有个无限for循环,但是不会造成CPU资源的过多占用

由于nextPollTimeoutMillis第一次for循环时是等于0的,所以第一次不会使得这个线程进入阻塞的,但是如果在接下来的循环中没有找到一个合适的、需要处理的消息,这个nextPollTimeoutMillis会被更新,在第二次循环的时候如果这个值不等于0,这个线程就会进入阻塞状态,如果这个值等于-1,当前线程就会进入无限阻塞状态,直到有新消息插入时,才会被唤醒

nextPollTimeoutMillis等于多少,这个线程就会被阻塞多少ms,超时之后就会继续执行以下代码

延迟消息是怎么得到保证的?

首先是通过uptimeMillis去计算出应该被执行的时间戳,然后借助nativePoll阻塞一段时间,超时之后自动恢复 ,然后继续往下检查是否有满足条件的消息,如果有就拿出来去执行

如何去取消息?

把队头消息赋值给一个临时变量msg,防止插入消息,队头可能被改变,然后判断msg是否是同步屏障消息(也就是判断msg.target == null),如果是通过do-while循环,在while中判断这个msg 不是一个异步消息,也就是说如果这个消息是异步消息,就会退出do-while循环

这个屏障消息唯一的作用就是:当它处于队头时,next方法在检索消息时会跳过同步消息,会优先检索出所有的异步消息,让它们优先执行

msg.target对象如果不为空,不会执行do-while循环,就会按照时间顺序来检索分发消息

判断消息msg是否为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,线程进入无限阻塞状态,直到有新消息到达

不为空时,也就是找到了一条消息,检查是否到达需要执行的时机

如果没有,去更新nextPollTimeoutMillis值,等于应该执行的时间 - 当前时间 ,计算出还应该延迟多久

否则说明找到了这条需要处理的消息,首先需要从队列中移除掉

preMsg不等于null,说明被删除的这条消息是队列中间的这条消息,preMsg代表被删除消息的前一条消息,删除这条消息,只需要将preMsg的next节点指向这个消息的next节点

preMsg等于null,说明被删除的这条消息是队头消息,需要将mMessage指向要删除消息的下一个节点

最后把msg对象返回回去,让它去分发,去执行

idler.queueIdle()

                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

判断队头Message是否为空,也就是队列中没有任务了,或者队头消息时间还没有到达可执行的时机,在第二次for循环中,线程即将进入阻塞状态,在进入阻塞状态之前,称之为空闲状态,判断有没有向MessageQueue中注册IdleHandler,用于监听这个状态

IdleHandler:

        可以监听当前线程是否即将进入空闲状态,也就是说通过事件的监听来做一些延迟的初始化,以及数据加载、日志上报等工作,而不是有任务就提交,从而避免抢占重要的资源

如果pendingIdleHandlerCount大于0,就会去调用idler.queueIdle()方法

            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

最后会将nextPollTimeoutMillis延迟时间置为0,既然业务层监听了线程的空闲状态,在queueIdle这个方法里,就有可能再次产生新的消息,为了让新消息尽可能早的得到执行,此时不需要让线程进入休眠了

nextPollTimeoutMillis = 0;

在Android中存在两套消息机制,一套是Java的,一套是C++的,本质上是独立的,在Java中MessageQueue调用nativePollOnce的主要原因是借助native消息机制所实现的线程阻塞能力文章来源地址https://www.toymoban.com/news/detail-833003.html

六、问题解惑

到了这里,关于Android的消息机制--Handler的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android Looper Handler 机制浅析

    最近想写个播放器demo,里面要用到 Looper Handler,看了很多资料都没能理解透彻,于是决定自己看看相关的源码,并在此记录心得体会,希望能够帮助到有需要的人。 本文会以 猜想 + log验证 的方式来学习 Android Looper Handler,对于一些复杂的代码会进行跳过,能够理解它们的设

    2024年02月11日
    浏览(17)
  • Android面试题精选——再聊Android-Handler机制-1,已开源

    下面想看Handler的工作流程图:(第一次画图,有点丑,凑合着看吧) 因为Handler的主要作用就是线程切换,所以在图中我把Handler线程变化也画了出来。从这张图我们能看出几点信息: **1、Handler负责消息的发送和处理:**Handler发送消息给MessageQueue和接收Looper返回的消息并且处理

    2024年04月12日
    浏览(24)
  • Handler原理机制解析,Android开发中的重要性

    Handler在android程序开发中使用的非常频繁、我们知道android是不允许在子线程中更新UI的,这就需要借助Handler来实现,那么你是否想过为什么一定要这个这样子做呢?而且Handler的内部消息处理机制究竟是什么样的呢?Handler的原理是需要通过源代码才能说的清楚的,而且它处理

    2024年02月06日
    浏览(18)
  • 结合源码拆解Handler机制

    作者:Pingred 当初在讲App启动流程的时候,它的整个流程涉及到的类可以汇总成下面这张图: 那时着重讲了AMS、PMS、Binder这些知识点,有一个是没有对它进行详细讲解的,那就是常见的Handler,它不仅在这个流程里作用在ApplicationThread和ActivityThread进行通信,它在整个安卓体系

    2024年02月11日
    浏览(22)
  • Android Handler被弃用,那么以后怎么使用Handler,或者类似的功能

    Android API30左右,Android应用在使用传统写法使用Handler类的时候会显示删除线,并提示相关的方法已经被弃用,不建议使用。 Android studio中的显示和建议: 看下官方API关于此处的解释:  简要说就是如果在实例化Handler的时候不提供Looper, 可能导致操作丢失(Handler 没有预估到新

    2023年04月21日
    浏览(20)
  • Android:Handler

    参考来源 参考来源 参考来源 参考来源 Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线程间通信 handler机制是android系统运行的基础,它采用生产者,消费者模式进行设计。其中生产者和消费者都是handler,多个handler会生产消息message投递到线程共享的messagequeue有序

    2024年02月02日
    浏览(18)
  • Android handler用法及分析

    这里将handler机制中的message,looper和messagequeue分开分析,分开了解之后,会在进行一个总结。先来看handler里面都有哪些方法都做了哪些事情, hide方法和带有@UnsupportedAppUsage注释的方法(此方法不对外暴露使用)暂不描述 。 handler要传递的callback接口在handler类里面,该接口里面

    2024年02月05日
    浏览(28)
  • Android学习之路(13) Handler详解

    Handler是一套 Android 消息传递机制,主要用于线程间通信。 用最简单的话描述: handler其实就是主线程在起了一个子线程,子线程运行并生成Message,Looper获取message并传递给Handler,Handler逐个获取子线程中的Message. Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线程间

    2024年02月09日
    浏览(22)
  • Android中正确使用Handler的姿势

    在Android中,Handler是一种用于在不同线程之间传递消息和任务的机制。以下是在Android中正确使用Handler的一些姿势: 1. 在主线程中创建Handler对象 在Android中,只有主线程(也称为UI线程)可以更新UI。因此,如果您需要在后台线程中执行某些任务并更新UI,则需要使用Handler将任

    2024年02月11日
    浏览(24)
  • 带你深入了解Android Handler的用法

    Android中,Handler是一类用于异步消息传递和线程之间通信的基础框架。一个Handler是一个线程的处理器,可以接收消息,并调度运行它们。使用Handler,应用程序可以将处理器与一个线程关联,以将来的时间运行任务。而使用Handler,就可以避免启动额外的线程,从而提高代码的

    2024年02月07日
    浏览(20)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包