硬件渲染_樹形視圖節(jié)點(diǎn)繪制記錄


視圖樹形結(jié)構(gòu),每一個(gè)節(jié)點(diǎn)均有Java層DisplayListCanvas和底層DisplayListCanvas。在Java層,調(diào)用Canvas#drawXxx方法,如drawPoint,drawPath,drawRect,DisplayListCanvas或Canvas中有JNI#方法,根據(jù)mNativeCanvasWrapper指針獲取底層DisplayListCanvas。底層DisplayListCanvas繼承底層Canvas。頭文件定義在/frameworks/base/libs/hwui/Canvas.h。
此外,除了繪制方法drawXxx,還有變換方法,如translate,scale等,以及save和restore方法,下面會(huì)通過一個(gè)繪圖實(shí)例具體分析這些方法實(shí)現(xiàn)的操作。
前面已經(jīng)介紹過,從頂層視圖DecorView#updateDisplayListIfDirty方法開始繪制樹形視圖結(jié)構(gòu),為了分析簡(jiǎn)單,截取一小段樹分支(虛線框內(nèi)),通過下面視圖結(jié)構(gòu),分析繪制時(shí)具體操作。

樹形節(jié)點(diǎn)繪制示例圖.jpg
手機(jī)上顯示的視圖圖片如下。
手機(jī)上顯示的視圖圖片.jpg

容器視圖繪制

虛線框的樹形結(jié)構(gòu)截取如圖。
截取虛線框的視圖.jpg

父視圖0是LinearLayout,三個(gè)子視圖分別是自定義CanvasView和兩個(gè)TextView。
當(dāng)遍歷到達(dá)LinearLayout節(jié)點(diǎn)#updateDisplayListIfDirty方法時(shí),再看一下此方法代碼,如下。

public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    // ThreadedRenderer是空,直接返回節(jié)點(diǎn)
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.isValid()//false,還未記錄繪制
            || (mRecreateDisplayList)) {//重建Canvas
        ...//省略掉不需要重建Canvas的部分代碼
        //第一次進(jìn)來肯定需要建立Canvas,renderNode也還未記錄。
        mRecreateDisplayList = true;//重建Canvas
        int width = mRight - mLeft;
        int height = mBottom - mTop;
        int layerType = getLayerType();
        //創(chuàng)建DisplayListCanvas
        final DisplayListCanvas canvas = renderNode.start(width, height);
        //判斷LayerType,以及獲取HardwareLayer的部分代碼省略掉,LinearLayout不需要。
        try {
            // 一般視圖走硬件渲染都執(zhí)行下面程序
            computeScroll();
            canvas.translate(-mScrollX, -mScrollY);
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                //LinearLayout視圖會(huì)跳過繪制,則直接派發(fā)給子視圖
                dispatchDraw(canvas);
            } else {
                draw(canvas);//繪制,包括繪制自身,修飾,以及派發(fā),共六個(gè)步驟,此處不執(zhí)行。
            }
        } finally {
            renderNode.end(canvas);//繪制結(jié)束,保存canvas記錄內(nèi)容
            setDisplayListProperties(renderNode);
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}

LinearLayout視圖創(chuàng)建畫布DisplayListCanvas后,因有PFLAG_SKIP_DRAW標(biāo)志,選擇跳過繪制,它沒有設(shè)置Background,在View框架源碼中會(huì)設(shè)置此標(biāo)志。跳過繪制,即不會(huì)走LinearLayout的onDraw方法,另外四個(gè)步驟都不會(huì)觸發(fā),僅僅觸發(fā)父類ViewGroup的dispatchDraw方法。繪制直接向子視圖派發(fā)。
ViewGroup#dispatchDraw方法。

@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    boolean more = false;
    final long drawingTime = getDrawingTime();
    //LinearLayout的畫布寫入Reorder柵欄 
    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    ...
    //繪制子視圖
    for (int i = 0; i < childrenCount; i++) {
        ...
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
      }
    ...
    //LinearLayout的畫布寫入Inorder柵欄 
    if (usingRenderNodeProperties) canvas.insertInorderBarrier();
    ...
}
//drawChild方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

dispatchDraw源碼比較多,主要功能是派發(fā)子視圖繪制。觸發(fā)每個(gè)子視圖三個(gè)參數(shù)的draw重載方法。在遍歷子視圖前后,有兩個(gè)方法,LinearLayout畫布的insertReorderBarrier和insertInorderBarrier方法。
他們?cè)谟|發(fā)底層DisplayListCanvas的方法一樣,只是enableReorder標(biāo)志不同,insertReorderBarrier支持重排。

void DisplayListCanvas::insertReorderBarrier(bool enableReorder) {
    flushRestoreToCount();
    flushTranslate();
    mDeferredBarrierType = enableReorder ? kBarrier_OutOfOrder : kBarrier_InOrder;
}

柵欄設(shè)置成kBarrier_OutOfOrder或kBarrier_InOrder類型,kBarrier_OutOfOrder用于標(biāo)記Chunk的一個(gè)變量值reorderChildren。DisplayListCanvas的prepareDirty方法初始化值默認(rèn)是kBarrier_InOrder,這個(gè)值后續(xù)addOpAndUpdateChunk方法會(huì)用到,在繪制子視圖之前,已經(jīng)創(chuàng)建過第一個(gè)Chunk。
在繪制子視圖前,插入一個(gè)kBarrier_OutOfOrder柵欄,LinearLayout畫布drawXxx方法繪制會(huì)創(chuàng)建一個(gè)新Chunk,該Chunk索引LinearLayout畫布繪制的子視圖節(jié)點(diǎn)。繪制子視圖結(jié)束后,再次插入一個(gè)kBarrier_InOrder柵欄,再次新建一個(gè)Chunk塊,索引LinearLayout畫布后續(xù)繪制的內(nèi)容。

LinearLayout子視圖是葉子節(jié)點(diǎn),子視圖繪制,三個(gè)參數(shù)的draw重載方法。
View#draw方法。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
    boolean drawingWithRenderNode = mAttachInfo != null
            && mAttachInfo.mHardwareAccelerated
            && hardwareAcceleratedCanvas;
    boolean more = false;
    ...
    RenderNode renderNode = null;
    Bitmap cache = null;
    ...
    //硬件渲染
    if (drawingWithRenderNode) {
        //在這里觸發(fā)子視圖渲染方法。
        renderNode = updateDisplayListIfDirty();
        if (!renderNode.isValid()) {
            renderNode = null;
            drawingWithRenderNode = false;
        }
    }
    ...
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            //父視圖畫布繪制子視圖RenderNode節(jié)點(diǎn)
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            ...
        }
    } else if (cache != null) {
        ...
    }

    if (restoreTo >= 0) {
        canvas.restoreToCount(restoreTo);
    }
    ..
    mRecreateDisplayList = false;

    return more;
}

draw方法很長,這里指摘取硬件渲染相關(guān)的。關(guān)注兩個(gè)點(diǎn)。
1:子視圖#updateDisplayListIfDirty方法,重建Canvas,傳遞給一個(gè)參數(shù)的draw重載方法,繪制,返回子視圖RenderNode節(jié)點(diǎn),該節(jié)點(diǎn)相關(guān)畫布已經(jīng)完成繪制內(nèi)容記錄。
2:LinearLayout畫布drawRenderNode繪制子視圖RenderNode節(jié)點(diǎn)。

void DisplayListCanvas::drawRenderNode(RenderNode* renderNode) {
    DrawRenderNodeOp* op = new (alloc()) DrawRenderNodeOp(
            renderNode,
            *mState.currentTransform(),
            mState.clipIsSimple());
    addRenderNodeOp(op);
}

創(chuàng)建一個(gè)DrawRenderNodeOp,DrawRenderNodeOp繼承DrawBoundedOp,DrawBoundedOp繼承DrawOp,基類是DisplayListOp。

size_t DisplayListCanvas::addRenderNodeOp(DrawRenderNodeOp* op) {
    //增加一個(gè)繪制操作到mDisplayListData的displayListOps數(shù)組。
    int opIndex = addDrawOp(op);
    //mDisplayListData是LinearLayout底層存儲(chǔ)繪制數(shù)據(jù)的對(duì)象
    //增加操作到DisplayListData的mChildren數(shù)組,代表子視圖
    int childIndex = mDisplayListData->addChild(op);
    //寫入了一個(gè)繪制子節(jié)點(diǎn)的操作,告訴Chunk的endChildIndex自增。
    DisplayListData::Chunk& chunk = mDisplayListData->chunks.editTop();
    chunk.endChildIndex = childIndex + 1;

    if (op->renderNode()->stagingProperties().isProjectionReceiver()) {
        mDisplayListData->projectionReceiveIndex = opIndex;
    }
    return opIndex;
}

addDrawOp方法增加一個(gè)繪制Op,后面會(huì)詳細(xì)介紹,總之,將DrawRenderNodeOp加入到DisplayListData的displayListOps數(shù)組中。
addChild方法將操作加入到DisplayListData的mChildren數(shù)組中。
注意,因前期插入一個(gè)kBarrier_OutOfOrder柵欄,因此在addDrawOp觸發(fā)的addOpAndUpdateChunk中,會(huì)創(chuàng)建一個(gè)新Chunk塊,寫入DrawRenderNodeOp后,更新該Chunk中指向mChildren數(shù)組的endChildIndex 索引。
三個(gè)葉子RenderNode節(jié)點(diǎn)操作寫入LinearLayout底層數(shù)據(jù)數(shù)組中,他們?cè)谝粋€(gè)Chunk塊中。子視圖繪制完畢,再次插入一個(gè)kBarrier_InOrder柵欄,創(chuàng)建一個(gè)新Chunk。

父視圖LinearLayout繪制時(shí)Chunk與記錄.jpg

葉子節(jié)點(diǎn)視圖繪制

葉子節(jié)點(diǎn)繪制主要關(guān)注onDraw方法,CanvasView的onDraw方法,繪制三個(gè)矩形區(qū)域。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.parseColor("#ffffcc")); //畫布顏色
    Paint paint = new Paint();
    paint.setStyle(Paint.Style.STROKE);//設(shè)置非填充風(fēng)格
    Rect rect1 = new Rect(20, 20, 180, 180); 
    canvas.drawRect(rect1, paint);//繪制一個(gè)正方形
    /************************第一層*****************************/
    canvas.save();//當(dāng)前狀態(tài)保存
    /***********************第二層*****************************/
    canvas.translate(180, 180);
    Rect rect2 = new Rect(20, 20, 180, 180); 
    canvas.drawRect(rect2, paint);//再次繪制一個(gè)正方形
    /************************第二層***************************/
    canvas.restore();//恢復(fù)到第一層狀態(tài)
    /************************第一層**************************/
    Rect rect3 = new Rect(20, 200, 180, 360); 
    canvas.drawRect(rect3, paint);//再次繪制一個(gè)正方形
}

四個(gè)Canvas方法

drawRect,繪制一個(gè)矩形區(qū)域。
save,保存當(dāng)前圖層狀態(tài)。
restore,恢復(fù)上一個(gè)圖層狀態(tài)。
translate,兩個(gè)方向x和y,偏移一定距離。

先繪制一個(gè)矩形,然后保存狀態(tài),進(jìn)行偏移,繪制第二個(gè)矩形,第二個(gè)矩形受到偏移影響,恢復(fù)狀態(tài),繪制第三個(gè)矩形,第三個(gè)矩形不受偏移影響。

詳細(xì)分析

執(zhí)行順序
drawRect(1)
save
translate
drawRect(2)
restore
drawRect(3)

1,drawRect:觸發(fā)底層DisplayListCanvas的drawRect方法。該方法是
在DisplayListCanvas的頭文件中定義虛函數(shù),在Canvas的頭文件中也有定義。
底層 DisplayListCanvas#drawRect方法。

void DisplayListCanvas::drawRect(float left, float top, float right, float bottom,
        const SkPaint& paint) {
    addDrawOp(new (alloc()) DrawRectOp(left, top, right, bottom, refPaint(&paint)));
}

addDrawOp增加一個(gè)繪制Op,DrawRectOp類型,不同的繪制Op不同,如其drawPoints繪制增加的Op是DrawPointsOp類型,總之,他們都繼承DrawOp類。

addDrawOp增加一個(gè)繪制操作DrawOp。DrawOp繼承DisplayListOp類。

底層DisplayListCanvas#addDrawOp方法。

size_t DisplayListCanvas::addDrawOp(DrawOp* op) {
    Rect localBounds;
    ...
    mDisplayListData->hasDrawOps = true;
    return flushAndAddOp(op);
}

觸發(fā)flushAndAddOp方法。
2,save:觸發(fā)底層DisplayListCanvas的save方法。注意:save和restore一般是成對(duì)出現(xiàn)的。
底層DisplayListCanvas#save方法。

int DisplayListCanvas::save(SkCanvas::SaveFlags flags) {
    addStateOp(new (alloc()) SaveOp((int) flags));
    return mState.save((int) flags);
}

addStateOp增加狀態(tài)操作StateOp,前面的是增加DrawOp,而現(xiàn)在是StateOp

和DrawOp一樣,狀態(tài)StateOp也繼承DisplayListOp類。

底層DisplayListCanvas#addStateOp方法。

size_t DisplayListCanvas::addStateOp(StateOp* op) {
    return flushAndAddOp(op);
}

觸發(fā)flushAndAddOp方法。
除了flushAndAddOp保存,還會(huì)觸發(fā)CanvasState保存。

總結(jié):
DisplayListCanvas的save方法,addStateOp增加一個(gè)SaveOp,addStateOp方法和addDrawOp類似,都觸發(fā)flushAndAddOp方法,入?yún)⑹峭粋€(gè)基類DisplayListOp。

CanvasState#save方法。

int CanvasState::save(int flags) {
    return saveSnapshot(flags);
}

int CanvasState::saveSnapshot(int flags) {
    mSnapshot = new Snapshot(mSnapshot, flags);
    return mSaveCount++;
}

在CanvasState類save操作中,創(chuàng)建一個(gè)Snapshot對(duì)象,封裝當(dāng)前對(duì)象,新建的Snapshot放到鏈表最前面,mSaveCount自增,mSaveCount代表當(dāng)前操作的是位于第幾層次,若上層調(diào)用了restore,則觸發(fā)CanvasState的restore方法,回退到上一層。

可以看出,每一個(gè)層次狀態(tài)的save都保存在Snapshot中。save之后的改變?cè)谛陆ǖ腟napshot保存,restore后回到之前的Snapshot。

3,translate:觸發(fā)底層DisplayListCanvas的translate方法,x和y軸方向上的偏移。
底層DisplayListCanvas#translate方法

void DisplayListCanvas::translate(float dx, float dy) {
    if (dx == 0.0f && dy == 0.0f) return;

    mHasDeferredTranslate = true;
    mTranslateX += dx;
    mTranslateY += dy;
    flushRestoreToCount();
    mState.translate(dx, dy, 0.0f);
}

增加偏移量dx和dy。
flushRestoreToCount方法,在mRestoreSaveCount>=0時(shí)才起作用,即執(zhí)行過restore或restoreToCount。
寫入一個(gè)RestoreToCountOp操作。
CanvasState的translate方法,當(dāng)前mSnapshot的transform觸發(fā)translate偏移。因前面觸發(fā)過一個(gè)save方法,因此有兩個(gè)Snapshot,當(dāng)前mSnapshot是表頭第一個(gè)。

4,restore:觸發(fā)底層DisplayListCanvas的restore方法,恢復(fù)到上一個(gè)繪制狀態(tài)。
底層DisplayListCanvas#restore方法。

void DisplayListCanvas::restore() {
    if (mRestoreSaveCount < 0) {
        restoreToCount(getSaveCount() - 1);
        return;
    }

    mRestoreSaveCount--;
    flushTranslate();
    mState.restore();
}

若mRestoreSaveCount已經(jīng)>=0,則直接自減。每次restore只能回退一層。同時(shí)CanvasState回退,內(nèi)部mSaveCount保存層級(jí)。
若mRestoreSaveCount小于0,即有可能是初始值-1,則觸發(fā)restoreToCount方法,getSaveCount獲取當(dāng)前層級(jí)。若執(zhí)行過一個(gè)save,restore時(shí),getSaveCount得到2,那么restoreToCount恢復(fù)的值就是1,mRestoreSaveCount設(shè)置成1。

總結(jié):restore回退一層。

上層直接調(diào)用restoreToCount方法時(shí),直接回退saveCount層,這個(gè)方法中會(huì)設(shè)置mRestoreSaveCount值。

底層DisplayListCanvas#restoreToCount方法。

void DisplayListCanvas::restoreToCount(int saveCount) {
    mRestoreSaveCount = saveCount;
    flushTranslate();
    mState.restoreToCount(saveCount);
}

save和restore成對(duì)出現(xiàn),多個(gè)save后,第一次restore時(shí),mRestoreSaveCount將會(huì)是初始值-1,restoreToCount方法會(huì)第一次設(shè)置其值,設(shè)置成restore回退后的層級(jí)。
即mRestoreSaveCount存儲(chǔ)當(dāng)前層級(jí)。

例如,3個(gè)save后,第一次restore后,則mRestoreSaveCount變?yōu)?。

總結(jié):
在上層一次save,將當(dāng)前的狀態(tài)保存下來,包括偏移,縮放等狀態(tài),后續(xù)的改變?nèi)匀辉诖嘶A(chǔ)上,偏移,繪制等,若執(zhí)行restore,則回到上一個(gè)save保存的狀態(tài)中,中間的改變忽略掉。

硬件渲染節(jié)點(diǎn)繪制寫入流程.jpg

1:drawRect,繪制正方形,第一層,寫入DrawRectOp,mRestoreSaveCount是-1,flushRestoreCount無作用,無偏移,flushTranslate無作用。
addOpAndUpdateChunk寫入繪制。
2:save,保存當(dāng)前層級(jí)狀態(tài),mRestoreSaveCount是-1,flushRestoreCount無作用,無偏移,flushTranslate無作用。
addOpAndUpdateChunk寫入狀態(tài)。
3:translate,偏移,第二層,mRestoreSaveCount是-1,flushRestoreCount無作用,設(shè)置偏移值mTranslateX與mTranslateY,當(dāng)前Snapshot(第二層)偏移。
4:drawRect,繪制正方形,第二層,寫入DrawRectOp,mRestoreSaveCount是-1,flushRestoreCount無作用,有偏移,
addOpAndUpdateChunk寫入TranslateOp,并恢復(fù)偏移值。
addOpAndUpdateChunk寫入繪制。

5:restore,恢復(fù)上一個(gè)層級(jí),前面僅有一個(gè)save,因此mRestoreSaveCount是-1,觸發(fā)restoreToCount方法,設(shè)置mRestoreSaveCount=1。無偏移,flushTranslate無作用。
6:drawRect,繪制正方形,第一層,寫入DrawRectOp,mRestoreSaveCount是1,無偏移,flushTranslate無作用,
addOpAndUpdateChunk寫入RestoreToCountOp。并恢復(fù)mRestoreSaveCount為-1。
addOpAndUpdateChunk寫入繪制。

flushAndAddOp方法在上層寫入DrawOp或StateOp時(shí)觸發(fā)。
最終目的是addOpAndUpdateChunk。
當(dāng)前存在mRestoreSaveCount>=0或偏移,首先寫入相應(yīng)Op,即RestoreToCountOp與TranslateOp。

底層DisplayListCanvas#flushAndAddOp方法。

size_t DisplayListCanvas::flushAndAddOp(DisplayListOp* op) { //
    flushRestoreToCount();
    flushTranslate();
    return addOpAndUpdateChunk(op);
}

三個(gè)方法。
flushRestoreToCount:寫入RestoreToCountOp。
flushTranslate:寫入TranslateOp。
addOpAndUpdateChunk:執(zhí)行寫入。

DisplayListCanvas#flushRestoreToCount方法。

void DisplayListCanvas::flushRestoreToCount() {
    if (mRestoreSaveCount >= 0) {
        addOpAndUpdateChunk(new (alloc()) RestoreToCountOp(mRestoreSaveCount));
        mRestoreSaveCount = -1;
    }
}

若mRestoreSaveCount>=0,增加一個(gè)RestoreToCountOp操作。mRestoreSaveCount在剛初始化時(shí)prepareDirty,設(shè)置成-1。
DisplayListCanvas#flushTranslate方法。

void DisplayListCanvas::flushTranslate() {
    if (mHasDeferredTranslate) {
        if (mTranslateX != 0.0f || mTranslateY != 0.0f) {
            addOpAndUpdateChunk(new (alloc()) TranslateOp(mTranslateX, mTranslateY));
            mTranslateX = mTranslateY = 0.0f;
        }
        mHasDeferredTranslate = false;
    }
}

mTranslateX或mTranslateY不是0時(shí)寫入TranslateOp。

addOpAndUpdateChunk方法負(fù)責(zé)增加Op,不僅包括RestoreToCountOp和TranslateOp。

DisplayListCanvas#addOpAndUpdateChunk方法。

size_t DisplayListCanvas::addOpAndUpdateChunk(DisplayListOp* op) {//增加op,更新chunk
    int insertIndex = mDisplayListData->displayListOps.add(op);
    if (mDeferredBarrierType != kBarrier_None) {
        // op is first in new chunk
        mDisplayListData->chunks.push();
        DisplayListData::Chunk& newChunk = mDisplayListData->chunks.editTop();//新Chunk
        newChunk.beginOpIndex = insertIndex;
        newChunk.endOpIndex = insertIndex + 1;
        newChunk.reorderChildren = (mDeferredBarrierType == kBarrier_OutOfOrder);

        int nextChildIndex = mDisplayListData->children().size();
        newChunk.beginChildIndex = newChunk.endChildIndex = nextChildIndex;
        mDeferredBarrierType = kBarrier_None;
    } else {
        // standard case - append to existing chunk
        mDisplayListData->chunks.editTop().endOpIndex = insertIndex + 1;//最上面Chunk修改
    }
    return insertIndex;
}

將操作DisplayListOp加入DisplayListData的內(nèi)部數(shù)組,類型全部都是DisplayListOp,返回插入的索引。
DisplayListData在底層Canvas的prepareDirty時(shí)創(chuàng)建,不管是繪制操作還是狀態(tài)操作,都屬于DisplayListOp類型,按照順序放到相同數(shù)組。

上述例子中DisplayListData內(nèi)部數(shù)組的內(nèi)容如下圖所示。
DisplayListData內(nèi)部數(shù)組寫入的操作.jpg

依次是DrawRectOp,SaveOp,TranslateOp,DrawRectOp,RestoreToCountOp,DrawRectOp。


任重而道遠(yuǎn)

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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