從 View#post 源碼解析教你如何獲取 View 的寬高

之前的文章里寫(xiě)到過(guò),我們?cè)?onCreate() 和 onResume() 方法中無(wú)法獲取 View 的寬高信息,但在平時(shí)開(kāi)發(fā)中,我們經(jīng)常會(huì)用到 View#post 來(lái)進(jìn)行 View 寬高信息的獲取。

那么問(wèn)題就來(lái)了,為什么 View#post 就可以獲取到寬高信息?里邊那個(gè) run() 是在什么時(shí)候執(zhí)行的?具體實(shí)現(xiàn)原理又是什么?

帶著這些疑問(wèn),我最近研究了一下 View#post 的源碼。本來(lái)以為挺簡(jiǎn)單的一個(gè)東西,但是沒(méi)想到坑越挖越深,最過(guò)分的是,不同的版本源碼還不相同,實(shí)現(xiàn)原理也有細(xì)微的差別。集中攻克了一個(gè)周末以后,感覺(jué)大概理解了,索性寫(xiě)下篇博客進(jìn)行記錄備忘。

文章大概分為以下幾個(gè)方面:

  • View#post 基本使用
  • post() 執(zhí)行過(guò)程以及源碼分析
  • post() 中 Runnable#run 執(zhí)行的時(shí)機(jī)
  • View#post 整體流程的簡(jiǎn)單總結(jié)
  • Android 7.0 里 View#post 的變動(dòng)以及原因
  • 致謝

View#post 基本使用

具體代碼如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    View view = findViewById(R.id.test);
    view.post(new Runnable() {
        @Override
        public void run() {
            // 可以正常獲取到 View 的寬高信息
            Log.e("Test", "view.post ---- > " + view.getHeight());
        }
    });
}

這里我們以 API 26 為例,來(lái)嘗試解答一下這個(gè)問(wèn)題。

實(shí)際上,Android 系統(tǒng)以 API 24 為界,之前之后的版本,對(duì)此處的實(shí)現(xiàn)有細(xì)微的差別,具體的改動(dòng)以及原因在后文會(huì)一一給出分析。

post() 執(zhí)行過(guò)程以及源碼分析

1. View#post 入口

先來(lái)看 View#post 源碼,重點(diǎn)注意注釋:

/**
 * Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.
 * 將 Runnable 添加到執(zhí)行隊(duì)列中,其最終會(huì)在 UI 線程中執(zhí)行
 */
public boolean post(Runnable action) {
    // AttachInfo 是 View 的內(nèi)部類,用來(lái)存儲(chǔ)一些基本信息
    // 此處可以暫時(shí)認(rèn)為 mAttachInfo 為 null
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // attachInfo 不為空時(shí),轉(zhuǎn)而使用其內(nèi)部的 Handler 對(duì)象操作
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    // 在我們確定當(dāng)前 Runnable 的目標(biāo)運(yùn)行線程之前,先將其推遲執(zhí)行
    // 假設(shè)在 attach 完成之后,此 Runnable 對(duì)象會(huì)被成功的「placed」(暫且翻譯成「放置」)
    // 好好理解一下這個(gè)注釋,我們繼續(xù)往下走
    getRunQueue().post(action);
    return true;
}

首先,明確一點(diǎn):Runnable 會(huì)在 UI 線程中執(zhí)行

然后,我們來(lái)看一下這個(gè)看上去很重要的 mAttachInfo 是在哪里賦值的:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // Transfer all pending runnables. 轉(zhuǎn)移所有待辦任務(wù)
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // 回調(diào)方法
    onAttachedToWindow();
}

先不在意除了賦值以外的其他操作,我們繼續(xù)追蹤 dispatchAttachedToWindow 方法,發(fā)現(xiàn)其最初調(diào)用是在 ViewRootImpl#performTraversals 方法。好了,記住這個(gè)結(jié)論,我們先把它放在一旁。

接下來(lái),我們來(lái)看一看這個(gè) getRunQueue().post() 又做了什么:

/**
 * 獲取一個(gè) RunQueue 對(duì)象,用來(lái)進(jìn)行 post 操作
 * Returns the queue of runnable for this view.
 * 注釋是:為當(dāng)前 View 對(duì)象返回一個(gè)執(zhí)行隊(duì)列,記住這個(gè)「當(dāng)前 View 對(duì)象」
 */
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

2. HandlerActionQueue 又是個(gè)啥

很明顯,執(zhí)行 post 方法的是 HandlerActionQueue 對(duì)象,那這又是個(gè)什么東西:

/**
 * Class used to enqueue pending work from Views when no Handler is attached.
 * 此類用于在當(dāng)前 View 沒(méi)有 Handler 依附的時(shí)候,將其待完成的任務(wù)入隊(duì)
 */
public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;

    // 這個(gè)就是我們?cè)谕膺呎{(diào)用的 post 方法,最終會(huì)調(diào)用到 postDelayed 方法
    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    // 將傳入的 Runnable 對(duì)象存入數(shù)組中,等待調(diào)用
    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++;
        }
    }

    // 這里才是真的執(zhí)行方法
    public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }
}

通過(guò)查看 HandlerActionQueue 的源碼,我們發(fā)現(xiàn)了一個(gè)問(wèn)題:不同于在 onCreate() 直接獲取 View 的寬高,我們調(diào)用 post 方法,其中的 run 方法并沒(méi)有被馬上執(zhí)行。

這樣就不難解釋為什么用這種方式可以獲取到寬高了。那我們可以猜測(cè)一下,這種情況下,一定是 View 完成測(cè)量后才執(zhí)行了這個(gè)方法,所以我們才可以拿到寬高信息。

事實(shí)上也正是這樣的,那么這個(gè)方法到底是在什么時(shí)候執(zhí)行的呢?很明顯,HandlerActionQueue#executeActions 才是真正完成調(diào)用的方法,那這個(gè)方法又做了些什么工作呢?

根據(jù)代碼可知,該方法接收一個(gè) Handler,然后使用這個(gè) Handler 對(duì)當(dāng)前隊(duì)列中的所有 Runnable 進(jìn)行處理,即 post 到該 Handler 的線程中,按照優(yōu)先級(jí)對(duì)這些 Runnable 依次進(jìn)行處理。

簡(jiǎn)單來(lái)說(shuō),就是傳入的 Handler 決定著這些 Runnable 的執(zhí)行線程。

接下來(lái),我們來(lái)追蹤這個(gè)方法的調(diào)用情況。

executeActions() 的調(diào)用情況

我們注意到,對(duì)于該方法出現(xiàn)了兩次調(diào)用,一次在 View#dispatchAttachToWindow(就是我們最開(kāi)始找到的那個(gè)方法),另一次是在 ViewRootImpl#performTraversals。

3. 萬(wàn)惡之源 performTraversals()

很明顯,所有的證據(jù)都指向了 performTraversals ,那么下面我們就來(lái)重點(diǎn)分析一下這個(gè)方法。

如果你了解過(guò) View 的測(cè)繪流程,那你對(duì)這個(gè)方法一定不會(huì)陌生,因?yàn)檫@個(gè)方法就是 View 繪制流程的起點(diǎn)。

private void performTraversals() {
    
    // 此處的 host 是根布局 DecorView,用遞歸的方式一層一層的調(diào)用 dispatchAttachedToWindow
    // mAttachInfo 是不是很眼熟,就是最開(kāi)始 View#post 的第一層判斷
    // 這個(gè) mAttachInfo 在 ViewRootImpl 的構(gòu)造器中初始化的,其持有 ViewRootImpl 的 Handler 對(duì)象
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    getRunQueue().executeActions(mAttachInfo.mHandler);
    
    // 繪制流程就從這里開(kāi)始
    performMeasure();
    performLayout();
    performDraw();
}

我們先從 dispatchAttachedToWindow 開(kāi)始,我們之前已經(jīng)看過(guò)這個(gè)方法的源碼了:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // Transfer all pending runnables. 轉(zhuǎn)移所有待辦任務(wù)
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // 回調(diào)方法
    onAttachedToWindow();
}

現(xiàn)在來(lái)進(jìn)行分析:

  1. 我們已經(jīng)知道了此方法是從根視圖開(kāi)始遞歸向下調(diào)用的,那么遞歸到最深處,就會(huì)輪到最開(kāi)始我們調(diào)用 post 方法的 View 對(duì)象來(lái)執(zhí)行該方法,也就是該方法內(nèi)的所有屬性,都是我們 findViewById 獲得的那個(gè) View 對(duì)象的屬性;
  2. 而且我們也知道,第一個(gè)參數(shù) AttachInfo 就是 ViewRootImpl 中初始化的 AttachInfo,它持有當(dāng)前 ViewRootImpl 的 Handler 對(duì)象引用,并將該引用傳給了 executeActions()。此時(shí),我們?cè)賮?lái)回顧一下 executeActions() 方法的作用,傳入的 Handler 決定著隊(duì)列里這些 Runnable 的執(zhí)行線程。

很明顯,此處的 mRunQueue 就是我們最開(kāi)始調(diào)用 post() 時(shí),調(diào)用 View#getRunQueue 返回的那個(gè)對(duì)象,這個(gè)對(duì)象中有準(zhǔn)備獲取View高度的 Runnable 對(duì)象,也就是說(shuō) mRunQueue 通過(guò)調(diào)用 executeActions() 將當(dāng)前 View 的所有 Runnable ,都會(huì)轉(zhuǎn)由 ViewRootImpl 的 Handler 來(lái)處理!而在完成這個(gè)工作之后,當(dāng)前 View 也顯示地將 mRunQueue 置空,因?yàn)樗械拇k任務(wù)都已經(jīng)交給 ViewRootImpl 去處理了。

現(xiàn)在再回過(guò)頭看代碼的注釋,就差不多可以理解了:

// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
// 所有的 Runnable 都會(huì)在 attach 之后被正確的放到其應(yīng)該運(yùn)行的線程上去
getRunQueue().post(action);

// Transfer all pending runnables.
// 轉(zhuǎn)移所有待辦任務(wù)(到 ViewRootImpl 中進(jìn)行處理)
if (mRunQueue != null) {
    mRunQueue.executeActions(info.mHandler);
    mRunQueue = null;
}

dispatch 方法執(zhí)行完了,我們繼續(xù)回來(lái)走 performTraversals() ,接下來(lái)一句是:

// 有之前的經(jīng)驗(yàn),我們知道這句話的意思是
// 使用 mAttachInfo.mHandler 來(lái)處理 getRunQueue() 中的 Runnable 任務(wù)
getRunQueue().executeActions(mAttachInfo.mHandler);

要明確的一點(diǎn)是,此時(shí)我們處在 ViewRootImpl 類中,此處的 getRunQueue() 方法有別于 View#post:

// ViewRootImpl#getRunQueue
// 使用 ThreadLocal 來(lái)存儲(chǔ)每個(gè)線程自身的執(zhí)行隊(duì)列 HandlerActionQueue
static HandlerActionQueue getRunQueue() {
    // sRunQueues 是 ThreadLocal<HandlerActionQueue> 對(duì)象
    HandlerActionQueue rq = sRunQueues.get();
    if (rq != null) {
        return rq;
    }
    rq = new HandlerActionQueue();
    sRunQueues.set(rq);
    return rq;
}

// View#post
// 為當(dāng)前 View 返回一個(gè)執(zhí)行隊(duì)列,但是在 dispatchAttachToWindow 時(shí)轉(zhuǎn)到 UI 線程去
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

說(shuō)回 performTraversals() ,很明顯 getRunQueue() 是 UI 線程執(zhí)行隊(duì)列的第一次初始化,也就是說(shuō)當(dāng)前這個(gè)任務(wù)隊(duì)列里并沒(méi)有待執(zhí)行任務(wù)!

但是需要注意的是,當(dāng)前沒(méi)有執(zhí)行任務(wù)(HandlerActionQueue),不代表 Handler 消息隊(duì)列中沒(méi)有消息,這是兩個(gè)概念,需要注意區(qū)分開(kāi)。

總結(jié)一下:

  1. View#post 方法調(diào)用時(shí),會(huì)為當(dāng)前 View 對(duì)象初始化一個(gè) HandlerActionQueue ,并將 Runnable 入隊(duì)存儲(chǔ);
  2. 等在 ViewRootImpl#performTraversals 中遞歸調(diào)用到 View#dispatchAttachedToWindow 時(shí),會(huì)將 ViewRootImpl 的 Handler 對(duì)象傳下來(lái),然后通過(guò)這個(gè) Handler 將最初的 Runnable 發(fā)送到 UI 線程(消息隊(duì)列中)等待執(zhí)行,并將 View 的 HandlerActionQueue 對(duì)象置空,方便回收;
  3. ViewRootImpl#performTraversals 繼續(xù)執(zhí)行,才會(huì)為 UI 線程首次初始化 HandlerActionQueue 對(duì)象,并通過(guò) ThreadLocal 進(jìn)行存儲(chǔ),方便之后的復(fù)用,但需要注意的是,此處初始化的隊(duì)列中是沒(méi)有任何 Runnable 對(duì)象的;
  4. 然后 ViewRootImpl#performTraversals 繼續(xù)執(zhí)行,開(kāi)始 View 的測(cè)量流程。

View#post 中 Runnable#run 執(zhí)行的時(shí)機(jī)

但現(xiàn)在的問(wèn)題是,無(wú)論怎么說(shuō),HandlerActionQueue#executeActions 都是先于 View 測(cè)繪流程的,為什么在還沒(méi)有完成測(cè)量的時(shí)候,就可以拿到寬高信息?

我們都知道,Android 系統(tǒng)是基于消息機(jī)制運(yùn)行的,所有的事件、行為,都是基于 Handler 消息機(jī)制在運(yùn)行的。所以,當(dāng) ViewRootImpl#performTraversals 在執(zhí)行的時(shí)候,也一定是基于某個(gè)消息的。而且,HandlerActionQueue#executeActions 執(zhí)行的時(shí)候,也只是通過(guò) Handler 將 Runnable post 到了 UI 線程等待執(zhí)行(還記得 View#post 的注釋嗎?)。

不出意外的話,此時(shí) UI 線程正忙著執(zhí)行 ViewRootImpl#performTraversal ,等該方法執(zhí)行完畢,View 已經(jīng)完成了測(cè)量流程,此時(shí)再去執(zhí)行 Runnable#run ,也就自然可以獲取到 View 的寬高信息了。

下面用具體的實(shí)例佐證一下我們的猜想。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();

    // 等待 Add 到父布局中
    view = new View(this) {
        @Override
        protected void onLayout( ... ... ) {
            super.onLayout(changed, left, top, right, bottom);
            Log.e("Test", "執(zhí)行了onLayout()");
        }
    };

    // 自己聲明的 Handler 
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            Log.e("Test", "mHandler.post ---- > " + view.getHeight());
        }
    });

    // onCreate() 中 mAttachInfo 還未被賦值,這里會(huì)交給 ViewRootImpl 的 Handler 來(lái)處理
    // 即加入消息隊(duì)列,等待執(zhí)行
    view.post(new Runnable() {
        @Override
        public void run() {
            Log.e("Test", "view.post ---- > " + view.getHeight());
        }
    });

    viewGroup.addView(view);
}

最終打印日志如下:

image

也就是說(shuō):

  1. Handler#post 首先執(zhí)行,其 post 的時(shí)間點(diǎn)在 onCreate() 方法內(nèi),在消息隊(duì)列中的位置一定比 performTraversals() 靠前;
  2. ViewRootImpl#performTraversal 執(zhí)行,過(guò)程中執(zhí)行了 View#dispatchAttachedToWindow 方法,將最初的 Runnable 入隊(duì)后進(jìn)行測(cè)量流程,完成了 layout 過(guò)程;
  3. 之后才執(zhí)行了最初的 View#post 方法,也就說(shuō)明了,在 View#dispatchAttachedToWindow 中使用 ViewRootImpl 的 Handler postDelay 的 Runnable 對(duì)象,在主線程消息隊(duì)列中,確實(shí)是排在 ViewRootImpl#performTraversal 之后的

View#post 整體流程的簡(jiǎn)單總結(jié)

最后大概總結(jié)一下:

當(dāng)我們使用 View#post 時(shí),會(huì)有兩種情況:

  1. 在當(dāng)前 View attach 到 Window 之前,會(huì)自己先維護(hù)一個(gè) HandlerActionQueue 對(duì)象,用來(lái)存儲(chǔ)當(dāng)前的 Runnable 對(duì)象,然后等到 Attach 到 Window 的時(shí)候 (也就是 ViewRootImpl 執(zhí)行到 performTraversal 方法時(shí)) ,會(huì)統(tǒng)一將 Runnable 轉(zhuǎn)交給 ViewRootImpl 處理;
  2. 而在 View#dispatchAttachedToWindow 時(shí),也會(huì)為當(dāng)前 View 初始化一個(gè) AttachInfo 對(duì)象,該對(duì)象持有 ViewRootImpl 的引用,當(dāng) View 有此對(duì)象后,后續(xù)的所有 Runnable 都將直接交給 ViewRootImpl 處理;
  3. 而 ViewRootImpl 也會(huì)在執(zhí)行 performTraversal 方法,也會(huì)調(diào)用 ViewRootImpl#getRunQueue ,利用 ThreadLocal 來(lái)為主線程維護(hù)一個(gè) HandlerActionQueue 對(duì)象,至此,ViewRootImpl 內(nèi)部都將使用該隊(duì)列來(lái)進(jìn)行 Runnable 任務(wù)的短期維護(hù);
  4. 但需要注意的是,各個(gè) View 調(diào)用的 post 方法,仍然是由各自的 HandlerActionQueue 對(duì)象來(lái)入隊(duì)任務(wù)的,然后在 View#dispatchAttachedToWindow 的時(shí)候轉(zhuǎn)移給 ViewRootImpl 去處理。

Android 7.0 里 View#post 的變動(dòng)以及原因

View#post 說(shuō)到這里大概就差不多了,文章開(kāi)篇的時(shí)候說(shuō)到:

Android 系統(tǒng)以 API 24 為界,之前之后的版本,對(duì)此處的實(shí)現(xiàn)有細(xì)微的差別

下面來(lái)簡(jiǎn)單對(duì)比一下具體的差別,順便分析一下具體為什么要這樣改動(dòng)。

實(shí)際上這個(gè)方法的改動(dòng)主要是為了解決一個(gè) bug,這個(gè) bug 就是:在 View 被 attach 到 window 之前,從子線程調(diào)用的 View#post ,永遠(yuǎn)無(wú)法得到執(zhí)行。

具體原因,我們來(lái)看一下 API23 版本的 View#post,就大概都明白了:

// Android API23 View#post
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    // 注意此處,不同于我們之前介紹的,這里是直接使用 ViewRootImpl#getRunQueue 來(lái)入隊(duì)任務(wù)的
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

我們可以看到,不同于我們之前介紹的,API23 版本中,View#post 在沒(méi)有 attach 到 window 之前,也就是 mAttachInfo 是 null 的時(shí)候,不是自己維護(hù)任務(wù)隊(duì)列,而是直接使用 ViewRootImpl#getRunQueue 來(lái)入隊(duì)任務(wù)的。

再來(lái)看一下 ViewRootImpl#getRunQueue 方法,我們就會(huì)發(fā)現(xiàn)問(wèn)題出在哪里了:

static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {
    RunQueue rq = sRunQueues.get();
    if (rq != null) {
        return rq;
    }
    rq = new RunQueue();
    sRunQueues.set(rq);
    return rq;
}

沒(méi)錯(cuò),這個(gè)隊(duì)列的保存與獲取,是通過(guò)以線程為 key 值來(lái)存取對(duì)象 ThreadLocal 來(lái)維護(hù)的。而在這個(gè)版本的源碼中,executeActions() 方法的執(zhí)行,只有一次調(diào)用,那就是 ViewRootImpl#performTraversal 中(感興趣的可以去 23 版本的源碼中查看,這里就不貼圖了),與此同時(shí),該方法肯定是執(zhí)行在主線程中的。

現(xiàn)在的問(wèn)題就變成了:我在子線程中 post 了一個(gè) runnable,并且系統(tǒng)以該子線程為 key 將隊(duì)列存了起來(lái)等待執(zhí)行;但是在具體執(zhí)行的時(shí)候,系統(tǒng)卻是去主線程中尋找待執(zhí)行的 Runnable,那么當(dāng)然是永遠(yuǎn)都得不到執(zhí)行的了。

而在具體 attach 到 window 之后,View 的 mAttachInfo 持有 ViewRootImpl 引用,會(huì)直接將所有的 Runnable 轉(zhuǎn)交給 ViewRootImpl 的 Handler 處理,也就都能得到妥善處理,就與線程無(wú)關(guān)了。

除此以外,ViewRootImpl 使用 ThreadLocal 來(lái)存儲(chǔ)隊(duì)列信息,在某些情境下,還會(huì)導(dǎo)致內(nèi)存泄漏。詳細(xì)信息可以參考:https://blog.csdn.net/a740169405/article/details/69668957

所以,Google 工程師為了解決這兩個(gè)問(wèn)題(內(nèi)存泄漏的問(wèn)題更嚴(yán)重一些),就在 View#post 方法中使用 View 對(duì)象來(lái)進(jìn)行隊(duì)列的存儲(chǔ),然后在 attach 到 window 的時(shí)候,通過(guò)持有 ViewRootImpl 引用的 AttachInfo 對(duì)象直接將 View 對(duì)象的 Runnable 處理掉,就完美解決了這些問(wèn)題。

致謝

下邊是自己研究的時(shí)候具體參考過(guò)的文章,給各位前輩加個(gè)雞腿:

https://blog.csdn.net/a740169405/article/details/69668957
https://blog.csdn.net/scnuxisan225/article/details/49815269
https://www.cnblogs.com/plokmju/p/7481727.html
最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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