Android中的Handler机制

  • 一、Handler概述
  • 二、Handler发送消息的方法
  • 三、MessageQueue的enqueueMessage()
  • 四、Message的when字段
  • 五、子线程中使用Handler
  • 六、Looper.loop()
  • 七、获取下一个消息MessageQueue的next()
  • 八、Handler的dispatchMesssage()

Handler在native层还有很多知识点,这篇主要围绕java层。

一、Handler概述

1. 什么是Handler?

Handler可以将一个任务切换到指定线程中执行,常用来实现在子线程工作完后切换到UI线程更新UI。

在ViewRootImpl中的checkThread()会检查当前线程,在更新UI时会调用到这个方法,所以在非UI线程中执行UI操作就会抛出此异常。

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

UI控件不是线程安全的,多线程并发访问就会出现问题,所以只允许在主线程更新UI。之所以UI控件没有考虑上锁保证线程安全,是因为上锁会让代码逻辑变得复杂且会降低UI访问的效率。

2. Handler底层

Handler的运行需要底层的MessageQueue、Message和Looper来支撑。

a. MessageQueue

  • MessageQueue是消息队列。
  • 采用单链表的形式存储消息列表。
  • 向外提供对Message的插入和删除工作。

b. Message

  • 消息实体。

c. Looper

  • Handler创建的时候采用当前线程的Looper来构造消息循环系统。
  • Looper以无限循环的形式查看是否有新消息。
  • 有新消息时,就将Message对象从MessageQueue中取出,并将其交给Handler的dispatchMessage()。
  • Looper属于单个线程实例,通过ThreadLocal获得。

ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal可以很轻松地获取每个线程的Looper。

  • 线程中默认是没有Looper的,如果需要,就要为线程创建Looper。
  • 主线程在ActivtyThread创建时会初始化Looper,所以主线程默认可以使用Handler。

3. Handler工作步骤

  1. Handler创建,采用当前线程的Looper构建内部消息系统。
  2. Handler的post()、send()等方法调用MessageQueue的enqueueMessage()将一个Message加入到消息队列。
  3. Looper发现新消息的到来,处理这个消息。


二、Handler发送消息的方法

发送消息的方法比较多,主要可以分为两类,一类sendXXXX(),一类postXxxx(),两类的主要区别就是send系的主要接受一个Message对象,而post系的主要接收一个Runnable对象,但最终也是封装成了Message对象。


boolean sendMessage(Message msg)
public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}
boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
boolean sendMessageAtTime(Message msg, long uptimeMillis)
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);
}

最后调用到MessageQueue的enqueueMessage()。

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

上面三个send系的方法最终都是调用了MessageQueue的enqueueMessage(),出入了Message对象和执行时间。

boolean sendEmptyMessageDelayed(int what, long delayMillis)

sendEmptyMessage()系列的方法和上述方法类似,只是不需要传入一个Message,Message在方法中默认构造了。

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}

再来看post系的方法。

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

虽然传入的是Runnable对象,但是在方法调用过程中同样构造了Message,调用了send的方法,所以最终也是通过Message对象的形式交给MessageQueue的enqueueMessage()的。

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
boolean postDelayed(Runnable r, long delayMillis)

postDelay()也同样是构造了Message对象,调用了sendMessageDelayed(),最终传递给MessageQueue的。

public final boolean postDelayed(Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

三、MessageQueue的enqueueMessage()

在Handler的enqueueMessage()中,赋值了Message对象的target属性为执行的Handler对象,之后就调用了MessageQueue的enqueueMessage()。

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

MessageQueue的enqueueMessage()还是比较长的。

  1. 第一步是一些异常处理,先判断msg的target是否为空、判断msg是否正在使用,这个是否正在使用是在下面同步代码块中进行赋值的。
  2. 进入同步代码块了,判断如果该msg已经调用了quit()边不再继续,直接回收返回。
  3. 进行了一些赋值操作后就开始加队列的工作了。
  4. 首先如果队列中还没有Message或该Message执行时间为0或是小于队列中的第一个Message的时间,就将该Message作为队列的头,并更新头Message记录。
  5. 否则遍历队列,将Message插入到合适的位置,可以看出整个队列是按Message的when(一个相对时间,表示执行时间)字段从小到大排列的,如果两个Mssage的when字段相同,则先入队列的排在前面。
  6. 最后,如果需要唤醒MesssageQueue的next()就唤醒(MessageQueue的next()在没有消息的时候会阻塞,而一旦添加了新消息,enqueueMessage()被调用,自然就唤醒了next())。
boolean enqueueMessage(Message msg, long when) {
    // 1.
    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) {
        // 2.
        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;
        }
        
        //3.
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        
        // 4.
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 5.
            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;
        }
        
        // 6.
        if (needWake) {
            // 此处唤醒next()。
            nativeWake(mPtr);
        }
    }
    return true;
}

四、Message的when字段

上面我们通过分析MessageQueue的enqueueMessage(),知道了Message队列是按照Message对象的when字段去排列的,那么when字段是什么呢?

在Handler的sendMessageDelayed中可以看到Message对象的when字段是如何初始化的。when的值是SystemClock.uptimeMillis() + delayMillis

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

SystemClock#uptimeMillis()表示当前时间的一个相对时间,代表自系统启动开始从到调用该方法时度过的毫秒数。加上传参设置的delayMillis,整个when表示的时间代表该Message期望被分发的相对时间。

提供时间获取的方法有很多,为什么要用该系统启动到调用方法的相对时间,而不用System.currentTimeMillis()呢,是因为它代表的是从1970-01-01 00:00:00到当前时间的毫秒数,这个值是一个强关联系统时间,我们可以通过修改系统时间达到修改该值的目的,所以该值是不可靠的值,会有可能导致延时消息失效。when字段只是用时间差来表示先后关系,所以只需要一个相对时间就可以达成目的。

五、子线程中使用Handler

在UI线程中,Looper早已在app启动过程中就为我们初始化好了,所以可以直接获取Handler对象并使用,但是在子线程中,必须手动去初始化Looper,之后将Handler绑定该子线程的Looper才能够使用。

Looper.prepare(); // 为子线程创建一个Looper
mHandler = new Handler(){ 
    @Override 
    public void handleMessage(Message msg) { 
        Log.d(TAG, "子线程的Handler"); 
    } 
}; 
mHandler.sendEmptyMessage(1);
Looper.loop();// 开始循环

可以向上面一样写,这里面有一些注意点。

  • 在调用Handler之前必须调用Looper.prepare()方法,在当前线程中创建一个looper。我们也可以直接使用HandlerThread,其中是带有Looper的。
  • Looper.loop()是无限循环的,所以在Looper.loop()后边的程序代码块是无法执行到的。loop()方法的主要作用是一直不断的通过queue.next()方法来读取来自messagequeue中的msg,这个方法是block的状态,如果queue中没有消息的话会一直阻塞在这里。

除了上面的写法,也可以在Handler构造函数传入Looper对象来绑定。

Handler handler = new Handler(looper);

上面两种绑定Looper的方法中,第二种很直观通过传参绑定了,那么第一种方法是如何绑定的呢?

public Handler() {
    this(null, false);
}

看到这里调用了Looper的myLooper(),猜想就是利用了ThreadLocal获取到当前线程的Looper对象了。

public Handler(Callback callback, boolean async) {
    // ......
    mLooper = Looper.myLooper();
    // ......
}

果然是调用了TreadLocal对象的get,该ThreadLocal对象泛型是Looper的。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

在Looper的prepare()中对该ThreadLocal放入了Looper对象,所以在使用Handler之前要先调用Looper的prepare()。

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.loop()

Looper.loop()
  1. 获取Looper对象并判空,再获取MessageQueue对象。
  2. 进入主循环,调用MessageQueue的next()去获取下一个消息,如果没有消息就一直阻塞。
  3. 如果返回消息为null,直接return,此时说明整个系统要处于退出状态了。
  4. 如果获取到了,就调用msg的Handler的dispatchMessage()执行消息。
  5. 之后将msg放入回收池中等待复用。
  6. 继续主循环,循环上述步骤,一直到退出。
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.");
    }
    // 获取looper的MessageQueue
    final MessageQueue queue = me.mQueue;
    
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    
    // 开始死循环,不停获取消息队列中的消息并执行
    for (;;) {
        // 调用MessageQueue的next()获取新消息,这里可能被阻塞。
        Message msg = queue.next(); // might block
        // 如果返回了null,那么就说明MessageQueue已经quit了
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        
        // ...... 日志输出等
        
        try {
            // 调用msg的Handler对象的dispatchMessage()去执行消息。
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        
        // ...... 日志输出等
        
        // 将此msg放入回收对象池中,obtain()就是去复用这些对象。
        msg.recycleUnchecked();
    }
}

loop()不会自动退出,Looper#quit() 或者 Looper#quitSafely() 让它退出。两个方法都调用了MessageQueue#quit(boolean)方法,当MessageQueue#next()方法发现已经调用过 MessageQueue#quit(boolean)时会return null结束当前调用,否则的话即使MessageQueue 已经是空的了也会阻塞等待。

Q:既然是一个死循环,那为什么没有阻塞主线程呢?

如果说操作系统是由中断驱动的,那么Android的应用在宏观上可以说是 Handler机制驱动的,所以主线程中的 Looper不会一直阻塞的,原因如下:

  • 当队列中只有延迟消息的时候,阻塞的时间等于头结点的 when 减去 当前时间,时间到了以后会自动唤醒。
  • 在Android中 一个进程中不会只有一个线程,由于 Handler 的机制,导致我们如果要操作 View 等都要通过 Handler 将事件发送到主线程中去,所以会唤醒阻塞。
  • 传感器的事件,如:触摸事件、键盘输入等。
  • 绘制事件:我们知道要想显示流畅那么屏幕必须保持60fps的刷新率,那绘制事件在入队列时也会唤醒。

更多的知识Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

七、获取下一个消息MessageQueue的next()

MessgaeQueue # next()

这个方法可能是Handler机制中最长知识点最多的方法了,被上面的loop()调用,来获取下一个Message对象。

  1. 处理已经取消的情况。
  2. 进入循环调用nativePollOnce(),如果没有消息,就阻塞,如果有消息就继续。
  3. 获取第一个消息,如果第一个消息是barrier,就遍历去获取第一个异步消息。

enqueueMessage()会判断target不为空,barrier消息是在postSyncBarrier()中添加的,而异步消息是在Handler构造函数中指定的该Handlerf发送的消息是否为异步的。

  1. 如果消息时间到了就整理队列并返回该消息,如果每到就更新时间点。
  2. 如果此为第一遍循环,就去执行idleHandler。执行完之后复制变量让下一次循环不再执行此条。
Message next() {
    // 处理Looper已经取消的情况,调用disposed()方法后mPtr=0
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }
    // 记录空闲时需要处理的IdleHandler的数量。
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    // 表示距离处理下一个消息的时间,只要大于0就表明还有消息等待处理
    int nextPollTimeoutMillis = 0;
    
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
           // 刷新Binder命令
           Binder.flushPendingCommands();
        }
        
        // 调用native层,如果返回了就说明可以从队列中取出一条消息,如果消息队列中没有消息就阻塞等待
        // 靠enqueueMessage()中最后一步调用nativeWake(mPtr)来唤醒该方法
        nativePollOnce(ptr, nextPollTimeoutMillis);
        
        // 上锁
        synchronized (this) {
            // 获取开机到现在的时间
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            // 表头第一个消息
            Message msg = mMessages;
            
            // 判断该Messagehi否是barrier
            if (msg != null && msg.target == null) {
                
                // 循环遍历出第一个异步消息,如果设置了barrier,就不能再执行同步消息了,除非将barrier移除。
                // 但是异步消息不受影响照样执行,所以在这里要找到异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                    // msg为null说明已经退出循环,为异步消息则消息为要找的
                } 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 (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    // 标记msg正在使用
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 进此处是msg==null,再没有其它消息了
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
            // Process the quit message now that all pending messages have been handled.
            // 正在退出了,返回null。
            if (mQuitting) {
                dispose();
                // 返回null,通知looper也停止
                return null;
            }
            
            // 判断如果这是第一次循环(只有第一次循环时会小于0)并且队列为空或还没到处理第一个的时间
            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;
            }
            // 初始化最少四个要被执行的IdleHandler
            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }
        
        // 开始循环执行所有的IdleHandler并根据返回值判断是否保留
        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);
                }
            }
        }
        
        // IdleHandler只会在消息队列阻塞之前执行一次,之后再不会执行,知道下一次被调用next()。
        pendingIdleHandlerCount = 0;
        
        // 当执行了IdleHandler后,会消耗一段时间,刺死可能已经到达执行消息的时间了,所以重置该变量再重新检查时间。
        nextPollTimeoutMillis = 0;
    }
}

八、Handler的dispatchMesssage()

在Looper的loop()中会调用该方法去处理msg,其实是一个分发过程。

msg.target.dispatchMessage(msg);

这里调用了target的方法,target字段是在Handler的enqueueMessage()中赋值的,为其Handler对象。这里调用了dispatchMessage()。

  1. 首先看msg中有没有callback对象,如果有,就交给callback执行。
  2. 如果没有再看有没有全局callback对象,如果有,就交给全局callback处理。
  3. 如果都没有或者全局callback处理不了,再调用handleMessage()。
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

这里的一系列优先级让我想起了View的事件分发。
首先判断的msg.callbck是调用post系方法时传入的runnable,这个执行的优先级最高。

private static void handleCallback(Message message) {
    message.callback.run();
}

再就是判断mCallback,这个callback是在某些带有Callback参数的Handler构造函数中传入的。

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

当然了,这个runnable完全可以返回false,设计在个人,如果想让这个msg还有机会交给Handler的handleMessage()去处理,就让它在合适的情况返回false。

最终到达handleMessage(),这个优先级最低了。

public void handleMessage(Message msg) {
}

我们最常用的就是在定义Handler对象时重写这个方法添加自己的逻辑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容