Cocos Creator ScrollView 優化系列-1-分幀加載

本系列教程指引:

  1. Cocos Creator ScrollView 優化系列-1-分幀加載
  2. Cocos Creator ScrollView 優化系列-2-可視區域渲染
  3. Cocos Creator ScrollView 優化系列-3-復用實現(待續)
  4. Cocos Creator ScrollView 優化系列-4-合批優化(待續)

本項目中所有圖示、代碼都在Github倉庫中,如果需要運行驗證,可直接拉下項目即可,不用自己手擼代碼驗證

????https://github.com/zhitaocai/CocosCreator-ScrollVIewPlus????

一、 前言

JS是單線程的,也就意味著所有任務需要排隊,只有當前一個任務結束了,后一個任務才會執行。如果前一個任務耗時很長,后一個任務就不得不一直等著。

Cocos Creator 是采用 Java Script/Type Script語言開發,本質上是JS,同樣會擁有以上特征。特別地,如果使用不當,極有可能導致界面卡頓。

比如:在為一個ScrollView的Content創建500個節點的的時候,可能就會出現下面界面卡死的問題

PS:本來加載過程中有一個loading對話框,因為卡死了,就感覺從來沒出現

卡死問題演示

通過閱讀本文,你將了解到如何利用「分幀加載」技術解決上述問題,最終效果對比如下:

分幀加載演示

二、卡死問題分析

在正常情況下,我們為ScrollView創建一定數量的子節點的時候,代碼可能是這樣子的

public directLoad(length: number) {
    for (let i = 0; i < length; i++) {
        this._initItem(i);
    }
}

private _initItem(itemIndex: number) {
    let itemNode = cc.instantiate(this.itemPrefab);
    itemNode.width = this.scrollView.content.width / 10;
    itemNode.height = itemNode.width;
    itemNode.parent = this.scrollView.content;
    itemNode.setPosition(0, 0);
}

一般而言,當length的值很小,比如10個的時候,程序跑起來的時候,看上去可能會沒什么問題,但其實如果仔細一點觀察,就發現其實也是會卡死一會,只是很快就結束了。

特別地,如果length的值到一點量級,比如50+個,那么這段代碼就會出現上面截圖那樣子—— 卡死

歸根到底,問題在于通過 cc.instantiate 創建節點以及為這個節點 setParent 時,所需要的時間并沒有想象中那么小,當然,也沒有想象中那么大。但是當連續創建一定數量的時候,問題就會被放大,也就是說,這個創建節點的時間可能需要一段時間。

可視化一點去理解這個問題的話,恩,大概就是下圖這樣子

Direct Load

很明顯,按照上圖,第1到4幀都被完成占用了,導致這期間所有的其他邏輯都會不能執行(Loading對話框出不來,旋轉動畫卡死等等)。

那么怎么解決呢?

三、解決方案(理論篇)

可能有同學第一時間想到用Promise異步解決,但是在這個問題上,Promise只是把紅色的這段連續創建節點的代碼放到后面一點的時間去執行,但是當紅色的代碼執行的時候,它依舊會卡死那段時間,所以Promise是不能應對這種場合的。

那么應該怎么解決呢?

其中,一種解決方案,就是我們今天要講的 「分幀加載」 ,怎么理解「分幀加載」呢?

慣例,先上圖:

Framing Load

配合上圖,就比較好理解「分幀加載」了,具體執行過程為

  1. 先將耗時卡死的代碼拆分為很多小段
  2. 然后每一幀,分配一點時間去執行這些小段
  3. 這樣子一來,每一幀,我們就留了時間給其他邏輯去跑(那么Loading對話框也可以出來了,旋轉動畫也可以繼續了)

OK,理論說清楚了,那么實際怎么弄呢?

比如:

  1. 怎么拆分代碼為很多小段?
  2. 怎么分配每一幀的一些時間去執行這些小段呢?

這個時候,我們需要用到 ES6(ES2015)的協程——Generator,去幫助我們實現。

ps: 我們不會在這里探討什么是Generator,怎么用,如果你對Generator感到陌生,不妨可以嘗試閱讀下面文章去了解

四、解決方案(代碼篇)

以我們第二節舉例用到的代碼(為ScrollView創建一定數量的子節點)為例子,我們將 實現代碼為多個小段 以及 分配每一幀的一些時間去執行這些小段

4.1 利用 Generator 將代碼拆分為多個小段

拆分前:

public directLoad(length: number) {
    for (let i = 0; i < length; i++) {
        this._initItem(i);
    }
}

private _initItem(itemIndex: number) {
    let itemNode = cc.instantiate(this.itemPrefab);
    itemNode.width = this.scrollView.content.width / 10;
    itemNode.height = itemNode.width;
    itemNode.parent = this.scrollView.content;
    itemNode.setPosition(0, 0);
}

拆分后:

/**
 * (新增代碼)獲取生成子節點的Generator
 */
private *_getItemGenerator(length: number) {
    for (let i = 0; i < length; i++) {
        yield this._initItem(i);
    }
}

/**
 * (和拆分前的代碼一致)
 */
private _initItem(itemIndex: number) {
    let itemNode = cc.instantiate(this.itemPrefab);
    itemNode.width = this.scrollView.content.width / 10;
    itemNode.height = itemNode.width;
    itemNode.parent = this.scrollView.content;
    itemNode.setPosition(0, 0);
}

這里的原理就是 利用 Generator 將一次 for 循環里創建所有節點,改為拆分 for 循環的每一步為一個小段

當然,這份「拆分后」的代碼并不能跑起來,因為它只是實現了拆分步驟,要讓它跑起來,我們要上下面的第二段代碼

4.2 分配每一幀的一些時間去執行

在看一次我們剛才的圖

Framing Load

配合圖,得出的代碼

/**
 * 實現分幀加載
 */
async framingLoad(length: number) {
    await this.executePreFrame(this._getItemGenerator(length), 1);
}

/**
 * 分幀執行 Generator 邏輯
 *
 * @param generator 生成器
 * @param duration 持續時間(ms)
 *          每次執行 Generator 的操作時,最長可持續執行時長。
 *          假設值為8ms,那么表示1幀(總共16ms)下,分出8ms時間給此邏輯執行
 */
private executePreFrame(generator: Generator, duration: number) {
    return new Promise((resolve, reject) => {
        let gen = generator;
        // 創建執行函數
        let execute = () => {

            // 執行之前,先記錄開始時間戳
            let startTime = new Date().getTime();

            // 然后一直從 Generator 中獲取已經拆分好的代碼段出來執行
            for (let iter = gen.next(); ; iter = gen.next()) {

                // 判斷是否已經執行完所有 Generator 的小代碼段
                // 如果是的話,那么就表示任務完成
                if (iter == null || iter.done) {
                    resolve();
                    return;
                }

                // 每執行完一段小代碼段,都檢查一下是否
                // 已經超過我們分配給本幀,這些小代碼端的最大可執行時間
                if (new Date().getTime() - startTime > duration) {
                    
                    // 如果超過了,那么本幀就不在執行,開定時器,讓下一幀再執行
                    this.scheduleOnce(() => {
                        execute();
                    });
                    return;
                }
            }
        };

        // 運行執行函數
        execute();
    });
}

代碼中已經附有大量注釋,但還是有幾個點需要說明一下:

  1. 為了方便知道這些小任務是否已經都執行完了,我采用了Promise,當都完成了的時候,resolve 一下
  2. 每一個小代碼段的執行時間可能不固定的,可能會超出占用我們的一些期望時間。比如我們期望每一幀分配1ms 去執行這些小代碼段,假設前3段小代碼段,每一段的執行時間假設為 0.2ms,0.5ms, 0.4ms,那么在我給出的這段代碼中,是會執行完這3段小代碼段,然后就終止本幀繼續執行這些小代碼段,因為這里的耗時已經是 1.1ms,比我設定的 1ms 已經多出了 0.1ms 。當然你可以自行改動代碼,讓這些執行嚴格按照最大1ms去執行,以實現不超時執行(即不再執行第3個小段)

至此,我們一定程度上已經實現了「分幀加載」了~

本項目中所有圖示、代碼都在Github倉庫中,如果需要運行驗證,可直接拉下項目即可,不用自己手擼代碼驗證

????https://github.com/zhitaocai/CocosCreator-ScrollVIewPlus????

五、總結

  1. 盡管我們標題是 「ScrollView 優化系列」,但我更加傾向于,「利用分幀加載去優化ScrollView」。在這篇文章上,我們舉的例子是創建節點,但是我刻意不說「分幀創建」,這是因為我認為 「分幀加載」是一種性能優化方案 ,可以「分幀創建」、「分幀運行」、「分幀計算」、「分幀渲染」等。
  2. 在實現分幀上,我們用到了 this.scheduleOnce函數,但是其實可以嘗試在 update(dt:number) 上執行,不妨嘗試修改我的 「測試項目」去驗證呢~
  3. TypeScript 要用上 Generator 還需要需改一下Cocos項目中的 tsconfig.jsoncompilerOptions.lib 數組中添加 es2015

六、進入下一個章節

至此,我們的「分幀加載」基本告一段落了,但細心的你肯定發現了,目前這個案例里面 Draw call 太高了,這是一個能忽視的問題,這個問題,我們將會在下個章節中解決。

本系列教程指引:

  1. Cocos Creator ScrollView 優化系列-1-分幀加載
  2. ??Cocos Creator ScrollView 優化系列-2-可視區域渲染
  3. Cocos Creator ScrollView 優化系列-3-復用實現(待續)
  4. Cocos Creator ScrollView 優化系列-4-合批優化(待續)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,136評論 1 32
  • 那條叫‘卡卡’的惡龍頓時變得兇惡起來,它張牙舞爪地向我撲來。我被它撲倒了,一頭撞在了濕漉漉的石壁上。就在它伸出兩爪...
    小小夕顏花閱讀 681評論 3 5
  • 【做人智商不高沒關系,情商不高也問題不大,但做人的格局已定要大】說白了,你可以不聰明,也可以不懂交際,但一定要大氣...
    最愛的桐嘉閱讀 456評論 0 0