07.WebApp2.0時代啟程:倒立者贏,從CPU到GPU,一張圖片的旅行

淘朗英發表於2016-10-26

緊接上文,終端開發使用的WindVane、wax、ReactNative等已經是一種跨平臺的技術,我們稱之為上層跨平臺,Cocos2d-x這種直接使用C/C++,我們成為底層跨平臺。上層跨平臺,提升開發效率;下層跨平臺,提升程式效能。

1. 為什麼Cocos2d-x效能比Native開發要好?

因為Cocos2d-X是遊戲引擎唄,人家是專業做遊戲特效的好不好,直接呼叫GPU的OpenGL繪圖的好不好。開啟Cocos2d-X程式碼,感觸最深的不是CCNode這些遊戲節點,cocos2d-x已經開始為App開發做準備了!
screenshot
大家看到了什麼?UIImageView?UIScrollView?OMG!當我們還在糾結要不要學習一下游戲開發的時候,人家都已經開始做UI系統了,有木有!!

很長一段時間,我們以為Game和App之間有一種天然的鴻溝,長久以來,這兩種技術之間沒有直接的聯絡,App可以做各種UI控制元件,遊戲可以做各種UI特效,當遊戲開發者想NativeApp大步的走過來的時候,我們還在學習如何用UITableView去優化圖片的顯示,豈不知在遊戲世界裡,早已實現了圖片的非同步載入、解碼、快取了!

別忘了,從iOS7.0開始,蘋果已經預設整合了GameKit.framework,顫抖吧!遊戲和App即將面臨傻傻分不清楚的時候了。

2. 一張圖片在OpenGL的世界裡,是如何從SDCard顯示到螢幕

這一次,我們不是講UIImage如何載入一個圖片,而是自己讀取圖片檔案,解碼影像,上傳紋理,推送頂點緩衝,重新整理幀緩衝,一步步顯示圖片到螢幕上。

上圖之前,我們先了解幾個概念:
2.1 幀緩衝:
我們要顯示的檢視繪製到螢幕上,本質是一個int32 buffer[width x height x 4]的一個陣列,不管2D還是3D,我們都要吧GPU的物件投影到這個buffer中,一個畫素4位元組表示[R/G/B/A]。

2.2 頂點緩衝:
終端裝置,只支援精簡版的OpenGL,稱之為OpenGL ES,這裡我們只用2.0版本。在這個版本中,我們只能畫三角形,一個正方形的圖片紋理,需要兩個三角形才可以描述:
screenshot
我們要畫圖片之前,就要告訴OpenGLES圖片頂點的位置,先忽略三角形的順序問題,就這樣告訴GPU:【ABDBDC】,同時設定當前的啟用的紋理,就可以繪製圖片了。

2.3 上傳紋理:
現在絕大多數手機,都有獨立的顯示卡晶片,每個顯示卡晶片都有獨立的視訊記憶體,這麼就清楚了:我們常說的記憶體是CPU直接操控的記憶體條容量,算上視訊記憶體,就有兩個快取空間了,程式設計師可以發揮的空間又多了,是不是有點小激動?

2.4 圖片解碼:
常見的jpg、png等圖片檔案,都是壓縮後的檔案,如果不壓縮,一張1024×1024的圖片,至少需要4M空間。一張圖片檔案解碼到記憶體,需要解碼器來解碼,不幸的是,Android很多機型沒有整合硬體解碼,慶幸的是iOS裝置內部支援硬體解碼。如果一張圖片檔案可以瞬間在底層硬體解碼完成,要比上層程式碼優化,更加直接!這就是我們為什麼一直追求的是底層跨平臺的原因。

2.5 講了很多概念了,我們上圖吧
screenshot

3. 原來圖片可以不佔用記憶體!

我們用C的API來讀取一張圖片,用libpng來解碼一張png圖片,呼叫OpenGL ES2.0API上傳圖片到GPU視訊記憶體,指定繪圖座標,顯示一張圖片,我們上程式碼吧:

一)讀取檔案:


AEFileData AEFileDataWithPathfile(std::string& pathfile) {
    AEFileData data;
    FILE* file = fopen(pathfile.c_str(), "rb");
    if (file == NULL) {
        return data;
    }
    fseek(file, 0, SEEK_END);
    data.size = (GLuint)ftell(file);
    fseek(file, 0, SEEK_SET);
    data.bytes = (GLchar*)malloc(sizeof(GLchar) * data.size);
    fread(data.bytes, sizeof(GLchar), data.size, file);
    fclose(file);
    return data;
}

二)解碼圖片:
為了跨平臺通用,我們用libpng來解碼圖片,獲取png的format和rgba陣列,【程式碼太多,就不貼了】

三)上傳GPU:


glGenTextures(1, &_textureId);
glBindTexture(type, _textureId);
glTexImage2D(type, 0, _format, _size.width, _size.height, 0, _format, GL_UNSIGNED_BYTE, pixels);

四)釋放記憶體


AEFileData file = AEFileDataWithPathfile(pathfile);
this->initPNG((GLuchar*)file.bytes, file.size);
__FREE(file.bytes);

五)指定頂點索引,並繪製


vb[0] = {d11, t11, color};
vb[1] = {d21, t21, color};
vb[2] = {d12, t12, color};
vb[3] = {d21, t21, color};
vb[4] = {d12, t12, color};
vb[5] = {d22, t22, color};
glDrawArrays(GL_TRIANGLES, 0, _vertexIndex);

4. 對比iOS與Android,圖片的渲染和展示做了什麼?

screenshot
Android的技術框架更偏於上層,看不到底層C/C++程式碼實現,優化的空間有限;Objective-C更靠近C語言,優化的空間明顯。下面我們以iOS為例:

5. 主流框架的圖片優化思路

要讀懂圖片的展示與優化,就要看看當前主流的圖片優化框架,他們是如何做優化的?主流的ImageCache框架:SDWebImage/FastImageCache/TBImageCache

screenshot

左邊的是SDWebImage,中間是FastImageCache,右邊是TBImageCache

SDWebImage:
將解碼後的圖片分別儲存到記憶體和SDCard,將複雜的操作放到了子執行緒中;

FastImageCache:
直接將檔案讀取到mmap對映的檔案中,減少了一次從系統核心到使用者空間的一次拷貝【然並卵】

TBImageCache:
在首次載入圖片時,根據使用者需要,對圖片增加圓角陰影處理,增加了非同步執行緒的處理的工作量,同時儲存到sdcard中,消耗了額外的資源,換取的是更好的圖片效能【解決了圓角陰影在終端的痛點】

圖片快取框架,只是讓CPU負載曲線更均衡,有沒有可能再進一步優化?

6. 遊戲世界與App世界對比

CPU與GPU是終端的雙駕馬車,缺一不可,CPU load過高,導致終端卡頓,同理GPU也是如此。iOS的UIKit框架封裝的太好了,也是優化做的最好的,讓我們看不到底層的原理,看不到iOS是如何平衡CPU、GPU、RAM、GPU RAM的4者之間的關係,讓我們大膽大猜測一下吧:

screenshot

一)UIImageView管理者UIImage物件
UIImage就是記憶體中得bitmap陣列,當我們需要繪製一張圖片的時候,CPU發出指令,上傳BitMap到GPU的視訊記憶體中,GPU返回生成的紋理ID給CPU。

二)為了提高iOS的效能,蘋果做出了一個優化策略,不釋放CPU的bitmap
當GPU視訊記憶體空間不足的時候,iOS會釋放一部分圖片紋理,以節省視訊記憶體空間,但不會釋放記憶體的bitmap。當一張圖片重新需要繪製的時候,iO快速從記憶體bitmap快速上傳到視訊記憶體,完成繪製過程。

三)當記憶體的bitmap釋放的時候,同步釋放視訊記憶體的紋理物件
本質意義上說,bitmap在記憶體中得儲存,不是真正顯示的那個快取,當bitmap上傳到GPU的時候,GPU會在視訊記憶體中申請同樣大小的空間,來存放bitmap,並返回紋理ID,每當CPU需要顯示某張圖片的時候,只需要高速GPU要繪製那個紋理ID就可以了。

四)上傳GPU後,我們完全可以釋放記憶體bitmap
釋放記憶體的bitmap很簡單: free(bitmap);
這樣記憶體裡只剩下了對應的GPU紋理的ID,當GPU記憶體緊張的時候,程式要要自己釋放不需要的紋理ID,如果這個ID在某個時候有需要使用了,怎麼辦?那要把之前的讀取檔案到上傳GPU重走一遍,太耗效能了,我想蘋果也是不想看到這個原因,才不會輕易釋放bitmap的

總結:遊戲的架構模式,需要開發者自己優化所有場景

遊戲開發者不僅需要優化CPU記憶體,也要優化GPU記憶體,通過將所有圖片合成一張大的紋理圖片,來統一管理GPU記憶體,需要把大量浮點數運算根據不同的場景,或者交給CPU或者交給GPU,讓CPU和GPU的負載達到均衡,各司其職。
這一些優化,對於我們做系統上層應用的無線開發,是很難想象有多少坑等著我們,終端App的開發,還是使用上層框架好,圖形影像的世界水很深,有想法的同學,可以繼續關注下一章分享,想了解我們的同學,可以用Android下載安裝我們的Demo:
screenshot

下一章,我們一起見證所及即所得的開發方式,WebApp2.0初體驗


相關文章