ReactNative For Android(RN4A)源碼解讀-運行環(huán)境的創(chuàng)建流程

RN4A運行環(huán)境的創(chuàng)建流程

前言

國內近年來對ReactNaitve討論的火爆程度不言而喻肄鸽,可能你都已經用了一段時間的RN4A了。不過你是否清楚RN4A是如何初始化一個環(huán)境油啤?Js是何時通知Native渲染UI組件赏参?從RN4A的環(huán)境初始化到ReactView呈現到UI的時候真仲,RN4A都干了什么芥被?別急葫笼,接下來就為你揭曉這些問題的答案。(PS: 本文源碼分析基于當前最新ReactNative源碼 v0.40.0-rc.4础废,限于作者水平有限汛骂,如果有錯誤和理解不當之處感謝指出。)

總體流程

為了接下來細節(jié)分析的時候你心中有個整體印象评腺,我們先直接說下RN4A的框架初始化的總體流程帘瞭。(PS:根據項目實際接入RN4A的方式不同,流程可能有所不同蒿讥,這里只是官方使用的一種初始化流程蝶念。)

創(chuàng)建ReactRootView -> 創(chuàng)建ReactInstanceManager -> 創(chuàng)建ReactContext -> RN4A環(huán)境初始化完成 -> 通知Js渲染界面。

萬物之始ReactRootView

俗話說"擒賊先擒王"芋绸,一般分析代碼都會從源頭走起媒殉。如果你查看RN4A的接入文檔,就知道RN4A已經為我們封裝好了ReactActiviy類摔敛,只要通過繼承它廷蓉,你可以省掉RN4A與Activity之間的絕大部分邏輯交互,包括生命周期回調马昙,以及發(fā)送消息通知Js渲染UI的操作等等桃犬。通過源碼我們可以看到ReactActivity中所有的邏輯都是由ReactActivityDelegate類來代理刹悴,這是一個不錯的設計方式,你可以輕松地把ReactActivityDelegate集成到你自己的Activity中攒暇,自由定制RN4A環(huán)境的初始化方案土匀。好的,有點扯遠了形用,現在讓我們來看下ReactActivityDelegate中的關鍵方法吧就轧。

  protected void onCreate(Bundle savedInstanceState) {
    //省略判斷懸浮窗權限
    ...

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
  }

我們知道onCreate方法就是Activity開始執(zhí)行的地方,ReactActivityDelegateonCreate方法自然也會在Activity的onCreate中調用田度,這里可以看到钓丰,代碼會判斷當前App是否有顯示懸浮層的權限,然后開始調用loadApp方法每币,注意,這里就是RN4A官方方式加載的入口了琢歇。我們接下來繼續(xù)跟蹤下去:

  protected void loadApp(String appKey) {
    if (mReactRootView != null) {
      throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());
    getPlainActivity().setContentView(mReactRootView);
  }

這段代碼邏輯很簡單兰怠,createRootView方法創(chuàng)建了一個RN4A的根View(ReactRootView),所有RN4A的View都會創(chuàng)建在ReactRootView里李茫,然后將ReactRootView當成了Activity的內容布局揭保,一般將ReactRootView作為Activity的內容布局是比較省事的方式,當然魄宏,你也可以將它作為某個ViewGroup的子View秸侣,只不過這種方式你很容易會踩到一些坑,比如你需要處理RN的View和原生View之間的事件沖突宠互。好了味榛,那我們接著看代碼,我們看下關鍵的startReactApplication方法予跌,這里注意一下它的形參,第一個參數需要一個ReactInstanceManager的實例搏色,ReactNativeHostgetReactInstanceManager這個方法會創(chuàng)建一個ReactInstanceManager實例,ReactInstanceManager是RN4A的核心類券册,我們需要先來看下它是如何被初始化的频轿。

  protected ReactInstanceManager createReactInstanceManager() {
    ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
      .setApplication(mApplication)
      .setJSMainModuleName(getJSMainModuleName())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

    for (ReactPackage reactPackage : getPackages()) {
      builder.addPackage(reactPackage);
    }

    String jsBundleFile = getJSBundleFile();
    if (jsBundleFile != null) {
      builder.setJSBundleFile(jsBundleFile);
    } else {
      builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
    }
    return builder.build();
  }

由于ReactInstanceManager的參數很多,所以RN4A使用了建造者(Builder)模式烁焙,我們先簡單看一下這些參數的意義:

  • application - 這個就不說了航邢;
  • jsMainModuleName - 在Js文件中設置的模塊名稱,通過該名稱加載對應的Js組件骄蝇;
  • useDeveloperSupport - 設置是否使用Dev調試工具膳殷;
  • redBoxHandler - 設置紅框處理器,Js運行時的異常展示出來的紅框乞榨;
  • UIImplementationProvider - UIManagerModule的工人秽之,負責處理從Js過來的跟UI操作相關的消息(View的創(chuàng)建当娱、測量、更新等各種臟活)考榨;
  • initialLifecycleState - ReactInstanceManager實例初始化時候的生命周期跨细;
  • reactPackage - 自定義的ReactNative包:
  • jsBundleFile - 放在手機文件系統中的JsBundle的文件路徑;
  • bundleAssetName - 內置在Assets目錄下的JsBundle文件河质,如果設置了則不會走其他的JsBundle加載方式冀惭,需要注意在jsBundleFile有值的情況下不會生效;

看完ReactInstanceManager的創(chuàng)建,我們再返回到之前loadApp方法處掀鹅,繼續(xù)跟蹤ReactRootViewstartReactApplication方法:

public void startReactApplication(
      ReactInstanceManager reactInstanceManager,
      String moduleName,
      @Nullable Bundle launchOptions) {
    UiThreadUtil.assertOnUiThread();

    // TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
    // here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
    // it in the case of re-creating the catalyst instance
    Assertions.assertCondition(
        mReactInstanceManager == null,
        "This root view has already been attached to a catalyst instance manager");

    mReactInstanceManager = reactInstanceManager;
    mJSModuleName = moduleName;
    mLaunchOptions = launchOptions;

    if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
      mReactInstanceManager.createReactContextInBackground();
    }

    // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which
    // will make this view startReactApplication itself to instance manager once onMeasure is called.
    if (mWasMeasured) {
      attachToReactInstanceManager();
    }
  }

這里有兩個重要的操作, mReactInstanceManager.createReactContextInBackground方法完成了RN4A環(huán)境的創(chuàng)建和初始化散休,其中RN4A橋的創(chuàng)建和Js腳本的加載都是在這里面進行的;而另一個attachToReactInstanceManager則將ReactRootView實例與ReactInstanceManager實例綁定起來乐尊,并從Native發(fā)送runApplication消息到Js戚丸,Js收到消息便會開始執(zhí)行相應的業(yè)務,這里需要注意的是扔嵌,如果ReactInstanceManager是第一次創(chuàng)建的話由于它的內部還沒有創(chuàng)建好RN4A上下文實例(ReactContext)限府,Native此時并不會發(fā)送runApplication消息給Js,而是將這個操作放在RN4A所有的環(huán)境創(chuàng)建完成之后才被執(zhí)行痢缎,這里只是先提下胁勺,下面還會說到。
接著我們看下mReactInstanceManager.createReactContextInBackground這個方法独旷,由源碼可知XReactInstanceManagerImplReactInstanceManager的唯一實現類署穗,所以你可以一直跟蹤到下面的代碼,

private void recreateReactContextInBackgroundInner() {
    UiThreadUtil.assertOnUiThread();

    if (mUseDeveloperSupport && mJSMainModuleName != null) {
      final DeveloperSettings devSettings = mDevSupportManager.getDevSettings();

      // If remote JS debugging is enabled, load from dev server.
      if (mDevSupportManager.hasUpToDateJSBundleInCache() &&
          !devSettings.isRemoteJSDebugEnabled()) {
        // If there is a up-to-date bundle downloaded from server,
        // with remote JS debugging disabled, always use that.
        onJSBundleLoadedFromServer();
      } else if (mBundleLoader == null) {
        mDevSupportManager.handleReloadJS();
      } else {
        mDevSupportManager.isPackagerRunning(
            new DevServerHelper.PackagerStatusCallback() {
              @Override
              public void onPackagerStatusFetched(final boolean packagerIsRunning) {
                UiThreadUtil.runOnUiThread(
                    new Runnable() {
                      @Override
                      public void run() {
                        if (packagerIsRunning) {
                          mDevSupportManager.handleReloadJS();
                        } else {
                          // If dev server is down, disable the remote JS debugging.
                          devSettings.setRemoteJSDebugEnabled(false);
                          recreateReactContextInBackgroundFromBundleLoader();
                        }
                      }
                    });
              }
            });
      }
      return;
    }

    recreateReactContextInBackgroundFromBundleLoader();
  }

上面的代碼還挺長的嵌洼,其實只要關注recreateReactContextInBackgroundFromBundleLoader方法就行了案疲,不過這里還是需要簡單說下這一處源碼的邏輯。執(zhí)行過程大概是這樣的麻养,如果你啟用了RN4A的Dev支持络拌,并且Js模塊名(JsModuleName)不是空的,RN4A就會判斷你本地是否有最新的JsBundler文件回溺,如果有的話就直接讀取本地的JsBundle文件春贸,否則會從你的LocalServer中加載JsBundle文件,這里為了分析源碼方便遗遵,我們先假設RN4A是處在Release環(huán)境中執(zhí)行的萍恕,所以我們就直接走recreateReactContextInBackgroundFromBundleLoader代碼, 在RN4A源碼中經過幾處跳轉之后我們就會看到下面這段邏輯。

private void recreateReactContextInBackground(
      JavaScriptExecutor.Factory jsExecutorFactory,
      JSBundleLoader jsBundleLoader) {
    UiThreadUtil.assertOnUiThread();

    ReactContextInitParams initParams =
        new ReactContextInitParams(jsExecutorFactory, jsBundleLoader);
    if (mReactContextInitAsyncTask == null) {
      // No background task to create react context is currently running, create and execute one.
      mReactContextInitAsyncTask = new ReactContextInitAsyncTask();
      mReactContextInitAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, initParams);
    } else {
      // Background task is currently running, queue up most recent init params to recreate context
      // once task completes.
      mPendingReactContextInitParams = initParams;
    }
  }

這是RN4A的一段相對重要的邏輯车要,我們可以看到RN4A使用了Android的異步任務(ReactContextInitAsyncTask)來執(zhí)行初始化操作允粤,閱讀源碼可以知道RN4A先判斷當前有沒有ReactContextInitAsyncTask在進行,如果有的話,RN4A會將本次的初始化參數存放到initParams全局變量类垫,等ReactContextInitAsyncTask初始化完成之后再去重新執(zhí)行初始化操作司光,如果當前沒有ReactContextInitAsyncTask任務在執(zhí)行,則直接新建一個ReactContextInitAsyncTask任務并開始執(zhí)行初始化操作悉患。我們接著跟進源碼:

  /*
   * Task class responsible for (re)creating react context in the background. These tasks can only
   * be executing one at time, see {@link #recreateReactContextInBackground()}.
   */
  private final class ReactContextInitAsyncTask extends
      AsyncTask<ReactContextInitParams, Void, Result<ReactApplicationContext>> {
    @Override
    protected void onPreExecute() {
      if (mCurrentReactContext != null) {
        tearDownReactContext(mCurrentReactContext);
        mCurrentReactContext = null;
      }
    }

    @Override
    protected Result<ReactApplicationContext> doInBackground(ReactContextInitParams... params) {
        //省略一些代碼
      Assertions.assertCondition(params != null && params.length > 0 && params[0] != null);
      try {
        JavaScriptExecutor jsExecutor = params[0].getJsExecutorFactory().create();
        return Result.of(createReactContext(jsExecutor, params[0].getJsBundleLoader()));
      } catch (Exception e) {
        // Pass exception to onPostExecute() so it can be handled on the main thread
        return Result.of(e);
      }
    }

    @Override
    protected void onPostExecute(Result<ReactApplicationContext> result) {
      try {
        setupReactContext(result.get());
      } catch (Exception e) {
        mDevSupportManager.handleException(e);
      } finally {
        mReactContextInitAsyncTask = null;
      }

      // Handle enqueued request to re-initialize react context.
      if (mPendingReactContextInitParams != null) {
        recreateReactContextInBackground(
            mPendingReactContextInitParams.getJsExecutorFactory(),
            mPendingReactContextInitParams.getJsBundleLoader());
        mPendingReactContextInitParams = null;
      }
    }

    @Override
    protected void onCancelled(Result<ReactApplicationContext> reactApplicationContextResult) {
      //省略一些代碼
    }
  }

這也算是RN4A的核心一部分了残家,通過源碼我們可以知道RN4A會在任務開始時候卸載掉舊的RN4A上下文實例(ReactContext是一個RN4A的上下文環(huán)境,持有了UI售躁、Js和Native三線程坞淮,并維持了一個和Js通信的橋等),然后在異步任務線程池中RN4A會開始創(chuàng)建一個新的RN4A上下文實例(ReactContext)陪捷,并在執(zhí)行結束之后設置這個RN4A上下文實例(ReactContext)回窘;當RN4A執(zhí)行完成之后如果發(fā)現有initParams(上文提到的參數),就會重新開始執(zhí)行ReactContextInitAsyncTask任務市袖。接下來我們去看下createReactContext方法都在做啥啡直。

  /**
   * @return instance of {@link ReactContext} configured a {@link CatalystInstance} set
   */
  private ReactApplicationContext createReactContext(
      JavaScriptExecutor jsExecutor,
      JSBundleLoader jsBundleLoader) {
    mSourceUrl = jsBundleLoader.getSourceUrl();
    List<ModuleSpec> moduleSpecs = new ArrayList<>();
    Map<Class, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
    JavaScriptModuleRegistry.Builder jsModulesBuilder = new JavaScriptModuleRegistry.Builder();

    final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
    if (mUseDeveloperSupport) {
      reactContext.setNativeModuleCallExceptionHandler(mDevSupportManager);
    }
    try {
      CoreModulesPackage coreModulesPackage =
        new CoreModulesPackage(this, mBackBtnHandler, mUIImplementationProvider);
      processPackage(
        coreModulesPackage,
        reactContext,
        moduleSpecs,
        reactModuleInfoMap,
        jsModulesBuilder);
    } finally {
        //省略一些代碼 
    }
    for (ReactPackage reactPackage : mPackages) {
      try {
        processPackage(
          reactPackage,
          reactContext,
          moduleSpecs,
          reactModuleInfoMap,
          jsModulesBuilder);
      } finally {
        //省略一些代碼
      }
    }
    NativeModuleRegistry nativeModuleRegistry;
    try {
       nativeModuleRegistry = new NativeModuleRegistry(moduleSpecs, reactModuleInfoMap);
    } finally {
        //省略一些代碼
    }

    NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
        ? mNativeModuleCallExceptionHandler
        : mDevSupportManager;
    CatalystInstanceImpl.Builder catalystInstanceBuilder = new CatalystInstanceImpl.Builder()
        .setReactQueueConfigurationSpec(ReactQueueConfigurationSpec.createDefault())
        .setJSExecutor(jsExecutor)
        .setRegistry(nativeModuleRegistry)
        .setJSModuleRegistry(jsModulesBuilder.build())
        .setJSBundleLoader(jsBundleLoader)
        .setNativeModuleCallExceptionHandler(exceptionHandler);

    final CatalystInstance catalystInstance;
    try {
      catalystInstance = catalystInstanceBuilder.build();
    } finally {
        //省略一些代碼
    }

    if (mBridgeIdleDebugListener != null) {
      catalystInstance.addBridgeIdleDebugListener(mBridgeIdleDebugListener);
    }

    reactContext.initializeWithInstance(catalystInstance);
    catalystInstance.runJSBundle();

    return reactContext;
  }

上面的代碼看著有點長,不過邏輯也就這么幾步:

  1. 生成一個RN4A上下文實例(ReactContext)苍碟,并在開啟Dev模式情況下設置一個Native異常處理器付枫,用于捕獲三個線程(UI、Js和Native)中發(fā)生的異常驰怎;
  2. 處理RN包,包括核心包(CorePackage)以及注冊的自定義包(如官方提供的MainPackage)二打,NativeModule信息被存放到moduleSpecsreactModuleInfoMap中县忌,而JsModule則被放到jsModulesBuilder中;
  3. 通過橋構造器(CatalystInstanceImpl.Builder)構建出一個CatalystInstance實例继效,它在RN4A中負責中掌管Js和Native之間的通信症杏;
  4. 將創(chuàng)建好的橋(CatalystInstance)放到RN4A上下文(ReactContext)中并進行初始化,這個過程實際上只是讓RN4A上下文(ReactContext)持有三個關鍵線程管理實例(UI瑞信、JS和Native)厉颤,RN4A的全部工作依賴這三條線程之間的相互協作;
  5. 最后就是運行在橋實例(CatalystInstance)中的JsBundle了凡简,這里會從Jni層去調用Js引擎解釋執(zhí)行Js代碼逼友,關于RN4A的Jni層的邏輯,限于篇幅留待之后分析秤涩。

接下來讓我們從ReactContextInitAsyncTask類的onPostExecute方法接著看帜乞,進入setupReactContext方法。

private void setupReactContext(ReactApplicationContext reactContext) {
    UiThreadUtil.assertOnUiThread();
    Assertions.assertCondition(mCurrentReactContext == null);
    mCurrentReactContext = Assertions.assertNotNull(reactContext);
    CatalystInstance catalystInstance =
        Assertions.assertNotNull(reactContext.getCatalystInstance());

    catalystInstance.initialize();
    mDevSupportManager.onNewReactContextCreated(reactContext);
    mMemoryPressureRouter.addMemoryPressureListener(catalystInstance);
    moveReactContextToCurrentLifecycleState();

    for (ReactRootView rootView : mAttachedRootViews) {
      attachMeasuredRootViewToInstance(rootView, catalystInstance);
    }

    ReactInstanceEventListener[] listeners =
      new ReactInstanceEventListener[mReactInstanceEventListeners.size()];
    listeners = mReactInstanceEventListeners.toArray(listeners);

    for (ReactInstanceEventListener listener : listeners) {
      listener.onReactContextInitialized(reactContext);
    }
  }

這里RN4A會去執(zhí)行一次橋CatalystInstance的初始化邏輯筐眷,并把初始化完成的消息發(fā)送出去黎烈,比如通知注冊在橋的Module執(zhí)行初始化的一些操作,告訴綁定的ReactRootView可以通過attachMeasuredRootViewToInstance方法通知Js"真正"執(zhí)行業(yè)務邏輯了,之后Js就會開始通過一系列的消息指揮Native渲染展示UI等等照棋。

結語

至此资溃,RN4A從環(huán)境初始化到RN界面展示出來所經過的一個流程我們已經走了一遍,這篇文章只是分析RN4A框架的開篇烈炭,Native和Js之間的通信方式溶锭,Js Dom的解析渲染等等,會在后續(xù)的文章中分析梳庆,敬請期待暖途。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膏执,隨后出現的幾起案子驻售,更是在濱河造成了極大的恐慌,老刑警劉巖更米,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欺栗,死亡現場離奇詭異,居然都是意外死亡征峦,警方通過查閱死者的電腦和手機迟几,發(fā)現死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來栏笆,“玉大人类腮,你說我怎么就攤上這事◎燃樱” “怎么了蚜枢?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長针饥。 經常有香客問我厂抽,道長,這世上最難降的妖魔是什么丁眼? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任筷凤,我火速辦了婚禮,結果婚禮上苞七,老公的妹妹穿的比我還像新娘藐守。我一直安慰自己,他們只是感情好蹂风,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布吗伤。 她就那樣靜靜地躺著,像睡著了一般硫眨。 火紅的嫁衣襯著肌膚如雪足淆。 梳的紋絲不亂的頭發(fā)上巢块,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音巧号,去河邊找鬼族奢。 笑死,一個胖子當著我的面吹牛丹鸿,可吹牛的內容都是我干的越走。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼靠欢,長吁一口氣:“原來是場噩夢啊……” “哼廊敌!你這毒婦竟也來了?” 一聲冷哼從身側響起门怪,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤骡澈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后掷空,有當地人在樹林里發(fā)現了一具尸體肋殴,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年坦弟,在試婚紗的時候發(fā)現自己被綠了护锤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡酿傍,死狀恐怖烙懦,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情赤炒,我是刑警寧澤氯析,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站可霎,受9級特大地震影響,放射性物質發(fā)生泄漏宴杀。R本人自食惡果不足惜癣朗,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旺罢。 院中可真熱鬧旷余,春花似錦、人聲如沸扁达。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跪解。三九已至炉旷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窘行。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工饥追, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人罐盔。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓但绕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親惶看。 傳聞我的和親對象是個殘疾皇子捏顺,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容