Android消息机制--Handler 深入解析(JAVA层)

1. Android 消息机制概述

阅读本文之前,你需要知道一下几点

1.Handler的使用必须依赖于一个Looper对象

2.线程是默认没有Looper的,但是UI线程有一个Looper对象;

3.在启动APP的时候,UI线程Looper会初始化完毕,所以可以得出,UI线程可以直接使用Handler。

1.1Android消息机制是什么?

Android消息机制 主要指Handler的运行机制以及Handler所附带的MessageQueue和Looperde的工作流程。Handler的主要作用是将任务切换到指定线程去执行,我们常用的就是通过Handler来异步更新UI(线程间的信息传递)。

1.2Android消息机制架构

消息机制架构.png

图片来自http://wangkuiwu.github.io/2014/08/26/MessageQueue/
侵图删!

(01) Looper是消息循环类,它包括了mQueue成员变量;mQueue是消息队列MessageQueue的实例。Looper还包含了loop()方法,通过调用loop()就能进入到消息循环中。
(02) MessageQueue是消息队列类,它包含了mMessages成员;mMessages是消息Message的实例。MessageQueue提供了next()方法来获取消息队列的下一则消息和enqueueMessage()插入消息。
(03) Message是消息类。Message包含了next,next是Message的实例;由此可见,Message是一个单链表。Message还包括了target成员,target是Handler实例。此外,它还包括了arg1,arg2,what,obj等参数,它们都是用于记录消息的相关内容。
(04) Handler是消息句柄类。Handler提供了sendMessage()来向消息队列发送消息;发送消息的API有很多,它们的原理都是一样的,这里仅仅只列举了sendMessage()一个。 此外,Handler还提供了handleMessage()来处理消息队列的消息;这样,用户通过覆盖handleMessage()就能处理相应的消息。

消息机制位于Java层的框架主要就有上面4个类所组成。在C++层,比较重要的是NativeMessageQueue和Loop这两个类。
当我们启动一个APK时,ActivityManagerService会为我们的Activity创建并启动一个主线程(ActivityThread对象);在启动主线程时,就会创建主线程对应的消息循环,并通过调用loop()进入到消息循环中。当我们需要往消息队列发送消息时,可以继承Handler类,然后创建Handler类的实例;接着,通过该实例的sendMessage()方法就可以向消息队列发送消息。 也就是说,主线程的消息队列也一直存在的。当消息队列中没有消息时,消息队列会进入空闲等待状态;当有消息时,则消息队列会进入运行状态,进而将相应的消息发送给handleMessage()进行处理。

2.Android消息机制的引入

2.1 为什么需要Handler?

1.UI线程不能做耗时操作。
我们都知道,Android是只允许我们在UI线程更新UI,但是UI线程又不能做耗时的操作,所以,我们经常会用到Handler来异步更新UI。
2.子线程不能访问UI。
如果所有的线程都能够更改UI,将会造成UI处于不可预期的状态。同时加锁将会降低UI的访问效率。

基于此,Android提供了一种全新的方式来异步更新UI,在子线程进行耗时操作,通过Handler的协作来完成主线程更新UI。

2.2 Handler来更新UI

step1: 在子线程发送Message

  Message msg=handler.obtainMessage();
                        msg.obj=list;//发送了一个list集合
   //sendMessage()方法,在主线程或者Worker Thread线程中发送,都是可以的,都可以被取到
                        handler.sendMessage(msg);

step 2:在主线程handleMessage

handler=new MyHandler();
  class MyHandler extends  Handler
    {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.i(">>>>>>>",Thread.currentThread().getName());
            list= (List<NewsBean.Second.Third>) msg.obj;//接收传过来的集合
            listView.setAdapter(new NewsListBaseAdapter(list,MainActivity.this));//更新UI
        }
    }

这是我们Handler的最简单的用法。
接下来我们再以这个例子为引入,进行深入的源码分析。

3.Android消息机制源码解析

源码分析基于8.0

我们都知道,线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
这一点我们可以从源码得知

ActivityThread.main()--表示源码中ActivityThread类中的main方法,下同

         public static void main(String[] args) {
6506        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
6507        SamplingProfilerIntegration.start();
6508
6509        // CloseGuard defaults to true and can be quite spammy.  We
6510        // disable it here, but selectively enable it later (via
6511        // StrictMode) on debug builds, but using DropBox, not logs.
6512        CloseGuard.setEnabled(false);
6513
6514        Environment.initForCurrentUser();
6515
6516        // Set the reporter for event logging in libcore
6517        EventLogger.setReporter(new EventLoggingReporter());
6518
6519        // Make sure TrustedCertificateStore looks in the right place for CA certificates
6520        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
6521        TrustedCertificateStore.setDefaultUserDirectory(configDir);
6522
6523        Process.setArgV0("<pre-initialized>");
6524
6525        Looper.prepareMainLooper();
6526
6527        ActivityThread thread = new ActivityThread();
6528        thread.attach(false);
6529
6530        if (sMainThreadHandler == null) {
6531            sMainThreadHandler = thread.getHandler();
6532        }
6533
6534        if (false) {
6535            Looper.myLooper().setMessageLogging(new
6536                    LogPrinter(Log.DEBUG, "ActivityThread"));
6537        }
6538
6539        // End of event ActivityThreadMain.
6540        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
6541        Looper.loop(); //开始循环
6542
6543        throw new RuntimeException("Main thread loop unexpectedly exited");
6544    }

通常在新打开一个APK界面时,系统会为APK启动创建一个ActivityThread对象,并调用它的main()方法。该main函数主要做了两件事:(01),新建ActivityThread对象。 (02),使用主线程进入消息循环。

3.1 发送消息

1.Message msg=handler.obtainMessage();

首先我们通过obtainMessage()方法获取Message的实例。

Handler.obtainMessage()

   public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();//sPool为Message实例化对象,当sPool为空时,我们通过new实例化。
    }

2.handler.sendMessage(msg);
通过handler发送Message。

Handler.sendMessage(msg)

public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

调用sendMessageDelayed(msg, 0)方法,第二个参数为延迟时间,如果不带参数延迟为0;

Handler.sendMessageDelayed

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

调用sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis),

Handler.sendMessageAtTime

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;//获取MessageQueue实例
        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 实例,这里只有MessageQueue实例不为空的时候我们才会去添加Message。我们看看mQueue是在哪里赋值的。
我们在Handler中找到了他的构造方法

Handler

 public Handler(Callback callback, boolean async) {
        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());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;//给MessageQueue实例化
        mCallback = callback;
        mAsynchronous = async;
    }

我们可以看到这个有参的构造函数通过 mQueue = mLooper.mQueue;进行实例化。

Handler

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

而我们初始化Handler的时候不管是通过继承Handler还是匿名CallBack接口的都会调用Handler(Callback callback, boolean async) 方法,所以,我们实例化Handler的时候就会实例化MessageQueue。

mLooper 是Looper的一个实例化对象,我们去Looper.java看看mQueue的初始化。

在Looper的构造方法中找到了mQueue的初始化。

Looper

 private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);//初始化Looper的时候就会初始化MessageQueue
        mThread = Thread.currentThread();
    }

我们前面有提到,使用Handler之前一定要初始化一个Looper,而这里可以看到,初始化Looper的时候也会初始化MessageQueue。我们可以通过两个方法来创建Looper,

分别是prepareMainLooper()和prepare()。

prepareMainLooper()是给UI线程使用的,我们在前面的分析已经知道,ActivityThread被创建时就会初始化Looper,就已经调用了prepareMainLooper()方法,我们看看prepareMainLooper()的实现:

Looper.prepareMainLooper()

    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);//还是调用Prepare方法
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();//return sThreadLocal.get();通过ThreadLocal返回一个Looper
        }
    }

我们可以看到这个方法的注释,也是将当前线程作为一个Looper。

我们再接着看prepare()方法

Looper.prepare(boolean quitAllowed)

 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存储到ThreadLocal中
    }

到这里我们可以看到Looper进行了创建,并被保存到了ThreadLocal中。我们终于知道了在Looper创建的时候同时也会创建MessageQueue对象。

ThreadLocal: 线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。我们会在后面详细讲解ThreadLocal。

回到前面的方法sendMessageAtTime(Message msg, long uptimeMillis),这个时候我们知道里面的queue(MessageQueue对象)不为空,然后返回return enqueueMessage(queue, msg, uptimeMillis);

MessageQueue.enqueueMessage

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//this为当前handler的对象
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

这里直接调用queue.enqueueMessage(msg, uptimeMillis);

由于queue是MessageQueue的对象,我们去MessageQueue.java看看
enqueueMessage(msg, uptimeMillis)

MessageQueue.enqueueMessage(msg, uptimeMillis)

 boolean enqueueMessage(Message msg, long when) {
        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) {//mQuitting标识Looper是否调用了quit()方法
                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;
            Message p = mMessages;
            boolean needWake;
            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 {
                // 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();
                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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);//调用本地方法
            }
        }
        return true;
    }

这里主要是将消息加入消息队列中,然后调用底层的方法。至此,我们sendMessage()已经分析完了。

3.2 处理消息

我们继续往下面分析,前面我们知道,UI线程在进行Looper创建的时候会调用 Looper.loop();方法,而这个方法也是最重要的我们来看看他的源码:

Looper.loop()

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();//return sThreadLocal.get();通过ThreadLocal获取当前线程的Looper对象。
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;//获取MessageQueue对象

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {//没有消息,则退出循环
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging; //默认为null,可通过setMessageLogging()方法来指定输出,用于debug功能
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);//将message分发下去,target为当前Handler对象
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

loop方法小结:

loop()进入循环模式,不断重复下面的操作,直到没有消息时退出循环
读取MessageQueue的下一条Message;
把Message分发给相应的target;
再把分发后的Message回收到消息池,以便重复利用。

final Looper me = myLooper(),通过myLooper()方法获取当前线程的Looper对象

Looper.myLooper()

 /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

这里我们通过ThreadLocal.get()来获取Looper对象,前面我们通过prepare()方法进行了set,这里通过get()方法获取出来。

这里我们主要看 msg.target.dispatchMessage(msg);

Handler.dispatchMessage(msg)

     /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {//是一个Runnable对象,实际就是Handler的post方法所传递的Runnable参数
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);//处理消息
        }
    }

这里调用了handlMessage(msg)方法处理消息;

Handler.handlMessage(msg)

    /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(Message msg) {
    }

到这里是不是恍然大悟,没错,这个就是我们前面重写的方法。注释也很清楚,子类必须重写这个方法。

到这里,我们就已经理清了所有的流程了。

4.附加知识

4.1ThreadLocal

前面由于在分析整个流程,怕影响思路,所以把ThreadLocal放到这里来讲。

ThreadLocal: 线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。

前面我们知道,

Looper的prepare()方法会调用ThreadLocal.set(T value)
Looper的loop()方法会调用ThreadLocal.get().

ThreadLocal.set(T value):将value存储到当前线程的TLS区域,源码如下:

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//将Looper存储到ThreadLocalMap 
        else
            createMap(t, value);
    }

ThreadLocal.get():获取当前线程TLS区域的数据,源码如下:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;//获取前面存储的Looper
            }
        }
        return setInitialValue();
    }

5.总结

1.UI线程默认会有一个Looper
2.Looper通过ThreadLocal的set和get方法进行设置和获取
3.初始化的Handler的时候同时会初始化一个MessageQueue,Handler必须依赖于一个Looper;
4.Looper 中的loop方法非常的重要,这是一个循环监听MessageQueue是否更新的方法。
5.当有数据更新,则调用dispatchMessage()方法处理数据,这个方法会调用回调方法handleMessage(Message msg)来处理消息。
6.Message是信息载体。

消息机制还有许多方法这里没有进行讲解
如:
1.我们也可以通过Handler handler =new Handler(new CallBack)来创建handler
2.Looper.quit()方法退出循环,当然本质还是通过MessageQueue.quit()方法。前面有提到一个mQuitting就是通过这个判断的。
3.发送消息也可以通过post(Runnable r)方法
等等
需要你们自己去看源码。
这里主要是通过一个简单的流程对源码进行解析。

参考:
Android开发艺术探索
Android消息机制架构和源码解析
Android消息机制1-Handler(Java层)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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