摘要:在子線程中發送消息,主線程接受到消息并且處理邏輯。子線程往消息隊列發送消息,并且往管道文件寫數據,主線程即被喚醒,從管道文件讀取數據,主線程被喚醒只是為了讀取消息,當消息讀取完畢,再次睡眠。
目錄介紹
1.Handler的常見的使用方式
2.如何在子線程中定義Handler
3.主線程如何自動調用Looper.prepare()
4.Looper.prepare()方法源碼分析
5.Looper中用什么存儲消息
6.Handler發送消息如何運作
7.Looper.loop()方法源碼分析
8.runOnUiThread如何實現子線程更新UI
9.Handler的post方法和view的post方法
10.主線程中Looper的輪詢死循環為何沒阻塞主線程
11.得出部分結論
好消息01.基礎組件(9篇)
02.IPC機制(0篇)
03.View原理(7篇)
04.動畫機制(2篇)
05.View事件(9篇)
06.消息機制(6篇)
07.多媒體(9篇)
08.View事件(4篇)
09.多線程(4篇)
10.Window(11篇)
11.WebView(4篇)
12.網絡相關(7篇)
13.注解(14篇)
14.音視頻(13篇)
15.優化相關(8篇)
16.設計模式(4篇)
20.零碎筆記(12篇)
21.kotlin學習(1篇)
22.源碼分析(11篇)
23.架構技術(13篇)
25.RecyclerView(21篇)
博客筆記大匯總【16年3月到至今】,包括Java基礎及深入知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug匯總,當然也在工作之余收集了大量的面試題,長期更新維護并且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請注明出處,謝謝!
鏈接地址:https://github.com/yangchong2...
如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!
1.Handler的常見的使用方式
handler機制大家都比較熟悉呢。在子線程中發送消息,主線程接受到消息并且處理邏輯。如下所示
一般handler的使用方式都是在主線程中定義Handler,然后在子線程中調用mHandler.sendXx()方法,這里有一個疑問可以在子線程中定義Handler嗎?
public class MainActivity extends AppCompatActivity { private TextView tv ; /** * 在主線程中定義Handler,并實現對應的handleMessage方法 */ public static Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 101) { Log.i("MainActivity", "接收到handler消息..."); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv); tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread() { @Override public void run() { // 在子線程中發送異步消息 mHandler.sendEmptyMessage(1); } }.start(); } }); } }2.如何在子線程中定義Handler
直接在子線程中創建handler,看看會出現什么情況?
運行后可以得出在子線程中定義Handler對象出錯,難道Handler對象的定義或者是初始化只能在主線程中?其實不是這樣的,錯誤信息中提示的已經很明顯了,在初始化Handler對象之前需要調用Looper.prepare()方法
tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread() { @Override public void run() { Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { Log.i(TAG, "在子線程中定義Handler,接收并處理消息"); } } }; } }.start(); } });
如何正確運行。在這里問一個問題,在子線程中可以吐司嗎?答案是可以的,只不過又條件,詳細可以看這篇文章02.Toast源碼深度分析
這樣程序已經不會報錯,那么這說明初始化Handler對象的時候我們是需要調用Looper.prepare()的,那么主線程中為什么可以直接初始化Handler呢?難道是主線程創建handler對象的時候,會自動調用Looper.prepare()方法的嗎?
tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread() { @Override public void run() { Looper.prepare(); Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { Log.i(TAG, "在子線程中定義Handler,接收并處理消息"); } } }; Looper.loop(); } }.start(); } });3.主線程如何自動調用Looper.prepare()
首先直接可以看在App初始化的時候會執行ActivityThread的main方法中的代碼,如下所示
可以看到Looper.prepare()方法在這里調用,所以在主線程中可以直接初始化Handler了。
public static void main(String[] args) { //省略部分代碼 Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } if (false) { Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); } Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited"); }
并且可以看到還調用了:Looper.loop()方法,可以知道一個Handler的標準寫法其實是這樣的
Looper.prepare(); Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 101) { Log.i(TAG, "在子線程中定義Handler,并接收到消息"); } } }; Looper.loop();4.Looper.prepare()方法源碼分析
源碼如下所示
可以看到Looper中有一個ThreadLocal成員變量,熟悉JDK的同學應該知道,當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
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)); }
思考:Looper.prepare()能否調用兩次或者多次
如果運行,則會報錯,并提示prepare中的Excetion信息。由此可以得出在每個線程中Looper.prepare()能且只能調用一次
//這里Looper.prepare()方法調用了兩次 Looper.prepare(); Looper.prepare(); Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { Log.i(TAG, "在子線程中定義Handler,并接收到消息。。。"); } } }; Looper.loop();5.Looper中用什么存儲消息
先看一下下面得源代碼
看Looper對象的構造方法,可以看到在其構造方法中初始化了一個MessageQueue對象。MessageQueue也稱之為消息隊列,特點是先進先出,底層實現是單鏈表數據結構
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)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
得出結論
Looper.prepare()方法初始話了一個Looper對象并關聯在一個MessageQueue對象,并且一個線程中只有一個Looper對象,只有一個MessageQueue對象。
6.Handler發送消息如何運作
首先看看構造方法
可以看出在Handler的構造方法中,主要初始化了一下變量,并判斷Handler對象的初始化不應再內部類,靜態類,匿名類中,并且保存了當前線程中的Looper對象。
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; mCallback = callback; mAsynchronous = async; }
看handler.sendMessage(msg)方法
關于下面得源碼,是步步追蹤,看enqueueMessage這個方法,原來msg.target就是Handler對象本身;而這里的queue對象就是我們的Handler內部維護的Looper對象關聯的MessageQueue對象。
handler.sendMessage(message); //追蹤到這一步 public final boolean sendMessage(Message msg){ return sendMessageDelayed(msg, 0); } 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); } private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
看MessageQueue對象的enqueueMessage方法
看到這里MessageQueue并沒有使用列表將所有的Message保存起來,而是使用Message.next保存下一個Message,從而按照時間將所有的Message排序
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) { 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; }7.Looper.loop()方法源碼分析
看看里面得源碼,如下所示
看到Looper.loop()方法里起了一個死循環,不斷的判斷MessageQueue中的消息是否為空,如果為空則直接return掉,然后執行queue.next()方法
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn"t called on this thread."); } final MessageQueue queue = me.mQueue; 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; 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); 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(); } }
看queue.next()方法源碼
大概的實現邏輯就是Message的出棧操作,里面可能對線程,并發控制做了一些限制等。獲取到棧頂的Message對象之后開始執行:msg.target.dispatchMessage(msg)
Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } 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; 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; } // Process the quit message now that all pending messages have been handled. if (mQuitting) { dispose(); return null; } // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. 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; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // Run the idle handlers. // We only ever reach this code block during the first iteration. 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); } } } // Reset the idle handler count to 0 so we do not run them again. pendingIdleHandlerCount = 0; // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; } }
那么msg.target是什么呢?通過追蹤可以知道就是定義的Handler對象,然后查看一下Handler類的dispatchMessage方法:
可以看到,如果我們設置了callback(Runnable對象)的話,則會直接調用handleCallback方法
在初始化Handler的時候設置了callback(Runnable)對象,則直接調用run方法。
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } private static void handleCallback(Message message) { message.callback.run(); }8.runOnUiThread如何實現子線程更新UI
看看源碼,如下所示
如果msg.callback為空的話,會直接調用我們的mCallback.handleMessage(msg),即handler的handlerMessage方法。由于Handler對象是在主線程中創建的,所以handler的handlerMessage方法的執行也會在主線程中。
在runOnUiThread程序首先會判斷當前線程是否是UI線程,如果是就直接運行,如果不是則post,這時其實質還是使用的Handler機制來處理線程與UI通訊。
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } @Override public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } }9.Handler的post方法和view的post方法
Handler的post方法實現很簡單,如下所示
mHandler.post(new Runnable() { @Override public void run() { } }); public final boolean post(Runnable r){ return sendMessageDelayed(getPostMessage(r), 0); }
view的post方法也很簡單,如下所示
可以發現其調用的就是activity中默認保存的handler對象的post方法
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } ViewRootImpl.getRunQueue().post(action); return true; } public void post(Runnable action) { postDelayed(action, 0); } public void postDelayed(Runnable action, long delayMillis) { final HandlerAction handlerAction = new HandlerAction(action, delayMillis); synchronized (this) { if (mActions == null) { mActions = new HandlerAction[4]; } mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); mCount++; } }10.主線程中Looper的輪詢死循環為何沒阻塞主線程
造成ANR的原因
造成ANR的原因一般有兩種:
當前的事件沒有機會得到處理(即主線程正在處理前一個事件,沒有及時的完成或者looper被某種原因阻塞住了)
當前的事件正在處理,但沒有及時完成
為了避免ANR異常,android使用了Handler消息處理機制。讓耗時操作在子線程運行。
問題描述
在處理消息的時候使用了Looper.loop()方法,并且在該方法中進入了一個死循環,同時Looper.loop()方法是在主線程中調用的,那么為什么沒有造成阻塞呢?
ActivityThread中main方法
ActivityThread類的注釋上可以知道這個類管理著我們平常所說的主線程(UI線程)
首先 ActivityThread 并不是一個 Thread,就只是一個 final 類而已。我們常說的主線程就是從這個類的 main 方法開始,main 方法很簡短
public static final void main(String[] args) { ... //創建Looper和MessageQueue Looper.prepareMainLooper(); ... //輪詢器開始輪詢 Looper.loop(); ... }
Looper.loop()方法無限循環
看看Looper.loop()方法無限循環部分的代碼
while (true) { //取出消息隊列的消息,可能會阻塞 Message msg = queue.next(); // might block ... //解析消息,分發消息 msg.target.dispatchMessage(msg); ... }
為什么這個死循環不會造成ANR異常呢?
因為Android 的是由事件驅動的,looper.loop() 不斷地接收事件、處理事件,每一個點擊觸摸或者說Activity的生命周期都是運行在 Looper.loop() 的控制之下,如果它停止了,應用也就停止了。只能是某一個消息或者說對消息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
處理消息handleMessage方法
如下所示
可以看見Activity的生命周期都是依靠主線程的Looper.loop,當收到不同Message時則采用相應措施。
如果某個消息處理時間過長,比如你在onCreate(),onResume()里面處理耗時操作,那么下一次的消息比如用戶的點擊事件不能處理了,整個循環就會產生卡頓,時間一長就成了ANR。
public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; case RELAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart"); ActivityClientRecord r = (ActivityClientRecord) msg.obj; handleRelaunchActivity(r); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; case PAUSE_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); handlePauseActivity((IBinder) msg.obj, false, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 2) != 0); maybeSnapshot(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case PAUSE_ACTIVITY_FINISHING: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); handlePauseActivity((IBinder) msg.obj, true, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 1) != 0); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; ........... } }
loop的循環消耗性能嗎?
主線程Looper從消息隊列讀取消息,當讀完所有消息時,主線程阻塞。子線程往消息隊列發送消息,并且往管道文件寫數據,主線程即被喚醒,從管道文件讀取數據,主線程被喚醒只是為了讀取消息,當消息讀取完畢,再次睡眠。因此loop的循環并不會對CPU性能有過多的消耗。
得出結論
簡單的來說:ActivityThread的main方法主要就是做消息循環,一旦退出消息循環,那么你的程序也就可以退出了。
11.得出部分結論
得出得結論如下所示
1.主線程中定義Handler對象,ActivityThread的main方法中會自動創建一個looper,并且與其綁定。如果是子線程中直接創建handler對象,則需要手動創建looper。不過手動創建不太友好,需要手動調用quit方法結束looper。這個后面再說
2.一個線程中只存在一個Looper對象,只存在一個MessageQueue對象,可以存在N個Handler對象,Handler對象內部關聯了本線程中唯一的Looper對象,Looper對象內部關聯著唯一的一個MessageQueue對象。
3.MessageQueue消息隊列不是通過列表保存消息(Message)列表的,而是通過Message對象的next屬性關聯下一個Message從而實現列表的功能,同時所有的消息都是按時間排序的。
其他介紹 01.關于博客匯總鏈接1.技術博客匯總
2.開源項目匯總
3.生活博客匯總
4.喜馬拉雅音頻匯總
5.其他匯總
02.關于我的博客github:https://github.com/yangchong211
知乎:https://www.zhihu.com/people/...
簡書:http://www.jianshu.com/u/b7b2...
csdn:http://my.csdn.net/m0_37700275
喜馬拉雅聽書:http://www.ximalaya.com/zhubo...
開源中國:https://my.oschina.net/zbj161...
泡在網上的日子:http://www.jcodecraeer.com/me...
郵箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV
segmentfault頭條:https://segmentfault.com/u/xi...
掘金:https://juejin.im/user/593943...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74519.html
摘要:在子線程中發送消息,主線程接受到消息并且處理邏輯。也稱之為消息隊列,特點是先進先出,底層實現是單鏈表數據結構得出結論方法初始話了一個對象并關聯在一個對象,并且一個線程中只有一個對象,只有一個對象。 目錄介紹 1.Handler的常見的使用方式 2.如何在子線程中定義Handler 3.主線程如何自動調用Looper.prepare() 4.Looper.prepare()方法源碼分析...
閱讀 1446·2021-09-22 15:43
閱讀 2161·2019-08-30 15:54
閱讀 1162·2019-08-30 10:51
閱讀 2087·2019-08-29 18:35
閱讀 433·2019-08-26 11:58
閱讀 2481·2019-08-26 11:38
閱讀 2439·2019-08-23 18:35
閱讀 3633·2019-08-23 18:33