手记

Android消息机制分析:Handler、Looper、MessageQueue源码分析

1.前言

关于Handler消息机制的博客实际上是非常多的了。
之前也是看别人的博客过来的,但是过了一段时间之后,一些细节也就忘了。
所以,就自己撸一篇,权当笔记,方便以后翻阅。

这篇文章主要是分析Handler消息机制原理以及收集一些面试题来讲解,熟悉的话可以不用看了。

本文源码基于android 27

2.Android消息机制概述

2.1 本质

Android的消息机制本质就是一套消息发送,传递及处理的机制。

2.2 角色说明

角色作用
Handler
(处理者)
负责消息的发送及处理等等。
Message
(消息)
保存消息的内容,如保存一个消息类型(what)等等。
MessageQueue
(消息队列)
保存Message的数据结构,是一个链表。
Looper
(循环器)
从消息队列中循环的取出消息然后把消息交给Handler处理。

2.3 原理简介

消息机制在Android上的流程为:

  1. 应用启动时会在主线程中创建一个Looper(循环器),Looper(循环器)内部则会创建一个MessageQueue(消息队列);

  1. 然后Looper(循环器)就会不断的轮询MessageQueue(消息队列)中是否有Message(消息);

  2. 我们可以通过Handler去发送一个Message(消息),发送之后Message(消息)就会进入到MessageQueue(消息队列)中去,Looper(循环器)通过轮询取出Message(消息),然后交给相应的Handler(处理者)去处理。

下面会通过分析源码来验证这个过程。

上面这个流程总结成一句话就是:

Looper(循环器)通过不断的从MessageQueue(消息队列)中取出Message(消息),然后交给相应的Handler(处理者)去处理。

是不是很简单呢?

2.4 Handler的应用--UI更新

对于消息机制,我们平常接触的最多的场景就是:

在子线程中进行一些数据更新等耗时操作,然后使用Handler在主线程中去更新UI。

为什么要怎么操作呢?这里有两个前提:

1.Android开发中规定了UI的更新只能在主线程中去操作,在子线程中更新会报错。
2.我们在主线程中创建的Handler能够接受到同一个Handler在子线程中发送的消息。

可以看到,在这种场景下我们使用Handler的目的就是切换到主线程中去更新UI。而Handler的使用方式是很简单的,这里就不写例子了。

那么,为什么更新UI只能在主线程中去操作呢?

这是因为Android中的UI控件不是线程安全的,因此在多线程中并发,可能会出现线程安全的问题,即访问UI控件可能会出现跟预期不一样的结果。那么为什么不使用锁机制呢?因为加锁会降低访问UI的效率,锁机制会阻塞某些线程的执行。因此,最简单高效的方法就是使用单线程模型来进行UI的访问了。

那么,为什么主线程中的Handler能接受到其他线程发来的消息呢?
这是后面源码分析的内容,这里暂且不表。

2.5 Handler的其他应用

上面UI更新实际上只是消息机制其中一个应用场景。
如果我们了解四大组件的启动停止等过程的话,就会发现,都是在一个名为HHandler中处理状态切换等逻辑,这个HActivityThread的内部类。其本质就是切到主线程中去处理。

所以说,不要将Handler仅仅局限于UI更新。

3.源码分析

本节主要深入源码对消息机制进行分析。对涉及到LooperMessageQueueMessageHandler等类进行逐一分析。

3.1 Looper类

3.1.1 Looper(循环器)的创建

Looper的创建可以分为在主线程中创建以及在子线程中创建,我们分别来看下。

3.1.1.1 主线程中创建Looper

先来看下Looper在主线程中是什么时候创建的。

3.1.1.1.1 ActivityThread的main()

应用启动时,会调用到ActivityThread中的main()方法,这个main()方法是应用程序的入口。main()里面会创建一个Looper对象出来。我们来看下代码:

    public static void main(String[] args) {        //省略无关代码

        //为主线程创建1个Looper对象
        Looper.prepareMainLooper();        
        //创建主线程
        ActivityThread thread = new ActivityThread();
        thread.attach(false);        //省略无关代码
        
        //开启消息循环
        Looper.loop();
    }

可以看到,应用启动时就为主线程创建出一个Looper对象,并且开启消息循环。

3.1.1.1.2 Looper的prepareMainLooper()

再来看下prepareMainLooper()

    public static void prepareMainLooper() {        //最终还是调用prepare()
        //参数false表示主线程中的消息循环不允许退出
        prepare(false);        //判断sMainLooper是否为null,否则抛出异常
        //即prepareMainLooper()不能够被调用两次
        //保证了主线程中只存在一个Looper对象
        synchronized (Looper.class) {            if (sMainLooper != null) {                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
3.1.1.1.3 Looper的prepare()

再来看下prepare()方法:

    private static void prepare(boolean quitAllowed) {        //ThreadLocal可以保存一个线程内的局部变量
        //这里判断当前线程是否已经存在Looper对象,存在的话则抛异常
        //因为一个线程只能创建一个Looper对象
        if (sThreadLocal.get() != null) {            throw new RuntimeException("Only one Looper may be created per thread");
        }        //当前线程没有创建Looper对象的话
        //则新创建一个Looper对象
        //并把这个Looper对象保存到ThreadLocal中
        sThreadLocal.set(new Looper(quitAllowed));
    }

prepare()中就是创建一个Looper对象并把Looper对象保存到线程中的ThreadLocal

3.1.1.1.4 Looper的构造方法

再来看下Looper的构造方法:

    private Looper(boolean quitAllowed) {        //创建消息队列
        mQueue = new MessageQueue(quitAllowed);        //记录当前线程.
        mThread = Thread.currentThread();
    }

Looper内部中就是创建了一个消息队列。

3.1.1.1.5 小结

应用启动时,主线程会创建一个Looper对象出来,Looper内部则创建消息队列。

3.1.1.2 子线程中创建Looper

在子线程中创建Looper非常简单,直接看例子吧:

3.1.1.2.1 子线程中创建Looper例子
class LooperThread extends Thread {    public Handler mHandler; 
    public void run() {
        Looper.prepare();
 
        mHandler = new Handler() {            public void handleMessage(Message msg) {                //TODO ...
            }
        };

        Looper.loop(); 
    }

调用一下无参数的prepare()方法即可。

3.1.1.2.2 Looper的prepare()
    public static void prepare() {
        prepare(true);
    }

最终还是调用有参数的prepare()方法,有参数的prepare()方法祥见上面的代码分析。
true代表这个Looper是可以退出的。主线程中创建的Looper则是不能退出的。这就是他们的区别。

3.1.2 Looper(循环器)的消息循环

Looper是通过loop()这个方法来进行消息循环的,我们来看下代码:

3.1.2.1 Looper的loop()
    public static void loop() {        //获得当前线程的Looper对象
        //myLooper()实际上通过sThreadLocal.get()来获取的
        final Looper me = myLooper();        //如果当前线程没有创建过Looper,则抛出异常
        if (me == null) {            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }        //获得Looper对象中的消息队列
        final MessageQueue queue = me.mQueue;        //死循环
        for (;;) {            //从消息队列中取出一个消息
            //如果消息队列没有消息的话,会阻塞在这里
            Message msg = queue.next(); // might block
            //消息为null的话表示停止消息循环
            //可以通过queue.quit()来停止,前提是通过prepare(true);来创建的
            //主线程中不允许停止消息循环
            if (msg == null) {                // No message indicates that the message queue is quitting.
                return;
            }            //...
            
            //分发消息去处理
            //msg.target就是要处理的Handler,祥见后面分析
            msg.target.dispatchMessage(msg);            //...

            //回收消息
            msg.recycleUnchecked();
        }
    }
3.1.2.2 小结

Looper中通过一个死循环从消息队列中取消息,一旦取到消息之后,就分发交给Handler来处理。

3.1.3 Looper(循环器)的退出

    public void quit() {
        mQueue.quit(false);
    }    
    //安全退出
    public void quitSafely() {
        mQueue.quit(true);
    }

退出Looper有两个方法,如上。最终还是通过调用MessageQueue.quit(boolean safe)方法来实现,只是传的参数不一样而已。这个在MessageQueue那一小节再来分析。

3.2 Message类

Message类用来保存消息的内容。我们先来看下Message类会保存哪些消息:

3.2.1 Message类主要成员变量

成员变量类型含义
whatint消息类型
objObject消息内容
whenlong消息触发时间
targetHandler消息处理者
callbackRunnable回调方法
sPoolMessage消息池
sPoolSizeint消息池大小
nextMessage下一条消息

这里只列举了一部分,详细的可以去看Message类的源码。

3.2.2 获取消息

Message内部维护了一个消息池,我们可以通过obtain()来从消息池中获取消息,而不是直接去new,这样可以提高效率。

3.2.2.1  Message的obtain()
    public static Message obtain() {        synchronized (sPoolSync) {            //如果消息池不为null,则从消息池中取出一条消息
            if (sPool != null) {                //从sPool中取出头结点
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // 清除in-use标记
                sPoolSize--;                return m;
            }
        }        //如果消息池为null,则直接new
        return new Message();
    }

3.2.3 回收消息

3.2.3.1  Message的recycle()
    public void recycle() {        //判断消息是否正在使用
        if (isInUse()) {            if (gCheckRecycle) {                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }            return;
        }        //调用recycleUnchecked()
        recycleUnchecked();
    }

调用recycleUnchecked()来回收。

3.2.3.2  Message的recycleUnchecked()
    void recycleUnchecked() {        //将消息标记为使用状态
        //清空消息其他参数
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;        synchronized (sPoolSync) {            //如果没有达到消息池的最大容量,则将消息回收到消息池中去
            //最大容量默认为50
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

3.2.4 小结

Message内部维护了一个消息池,这个消息池是以链表的形式存在的。通过消息池去获取Message对象能够避免直接创建对象,可以起到一个提高效率的作用。

3.3 Handler类

通常,我们都会通过继承Handler类来自定义一个Handler子类,然后重写handleMessage()方法来处理消息。同时,也是通过这个Handler子类来进行发送消息的。

我们先来看下Handler的构造方法。

3.3.1 Handler的构造方法

3.3.1.1 Handler的无参构造方法
    public Handler() {        this(null, false);
    }

最终会调用有参数的构造方法Handler(Callback callback, boolean async)

3.3.1.2 Handler的有参构造方法

先来看下上面提到的Handler(Callback callback, boolean async)

    public Handler(Callback callback, boolean async) {        //匿名类、内部类或本地类应该申明为static,否则会警告可能出现内存泄露
        if (FIND_POTENTIAL_LEAKS) {            final Class<? extends Handler> klass = getClass();            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }        //获得当前线程的Looper对象
        //myLooper()实际上通过sThreadLocal.get()来获取的
        mLooper = Looper.myLooper();        //如果mLooper为null则抛出异常
        if (mLooper == null) {            throw new RuntimeException(                "Can't create handler inside thread that has not called Looper.prepare()");
        }        
        //获得Looper对象中的消息队列
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

下面列出所有Handler的所有有参构造方法

    public Handler(Callback callback);    public Handler(Looper looper);    public Handler(Looper looper, Callback callback);    public Handler(boolean async);    public Handler(Callback callback, boolean async);    public Handler(Looper looper, Callback callback, boolean async);

可以看到:除了callbackasync之外,还可以传递一个Looper进来,即可以指定跟Handler要绑定的Looper,相关代码就不贴了,还是很简单的。

3.3.2 Handler发送消息

通常我们都是通过HandlersendMessage()方法来发送消息的:

3.3.2.1 Handler的sendMessage()
    public final boolean sendMessage(Message msg)
    {   
        //调用sendMessageDelayed()
        return sendMessageDelayed(msg, 0);
    }
3.3.2.2 Handler的sendMessageDelayed()
   public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {        if (delayMillis < 0) {
            delayMillis = 0;
        }        //调用sendMessageDelayed()
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
3.3.2.3 Handler的sendMessageAtTime()
    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;
        }        //调用enqueueMessage(),消息入队
        return enqueueMessage(queue, msg, uptimeMillis);
    }
3.3.2.4 Handler的enqueueMessage()
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {        //把当前的Handler对象保存到消息中的target中去
        //这样消息分发时才能找到相应的Handler去处理
        msg.target = this;        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }        //把消息放到消息队列中去
        return queue.enqueueMessage(msg, uptimeMillis);
    }

最终,发送消息就是将消息放到消息队列中去。

3.3.3 Handler发送Runnable

Handler除了sendMessage(Message msg)外,还可以发送一个Runnable出来,这是通过其post()方法实现的:

3.3.3.1 Handler的post()
    public final boolean post(Runnable r)
    {       return  sendMessageDelayed(getPostMessage(r), 0);
    }

post()方法里面同样也是通过sendMessageDelayed()来发送消息,我们来看下getPostMessage(r)这个方法:

3.3.3.1 Handler的getPostMessage()
    private static Message getPostMessage(Runnable r) {        //获取消息
        Message m = Message.obtain();        //Runnable赋值给Message中的callback
        m.callback = r;        //返回一个Message
        return m;
    }

所以,post()最终还是将Runnable对象包装成一个Message来进行消息发送的。

3.3.4 分发消息

前面Looper在消息队列中取到消息后就调用msg.target.dispatchMessage(msg);来分发消息,这里的msg.target就是Handler。我们来看下dispatchMessage()方法:

3.3.4.1 Handler的dispatchMessagee()
    public void dispatchMessage(Message msg) {        if (msg.callback != null) {            //当msg.callback不为null时,会回调message.callback.run()方法
            //即执行Runnable的run()方法
            handleCallback(msg);
        } else {            //当Handler存在Callback时,回调Callback的handleMessage();
            if (mCallback != null) {                if (mCallback.handleMessage(msg)) {                    return;
                }
            }            //调用Handler的handleMessage(msg)
            //即我们继承Handler重写的handleMessage(msg)方法
            handleMessage(msg);
        }
    }

我们再来看下handleCallback()这个方法:

3.3.4.2 Handler的handleCallback()
    private static void handleCallback(Message message) {        //执行Runnable的run()方法
        message.callback.run();
    }
3.3.4.3 小结

从上面的代码可以看到,分发消息的流程如下:

  1. 如果msg.callback不为null,这个msg.callback实际是个Runnable对象,则调用这个Runnablerun()方法,结束;为null的话就走到步骤2。

  2. 如果Handler的成员变量mCallback不为null,则调用mCallback.handleMessage(msg),结束;为null的话就走到步骤3。

  3. 调用HandlerhandleMessage(msg)方法,结束。

3.4 MessageQueue类

我们再来看下MessageQueue类,主要包括消息入队、取出消息和退出等。

3.4.1 MessageQueue构造方法

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;        //通过native方法去初始化化消息队列
        mPtr = nativeInit();
    }

3.4.2 消息入队

3.4.2.1 MessageQueue的enqueueMessage()
    boolean enqueueMessage(Message msg, long when) {        //判断消息是否关联了Handler,若无则抛异常
        if (msg.target == null) {            throw new IllegalArgumentException("Message must have a target.");
        }        //判断消息是否用过,若用过则抛异常
        if (msg.isInUse()) {            throw new IllegalStateException(msg + " This message is already in use.");
        }        synchronized (this) {            //当前消息队列是否正在退出
            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();            //获取message的when时间(触发时间)
            msg.when = when;            //获取消息队列里的消息
            //Message是个链表结构
            Message p = mMessages;            boolean needWake;            //如果消息队列里没有消息
            //或者发生时间(when)在链表的头结点之前
            if (p == null || when == 0 || when < p.when) {                //将消息插入到链表的头结点,即放入队头
                msg.next = p;
                mMessages = msg;                //如果处于阻塞状态,则唤醒
                needWake = mBlocked;
            } else {                //如果消息队列中已存在消息且触发时间(when)在链表的头结点之后
                //则插入到队列中间
                
                //通常这里的needWake为false,即不需唤醒消息队列
                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; // invariant: p == prev.next
                prev.next = msg;
            }            if (needWake) {//如果需要唤醒
                //在native层唤醒消息队列
                nativeWake(mPtr);
            }
        }        return true;
    }

可以看到,消息队列是根据消息触发时间来进行排队的,触发时间最早的消息将会排到队列的头部。当有新消息需要加入消息队列时,会从队列头开始遍历,直到找到消息应该插入的合适位置,以保证所有消息的时间顺序。
如果消息队列需要唤醒,则会在消息加入消息队列后对消息队列进行唤醒。

3.4.3 取出消息

3.4.3.1 MessageQueue的next()
    Message next() {        final long ptr = mPtr;        if (ptr == 0) {            //当消息循环已经退出,直接返回
            return null;
        }        //只有首次迭代为-1
        int pendingIdleHandlerCount = -1; 
        int nextPollTimeoutMillis = 0;        //死循环
        for (;;) {            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }            //这里会阻塞,直到nextPollTimeoutMillis超时或者消息队列被唤醒
            nativePollOnce(ptr, nextPollTimeoutMillis);            synchronized (this) {                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;                if (msg != null && msg.target == null) {                    //当target为空时,在消息队列中循环查找到下一条异步消息
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }                if (msg != null) {                    //当消息触发时间大于当前时间
                    if (now < msg.when) {                        //下一条消息还没准备好,设置一个超时唤醒
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {                        // 获取一条消息
                        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;
                }                //如果消息正在退出,返回null
                if (mQuitting) {
                    dispose();                    return null;
                }                //如果当前是第一次循环时
                //且当前消息队列为空时,或者下一条消息还没准备好时
                //即当前处于空闲的状态
                //那么就获取Idle Handler的数量
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }                if (pendingIdleHandlerCount <= 0) {                    //没有idle handlers 需要运行,继续循环并等待。
                    mBlocked = true;                    continue;
                }                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }            //运行idle handlers
            //只有第一次循环时才会执行这些代码块
            for (int i = 0; i < pendingIdleHandlerCount; i++) {                final IdleHandler idler = mPendingIdleHandlers[i];                //释放handler的引用
                mPendingIdleHandlers[i] = null; 

                boolean keep = false;                try {                    //执行idler的queueIdle()
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }                if (!keep) {                    synchronized (this) {                        //根据idler.queueIdle()的返回值来判断是否移除idler
                        //即返回true的话能够重复执行
                        mIdleHandlers.remove(idler);
                    }
                }
            }            //重置IdleHandler的数量为0,这样就保证不会重复运行
            //即只有第一次循环时会运行
            pendingIdleHandlerCount = 0;            //重置超时时间为0
            //即当调用一个idle handler时, 一个新的消息能够被分发,因此无需等待即可回去继续查找还未被处理的消息
            nextPollTimeoutMillis = 0;
        }
    }

取出消息时,如果没有消息或者超时时间还没到,则会处于阻塞的状态,直到超时时间过去或者消息队列被唤醒。当消息准备好时,才会返回消息出去。
另外,如果当前处于空闲的状态,则会执行IdleHandler中的方法。

3.4.4 消息队列退出

3.4.4.1 MessageQueue的quit()
    void quit(boolean safe) {        //主线程的消息队列是不允许退出的,主要还是看mQuitAllowed的值
        if (!mQuitAllowed) {            throw new IllegalStateException("Main thread not allowed to quit.");
        }        synchronized (this) {            //如果正在退出,则直接返回,防止多次操作
            if (mQuitting) {                return;
            }            //设置为正在退出中
            mQuitting = true;            //判断是否安全移除
            if (safe) {                //移除尚未触发的所有消息
                removeAllFutureMessagesLocked();
            } else {                //移除所有的消息
                removeAllMessagesLocked();
            }

            nativeWake(mPtr);
        }
    }

如果是安全退出,那么只会移除尚未触发的所有消息,对于正在触发的消息并不移除;
如果不是安全退出,则直接移除所有的消息。

4.一些面试题

这里收集了一些常见的面试题来解答一下,大部分答案其实都能在上面的分析中找到的,所以嘛,要认真看代码。
这里只列出一部分题目,如果有好的题目也可以留言补充哈~

4.1 Q:Looper.loop()是个死循环,主线程为什么不会卡死?

对于一个线程,如果执行完代码之后就会正常退出。但是对于主线程,我们肯定是希望能够一直存活的,那么最简单的方法就是写个死循环让它一直在执行,这样就不会退出了。那么主线程为什么不会卡死呢?如果消息队列里面有消息,Looper就取出来出来;如果没有消息就会阻塞,直到有新消息进来唤醒消息队列去处理,这一过程就是在这个死循环中处理的,所以说Looper本身是不会卡死的。像ActivityonCreate()onResume()等生命周期实际上就是在这个死循环中执行的。如果我们在ActivityonCreate()onResume()中做一些耗时操作,可能就会发生掉帧,甚至出现ANR,这才是我们看到的卡顿卡死现象。

4.2 Q:一个线程中是否可以有多个Handler?如果有多个,分发消息是如何区分的?

是可以有多个的。我们使用Handler发送Message时,Message中的target变量会保存当前的Handler引用,分发消息时就是靠这个target来区分不同的Handler



作者:四月葡萄
链接:https://www.jianshu.com/p/b3cd218cfbb7


0人推荐
随时随地看视频
慕课网APP