Android的文字渲染
一種使用OpenGL渲染文字的常用方法,是計算出一個包含了顯示文字的紋理圖片,這通常是使用相當複雜的打包演算法來最小化紋理中的冗餘部分,在建立這樣的圖片之前必須清楚應用運行時使用的字型,包括了字型形狀,尺寸和其他的一些屬性。
在Android上,提前產生文字紋理圖片是不太實際的,因為沒有方法提前知道應用使用了哪些字型和字形,應用甚至可以在運行時載入自訂字型,這是許多限制因素中的主要一個,Android字型渲染必須實現下面的工作:
必須可以在運行時建立字型緩衝 必須能夠處理大量的字型 必須能夠處理大量的字形 最小化紋理的浪費 速度必須快 在低端和高端裝置上都啟動並執行很快 在任何的驅動/GPU組合上正常運行實現文字渲染
在我們查看低層級的OpenGL字型渲染是如何工作之前,我們先從應用直接使用的進階別API開始,這些API對於理解libhwui的工作是非常重要的。
Text APIs
應用主要使用下面的四個API來布局和繪製文字:
android.widget.TextView 一個用來處理布局和渲染的view android.text.* 建立格式化的文字和布局的類集合 android.graphics.Paint 用來測量文字(to measure text) android.graphics.Canvas 用來渲染文字(to render text)
TextView和android.text是在Paint和Canvas上面的進階別實現。直到Android 3.0,Paint和Canvas是直接在Skia上面實現的,Skia是一個軟體渲染庫,它提供了一個非常好的Freetype(非常流行的文字光柵化開原始碼)抽象。
vcHLNC40uf2zzLHktcO4/LzTuLTU08HLo6xQYWludLrNQ2FudmFzyrnTw8Tasr+1xEpOSb3Tv9pUZXh0TGF5b3V0Q2FjaGXAtLSmwO24tNTTzsTX1rXEbGF5b3V0o6zV4rj2QVBJ0sDAtdPaSGFyZmJ1enqjqNK7uPa/qtS0tcTOxNfWs8nQzdL9x+ajqaOsVGV4dExheW91dENhY2hltcTK5MjrysfSu7j2Zm9udLrN0ru49kphdmEgVVRGLTE2tcRzdHJpbmejrMrks/bKx7D8uqx4o6x5zrvWw7XE19bQzrHqyra3+8HQse2hozwvcD4NCjxwPlRleHRMYXlvdXRDYWNoZcrH1f3It9ans9bQ7bbgt8fArbah0+/R1LXEudi8/KOsscjI57CiwK2yrtPvoaLPo7KuwLTT76GizKnT77XIoaO75tbGzsTX1rHIvPK1pbXEtNPX87W909LSu7j2vdPSu7j2tcS3xdbDzsTX1ri01NO1xLbgo6zSu9Cp0+/R1LHIyOewosCtsq7T76Osyse009PStb3X87XEo6zMqdPvyfXWwdDo0qrOxNfWsbu3xdbDtb3HsNK7uPbOxNfWtcTJz8Pmu/LV38/Cw+ahozwvcD4NCjxwPjxpbWcgYWNjZWxlcmF0ZWQ9"" alt="Android" hardware="" rendering="" src="http://www.bkjia.com/uploads/allimg/150521/04151MK0-1.png" text="" title="\" />
這意味這當你調用Canvas.drawText()方法時,openGL渲染引擎不會直接或者間接的接收到你發送的參數,但是會收到字形標識符數組和x/y位置數組
光柵化與緩衝
字型渲染的每一次draw調用都是和一個單獨的font關聯的,font是用來緩衝各個字形,字形反過來被儲存在一個緩衝結構中(cache texture),這個緩衝結構可以用來包含多種字型的字形。cache texture是一個非常重要的對象,它有多種緩衝:空閑塊列表,像素緩衝,OpenGL紋理控制代碼,頂點緩衝(the mesh,網格,構成圖形的基本單位)。
用來儲存這些對象的資料結構是相當簡單的:
Fonts被儲存在font渲染器的LRU緩衝中 字形(Glyphs)被儲存在每一個font的map中,key是字形標識符 Cache textures(緩衝結構)跟蹤鏈表塊的空閑位置 像素緩衝是uint8_t或者uint_32t的數組(alpha和RGBA的緩衝) mesh是有兩個屬性的頂點緩衝:x/y位置和u/v座標 texture是一個GLuint類型的控制代碼
當字型渲染器初始化的時候,它建立了兩種類型的cache textures:alpha和RGBA。Alpha textures用來儲存正常的字形,因為字型不包含顏色資訊,我們只需要儲存消除鋸齒資訊。RGBA緩衝被用來儲存Emoji。
對每一種類型的cache texture,字型渲染器建立一些不同尺寸大小的CacheTexture執行個體,根據裝置的不同緩衝的大小也不同,下面是一些預設的尺寸:
1024x512 alpha cache 2048x256 alpha cache 2048x256 alpha cache 2048x512 alpha cache 1024x512 RGBA cache 2048x256 RGBA cache
當一個CacheTexture被建立的時候,底層的緩衝並沒有被自動分配,字型渲染器在需要的時候才會進行分配,除了1024x512alpha緩衝,它總是被分配的。
字形在textures以列的形式進行打包,當渲染器遇到一個沒有緩衝的字形,它會按照上面列出來的順序尋找合適的CacheTexture緩衝字形。
這就是塊列表使用的地方,它包含了cache texture的當前分配列和可用的空閑位置。如果字形符合存在的列,它會被添加到該列所佔空間的末尾。
如果所有的列都被佔用了,一個新的列會在剩餘空間的左側劃分出來。因為很少的字型是等寬的,渲染器分配每一個字形的寬度為4像素的倍數。這是對列的重用和texture打包的好的折衷方案。打包不是最佳的,但是它提供了快速實現。
所有的字形周圍都有一個像素的空邊界,它們都儲存在textures中,對於採取雙重線性採樣的font textures來說,這麼做可以避免人工處理。
知道text在渲染的時候是否進行尺寸變換是非常重要的,變換被提交給Skia/Freetype,這意味著cache textures裡的字形儲存了變換,這在效能消耗方面提高了渲染品質。幸運的是,text是很少動態縮放的,即使有,也只有很少的字形被影響到。還有其他的paint屬性可以影響字形的光柵化和儲存:仿粗體,文字偏斜,X縮放,風格和線寬。
預先緩衝
由於libhwui是延遲渲染的,和Skia的直接模式相反,在一幀開始的時候,已經知道需要被繪製到螢幕上的字形了,在顯示列表操作排序的時候,字型渲染器會儘可能多的預緩衝字形。這樣做的好處是會完全避免或者最小化在幀中間上傳texture的數量。texture上傳是非常耗資源的操作,會導致CPU或者GPU暫停,更糟糕的是,在幀中間修改texture會對一些GPU產生嚴重的記憶體壓力。
ImaginationTech的PowerVR SGX GPU在幀中間修改的時候,會強制驅動複製每一個修改的texture,因為一些font的texture是非常大的,如果不小心texture上傳的話,會更加容易導致oom。Google paly上面有一個計算機應用出現過這種情況,它用數學符號和數字繪製按鈕,在字型渲染的時候,可能會出現oom,因為按鈕的繪製是一個接一個的, 每一次繪製都會觸發texture上傳,這導致了整個font緩衝的複製,系統沒有那麼多的記憶體來放置這麼多份緩衝的拷貝。
重新整理緩衝
用來緩衝字形的textures是相當大的,所以當其他應用需要更多記憶體的時候它們可能會被系統回收。當使用者隱藏當前應用的時候,系統會發訊息通知應用釋放儘可能多的記憶體,應用在大部分情況下會摧毀最大的textures緩衝。在Andorid裝置上,除了第一個被建立的(預設為1024x512)其他所有的textures緩衝都被認為是大的textures。
當沒有所有的緩衝中都沒有空間的時候,textures會被重新整理,font渲染器會保持一個LRU(最近最少使用)的列表,但是沒有使用它做任何事情,如果需要的話,通過重新整理很少使用的記憶體來是緩衝的重新整理更加智能,目前位置還沒有需要這樣做,但是要記住它是潛在的最佳化方向。
批量處理與合并
Android 4.3介紹了繪製操作的批處理與合并,一個非常重要的最佳化是大幅減少了傳遞給OpenGL驅動的命令數。
為了實現合并,font渲染器緩衝了多個繪製命令的文本幾何資料。每一個緩衝texture有一個用戶端數組,裡麵包含了2048個四邊形(1四邊形 = 1 字形),他們都共用一個獨立的索引緩衝(在GPU中以VBO存在)。
當libhwui執行一個文字繪製命令時,文字渲染器會為每一個字形擷取合適的mesh(網格,構成圖形的基本單位)並會將位置和u/v座標寫進去。Mesh會在一批結束或者quad緩衝滿了之後傳送到GPU。渲染一個單獨的string可能需要多個mesh,每個緩衝texture有一個mesh。
這個最佳化是比較容易實現,並且會極大的提高效能。由於文字渲染器使用多個緩衝texture,有可能會出現string中的大部分字形是一個texture的一部分,而一些字形是另一個texture的一部分。如果沒有批處理和合併作業,在每一次文字渲染器需要切換到不同的緩衝texture時,都會給GPU提交一個繪製命令。
我曾經在一個test的app中碰到過這種問題,這個應用只是渲染不同風格和尺寸的“hello world”,在“o”字母和其他的字形被存在不同的texture中的特定情況下。這會導致文字渲染器會先渲染“hell”,然後“o”,然後“w”,然後“o”,然後“rld”,總共執行了5個繪製命令和5個texture綁定,而實際上兩個都之需要2次,現在的渲染器先繪製“hell w rld”然後一塊繪製兩個“o”。
最佳化texture上傳
前面曾經提過文字渲染器會在更新緩衝texture時嘗試儘可能少的上傳資料,通過跟蹤每一個texture的被變動的部分(dirty rect),可惜的時這種方法有兩個限制。
首先,OpenGL ES2.0不允許上傳一個隨意的子矩形。glTexSubImage2D允許你指定矩形的x/y和width/height來更新texture的一部分,但是它是假設存在於主記憶體資料的stride(步幅)是矩形的寬度。這個可以通過建立合適尺寸的CPU緩衝來實現,但前提是必須知道變動矩形的大小。一個比較好的折中方法是上傳一個最小的包含dirty rect的像素帶,因為這個像素帶的寬度是和texture一樣的,所以我們可能會浪費一些頻寬,但還是比上傳整個texture好。
第二個問題是,texture上傳是同步的操作,這可能會導致cpu長時間的等待,對於已經預先緩衝的其實影響不大。但是對於使用了很多文字的應用或者有很多字形的語言環境,比如中文,這個問題可能會被使用者感覺到。
幸運的是,OpenGL ES3.0對這兩個問題提供瞭解決方法,現在可以使用新的像素儲存屬性GL_UNPACK_ROW_LENGTH上傳字矩形了。這個屬性指定了stride或者記憶體中的來源資料,但是要小心的是:這個屬性影響當前OpenGL context的全域狀態。
在texture上傳期間的CPU等待可以通過使用像素緩衝對象(PBO)來避免,就想OpengGL中所有的buffer對象,PBO存在與GPU,但可以被映射進記憶體。PBO有很多有趣的屬性但是我們關心的是在從記憶體中取消映射後,它可以非同步上傳texture,這樣操作的順序就變成了:
glMapBufferRange → write glyphs to buffer → glUnmapBuffer → glPixelStorei(GL_UNPACK_ROW_LENGTH) → glTexSubImage2D
現在調用glTexSubImage2D會直接返回,而不是阻塞渲染器。文字渲染器現在將整個buffer映射進記憶體,儘管它看起來沒有引起效能問題,但是將需要更新的cache texture的範圍映射進來而不是全部會更好。
陰影
文字通常是和陰影一起渲染的,一個相當耗費資源的操作。由於相鄰的字形會模糊進彼此,文字渲染器不能獨立的預先模糊字形。有許多方法來實現模糊,但是要在一幀中最小化混合操作和紋理採樣,陰影被以textures儲存並且會在多個幀中存在。