React Native UI渲染流程分析(安卓)

前言

React Native App(后稱RN App)的UI由JS端的View tree構(gòu)成,在App運(yùn)行時(shí)會(huì)創(chuàng)建相應(yīng)的原生View tree。從結(jié)果看,這和安卓原生開發(fā)時(shí)用xml布局文件是一樣的,最終結(jié)果都是由Java對(duì)象構(gòu)成的View tree。View tree中每個(gè)節(jié)點(diǎn)必須擁有正確的位置和尺寸數(shù)據(jù),才能渲染出正確的界面。安卓原生App渲染流程(測量,布局,繪制)中前兩步剛好在做這個(gè)工作,那么RN App里渲染流程是怎么樣的?RN采用的是Flexbox布局(實(shí)現(xiàn)體稱為Yoga),這種布局方式如何應(yīng)用到原生渲染流程中?本文先簡單介紹安卓平臺(tái)原生渲染流程,然后在此基礎(chǔ)上著重分析RN在安卓平臺(tái)的渲染流程。本文將從以下幾個(gè)方面進(jìn)行闡述:

1、安卓原生平臺(tái)的渲染流程概述

2、RN渲染流程介紹

? ? 2.1、根據(jù)js端view tree,創(chuàng)建平臺(tái)原生view tree流程概述

? ? 2.2、使用Yoga的計(jì)算結(jié)果,來跑原生渲染流程

3、Yoga布局和原生布局共存,安卓自定義View也可以正常工作

4、總結(jié)

本文的主要讀者是有安卓基礎(chǔ)同學(xué),在有安卓知識(shí)的前提下閱讀會(huì)更容易一些,比如其中的MessageQueue切換,F(xiàn)rameLayout等控件,原生渲染流程等內(nèi)容將不會(huì)有理解負(fù)擔(dān),能直接過渡到RN部分的閱讀。其他讀者可能就需要先建立安卓中這些概念的理解。閱讀完本文后,你將會(huì)理解RN在安卓平臺(tái)的渲染流程實(shí)現(xiàn)原理,對(duì)RN App在渲染時(shí)都執(zhí)行了哪些邏輯有一個(gè)具體的概念,理解我們開發(fā)時(shí)寫的MRN代碼究竟都做了什么,知其然也知其所以然。在遇到問題時(shí)也可從流程上進(jìn)行分析定位,或者對(duì)這個(gè)流程的某一步進(jìn)行修改來滿足定制化需求。文中難免有紕漏之處,歡迎大家不吝指出,共同學(xué)習(xí)成長。

參考源碼版本:RN:0.59.8,安卓:28。

1、安卓原生App UI渲染流程簡介

GUI程序都用View tree來描述界面內(nèi)容,不管界面多復(fù)雜,元素有多少,都可以收納在這顆樹里。View tree很好的提供了渲染所需的必要信息:位置(含尺寸)和顏色。簡單來說,有了這倆信息,系統(tǒng)就可以生成圖形庫(OpenGL ES或者Skia)所需的繪制指令,繪制出由View tree所表達(dá)的一幀畫面。雖然我們?cè)趯慥iew時(shí)可以用{ flex: 1 }(react)或者android:layout_width="match_parent"等來指定組件的尺寸,但最終尺寸屬性還是需要被計(jì)算成具體的數(shù)值。

在App啟動(dòng)過程的onResume階段,系統(tǒng)會(huì)觸發(fā)ViewRootImpl.performTraversals開始渲染流程,這個(gè)函數(shù)里會(huì)依次觸發(fā)root view的測量、布局、繪制,最后通知系統(tǒng)渲染到物理屏幕上,這個(gè)過程如下圖所示。測量遍歷用于計(jì)算每個(gè)節(jié)點(diǎn)的尺寸,布局遍歷時(shí)會(huì)參考尺寸進(jìn)行位置擺放,算出位置數(shù)據(jù)。經(jīng)過這兩步以后,每個(gè)節(jié)點(diǎn)就擁有了位置和尺寸,接著就可以遍歷繪制每個(gè)節(jié)點(diǎn)的內(nèi)容了。view tree中父子節(jié)點(diǎn)的遍歷銜接主要得益于measure/onMeasure,layout/onLayout,draw/onDraw的設(shè)計(jì),如下圖中measure過程所示,layout和draw同理。

安卓原生渲染流程

以上就是安卓原生渲染的三個(gè)主要步驟,測量和布局其實(shí)是為繪制服務(wù),前兩步所計(jì)算出的位置和尺寸數(shù)據(jù)在繪制時(shí)需要用到,用于生成圖形庫的繪制指令。這樣說來,如果view的位置和尺寸數(shù)據(jù)已經(jīng)準(zhǔn)備好,測量和布局這兩步就可以省去了。這也正是RN在原生渲染流程中的切入點(diǎn):把view中onMeasure和onLayout的計(jì)算邏輯都去掉,同時(shí)阻斷這兩步遍歷,通過Yoga來計(jì)算位置和尺寸,并將這些數(shù)據(jù)親自“交給”view tree中的每個(gè)節(jié)點(diǎn),然后只需要一次繪制遍歷,就完成了整個(gè)渲染流程。下面來看看RN是如何完成這個(gè)工作的。

2、RN渲染流程介紹

我們用js寫成的view tree勢必要翻譯成安卓平臺(tái)的原生view tree,才可以在安卓上正常工作。這個(gè)翻譯過程并不只是簡單的映射而已,RN并不是將映射后的原生view tree直接交給系統(tǒng),它還接管了測量和布局工作。安卓有自己的布局方式,體現(xiàn)在渲染流程的measure和layout中,RN采用的Flexbox是一種完全不同的布局方式,它如何參與到原生渲染流程中呢?事實(shí)上不管哪種布局方式,他們都是為了一個(gè)目的:給出view節(jié)點(diǎn)的邊界數(shù)據(jù)bounds(left,top,right,bottom),有了邊界位置,view的尺寸也就有了,比如width = right - left,因此我們可以猜測兩種布局方式的結(jié)合點(diǎn)就是View的邊界數(shù)據(jù)bounds,在下面介紹的渲染流程中進(jìn)行驗(yàn)證。RN在mqt_native線程中執(zhí)行Flexbox布局計(jì)算,計(jì)算結(jié)果將直接用于渲染流程,省去了主線程中measure和layou的計(jì)算量。如果這兩個(gè)線程是運(yùn)行的不同的核心上,在執(zhí)行復(fù)雜的布局動(dòng)畫時(shí)將會(huì)有明顯的優(yōu)勢。下面分小節(jié)來具體看看這個(gè)過程是如何進(jìn)展的。

2.1、根據(jù)js端view tree,創(chuàng)建平臺(tái)原生view tree流程概述

RN提供的View系列組件,都有相應(yīng)的原生端組件實(shí)現(xiàn),js端的view tree相當(dāng)于一個(gè)“劇本”,用來描述UI界面,RN會(huì)根據(jù)這個(gè)“劇本”來生成平臺(tái)原生View tree。對(duì)于js端View tree中的每一個(gè)節(jié)點(diǎn),都會(huì)在native端生成一個(gè)ReactShadowNode節(jié)點(diǎn)作為對(duì)應(yīng),同時(shí)還會(huì)創(chuàng)建一個(gè)原生View節(jié)點(diǎn)(先不考慮RN的布局優(yōu)化,可認(rèn)為他們是一一對(duì)應(yīng)關(guān)系)。ReactShadowNode承擔(dān)了Yoga布局的計(jì)算工作,其內(nèi)部會(huì)創(chuàng)建一個(gè)YogaNode節(jié)點(diǎn),YogaNode內(nèi)部再創(chuàng)建c++端的YGNode。YogaNode本身是一個(gè)jni承載類,代表的是c++端的YGNode,兩者都表示Yoga布局中的一個(gè)節(jié)點(diǎn),當(dāng)Yoga引擎計(jì)算完畢后,YGNode中就填充滿了尺寸和位置數(shù)據(jù),通過jni回設(shè)到j(luò)ava端的YogaNode中,留著給原生view使用。最終在native端會(huì)生成4顆tree:ReactShadowNode tree, YogaNode tree, YGNode tree, 原生View tree,如下圖所示。

RN UI渲染相關(guān)的數(shù)據(jù)結(jié)構(gòu)

RN App會(huì)在Activity的onCreate階段創(chuàng)建ReactRootView(其本質(zhì)是一個(gè)FrameLayout),并由此進(jìn)入RN的世界。下面我們來看看RN是如何創(chuàng)建native端的4顆tree結(jié)構(gòu)的。在RN Bridge完成初始化后,native端通過runApplication()觸發(fā)執(zhí)行我們的js業(yè)務(wù)代碼,根據(jù)js端view tree會(huì)執(zhí)行一系列的UIManager.createView, UIManager.setChildren, UIManager.manageChildren等函數(shù)。通過RN Bridge,這些函數(shù)會(huì)在nativeQueue中添加一系列的操作,用于創(chuàng)建ReactShadowNode節(jié)點(diǎn),并把js端view的各屬性值保存在節(jié)點(diǎn)中,最后形成tree結(jié)構(gòu)。根據(jù)之前的介紹,此時(shí)也會(huì)同步生成YoagNode tree和YGNode tree。這個(gè)過程如下圖中左側(cè)兩個(gè)queue所示。

從上圖還可以看出,每一次對(duì)ReactShadowNode的操作同時(shí)還會(huì)有相應(yīng)的原生View操作,以runnable的形式添加到batchUIQueue中(注意它并不是安卓中和線程相關(guān)的queue,僅僅是個(gè)隊(duì)列容器)。batchUIQueue的名字非常恰當(dāng)?shù)拿枋隽怂淖饔茫罕4嬉幌盗械脑鶹iew操作,最后在App主線程中一次性批處理完。所以當(dāng)ReactShadowNode tree形成時(shí),所有對(duì)應(yīng)的原生View操作(view創(chuàng)建,view屬性賦值,addView形成tree等)都添加到了batchUIQueue中,不過此時(shí)還不到執(zhí)行的時(shí)機(jī)。

js端view節(jié)點(diǎn)的屬性可以大致分為兩類,一類用于直接作用于原生view,比如背景顏色,透明度等等和布局不相關(guān)的;另一類主要是flexbox布局相關(guān)的屬性,比如flex,margin,padding,alignItems等等,這些會(huì)通過ReactShadowNode保存在YogaNode和YGNode中,用于Yoga引擎計(jì)算。

到目前為止ReactShadowNode tree已經(jīng)形成,原生view的操作也已經(jīng)添加到batchUIQueue中等待被執(zhí)行。那什么時(shí)候會(huì)執(zhí)行呢?答案是尺寸和位置數(shù)據(jù)計(jì)算出來以后。在本次js代碼執(zhí)行完之后,jsQueue里會(huì)根據(jù)是否是endOfBatch來執(zhí)行onBatchComplete,它會(huì)觸發(fā)在nativeQueue中執(zhí)行onBatchComplete,其中會(huì)調(diào)用dispatchViewUpdates,它的工作主要分為3步:

啟動(dòng)Yoga引擎對(duì)YGNode tree進(jìn)行計(jì)算,履行Flexbox布局協(xié)議,并將計(jì)算后的結(jié)果遞歸回設(shè)到j(luò)ava端YogaNode節(jié)點(diǎn)

根據(jù)YogaNode tree中的數(shù)據(jù),遞歸向batchUIQueue中添加runnable:給對(duì)應(yīng)的原生View tree節(jié)點(diǎn)設(shè)置尺寸和位置(下文還會(huì)詳細(xì)分析)

將batchUIQueue中所有的runnable,交給App主線程執(zhí)行,所有的這些runnable都在主線程的一個(gè)事件循環(huán)中執(zhí)行完

至此,batchUIQueue的所有view操作在App主線程執(zhí)行完,UI界面也該顯示出來了。其中關(guān)鍵代碼如下:

```

// ----- file: UIImplementation.java

public void dispatchViewUpdates(int batchId) {

? ...

? updateViewHierarchy();

? ...

? mOperationsQueue.dispatchViewUpdates(batchId, commitStartTime, mLastCalculateLayoutTime); // 3、執(zhí)行batchUIQueue中所有的view操作

}

protected void updateViewHierarchy() {

? ...

? calculateRootLayout(cssRoot); // 1、調(diào)用YogaNode.calculateLayout啟用yoga引擎,并將計(jì)算結(jié)果遞歸回設(shè)到j(luò)ava端的YogaNode tree的每個(gè)節(jié)點(diǎn)里

? ...

? applyUpdatesRecursive(cssRoot, 0f, 0f); // 2、將YogaNode tree節(jié)點(diǎn)的數(shù)據(jù),遞歸設(shè)置給原生view,將該操作添加到batchUIQueue中,最后統(tǒng)一執(zhí)行

}

protected void applyUpdatesRecursive(...) {

? ...

? for (int i = 0; i < cssNode.getChildCount(); i++) {

? ? applyUpdatesRecursive(...); // 遞歸

? }

? ...

? cssNode.dispatchUpdates(...) --> uiViewOperationQueue.enqueueUpdateLayout(...) // updateLayout具體做什么?看下面小節(jié)的分析。

}

// ----- file: YogaNode.java

public void calculateLayout(float width, float height) {

? jni_YGNodeCalculateLayout(mNativePointer, width, height);

}

// ----- file: YGJNI.cpp

void jni_YGNodeCalculateLayout(

? ? alias_ref<jclass>,

? ? jlong nativePointer,

? ? jfloat width,

? ? jfloat height) {

? const YGNodeRef root = _jlong2YGNodeRef(nativePointer);

? YGNodeCalculateLayout( // Yoga引擎執(zhí)行計(jì)算,里面是漫長的c++代碼,F(xiàn)lexbox布局協(xié)議的實(shí)現(xiàn)

? ? ? root,

? ? ? static_cast<float>(width),

? ? ? static_cast<float>(height),

? ? ? YGNodeStyleGetDirection(_jlong2YGNodeRef(nativePointer)));

? YGTransferLayoutOutputsRecursive(root); // 將計(jì)算結(jié)果,以遞歸方式回設(shè)給java端每個(gè)YogaNode節(jié)點(diǎn)

}

// ----- UIViewOperationQueue.java

public void dispatchViewUpdates(final int batchId, final long commitStartTime, final long layoutTime) {

? ...

? UiThreadUtil.runOnUiThread( // 切換到主線程處理原生View

? ? new GuardedRunnable(mReactApplicationContext) {

? ? ? @Override

? ? ? public void runGuarded() {

? ? ? ? flushPendingBatches();

? ? ? }

? ? }

? );

}

private void flushPendingBatches() {

? ...

? for (Runnable runnable : runnables) {

? ? runnable.run();

? }

}

```

2.2、使用Yoga的計(jì)算結(jié)果,來跑原生渲染流程

上文提到在Yoga計(jì)算完畢后,會(huì)遞歸將YogaNode tree中的數(shù)據(jù)設(shè)置給原生View tree,簡單的“設(shè)置”二字實(shí)在是太過敷衍,本小節(jié)來詳細(xì)看看這個(gè)過程。首先看下關(guān)鍵代碼:

```

// ----- file: UIImplementation.java

protected void applyUpdatesRecursive(...) {

? ...

? for (int i = 0; i < cssNode.getChildCount(); i++) {

? ? applyUpdatesRecursive(...); // 遞歸調(diào)用

? }

? ...

? cssNode.dispatchUpdates(...); // 這種遞歸方式產(chǎn)生的效果是從葉子節(jié)點(diǎn)開始,到根節(jié)點(diǎn)的遍歷

}

// ----- file: ReactShadowNodeImpl.java

public boolean dispatchUpdates(...) {

? ...

? uiViewOperationQueue.enqueueUpdateLayout( // 添加到batchUIQueue中

? ? ? getParent().getReactTag(),

? ? ? getReactTag(),

? ? ? getScreenX(), // 這些數(shù)據(jù)都是Yoga計(jì)算出來的

? ? ? getScreenY(),

? ? ? getScreenWidth(),

? ? ? getScreenHeight());

}

// ----- file: NativeViewHierarchyManager.java

public synchronized void updateLayout(...) {

? ...

? viewToUpdate.measure( // 看這里

? ? ? View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),

? ? ? View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));

? ...

? updateLayout(...) --> viewToUpdate.layout(x, y, x + width, y + height); // 再看這里,Yoga輸出給View的,就是簡單的left, top, right, bottom

}

```

從上面的代碼中可以看到,從YogaNode tree的根節(jié)點(diǎn)開始遞歸處理,但卻是反向地將原生View tree對(duì)應(yīng)節(jié)點(diǎn)的layout操作添加到batchUIQueue中,即先處理的是葉子結(jié)點(diǎn),最后到根節(jié)點(diǎn)。個(gè)人分析這里的遍歷順序沒有什么分別,top-down或者down-top結(jié)果是一樣的,畢竟各節(jié)點(diǎn)的尺寸和位置已經(jīng)算好,至于是先設(shè)置子節(jié)點(diǎn)還是父節(jié)點(diǎn)并無區(qū)別,最終都是在batchUIQueue中一次性執(zhí)行完,再進(jìn)行reqeustLayout,進(jìn)而執(zhí)行performTraversals完成渲染,此處若分析的不對(duì)還請(qǐng)大神指正。這里的重點(diǎn)是RN親力親為的調(diào)用了原生View tree里所有節(jié)點(diǎn)的measure和layout(僅限和YogaNode tree對(duì)應(yīng)的節(jié)點(diǎn),自定義view group里子節(jié)點(diǎn)不在此范圍),來完成渲染流程的前兩個(gè)階段,將Yoga的計(jì)算結(jié)果應(yīng)用進(jìn)去。上文有提到過,容器節(jié)點(diǎn)onMeasure/onLayout會(huì)調(diào)用子節(jié)點(diǎn)的measure/layout,來銜接tree結(jié)構(gòu)父子節(jié)點(diǎn)的遍歷,這里是否和RN的遍歷重復(fù)了呢?答案當(dāng)然是沒有重復(fù)。RN既然選擇親自遍歷所有節(jié)點(diǎn),當(dāng)然就會(huì)有處理:onMeasure和onLayout中的計(jì)算邏輯都去掉,并且不調(diào)用子節(jié)點(diǎn)的measure和layout。代碼節(jié)選如下:

```

// ----- file: ReactRootView.java RN原生端的根view,用于承載RN所有的界面

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

? ...

? setMeasuredDimension(width, height); // 沒有調(diào)用子節(jié)點(diǎn)的measure,僅履行了安卓的約定調(diào)用setMeasuredDimension。

}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

? // No-op since UIManagerModule handles actually laying out children.

? // 這里更簡單,什么都不做,上面的注釋說的很明白了。

}

// ----- file: ReactViewGroup.java 這個(gè)類表示的是js端的View.js,最基本的容器view。測量和布局均沒有任何計(jì)算,沒有觸發(fā)子節(jié)點(diǎn)。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

? ...

? setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));

}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

? // No-op since UIManagerModule handles actually laying out children.

}

```

3、Yoga布局和原生布局共存,安卓自定義View也可以正常工作

RN雖然是自己負(fù)責(zé)銜接安卓渲染流程的前兩步,但是接入自定view group并不需要額外做太多,只需跟安卓自定義view group一樣重寫onMeasure和onLayout,負(fù)責(zé)計(jì)算子節(jié)點(diǎn)的尺寸和位置。如下圖所示,自定義節(jié)點(diǎn)和RN的節(jié)點(diǎn)能夠完美合作,“各司其職”。

Yoga和安卓原生“協(xié)作”完成布局計(jì)算

不過事情總是會(huì)有一些遺憾,當(dāng)自定義view group進(jìn)行requestLayout時(shí)會(huì)觸發(fā)view tree的渲染流程遍歷,但是RN的view容器并沒有在onMeasure/onLayout里銜接遍歷,導(dǎo)致自定義view group界面更新不生效。解決方式比較簡單,在自定義view group里重寫requestLayout,然后手動(dòng)調(diào)用measure和layout,這樣自定義view就可以正常工作了。

```

// ----- 某自定義view group.java

@Override

public void requestLayout() {

? super.requestLayout();

? post(measureAndLayout);

}

private final Runnable measureAndLayout = new Runnable() {

? @Override

? public void run() {

? ? ? measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),

? ? ? ? ? ? ? MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));

? ? ? layout(getLeft(), getTop(), getRight(), getBottom());

? }

};

```

下面來看一個(gè)筆者之前在實(shí)際項(xiàng)目中遇到的例子,這個(gè)例子是電子答題,主要功能是展示一張題目圖片,學(xué)生可以在下面手寫作答,當(dāng)答題區(qū)域不夠時(shí)可以增加區(qū)域尺寸等等。該功能實(shí)現(xiàn)采用的是FrameLayout+自定義ImageView,如下示意圖所示,自定義ImageView用于設(shè)置題目圖片和實(shí)現(xiàn)畫筆功能,F(xiàn)rameLayout用于移動(dòng)和縮放ImageView等。初始化時(shí)ImageView和FrameLayout尺寸一樣,當(dāng)用戶點(diǎn)擊增加一屏畫布時(shí),在FrameLayout里增加ImageView的高度。這其中沒有重寫onMeasure和onLayout,F(xiàn)rameLayout和ImageView默認(rèn)的就夠用。

功能在安卓原生側(cè)是正常的,接入到RN以后展示正常,寫寫畫畫正常,對(duì)畫布進(jìn)行放大縮小(scale)正常,但是點(diǎn)擊增加一屏操作卻無效??偨Y(jié)一下上述這些現(xiàn)象會(huì)發(fā)現(xiàn):

? ? onDraw里的操作執(zhí)行正常,比如畫筆操作,setScale和setTranslationX等

? ? setLayoutParams操作無效,如設(shè)置ImageView高度

也就是說尺寸更新無效,繪制操作有效,原因就是當(dāng)在View里進(jìn)行invalidate和setLayoutParams時(shí),都會(huì)執(zhí)行requestLayout向上反饋到ViewRootImpl來觸發(fā)一次渲染流程,其中RN阻斷了measure和layout的遍歷,沒有阻斷draw。解決方式就是在FrameLayout.requestLayout中主動(dòng)觸發(fā)measure和layout,完成子View的尺寸刷新。另外,由于FrameLayout是作為自定義View接入到RN的,他的尺寸將會(huì)受RN來控制,原生側(cè)無需關(guān)心。

4、總結(jié)

以上就是RN在安卓端UI渲染流程的介紹,主要描述了我們?cè)趈s端寫的view tree,是如何一步一步轉(zhuǎn)化到原生view的,這也體現(xiàn)了RN確實(shí)就是native的一面。筆者一直以來就有一個(gè)疑惑,安卓原生View本身有大量的屬性,比如width,height,padding,margin等等,RN所采用的Flexbox也有很多的屬性,flex,padding,alignItems等等,兩者的差距還是很大的,這兩套屬性該如何對(duì)接?乍一想還是很頭疼的,大量的屬性剪不斷理還亂,但從RN源碼來看,其實(shí)兩者的銜接點(diǎn)只是view的邊界值而已(left, top, right, bottom)。分析一下可以發(fā)現(xiàn),不管是Flexbox還是安卓原生布局,其他屬性存在的價(jià)值就是為了計(jì)算出left,top,right,bottom。RN參與渲染流程的切入點(diǎn)也正是這里,剛好原生view就有一個(gè)layout(l, t, r, b)方法來接收這四個(gè)值,并且也剛好這個(gè)方法就是渲染流程中的一步,一切都恰到好處。另外,RN在一個(gè)子線程中來計(jì)算View的布局?jǐn)?shù)據(jù),省去了主線程刷新UI時(shí)的前兩步遍歷,減輕了負(fù)擔(dān),但從整體上來看UI的連貫性未必就有提升,線程之間的配合成本可能也不低。筆者也是在學(xué)習(xí)過程中,想法難免有欠缺或錯(cuò)誤之處,歡迎各位大神提出寶貴建議。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容