主頁 > 移動端開發 > 從設計模式角度看OkHttp原始碼

從設計模式角度看OkHttp原始碼

2021-04-02 06:43:33 移動端開發

前言

說到原始碼,很多朋友都覺得復雜,難理解,

但是,如果是一個結構清晰且完全解耦的優質原始碼庫呢?

OkHttp就是這樣一個存在,對于這個原生網路框架,想必大家也看過很多很多相關的原始碼決議了,

它的原始碼好看,易讀,清晰,所以今天我準備從設計模式的角度再來讀一遍 OkHttp的原始碼,

主要內容就分為兩類:

  • okhttp的基本運作流程
  • 涉及到的設計模式

(本文原始碼版本為okhttp:4.9.0,攔截器會放到下期再講)

使用

讀原始碼,首先就要從它的使用方法開始:

	val okHttpClient = OkHttpClient()
    val request: Request = Request.Builder()
        .url(url)
        .build()
    okHttpClient.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            Log.d(TAG, "onFailure: ")
        }

        override fun onResponse(call: Call, response: Response) {
            Log.d(TAG, "onResponse: " + response.body?.string())
        }
    })

從這個使用方法來看,我抽出了四個重要資訊:

  • okHttpClient
  • Request
  • newCall(request)
  • enqueue(Callback)

大體意思我們可以先猜猜看:

配置一個客戶端實體okHttpClient和一個Request請求,然后這個請求通過okHttpClientnewCall方法封裝,最后用enqueue方法發送出去,并收到Callback回應,

接下來就一個個去認證,并找找其中的設計模式,

okHttpClient

首先看看這個okhttp的客戶端物件,也就是okHttpClient

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new HttpLoggingInterceptor()) 
        .readTimeout(500, TimeUnit.MILLISECONDS)
        .build();

在這里,我們實體化了一個HTTP的客戶端client,然后配置了它的一些引數,比如攔截器、超時時間

這種我們通過一個統一的物件,呼叫一個介面或方法,就能完成我們的需求,而起內部的各種復雜物件的呼叫和跳轉都不需要我們關心的設計模式就是外觀模式(門面模式)

外觀模式(Facade Pattern)隱藏系統的復雜性,并向客戶端提供了一個客戶端可以訪問系統的介面,這種型別的設計模式屬于結構型模式,它向現有的系統添加一個介面,來隱藏系統的復雜性,

其重點就在于系統內部和各個子系統之間的復雜關系我們不需要了解,只需要去差遣這個門面 就可以了,在這里也就是OkHttpClient

它的存在就像一個接待員,我們告訴它我們的需求,要做的事情,然后接待員去內部處理,各種調度,最終完成,

外觀模式主要解決的就是降低訪問復雜系統的內部子系統時的復雜度,簡化客戶端與之的介面,

這個模式也是三方庫很常用的設計模式,給你一個物件,你只需要對這個物件使喚,就可以完成需求,

當然,這里還有一個比較明顯的設計模式是建造者模式,下面會說到,

Request

val request: Request = Request.Builder()
    .url(url)
    .build()

//Request.kt
open class Builder {
    internal var url: HttpUrl? = null
    internal var method: String
    internal var headers: Headers.Builder
    internal var body: RequestBody? = null

    constructor() {
      this.method = "GET"
      this.headers = Headers.Builder()
    }

    open fun build(): Request {
      return Request(
          checkNotNull(url) { "url == null" },
          method,
          headers.build(),
          body,
          tags.toImmutableMap()
      )
    }
}

Request的生成代碼中可以看到,用到了其內部類Builder,然后通過Builder類組裝出了一個完整的有著各種引數的Request類

這也就是典型的 建造者(Builder)模式

建造者(Builder)模式,將一個復雜的物件的構建與它的表示分離,是的同樣的構建程序可以創建不同的表示,

我們可以通過Builder,構建了不同的Request請求,只需要傳入不同的請求地址url,請求方法method,頭部資訊headers,請求體body即可,
(這也就是網路請求中的請求報文的格式)

這種可以通過構建形成不同的表示的 設計模式 就是 建造者模式,也是用的很多,主要為了方便我們傳入不同的引數進行構建物件,

又比如上面okHttpClient的構建,

newCall(request)

接下來是呼叫OkHttpClient類的newCall方法獲取一個可以去呼叫enqueue方法的介面,

//使用
val okHttpClient = OkHttpClient()
okHttpClient.newCall(request)

//OkHttpClient.kt
open class OkHttpClient internal constructor(builder: Builder) : Cloneable, Call.Factory, WebSocket.Factory {
  override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
}

//Call介面
interface Call : Cloneable {
  fun execute(): Response

  fun enqueue(responseCallback: Callback)

  fun interface Factory {
    fun newCall(request: Request): Call
  }
}

newCall方法,其實是Call.Factory介面里面的方法,

也就是創建Call的程序,是通過Call.Factory介面的newCall方法創建的,而真正實作這個方法交給了這個介面的子類OkHttpClient

那這種定義了統一創建物件的介面,然后由子類來決定實體化這個物件的設計模式就是 工廠模式

在工廠模式中,我們在創建物件時不會對客戶端暴露創建邏輯,并且是通過使用一個共同的介面來指向新創建的物件,

當然,okhttp這里的工廠有點小,只有一條生產線,就是Call介面,而且只有一個產品,RealCall

enqueue(Callback)

接下來這個方法enqueue,肯定就是okhttp原始碼的重中之重了,剛才說到newCall方法其實是獲取了RealCall物件,所以就走到了RealCall的enqueue方法:

  override fun enqueue(responseCallback: Callback) {
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

再轉向dispatcher,

//Dispatcher.kt

  val executorService: ExecutorService
    get() {
      if (executorServiceOrNull == null) {
        executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
            SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
      }
      return executorServiceOrNull!!
    }


  internal fun enqueue(call: AsyncCall) {
    promoteAndExecute()
  }


  private fun promoteAndExecute(): Boolean {
    //通過執行緒池切換執行緒
    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }


//RealCall.kt
  fun executeOn(executorService: ExecutorService) {

      try {
        executorService.execute(this)
        success = true
      } 
    }

這里用到了一個新的類Dispatcher,呼叫到的方法是asyncCall.executeOn(executorService)

這個executorService引數大家應該都熟悉吧,執行緒池,最后是呼叫executorService.execute方法執行執行緒池任務,

而執行緒池的概念其實也是用到了一種設計模式,叫做享元模式

享元模式(Flyweight Pattern)主要用于減少創建物件的數量,以減少記憶體占用和提高性能,這種型別的設計模式屬于結構型模式,它提供了減少物件數量從而改善應用所需的物件結構的方式,

其核心就在于共享物件,所有很多的池類物件,比如執行緒池、連接池等都是采用了享元模式 這一設計模式,當然,okhttp中不止是有執行緒池,還有連接池提供連接復用,管理所有的socket連接,

再回到Dispatcher,所以這個類是干嘛的呢?就是切換執行緒用的,因為我們呼叫的enqueue是異步方法,所以最后會用到執行緒池切換執行緒,執行任務,

繼續看看execute(this)中的this任務,

execute(this)

override fun run() {
      threadName("OkHttp ${redactedUrl()}") {
        try {
          //獲取回應報文,并回呼給Callback
          val response = getResponseWithInterceptorChain()
          responseCallback.onResponse(this@RealCall, response)
        } catch (e: IOException) {
          if (!signalledCallback) {
            responseCallback.onFailure(this@RealCall, e)
          } 
        } catch (t: Throwable) {
          cancel()
          if (!signalledCallback) {
            
            responseCallback.onFailure(this@RealCall, canceledException)
          }
        } 
      }

沒錯,這里就是請求介面的地方了,通過getResponseWithInterceptorChain方法獲取回應報文response,然后通過Callback的onResponse方法回呼,或者是有例外就通過onFailure方法回呼,

那同步方法是不是就沒用到執行緒池呢?去找找execute方法:

  override fun execute(): Response {
    //...
    return getResponseWithInterceptorChain()
  }

果然,通過execute方法就直接回傳了getResponseWithInterceptorChain,也就是回應報文,

到這里,okhttp的大體流程就結束了,這部分的流程大概就是:

設定請求報文 -> 配置客戶端引數 -> 根據同步或異步判斷是否用子執行緒 -> 發起請求并獲取回應報文 -> 通過Callback介面回呼結果

剩下的內容就全部在getResponseWithInterceptorChain方法中,這也就是okhttp的核心,

getResponseWithInterceptorChain

internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        interceptors = interceptors
        //...
    )

    val response = chain.proceed(originalRequest)
  }

代碼不是很復雜,就是 加加加 攔截器,然后組裝成一個chain類,呼叫proceed方法,得到回應報文response,

  override fun proceed(request: Request): Response {

    //找到下一個攔截器
    val next = copy(index = index + 1, request = request)
    val interceptor = interceptors[index]

   
    val response = interceptor.intercept(next)
    return response
  }

簡化了下代碼,主要邏輯就是獲取下一個攔截器(index+1),然后呼叫攔截器的intercept方法,

然后在攔截器里面的代碼統一都是這種格式:

  override fun intercept(chain: Interceptor.Chain): Response {
    //做事情A

    response = realChain.proceed(request)

    //做事情B
  }

結合兩段代碼,會形成一條鏈,這條鏈組織了所有連接器的作業,類似這樣:

攔截器1做事情A -> 攔截器2做事情A -> 攔截器3做事情A -> 攔截器3做事情B -> 攔截器2做事情B -> 攔截器1做事情B

應該是好理解的吧,通過proceed方法把每個攔截器連接起來了,

而最后一個攔截器ConnectInterceptor就是分割事情A和事情B,其作用就是進行真正的與服務器的通信,向服務器發送資料,決議讀取的回應資料,

所以事情A和事情B是什么意思呢?其實就代表了通信之前的事情和通信之后的事情,

再來個影片:

這種思想是不是有點像..遞回?沒錯,就是遞回,先遞進執行事情A,再回歸做事情B,

而這種遞回回圈,其實也就是用到了設計模式中的 責任鏈模式

責任鏈模式(Chain of Responsibility Pattern)為請求創建了一個接收者物件的鏈,這種模式給予請求的型別,對請求的發送者和接收者進行解耦,

簡單的說,就是讓每個物件都能有機會處理這個請求,然后各自完成自己的事情,一直到事件被處理,Android中的事件分發機制也是用到了這種設計模式,

接下來就是了解每個攔截器到底做了什么事,就可以了解到okhttp的整個流程了,這就是下期的內容了,

先預告一波:

  • addInterceptor(Interceptor),這是由開發者設定的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共引數,Header都可以在這里添加,
  • RetryAndFollowUpInterceptor,這里會對連接做一些初始化作業,以及請求失敗的重試作業,重定向的后續請求作業,
  • BridgeInterceptor,這里會為用戶構建一個能夠進行網路訪問的請求,同時后續作業將網路請求回來的回應Response轉化為用戶可用的Response,比如添加檔案型別,content-length計算添加,gzip解包,
  • CacheInterceptor,這里主要是處理cache相關處理,會根據OkHttpClient物件的配置以及快取策略對請求值進行快取,而且如果本地有了可?的Cache,就可以在沒有網路互動的情況下就回傳快取結果,
  • ConnectInterceptor,這里主要就是負責建立連接了,會建立TCP連接或者TLS連接,以及負責編碼解碼的HttpCodec,
  • networkInterceptors,這里也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由于位置不同,用處也不同,這個位置添加的攔截器可以看到請求和回應的資料了,所以可以做一些網路除錯,
  • CallServerInterceptor,這里就是進行網路資料的請求和回應了,也就是實際的網路I/O操作,通過socket讀寫資料,

總結

讀完okhttp的原始碼,感覺就一個字:舒服

一份好的代碼應該就是這樣,各模塊之間通過各種設計模式進行解耦,閱讀者可以每個模塊分別去去閱讀了解,而不是各個模塊纏綿在一起,雜亂無章,

最后再總結下okhttp中涉及到的設計模式:

  • 外觀模式,通過okHttpClient這個外觀去實作內部各種功能,
  • 建造者模式,構建不同的Request物件,
  • 工廠模式,通過OkHttpClient生產出產品RealCall,
  • 享元模式,通過執行緒池、連接池共享物件,
  • 責任鏈模式,將不同功能的攔截器形成一個鏈,

其實還是有一些設計模式沒說到的,比如

  • websocket相關用到的觀察者模式
  • Cache集合相關的迭代器模式

以后遇到了再做補充吧,

參考

https://www.runoob.com/design-pattern/design-pattern-tutorial.html
https://www.jianshu.com/p/ae2fe5481994
https://juejin.cn/post/6895369745445748749

拜拜

感謝大家的閱讀,有一起學習的小伙伴可以關注下我的公眾號——碼上積木????
每日一個知識點,積少成多,建立知識體系架構,
這里有一群很好的Android小伙伴,歡迎大家加入~

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/271189.html

標籤:Android

上一篇:怎么理解onStart可見但不可互動

下一篇:Android Toast使用的簡單小結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more