使用 ASDK 性能調(diào)優(yōu) - 提升 iOS 界面的渲染性能

這一系列的文章會(huì)從幾個(gè)方面對(duì) ASDK 在性能調(diào)優(yōu)方面策略的實(shí)現(xiàn)進(jìn)行分析,幫助讀者理解 ASDK 如何做到使復(fù)雜的 UI 界面達(dá)到 60 FPS 的刷新頻率的;本篇文章會(huì)從視圖的渲染層面講解 ASDK 對(duì)于渲染過(guò)程的優(yōu)化并對(duì) ASDK 進(jìn)行概述。

在客戶端或者前端開(kāi)發(fā)中,對(duì)于性能的優(yōu)化,尤其是 UI,往往都不是最先考慮的問(wèn)題。

因?yàn)樵诖蠖鄶?shù)場(chǎng)景下,使用更加復(fù)雜的高性能代碼替代可用的代碼經(jīng)常會(huì)導(dǎo)致代碼的可維護(hù)性下降,所以更需要我們開(kāi)發(fā)者對(duì)優(yōu)化的時(shí)間點(diǎn)以及原因有一個(gè)比較清楚的認(rèn)識(shí),避免過(guò)度優(yōu)化帶來(lái)的問(wèn)題。

對(duì) iOS 開(kāi)發(fā)比較熟悉的開(kāi)發(fā)者都知道,iOS 中的性能問(wèn)題大多是阻塞主線程導(dǎo)致用戶的交互反饋出現(xiàn)可以感知的延遲。

scrollview-demo

詳細(xì)說(shuō)起來(lái),大體有三種原因:

  1. UI 渲染需要時(shí)間較長(zhǎng),無(wú)法按時(shí)提交結(jié)果;
  2. 一些需要密集計(jì)算的處理放在了主線程中執(zhí)行,導(dǎo)致主線程被阻塞,無(wú)法渲染 UI 界面;
  3. 網(wǎng)絡(luò)請(qǐng)求由于網(wǎng)絡(luò)狀態(tài)的問(wèn)題響應(yīng)較慢,UI 層由于沒(méi)有模型返回?zé)o法渲染。

上面的這些問(wèn)題都會(huì)影響應(yīng)用的性能,最常見(jiàn)的表現(xiàn)就是 UITableView 在滑動(dòng)時(shí)沒(méi)有達(dá)到 60 FPS,用戶能感受到明顯的卡頓。

屏幕的渲染

相信點(diǎn)開(kāi)這篇文章的大多數(shù)開(kāi)發(fā)者都知道 FPS 是什么,那么如果才能優(yōu)化我們的 App 使其達(dá)到 60 FPS 呢?在具體了解方法之前,我們先退一步,提出另一個(gè)問(wèn)題,屏幕是如何渲染的?

對(duì)于第一個(gè)問(wèn)題,可能需要幾篇文章來(lái)回答,希望整個(gè)系列的文章能給你一個(gè)滿意的答案。3

CRT 和 LCD

屏幕的渲染可能要從 CRT(Cathode ray tube) 顯示器LCD(Liquid-crystal display) 顯示器講起。

CRT

CRT 顯示器是比較古老的技術(shù),它使用陰極電子槍發(fā)射電子,在陰極高壓的作用下,電子由電子槍射向熒光屏,使熒光粉發(fā)光,將圖像顯示在屏幕上,這也就是用磁鐵靠近一些老式電視機(jī)的屏幕會(huì)讓它們變色的原因。

而 FPS 就是 CRT 顯示器的刷新頻率,電子槍每秒會(huì)對(duì)顯示器上內(nèi)容進(jìn)行 60 - 100 次的刷新,哪怕在我們看來(lái)沒(méi)有任何改變。

lcd

但是 LCD 的原理與 CRT 非常不同,LCD 的成像原理跟光學(xué)有關(guān):

  • 在不加電壓下,光線會(huì)沿著液晶分子的間隙前進(jìn)旋轉(zhuǎn) 90°,所以光可以通過(guò);
  • 在加入電壓之后,光沿著液晶分子的間隙直線前進(jìn),被濾光板擋住。

如果你可以翻墻,相信下面的視頻會(huì)更好得幫助你理解 LCD 的工作原理:

<a >

</a>

LCD 的成像原理雖然與 CRT 截然不同,每一個(gè)像素的顏色可以在需要改變時(shí)才去改變電壓,也就是不需要刷新頻率,但是由于一些歷史原因,LCD 仍然需要按照一定的刷新頻率向 GPU 獲取新的圖像用于顯示。

屏幕撕裂

但是顯示器只是用于將圖像顯示在屏幕上,誰(shuí)又是圖像的提供者呢?圖像都是我們經(jīng)常說(shuō)的 GPU 提供的。

而這導(dǎo)致了另一個(gè)問(wèn)題,由于 GPU 生成圖像的頻率與顯示器刷新的頻率是不相關(guān)的,那么在顯示器刷新時(shí),GPU 沒(méi)有準(zhǔn)備好需要顯示的圖像怎么辦;或者 GPU 的渲染速度過(guò)快,顯示器來(lái)不及刷新,GPU 就已經(jīng)開(kāi)始渲染下一幀圖像又該如何處理?

screen-tearing

如果解決不了這兩個(gè)問(wèn)題,就會(huì)出現(xiàn)上圖中的屏幕撕裂(Screen Tearing)現(xiàn)象,屏幕中一部分顯示的是上一幀的內(nèi)容,另一部分顯示的是下一幀的內(nèi)容。

我們用兩個(gè)例子來(lái)說(shuō)明可能出現(xiàn)屏幕撕裂的兩種情況:

  • 如果顯示器的刷新頻率為 75 Hz,GPU 的渲染速度為 100 Hz,那么在兩次屏幕刷新的間隔中,GPU 會(huì)渲染 4/3 個(gè)幀,后面的 1/3 幀會(huì)覆蓋已經(jīng)渲染好的幀棧,最終會(huì)導(dǎo)致屏幕在 1/3 或者 2/3 的位置出現(xiàn)屏幕撕裂效果;
  • 那么 GPU 的渲染速度小于顯示器呢,比如說(shuō) 50 Hz,那么在兩次屏幕刷新的間隔中,GPU 只會(huì)渲染 2/3 幀,剩下的 1/3 會(huì)來(lái)自上一幀,與上面的結(jié)果完全相同,在同樣的位置出現(xiàn)撕裂效果。

到這里,有人會(huì)說(shuō),如果顯示器的刷新頻率與 GPU 的渲染速度完全相同,應(yīng)該就會(huì)解決屏幕撕裂的問(wèn)題了吧?其實(shí)并不是。顯示器從 GPU 拷貝幀的過(guò)程依然需要消耗一定的時(shí)間,如果屏幕在拷貝圖像時(shí)刷新,仍然會(huì)導(dǎo)致屏幕撕裂問(wèn)題。

how-to-solve-tearing-proble

引入多個(gè)緩沖區(qū)可以有效地緩解屏幕撕裂,也就是同時(shí)使用一個(gè)幀緩沖區(qū)(frame buffer)和多個(gè)后備緩沖區(qū)(back buffer);在每次顯示器請(qǐng)求內(nèi)容時(shí),都會(huì)從幀緩沖區(qū)中取出圖像然后渲染。

雖然緩沖區(qū)可以減緩這些問(wèn)題,但是卻不能解決;如果后備緩沖區(qū)繪制完成,而幀緩沖區(qū)的圖像沒(méi)有被渲染,后備緩沖區(qū)中的圖像就會(huì)覆蓋幀緩沖區(qū),仍然會(huì)導(dǎo)致屏幕撕裂。

解決這個(gè)問(wèn)題需要另一個(gè)機(jī)制的幫助,也就是垂直同步(Vertical synchronization),簡(jiǎn)稱 V-Sync 來(lái)解決。

V-Sync

V-Sync 的主要作用就是保證只有在幀緩沖區(qū)中的圖像被渲染之后,后備緩沖區(qū)中的內(nèi)容才可以被拷貝到幀緩沖區(qū)中,理想情況下的 V-Sync 會(huì)按這種方式工作:

normal-vsyn

每次 V-Sync 發(fā)生時(shí),CPU 以及 GPU 都已經(jīng)完成了對(duì)圖像的處理以及繪制,顯示器可以直接拿到緩沖區(qū)中的幀。但是,如果 CPU 或者 GPU 的處理需要的時(shí)間較長(zhǎng),就會(huì)發(fā)生掉幀的問(wèn)題:

lag-vsyn

在 V-Sync 信號(hào)發(fā)出時(shí),CPU 和 GPU 并沒(méi)有準(zhǔn)備好需要渲染的幀,顯示器就會(huì)繼續(xù)使用當(dāng)前幀,這就加劇了屏幕的顯示問(wèn)題,而每秒顯示的幀數(shù)會(huì)少于 60。

由于會(huì)發(fā)生很多次掉幀,在開(kāi)啟了 V-Sync 后,40 ~ 50 FPS 的渲染頻率意味著顯示器輸出的畫面幀率會(huì)從 60 FPS 急劇下降到 30 FPS,原因在這里不會(huì)解釋,讀者可以自行思考。

其實(shí)到這里關(guān)于屏幕渲染的內(nèi)容就已經(jīng)差不多結(jié)束了,根據(jù) V-Sync 的原理,優(yōu)化應(yīng)用性能、提高 App 的 FPS 就可以從兩個(gè)方面來(lái)入手,優(yōu)化 CPU 以及 GPU 的處理時(shí)間。

讀者也可以從 iOS 保持界面流暢的技巧這篇文章中了解更多的相關(guān)內(nèi)容。

性能調(diào)優(yōu)的策略

CPU 和 GPU 在每次 V-Sync 時(shí)間點(diǎn)到達(dá)之前都在干什么?如果,我們知道了它們各自負(fù)責(zé)的工作,通過(guò)優(yōu)化代碼就可以提升性能。

cpu-gpu

很多 CPU 的操作都會(huì)延遲 GPU 開(kāi)始渲染的時(shí)間:

  • 布局的計(jì)算 - 如果你的視圖層級(jí)太過(guò)于復(fù)雜,或者視圖需要重復(fù)多次進(jìn)行布局,尤其是在使用 Auto Layout 進(jìn)行自動(dòng)布局時(shí),對(duì)性能影響尤為嚴(yán)重;
  • 視圖的惰性加載 - 在 iOS 中只有當(dāng)視圖控制器的視圖顯示到屏幕時(shí)才會(huì)加載;
  • 解壓圖片 - iOS 通常會(huì)在真正繪制時(shí)才會(huì)解碼圖片,對(duì)于一個(gè)較大的圖片,無(wú)論是直接或間接使用 UIImageView 或者繪制到 Core Graphics 中,都需要對(duì)圖片進(jìn)行解壓;
  • ...

寬泛的說(shuō),大多數(shù)的 CALayer 的屬性都是由 GPU 來(lái)繪制的,比如圖片的圓角、變換、應(yīng)用紋理;但是過(guò)多的幾何結(jié)構(gòu)、重繪、離屏繪制(Offscrren)以及過(guò)大的圖片都會(huì)導(dǎo)致 GPU 的性能明顯降低。

上面的內(nèi)容出自 CPU vs GPU · iOS 核心動(dòng)畫高級(jí)技巧,你可以在上述文章中對(duì) CPU 和 GPU 到底各自做了什么有一個(gè)更深的了解。

也就是說(shuō),如果我們解決了上述問(wèn)題,就能加快應(yīng)用的渲染速度,大大提升用戶體驗(yàn)。

AsyncDisplayKit

文章的前半部分已經(jīng)從屏幕的渲染原理講到了性能調(diào)優(yōu)的幾個(gè)策略;而 AsyncDisplayKit 就根據(jù)上述的策略幫助我們對(duì)應(yīng)用性能進(jìn)行優(yōu)化。

asdk-logo

AsyncDisplayKit(以下簡(jiǎn)稱 ASDK)是由 Facebook 開(kāi)源的一個(gè) iOS 框架,能夠幫助最復(fù)雜的 UI 界面保持流暢和快速響應(yīng)。

ASDK 從開(kāi)發(fā)到開(kāi)源大約經(jīng)歷了一年多的時(shí)間,它其實(shí)并不是一個(gè)簡(jiǎn)單的框架它是一個(gè)復(fù)雜的框架,更像是對(duì) UIKit 的重新實(shí)現(xiàn),把整個(gè) UIKit 以及 CALayer 層封裝成一個(gè)一個(gè) Node將昂貴的渲染、圖片解碼、布局以及其它 UI 操作移出主線程,這樣主線程就可以對(duì)用戶的操作及時(shí)做出反應(yīng)。

很多分析 ASDK 的文章都會(huì)有這么一張圖介紹框架中的最基本概念:

asdk-hierarchy

在 ASDK 中最基本的單位就是 ASDisplayNode,每一個(gè) node 都是對(duì) UIView 以及 CALayer 的抽象。但是與 UIView 不同的是,ASDisplayNode 是線程安全的,它可以在后臺(tái)線程中完成初始化以及配置工作。

如果按照 60 FPS 的刷新頻率來(lái)計(jì)算,每一幀的渲染時(shí)間只有 16ms,在 16ms 的時(shí)間內(nèi)要完成對(duì) UIView 的創(chuàng)建、布局、繪制以及渲染,CPU 和 GPU 面臨著巨大的壓力。

apple-a9

但是從 A5 處理器之后,多核的設(shè)備成為了主流,原有的將所有操作放入主線程的實(shí)踐已經(jīng)不能適應(yīng)復(fù)雜的 UI 界面,所以 ASDK 將耗時(shí)的 CPU 操作以及 GPU 渲染紋理(Texture)的過(guò)程全部放入后臺(tái)進(jìn)程,使主線程能夠快速響應(yīng)用戶操作

ASDK 通過(guò)獨(dú)特的渲染技巧、代替 AutoLayout 的布局系統(tǒng)、智能的預(yù)加載方式等模塊來(lái)實(shí)現(xiàn)對(duì) App 性能的優(yōu)化。

ASDK 的渲染過(guò)程

ASDK 中到底使用了哪些方法來(lái)對(duì)視圖進(jìn)行渲染呢?本文主要會(huì)從渲染的過(guò)程開(kāi)始分析,了解 ASDK 底層如何提升界面的渲染性能。

在 ASDK 中的渲染圍繞 ASDisplayNode 進(jìn)行,其過(guò)程總共有四條主線:

  • 初始化 ASDisplayNode 對(duì)應(yīng)的 UIView 或者 CALayer
  • 在當(dāng)前視圖進(jìn)入視圖層級(jí)時(shí)執(zhí)行 setNeedsDisplay
  • display 方法執(zhí)行時(shí),向后臺(tái)線程派發(fā)繪制事務(wù);
  • 注冊(cè)成為 RunLoop 觀察者,在每個(gè) RunLoop 結(jié)束時(shí)回調(diào)。

UIView 和 CALayer 的加載

當(dāng)我們運(yùn)行某一個(gè)使用 ASDK 的工程時(shí),-[ASDisplayNode _loadViewOrLayerIsLayerBacked:] 總是 ASDK 中最先被調(diào)用的方法,而這個(gè)方法執(zhí)行的原因往往就是 ASDisplayNode 對(duì)應(yīng)的 UIViewCALayer 被引用了:

- (CALayer *)layer {
    if (!_layer) {
        ASDisplayNodeAssertMainThread();

        if (!_flags.layerBacked) return self.view.layer;
        [self _loadViewOrLayerIsLayerBacked:YES];
    }
    return _layer;
}

- (UIView *)view {
    if (_flags.layerBacked) return nil;
    if (!_view) {
        ASDisplayNodeAssertMainThread();
        [self _loadViewOrLayerIsLayerBacked:NO];
    }
    return _view;
}

這里涉及到一個(gè) ASDK 中比較重要的概念,如果 ASDisplayNodelayerBacked 的,它不會(huì)渲染對(duì)應(yīng)的 UIView 以此來(lái)提升性能:

- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked {
    if (isLayerBacked) {
        _layer = [self _layerToLoad];
        _layer.delegate = (id<CALayerDelegate>)self;
    } else {
        _view = [self _viewToLoad];
        _view.asyncdisplaykit_node = self;
        _layer = _view.layer;
    }
    _layer.asyncdisplaykit_node = self;

    self.asyncLayer.asyncDelegate = self;
}

因?yàn)?UIViewCALayer 雖然都可以用于展示內(nèi)容,不過(guò)由于 UIView 可以用于處理用戶的交互,所以如果不需要使用 UIView 的特性,直接使用 CALayer 進(jìn)行渲染,能夠節(jié)省大量的渲染時(shí)間。

如果你使用 Xcode 查看過(guò)視圖的層級(jí),那么你應(yīng)該知道,UIView 在 Debug View Hierarchy 中是有層級(jí)的;而 CALayer 并沒(méi)有,它門的顯示都在一個(gè)平面上。

上述方法中的 -[ASDisplayNode _layerToLoad] 以及 [ASDisplayNode _viewToLoad] 都只會(huì)根據(jù)當(dāng)前節(jié)點(diǎn)的 layerClass 或者 viewClass 初始化一個(gè)對(duì)象。

Layer Trees vs. Flat Drawing – Graphics Performance Across iOS Device Generations 這篇文章比較了 UIViewCALayer 的渲染時(shí)間。

view-layer-cg-compare

-[ASDisplayNode asyncLayer] 只是對(duì)當(dāng)前 node 持有的 layer 進(jìn)行封裝,確保會(huì)返回一個(gè) _ASDisplayLayer 的實(shí)例:

- (_ASDisplayLayer *)asyncLayer {
    ASDN::MutexLocker l(_propertyLock);
    return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil;
}

最重要的是 -[ASDisplayNode _loadViewOrLayerIsLayerBacked:] 方法會(huì)將當(dāng)前節(jié)點(diǎn)設(shè)置為 asyncLayer 的代理,在后面會(huì)使用 ASDisplayNodeCALayer 渲染內(nèi)容。

視圖層級(jí)

在初始化工作完成之后,當(dāng) ASDisplayNode 第一次被加入到視圖的層級(jí)時(shí),-[_ASDisplayView willMoveToWindow:] 就會(huì)被調(diào)用。

_ASDisplayView 和 _ASDisplayLayer

_ASDisplayView_ASDisplayLayer 都是私有類,它們之間的對(duì)應(yīng)關(guān)系其實(shí)和 UIViewCALayer 完全相同。

+ (Class)layerClass {
    return [_ASDisplayLayer class];
}

_ASDisplayView 覆寫了很多跟視圖層級(jí)改變有關(guān)的方法:

  • -[_ASDisplayView willMoveToWindow:]
  • -[_ASDisplayView didMoveToWindow]
  • -[_ASDisplayView willMoveToSuperview:]
  • -[_ASDisplayView didMoveToSuperview]

它們用于在視圖的層級(jí)改變時(shí),通知對(duì)應(yīng) ASDisplayNode 作出相應(yīng)的反應(yīng),比如 -[_ASDisplayView willMoveToWindow:] 方法會(huì)在視圖被加入層級(jí)時(shí)調(diào)用:

- (void)willMoveToWindow:(UIWindow *)newWindow {
    BOOL visible = (newWindow != nil);
    if (visible && !_node.inHierarchy) {
        [_node __enterHierarchy];
    }
}

setNeedsDisplay

當(dāng)前視圖如果不在視圖層級(jí)中,就會(huì)通過(guò) _node 的實(shí)例方法 -[ASDisplayNode __enterHierarchy] 加入視圖層級(jí):

- (void)__enterHierarchy {
    if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {
        _flags.isEnteringHierarchy = YES;
        _flags.isInHierarchy = YES;

        if (_flags.shouldRasterizeDescendants) {
            [self _recursiveWillEnterHierarchy];
        } else {
            [self willEnterHierarchy];
        }
        _flags.isEnteringHierarchy = NO;

        # 更新 layer 顯示的內(nèi)容
    }
}
        

_flagsASDisplayNodeFlags 結(jié)構(gòu)體,用于標(biāo)記當(dāng)前 ASDisplayNode 的一些 BOOL 值,比如,異步顯示、柵格化子視圖等等,你不需要知道都有什么,根據(jù)這些值的字面意思理解就已經(jīng)足夠了。

上述方法的前半部分只是對(duì) _flags 的標(biāo)記,如果需要將當(dāng)前視圖的子視圖柵格化,也就是將它的全部子視圖與當(dāng)前視圖壓縮成一個(gè)圖層,就會(huì)向這些視圖遞歸地調(diào)用 -[ASDisplayNode willEnterHierarchy] 方法通知目前的狀態(tài):

- (void)_recursiveWillEnterHierarchy {
  _flags.isEnteringHierarchy = YES;
  [self willEnterHierarchy];
  _flags.isEnteringHierarchy = NO;

  for (ASDisplayNode *subnode in self.subnodes) {
    [subnode _recursiveWillEnterHierarchy];
  }
}

-[ASDisplayNode willEnterHierarchy] 會(huì)修改當(dāng)前節(jié)點(diǎn)的 interfaceStateASInterfaceStateInHierarchy,表示當(dāng)前節(jié)點(diǎn)不包含在 cell 或者其它,但是在 window 中。

- (void)willEnterHierarchy {
  if (![self supportsRangeManagedInterfaceState]) {
    self.interfaceState = ASInterfaceStateInHierarchy;
  }
}

當(dāng)前結(jié)點(diǎn)需要被顯示在屏幕上時(shí),如果其內(nèi)容 contents 為空,就會(huì)調(diào)用 -[CALayer setNeedsDisplay] 方法將 CALayer 標(biāo)記為臟的,通知系統(tǒng)需要在下一個(gè)繪制循環(huán)中重繪視圖:

- (void)__enterHierarchy {
     if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) {

        # 標(biāo)記節(jié)點(diǎn)的 flag
        
        if (self.contents == nil) {
            CALayer *layer = self.layer;
            [layer setNeedsDisplay];

            if ([self _shouldHavePlaceholderLayer]) {
                [CATransaction begin];
                [CATransaction setDisableActions:YES];
                [self _setupPlaceholderLayerIfNeeded];
                _placeholderLayer.opacity = 1.0;
                [CATransaction commit];
                [layer addSublayer:_placeholderLayer];
            }
        }
    }
}

在將 CALayer 標(biāo)記為 dirty 之后,在繪制循環(huán)中就會(huì)執(zhí)行 -[CALayer display] 方法,對(duì)它要展示的內(nèi)容進(jìn)行繪制;如果當(dāng)前視圖需要一些占位圖,那么就會(huì)在這里的代碼中,為當(dāng)前 node 對(duì)應(yīng)的 layer 添加合適顏色的占位層。

placeholder-laye

派發(fā)異步繪制事務(wù)

在上一節(jié)中調(diào)用 -[CALayer setNeedsDisplay] 方法將當(dāng)前節(jié)點(diǎn)標(biāo)記為 dirty 之后,在下一個(gè)繪制循環(huán)時(shí)就會(huì)對(duì)所有需要重繪的 CALayer 執(zhí)行 -[CALayer display],這也是這一小節(jié)需要分析的方法的入口:

- (void)display {
  [self _hackResetNeedsDisplay];

  ASDisplayNodeAssertMainThread();
  if (self.isDisplaySuspended) return;

  [self display:self.displaysAsynchronously];
}

這一方法的調(diào)用棧比較復(fù)雜,在具體分析之前,筆者會(huì)先給出這個(gè)方法的調(diào)用棧,給讀者一個(gè)關(guān)于該方法實(shí)現(xiàn)的簡(jiǎn)要印象:

-[_ASDisplayLayer display]
    -[_ASDisplayLayer display:] // 將繪制工作交給 ASDisplayNode 處理
        -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:]
            -[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:]
                -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:]            
            -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_parentTransactionContainer]
            -[CALayer(ASDisplayNodeAsyncTransactionContainer) asyncdisplaykit_asyncTransaction]
                -[_ASAsyncTransaction initWithCallbackQueue:completionBlock:]
                -[_ASAsyncTransactionGroup addTransactionContainer:]
            -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:]
                ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
                    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

-[_ASDisplayLayer display] 在調(diào)用棧中其實(shí)會(huì)創(chuàng)建一個(gè) displayBlock,它其實(shí)是一個(gè)使用 Core Graphics 進(jìn)行圖像繪制的過(guò)程,整個(gè)繪制過(guò)程是通過(guò)事務(wù)的形式進(jìn)行管理的;而 displayBlock 會(huì)被 GCD 分發(fā)到后臺(tái)的并發(fā)進(jìn)程來(lái)處理。

調(diào)用棧中的第二個(gè)方法 -[_ASDisplayLayer display] 會(huì)將異步繪制的工作交給自己的 asyncDelegate,也就是第一部分中設(shè)置的 ASDisplayNode

- (void)display:(BOOL)asynchronously {
  [_asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}

ASDisplayNode(AsyncDisplay)

這里省略了一部分 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:] 方法的實(shí)現(xiàn):

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
  ASDisplayNodeAssertMainThread();

  ...

  asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
  
  if (!displayBlock) return;
  
  asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
    ASDisplayNodeCAssertMainThread();
    if (!canceled && !isCancelledBlock()) {
      UIImage *image = (UIImage *)value;
      _layer.contentsScale = self.contentsScale;
      _layer.contents = (id)image.CGImage;
    }
  };

  if (asynchronously) {
    CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
    _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
    [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
  } else {
    UIImage *contents = (UIImage *)displayBlock();
    completionBlock(contents, NO);
  }
}

省略后的代碼脈絡(luò)非常清晰,-[ASDisplayNode(AsyncDisplay) _displayBlockWithAsynchronous:isCancelledBlock:rasterizing:] 返回一個(gè)用于 displayBlock,然后構(gòu)造一個(gè) completionBlock,在繪制結(jié)束時(shí)執(zhí)行,在主線程中設(shè)置當(dāng)前 layer 的內(nèi)容。

如果當(dāng)前的渲染是異步的,就會(huì)將 displayBlock 包裝成一個(gè)事務(wù),添加到隊(duì)列中執(zhí)行,否則就會(huì)同步執(zhí)行當(dāng)前的 block,并執(zhí)行 completionBlock 回調(diào),通知 layer 更新顯示的內(nèi)容。

同步顯示的部分到這里已經(jīng)很清楚了,我們更關(guān)心的其實(shí)還是異步繪制的部分,因?yàn)檫@部分才是 ASDK 提升效率的關(guān)鍵;而這就要從獲取 displayBlock 的方法開(kāi)始了解了。

displayBlock 的構(gòu)建

displayBlock 的創(chuàng)建一般分為三種不同的方式:

  1. 將當(dāng)前視圖的子視圖壓縮成一層繪制在當(dāng)前頁(yè)面上
  2. 使用 - displayWithParameters:isCancelled: 返回一個(gè) UIImage,對(duì)圖像節(jié)點(diǎn) ASImageNode 進(jìn)行繪制
  3. 使用 - drawRect:withParameters:isCancelled:isRasterizing: 在 CG 上下文中繪制文字節(jié)點(diǎn) ASTextNode

這三種方式都通過(guò) ASDK 來(lái)優(yōu)化視圖的渲染速度,這些操作最后都會(huì)扔到后臺(tái)的并發(fā)線程中進(jìn)行處理。

下面三個(gè)部分的代碼經(jīng)過(guò)了刪減,省略了包括取消繪制、通知代理、控制并發(fā)數(shù)量以及用于調(diào)試的代碼。

柵格化子視圖

如果當(dāng)前的視圖需要柵格化子視圖,就會(huì)進(jìn)入啟用下面的構(gòu)造方式創(chuàng)建一個(gè) block,它會(huì)遞歸地將子視圖繪制在父視圖上:

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if (!rasterizing && self.shouldRasterizeDescendants) {
    NSMutableArray *displayBlocks = [NSMutableArray array];
    [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];

    CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay;
    BOOL opaque = self.opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f;

    displayBlock = ^id{

      UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);

      for (dispatch_block_t block in displayBlocks) {
        block();
      }

      UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();

      return image;
    };
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    #:繪制 UIImage
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
    #:提供 context,使用 CG 繪圖
  }

  return [displayBlock copy];
}

在壓縮視圖層級(jí)的過(guò)程中就會(huì)調(diào)用 -[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:] 方法,獲取子視圖的所有 displayBlock,在得到 UIGraphicsBeginImageContextWithOptions 需要的參數(shù)之后,創(chuàng)建一個(gè)新的 context,執(zhí)行了所有的 displayBlock 將子視圖的繪制到當(dāng)前圖層之后,使用 UIGraphicsGetImageFromCurrentImageContext 取出圖層的內(nèi)容并返回。

-[ASDisplayNode(AsyncDisplay) _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:displayBlocks:] 的實(shí)現(xiàn)還是有些繁瑣的,它主要的功能就是使用 Core Graphics 進(jìn)行繪圖,將背景顏色、仿射變換、位置大小以及圓角等參數(shù)繪制到當(dāng)前的上下文中,而且這個(gè)過(guò)程是遞歸的,直到不存在或者不需要繪制子節(jié)點(diǎn)為止。

繪制圖片

displayBlock 的第二種繪制策略更多地適用于圖片節(jié)點(diǎn) ASImageNode 的繪制:

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if (!rasterizing && self.shouldRasterizeDescendants) {
    #:柵格化
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    id drawParameters = [self drawParameters];
    
    displayBlock = ^id{
      UIImage *result = nil;
      if (flags.implementsInstanceImageDisplay) {
        result = [self displayWithParameters:drawParameters isCancelled:isCancelledBlock];
      } else {
        result = [[self class] displayWithParameters:drawParameters isCancelled:isCancelledBlock];
      }
      return result;
    };
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
    #:提供 context,使用 CG 繪圖
  }

  return [displayBlock copy];
}

通過(guò) - displayWithParameters:isCancelled: 的執(zhí)行返回一個(gè)圖片,不過(guò)這里的繪制也離不開(kāi) Core Graphics 的一些 C 函數(shù),你會(huì)在 -[ASImageNode displayWithParameters:isCancelled:] 中看到對(duì)于 CG 的運(yùn)用,它會(huì)使用 drawParameters 來(lái)修改并繪制自己持有的 image 對(duì)象。

使用 CG 繪圖

文字的繪制一般都會(huì)在 - drawRect:withParameters:isCancelled:isRasterizing: 進(jìn)行,這個(gè)方法只是提供了一個(gè)合適的用于繪制的上下文,該方法不止可以繪制文字,只是在這里繪制文字比較常見(jiàn):

- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing {
  asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
  ASDisplayNodeFlags flags = _flags;

  if (!rasterizing && self.shouldRasterizeDescendants) {
    #:柵格化
  } else if (flags.implementsInstanceImageDisplay || flags.implementsImageDisplay) {
    #:繪制 UIImage
  } else if (flags.implementsInstanceDrawRect || flags.implementsDrawRect) {
      if (!rasterizing) {
        UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
      }

      if (flags.implementsInstanceDrawRect) {
        [self drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
      } else {
        [[self class] drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
      }
      
      UIImage *image = nil;
      if (!rasterizing) {
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
      }

      return image;
    };
  }

  return [displayBlock copy];
}

上述代碼跟第一部分比較像,區(qū)別是這里不會(huì)柵格化子視圖;代碼根據(jù)情況會(huì)決定是否重新開(kāi)一個(gè)新的上下文,然后通過(guò) - drawRect:withParameters:isCancelled:isRasterizing: 方法實(shí)現(xiàn)繪制。

管理繪制事務(wù)

ASDK 提供了一個(gè)私有的管理事務(wù)的機(jī)制,由三部分組成 _ASAsyncTransactionGroup_ASAsyncTransactionContainer 以及 _ASAsyncTransaction,這三者各自都有不同的功能:

  • _ASAsyncTransactionGroup 會(huì)在初始化時(shí),向 Runloop 中注冊(cè)一個(gè)回調(diào),在每次 Runloop 結(jié)束時(shí),執(zhí)行回調(diào)來(lái)提交 displayBlock 執(zhí)行的結(jié)果
  • _ASAsyncTransactionContainer 為當(dāng)前 CALayer 提供了用于保存事務(wù)的容器,并提供了獲取新的 _ASAsyncTransaction 實(shí)例的便利方法
  • _ASAsyncTransaction 將異步操作封裝成了輕量級(jí)的事務(wù)對(duì)象,使用 C++ 代碼對(duì) GCD 進(jìn)行了封裝

從上面的小節(jié)中,我們已經(jīng)獲取到了用于繪制的 displayBlock,然后就需要將 block 添加到繪制事務(wù)中:

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously {
  ...

  if (asynchronously) {
    CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ? : _layer;
    _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
    [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
  } else {
    ...
  }
}

前兩行代碼是獲取 _ASAsyncTransaction 實(shí)例的過(guò)程,這個(gè)實(shí)例會(huì)包含在一個(gè) layer 的哈希表中,最后調(diào)用的實(shí)例方法 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:] 會(huì)把用于繪制的 displayBlock 添加到后臺(tái)并行隊(duì)列中:

+ (dispatch_queue_t)displayQueue {
  static dispatch_queue_t displayQueue = NULL;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
  });

  return displayQueue;
}

這個(gè)隊(duì)列是一個(gè)并行隊(duì)列,并且優(yōu)先級(jí)是 DISPATCH_QUEUE_PRIORITY_HIGH確保 UI 的渲染會(huì)在其它異步操作執(zhí)行之前進(jìn)行,而 -[_ASAsyncTransaction addOperationWithBlock:priority:queue:completion:] 中會(huì)初始化 ASDisplayNodeAsyncTransactionOperation 的實(shí)例,然后傳入 completionBlock,在繪制結(jié)束時(shí)回調(diào):

- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block priority:(NSInteger)priority queue:(dispatch_queue_t)queue completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion {
  ASDisplayNodeAssertMainThread();

  [self _ensureTransactionData];

  ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
  [_operations addObject:operation];
  _group->schedule(priority, queue, ^{
    @autoreleasepool {
      operation.value = block();
    }
  });
}

schedule 方法是一個(gè) C++ 方法,它會(huì)向 ASAsyncTransactionQueue::Group 中派發(fā)一個(gè) block,這個(gè) block 中會(huì)執(zhí)行 displayBlock,然后將結(jié)果傳給 operation.value

void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
  ASAsyncTransactionQueue &q = _queue;
  ASDN::MutexLocker locker(q._mutex);
  
  DispatchEntry &entry = q._entries[queue];
  
  Operation operation;
  operation._block = block;
  operation._group = this;
  operation._priority = priority;
  entry.pushOperation(operation);
  
  ++_pendingOperations;
  
  NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;

  if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
    --maxThreads;
  
  if (entry._threadCount < maxThreads) {
    bool respectPriority = entry._threadCount > 0;
    ++entry._threadCount;
    
    dispatch_async(queue, ^{
      while (!entry._operationQueue.empty()) {
        Operation operation = entry.popNextOperation(respectPriority);
        {
          if (operation._block) {
            operation._block();
          }
          operation._group->leave();
          operation._block = nil;
        }
      }
      --entry._threadCount;
      
      if (entry._threadCount == 0) {
        q._entries.erase(queue);
      }
    });
  }
}

ASAsyncTransactionQueue::GroupImpl 其實(shí)現(xiàn)其實(shí)就是對(duì) GCD 的封裝,同時(shí)添加一些最大并發(fā)數(shù)、線程鎖的功能。通過(guò) dispatch_async 將 block 分發(fā)到 queue 中,立刻執(zhí)行 block,將數(shù)據(jù)傳回 ASDisplayNodeAsyncTransactionOperation 實(shí)例。

回調(diào)

_ASAsyncTransactionGroup 調(diào)用 mainTransactionGroup 類方法獲取單例時(shí),會(huì)通過(guò) +[_ASAsyncTransactionGroup registerTransactionGroupAsMainRunloopObserver] 向 Runloop 中注冊(cè)回調(diào):

+ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup {
  static CFRunLoopObserverRef observer;
  CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  CFOptionFlags activities = (kCFRunLoopBeforeWaiting | kCFRunLoopExit);
  CFRunLoopObserverContext context = {0, (__bridge void *)transactionGroup, &CFRetain, &CFRelease, NULL};

  observer = CFRunLoopObserverCreate(NULL, activities, YES, INT_MAX, &_transactionGroupRunLoopObserverCallback, &context);
  CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
  CFRelease(observer);
}

上述代碼會(huì)在即將退出 Runloop 或者 Runloop 開(kāi)始休眠時(shí)執(zhí)行回調(diào) _transactionGroupRunLoopObserverCallback,而這個(gè)回調(diào)方法就是這一條主線的入口:

static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
  ASDisplayNodeCAssertMainThread();
  _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info;
  [group commit];
}

上一節(jié)中只是會(huì)將繪制代碼提交到后臺(tái)的并發(fā)進(jìn)程中,而這里才會(huì)將結(jié)果提交,也就是在每次 Runloop 循環(huán)結(jié)束時(shí)開(kāi)始繪制內(nèi)容,而 -[_operationCompletionBlock commit] 方法的調(diào)用棧能夠幫助我們理解內(nèi)容是如何提交的,又是如何傳回 node 持有的 layer 的:

-[_ASAsyncTransactionGroup commit]
    -[_ASAsyncTransaction commit]
        ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t, dispatch_block_t)
            _notifyList.push_back(GroupNotify)

-[_ASAsyncTransactionGroup commit] 方法的調(diào)用完成了對(duì)繪制事務(wù)的提交,而在 -[_ASAsyncTransaction commit] 中會(huì)調(diào)用 notify 方法,在上一節(jié)中的 displayBlock 執(zhí)行結(jié)束后調(diào)用這里傳入的 block 執(zhí)行 -[_ASAsyncTransaction completeTransaction] 方法:

- (void)commit {
  ASDisplayNodeAssertMainThread();
  __atomic_store_n(&_state, ASAsyncTransactionStateCommitted, __ATOMIC_SEQ_CST);
    
  _group->notify(_callbackQueue, ^{
    ASDisplayNodeAssertMainThread();
    [self completeTransaction];
  });
}

我們按照時(shí)間順序來(lái)分析在上面的 block 執(zhí)行之前,方法是如何調(diào)用的,以及 block 是如何被執(zhí)行的;這就不得不回到派發(fā)繪制事務(wù)的部分了,在 ASAsyncTransactionQueue::GroupImpl::schedule 方法中,使用了 dispatch_async 將派發(fā) block:

void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) {
  ...
  if (entry._threadCount < maxThreads) {
    ...    
    dispatch_async(queue, ^{
      ...
      while (!entry._operationQueue.empty()) {
        Operation operation = entry.popNextOperation(respectPriority);
        {
          ASDN::MutexUnlocker unlock(q._mutex);
          if (operation._block) {
            operation._block();
          }
          operation._group->leave();
          operation._block = nil;
        }
      }
      ...
    });
  }
}

displayBlock 執(zhí)行之后,會(huì)調(diào)用的 groupleave 方法:

void ASAsyncTransactionQueue::GroupImpl::leave() {
  if (_pendingOperations == 0) {
    std::list<GroupNotify> notifyList;
    _notifyList.swap(notifyList);
    
    for (GroupNotify & notify : notifyList) {
      dispatch_async(notify._queue, notify._block);
    }
  }
}

這里終于執(zhí)行了在 - commit 中加入的 block,也就是 -[_ASAsyncTransaction completeTransaction] 方法:

- (void)completeTransaction {
  if (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) != ASAsyncTransactionStateComplete) {
    BOOL isCanceled = (__atomic_load_n(&_state, __ATOMIC_SEQ_CST) == ASAsyncTransactionStateCanceled);
    for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) {
      [operation callAndReleaseCompletionBlock:isCanceled];
    }
    
    __atomic_store_n(&_state, ASAsyncTransactionStateComplete, __ATOMIC_SEQ_CST);
  }
}

最后的最后,-[ASDisplayNodeAsyncTransactionOperation callAndReleaseCompletionBlock:] 方法執(zhí)行了回調(diào)將 displayBlock 執(zhí)行的結(jié)果傳回了 CALayer:

- (void)callAndReleaseCompletionBlock:(BOOL)canceled; {
  if (_operationCompletionBlock) {
    _operationCompletionBlock(self.value, canceled);
    self.operationCompletionBlock = nil;
  }
}

也就是在 -[ASDisplayNode(AsyncDisplay) displayAsyncLayer:asynchronously:] 方法中構(gòu)建的 completionBlock

asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
  ASDisplayNodeCAssertMainThread();
  if (!canceled && !isCancelledBlock()) {
    UIImage *image = (UIImage *)value;
    BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
    if (stretchable) {
      ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image);
    } else {
      _layer.contentsScale = self.contentsScale;
      _layer.contents = (id)image.CGImage;
    }
    [self didDisplayAsyncLayer:self.asyncLayer];
  }
};

這一部分進(jìn)行的大量的數(shù)據(jù)傳遞都是通過(guò) block 進(jìn)行的,從 Runloop 中對(duì)事務(wù)的提交,以及通過(guò) notify 方法加入的 block,都是為了最后將繪制的結(jié)果傳回 CALayer 對(duì)象,而到這里可以說(shuō)整個(gè) ASDK 對(duì)于視圖內(nèi)容的繪制過(guò)程就結(jié)束了。

總結(jié)

ASDK 對(duì)于繪制過(guò)程的優(yōu)化有三部分:分別是柵格化子視圖、繪制圖像以及繪制文字。

它攔截了視圖加入層級(jí)時(shí)發(fā)出的通知 - willMoveToWindow: 方法,然后手動(dòng)調(diào)用 - setNeedsDisplay,強(qiáng)制所有的 CALayer 執(zhí)行 - display 更新內(nèi)容;

然后將上面的操作全部拋入了后臺(tái)的并發(fā)線程中,并在 Runloop 中注冊(cè)回調(diào),在每次 Runloop 結(jié)束時(shí),對(duì)已經(jīng)完成的事務(wù)進(jìn)行 - commit,以圖片的形式直接傳回對(duì)應(yīng)的 layer.content 中,完成對(duì)內(nèi)容的更新。

從它的實(shí)現(xiàn)來(lái)看,確實(shí)從主線程移除了很多昂貴的 CPU 以及 GPU 操作,有效地加快了視圖的繪制和渲染,保證了主線程的流暢執(zhí)行。

References

其它

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · Github

Source: http://draveness.me/asdk-rendering

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容