Android之解讀Handler

1.概述

Android中我們通常會把耗時操作放在子線程中评架,然后通過Handler來發(fā)送消息到主線程進行UI更新眷茁,本文通過探究源碼來分析Handler背后的原理是什么,本篇主要涉及Message纵诞、MessageQueue上祈、Looper、Handler這四個類的詳細(xì)分析挣磨。

2.Message

Message類是個final類不能被繼承雇逞,用作封裝數(shù)據(jù)的容器,是鏈表結(jié)構(gòu)茁裙,可以被發(fā)送給 Handler

  • **主要屬性
public int what; //用戶定義消息代碼以便收件人可以識別這是哪一個Message
public int arg1; //如果只是想向message內(nèi)放一些整數(shù)值塘砸,可以使用arg1和arg2來代替setData方法
public int arg2; 
public Object obj; //發(fā)送給接收器的任意對象,在使用 Messenger 跨進程傳遞消息時,通常使用它傳遞給接收者,在其他場景下我們一般使用 setData() 方法
/*package*/ static final int FLAG_IN_USE = 1 << 0; //標(biāo)識消息是否在被使用
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1; //標(biāo)識是否是異步消息
/*package*/ Bundle data;  //setData()用到的Bundle
/*package*/ Handler target; // 與消息關(guān)聯(lián)的Handler
/*package*/ Runnable callback;  //處理消息的回調(diào) 
/*package*/ Message next; // 有時以鏈表的形式關(guān)聯(lián)后一個消息
private static final Object sPoolSync = new Object();
private static Message sPool; //消息池
private static int sPoolSize = 0;

從這些屬性中可以看出:

  • Message.what 用來標(biāo)識干什么
  • 可以存儲Bundle對象
  • Message持有Handler的引用晤锥,將Handler引用賦值給target
  • Message內(nèi)部有一個消息池
獲取Message
//構(gòu)造方法
public Message() {
}

Message的構(gòu)造方法是個空方法掉蔬,官方更推薦通過Message.obtain()來獲取一個Message,我們也經(jīng)常使用Handler.obtainMessage()來獲取消息矾瘾,該方法內(nèi)部也是調(diào)用Message.obtain()

//Handler 的obtainMessage()
public final Message obtainMessage(){
        return Message.obtain(this);
}

Message.obtain()源碼如下:

    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // 清除在使用的標(biāo)識
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

如果消息池中有消息的話就取出女轿,設(shè)置標(biāo)識未使用,消息的next屬性設(shè)為null壕翩,將消息池指向該消息的下一個蛉迹,如果沒有的話就調(diào)用構(gòu)造函數(shù)。Message.obtain()的其它重載方法都是在調(diào)用了該方法獲取到Message之后放妈,為其屬性賦值北救。例如下面這個重載方法:

    public static Message obtain(Handler h, int what, int arg1, int arg2) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.arg1 = arg1;
        m.arg2 = arg2;

        return m;
    }

既然消息是從消息池獲取的,那么消息池的消息從哪來的呢芜抒?是消息被回收時放入的珍策,消息回收時調(diào)用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) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

將消息的數(shù)據(jù)清除之后,這個消息加入了回收消息的鏈表中宅倒。

3.MessageQueue

MessageQueue管理著消息列表攘宙,消息由Handler來發(fā)送到MessageQueue,由Looper循環(huán)的取出

  • 主要屬性
private final boolean mQuitAllowed;//表示MessageQueue是否允許退出
private long mPtr; //mPtr是native代碼相關(guān)的
Message mMessages; //表示消息隊列的頭Head
private boolean mBlocked; //next()調(diào)用是否被阻塞

MessageQueue持有消息列表的頭,是一個單鏈表的結(jié)構(gòu)

  • 構(gòu)造方法
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed; //true允許退出
        mPtr = nativeInit();
    }

MessageQueue調(diào)用native方法來進行初始化蹭劈,該方法通常由Looper.prepare()調(diào)用

  • 消息入隊:MessageQueue.enqueueMessage()
    當(dāng)Handler發(fā)送消息到MessageQueue時疗绣,由該隊列的enqueueMessage()方法來負(fù)責(zé)把Message插入到隊列中
    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) { // Message 必須關(guān)聯(lián)Handler
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {  //如果該Message 已經(jīng)在處理中,則拋出異常
            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();
            msg.when = when;
            Message p = mMessages; // 臨時變量p指向隊列頭
            boolean needWake;
            if (p == null || when == 0 || when < p.when) { // 插入的這個message應(yīng)該在第一個位置也就是隊首
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                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) {
                nativeWake(mPtr); //激活消息隊列去獲取下一個消息
            }
        }
        return true;
    }

Message入隊時先判斷是否要插入隊首,如果不是的話則按照時間順序插入到某個合適的位置祟蚀。

  • 消息出隊: MessageQueue.next()
    Message next() {
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            ... //省略代碼

            nativePollOnce(ptr, nextPollTimeoutMillis); //等待被激活,然后從消息隊列中獲取消息
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;  //msg指向隊首
                if (msg != null && msg.target == null) {  // 隊首是消息屏障
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // 下一個消息沒有到要處理的時機則設(shè)置激活等待時間
                        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 {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                 ... //省略部分無關(guān)代碼
        }
    }

MessageQueue開啟一個循環(huán)來取消息割卖,隊列被激活之后前酿,首先判斷隊首是不是消息屏障,如果是則跳過所有的同步消息鹏溯,查找最先要處理的異步消息罢维。如果第一個待處理的消息還沒有到要處理的時機則設(shè)置激活等待時間;否則這個消息就是需要處理的消息丙挽,將該消息設(shè)置為 inuse肺孵,并將隊列設(shè)置為非 blocked 狀態(tài),然后返回該消息颜阐。

4.Looper

用于為Thread運行消息循環(huán)的類平窘,Thread默認(rèn)沒有消息循環(huán)的Looper,需要在Thread中調(diào)用Looper.prepare()來創(chuàng)建一個Looper凳怨,然后調(diào)用Looper.loop()來執(zhí)行消息循環(huán)瑰艘。

  • 主要屬性
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;  // 主線程中的Looper
final MessageQueue mQueue; // 關(guān)聯(lián)的消息隊列
final Thread mThread; //Looper所在的線程

從屬性中可以看到Looper中持有一個消息隊列,就可以調(diào)用MessageQueue的相關(guān)方法肤舞。

  • 構(gòu)造函數(shù)
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed); 
        mThread = Thread.currentThread();
    }

構(gòu)造函數(shù)就兩行代碼紫新,新建了一個MessageQueue,把Looper所在當(dāng)前線程賦值給mThread屬性李剖,但是這個方法是private芒率,外部不能通過調(diào)用它來構(gòu)造一個Looper,該方法在Looper.prepare中被調(diào)用:

    public static void prepare() {
        prepare(true);
    }

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

prepare()方法通過構(gòu)造方法生一個Looper然后把它保存在了ThreadLocal中,不過在這之前先判斷了ThreadLoacl中是否保存了一個Looper篙顺,如果有的話就會拋出異常偶芍,這說明一個Thread只會有一個Looper與之關(guān)聯(lián),那么來看一下ThreadLocal是什么

  • ThreadLocal
    ThreadLocal的作用是提供線程內(nèi)的局部變量慰安,這種變量在線程的生命周期內(nèi)起作用腋寨,在本線程內(nèi)隨時隨地可取,隔離其他線程化焕。所以Looper通過ThreadLocal可以在線程中存取萄窜,線程取得與之關(guān)聯(lián)的Looper之后,就可以調(diào)用Looper.loop()循環(huán)取出消息,并且每個線程的Looper是獨立的查刻,不相關(guān)的键兜。
  • Looper.loop() 循環(huán)取出消息并處理
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {    //當(dāng)前線程必須創(chuàng)建 Looper 才可以執(zhí)行
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    //底層對 IPC 標(biāo)識的處理,不用關(guān)心 
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {    //無限循環(huán)模式
        Message msg = queue.next(); //從消息隊列中讀取消息穗泵,可能會阻塞
        if (msg == null) {    //當(dāng)消息隊列中沒有消息時就會返回普气,不過這只發(fā)生在 queue 退出的時候
            return;
        }

        //...
        try {
            msg.target.dispatchMessage(msg);    //調(diào)用消息關(guān)聯(lián)的 Handler 處理消息
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        //...
        msg.recycleUnchecked();    //標(biāo)記這個消息被回收
    }
}

loop()方法首先取到Looper關(guān)聯(lián)的MessageQueue,開啟一個無限循環(huán)調(diào)用MessageQueue.next()來不斷的取出消息佃延,取出消息之后調(diào)用msg.target.dispatchMessage(msg);前面介紹Message時說到Message的target屬性就是Handler现诀,這里就將消息交給了Handler來處理。要注意將消息交給Handler處理是在Loop()中履肃,所以Looper.loop()被調(diào)用的線程就是Handler處理消息的線程仔沿。例如通過Handler更新UI時,在子線程調(diào)用Handler.sendMessage()發(fā)送消息尺棋,由于UI線程的Looper.loop()已經(jīng)由系統(tǒng)在UI線程調(diào)用封锉,所以Handler處理消息也在主線程,總結(jié)來說:Looper.loop()在哪個線程調(diào)用的膘螟,Handler就切換到哪個線程處理消息成福。

5.Handler

Handler允許你發(fā)送和處理一個Message和Runnable對象,每個Handler實例都和一個單獨的線程和線程的MessageQueue相關(guān)聯(lián)荆残。

  • 主要屬性
final Looper mLooper; 
final MessageQueue mQueue;
final Callback mCallback;
final boolean mAsynchronous;
IMessenger mMessenger;

Callback 是Handler內(nèi)部的一個接口奴艾,它可以作為構(gòu)造函數(shù)的參數(shù)用于新建 Handler。

public interface Callback {
        public boolean handleMessage(Message msg);
}
//Handler的一個構(gòu)造函數(shù)
public Handler(Callback callback) {
        this(callback, false);
    }
// 例如:
Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        //這里處理消息
        return false;
    }
});

Handler有許多重載的構(gòu)造函數(shù)脊阴,他們的作用都是給上面的這幾個屬性賦值握侧,要注意的是mLooper在賦值時由兩種方式:
1.構(gòu)造Handler時不傳遞Looper對象,Looper從當(dāng)前線程獲取嘿期,當(dāng)前線程不是UI線程時品擎,需要先調(diào)用Looper.prepare()
2..構(gòu)造Handler時構(gòu)造參數(shù)傳遞Looper對象,Handler就與傳遞的Looper和Looper所在的線程相關(guān)聯(lián)
MessageQueue 由Looper.mQueue倆獲取备徐,所以構(gòu)造Handler時就已經(jīng)與所在線程萄传、Looper、MessageQueue向關(guān)聯(lián)了蜜猾。

  • Handler發(fā)送消息原理
    我們用Handler發(fā)送消息時通常使用 handler.sendMessage(message)秀菱,下面我們來看這個方法及其類似方法是怎么實現(xiàn)的:
public final boolean sendMessage(Message msg) {
        return sendMessageDelayed(msg, 0);
    }

public final boolean sendEmptyMessage(int what) {
        return sendEmptyMessageDelayed(what, 0);
    }

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

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

 public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
    }

這幾個發(fā)送Message的方法最后都調(diào)用了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;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

參數(shù)msg即是我們要發(fā)送的Message蹭睡,uptimeMillis參數(shù)則表示發(fā)送消息的時間衍菱,它的值等于自系統(tǒng)開機到當(dāng)前時間的毫秒數(shù)再加上延遲時間,方法中的mQueue是Handler初始化的時候賦值的肩豁,然后把這三個參數(shù)傳入enqueueMessage()方法:

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

把Message的target設(shè)置為當(dāng)前Handler脊串,最后queue.enqueueMessage(msg, uptimeMillis)把消息插入消息隊列中辫呻,至此消息的發(fā)送就完成了

  • Handler接受消息并處理
    既然Message進入了消息隊列,就有出隊的方法琼锋,出隊操作是由Looper.loop()來執(zhí)行的放闺,loop()方法開啟一個無限循環(huán)了從MessageQueue去取出Message,然后調(diào)用Message.target.dispatchMessage(),Message.target就是Handler缕坎,所以呢走到了Handler的dispatchMessage():
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

可以看到怖侦,Handler 在處理消息時會進行判斷:

  1. msg.callback 不為空
    如果Message的callback屬性不為空的話就直接調(diào)用Runnable的run()方法。
  2. mCallback 不為空
    如果使用Callback構(gòu)造了Handler谜叹,則會進入到Callback接口的handleMessage(),該方法返回true的話就不會往下走了匾寝。
  3. 最后就調(diào)用 Handler.handleMessage() 方法,例如:
        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //處理Message
            }
        };

至此Handler就可以獲取到Message并處理了

  • Handler.post()系列方法
public final boolean post(Runnable r){
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

public final boolean postAtTime(Runnable r, long uptimeMillis){
        return sendMessageAtTime(getPostMessage(r), uptimeMillis);
    }

public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
        return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
    }

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

post()系列方法和上面的sendMessage()系列方法一樣荷腊,最終都是調(diào)用sendMessageAtTime(),不同的地方在于參數(shù)傳了一個getPostMessage(r)旗吁,我們來看一下這個方法

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

該方法就是構(gòu)造了一個Message,并把參數(shù)Runnable設(shè)置為Message的callback屬性停局,sendMessageAtTime()接下里就和上面的流程一樣了

總結(jié)

  • Handler的構(gòu)造時會關(guān)聯(lián)一個線程,并發(fā)送消息到該線程關(guān)聯(lián)的MessageQueue中
  • Thread默認(rèn)沒有Looper香府,需要調(diào)用Looper.prepare()來初始化Looper董栽,且只能初始化一次
  • 每個looper會維護一個MessageQueue,Looper負(fù)責(zé)循環(huán)的從MessageQueue取消息并交給Handler處理

Handler異步處理消息流程如下:


圖片出自:http://blog.csdn.net/u011240877/article/details/72892321
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末企孩,一起剝皮案震驚了整個濱河市锭碳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌勿璃,老刑警劉巖擒抛,帶你破解...
    沈念sama閱讀 212,599評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異补疑,居然都是意外死亡歧沪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評論 3 385
  • 文/潘曉璐 我一進店門莲组,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诊胞,“玉大人,你說我怎么就攤上這事锹杈∧旃拢” “怎么了?”我有些...
    開封第一講書人閱讀 158,084評論 0 348
  • 文/不壞的土叔 我叫張陵竭望,是天一觀的道長邪码。 經(jīng)常有香客問我撒轮,道長只锭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,708評論 1 284
  • 正文 為了忘掉前任逗余,我火速辦了婚禮,結(jié)果婚禮上喻圃,老公的妹妹穿的比我還像新娘萤彩。我一直安慰自己,他們只是感情好斧拍,可當(dāng)我...
    茶點故事閱讀 65,813評論 6 386
  • 文/花漫 我一把揭開白布雀扶。 她就那樣靜靜地躺著,像睡著了一般肆汹。 火紅的嫁衣襯著肌膚如雪愚墓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評論 1 291
  • 那天昂勉,我揣著相機與錄音浪册,去河邊找鬼。 笑死岗照,一個胖子當(dāng)著我的面吹牛村象,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播攒至,決...
    沈念sama閱讀 39,120評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼厚者,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了迫吐?” 一聲冷哼從身側(cè)響起库菲,我...
    開封第一講書人閱讀 37,866評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎志膀,沒想到半個月后熙宇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,308評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡溉浙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,633評論 2 327
  • 正文 我和宋清朗相戀三年烫止,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戳稽。...
    茶點故事閱讀 38,768評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡烈拒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出广鳍,到底是詐尸還是另有隱情荆几,我是刑警寧澤,帶...
    沈念sama閱讀 34,461評論 4 333
  • 正文 年R本政府宣布赊时,位于F島的核電站吨铸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏祖秒。R本人自食惡果不足惜诞吱,卻給世界環(huán)境...
    茶點故事閱讀 40,094評論 3 317
  • 文/蒙蒙 一舟奠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧房维,春花似錦沼瘫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阿趁,卻和暖如春膜蛔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背脖阵。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評論 1 267
  • 我被黑心中介騙來泰國打工皂股, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人命黔。 一個月前我還...
    沈念sama閱讀 46,571評論 2 362
  • 正文 我出身青樓呜呐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悍募。 傳聞我的和親對象是個殘疾皇子卵史,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,666評論 2 350

推薦閱讀更多精彩內(nèi)容