Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android的文字渲染

Android的文字渲染

編輯:關於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(非常流行的文字光柵化開源代碼)抽象。

AndroidvcHLNC40uf2zzLHktcO4/LzTuLTU08HLo6xQYWludLrNQ2FudmFzyrnTw8Tasr+1xEpOSb3Tv9pUZXh0TGF5b3V0Q2FjaGXAtLSmwO24tNTTzsTX1rXEbGF5b3V0o6zV4rj2QVBJ0sDAtdPaSGFyZmJ1enqjqNK7uPa/qtS0tcTOxNfWs8nQzdL9x+ajqaOsVGV4dExheW91dENhY2hltcTK5MjrysfSu7j2Zm9udLrN0ru49kphdmEgVVRGLTE2tcRzdHJpbmejrMrks/bKx7D8uqx4o6x5zrvWw7XE19bQzrHqyra3+8HQse2hozwvcD4NCjxwPlRleHRMYXlvdXRDYWNoZcrH1f3It9ans9bQ7bbgt8fArbah0+/R1LXEudi8/KOsscjI57CiwK2yrtPvoaLPo7KuwLTT76GizKnT77XIoaO75tbGzsTX1rHIvPK1pbXEtNPX87W909LSu7j2vdPSu7j2tcS3xdbDzsTX1ri01NO1xLbgo6zSu9Cp0+/R1LHIyOewosCtsq7T76Osyse009PStb3X87XEo6zMqdPvyfXWwdDo0qrOxNfWsbu3xdbDtb3HsNK7uPbOxNfWtcTJz8Pmu/LV38/Cw+ahozwvcD4NCjxwPjxpbWcgYWNjZWxlcmF0ZWQ9"" alt="Android" hardware="" rendering="" src="/uploadfile/Collfiles/20150520/2015052008370816.png" text="" title="\" />

這意味這當你調用Canvas.drawText()方法時,openGL渲染引擎不會直接或者間接的接收到你發送的參數,但是會收到字形標識符數組和x/y位置數組

光柵化與緩存

字體渲染的每一次draw調用都是和一個單獨的font關聯的,font是用來緩存各個字形,字形反過來被存儲在一個緩存結構中(cache texture),這個緩存結構可以用來包含多種字體的字形。cache texture是一個非常重要的對象,它有多種緩存:空閒塊列表,像素緩存,OpenGL紋理句柄,頂點緩存(the mesh,網格,構成圖形的基本單位)。

Caching

用來存儲這些對象的數據結構是相當簡單的:

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緩存被用來存儲表情符號。
對每一種類型的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存儲並且會在多個幀中存在。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved