自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android字體渲染器:使用OpenGL ES進(jìn)行高效文字渲染

移動開發(fā) Android
任何有多年客戶端開發(fā)經(jīng)驗的開發(fā)者都應(yīng)該知道復(fù)雜的文字渲染是怎么工作的。至少在2010年以前,我剛開始寫libhwui的時候,我就意識到處理文字有時會比其他方面更復(fù)雜,特別是當(dāng)你嘗試用GPU在屏幕上進(jìn)行繪制的時候。本文只是對Android的字體渲染器進(jìn)行簡單介紹,還有很多實現(xiàn)的細(xì)節(jié)沒有考慮到,或者很多問題以后會說明。

任何有多年客戶端開發(fā)經(jīng)驗的開發(fā)者都應(yīng)該知道復(fù)雜的文字渲染是怎么工作的。至少在2010年以前,我剛開始寫libhwui的時候(這是一個基于 Android2.0的2D繪畫庫),我就意識到處理文字有時會比其他方面更復(fù)雜,特別是當(dāng)你嘗試用GPU在屏幕上進(jìn)行繪制的時候。

文字與Android

Android上的文字渲染加速器硬件最初是由Renderscript團(tuán)隊寫的,然后被很多工程師改進(jìn)和優(yōu)化,包括我和好友Chet Haase。在網(wǎng)絡(luò)上,可以很容易找到很多關(guān)于怎么使用OpenGL ES渲染文字的教程。如果覺得還不夠,可以看看關(guān)于游戲的文章,只看關(guān)于文字渲染部分就行。

本文說不是很新奇的知識,只是對于很多開發(fā)者來說,通過本文可以從深層次上了解如何實現(xiàn)一個基于GPU的文字渲染系統(tǒng),文章***還介紹了一些比較容易實現(xiàn)的優(yōu)化方法。

用OpenGL渲染文字的常用方法是計算包含所需字形的所有紋理集。這個操作通常是使用一些相當(dāng)復(fù)雜的算法進(jìn)行離線操作,這樣可以在構(gòu)造字形的時候更加高效。在創(chuàng)建這樣一個紋理集之前,首先需要知道應(yīng)用程序在運行時要使用的字體,包括字體樣式、大小以及其它屬性。

在Android上,提前進(jìn)行字體紋理生成不是一個實用的方案。Android上的UI工具并不能知道應(yīng)用系統(tǒng)會使用什么字體和字形,并且應(yīng)用還可以在運行時載入自定義的字體,這是主要的限制。Android字體渲染還必須遵循以下條例:

  • 它必須在運行時建立字體緩存;
  • 它必須能夠處理大量的字體;
  • 它必須可以處理大量的符號;
  • 它必須要盡可能減少字體上的資源消耗;
  • 必須運行要快速;
  • 在低端和高端機(jī)器上也能夠良好運行;
  • 能***與其它組件結(jié)合(驅(qū)動程序或GPU)。

字體渲染器的實現(xiàn)

在進(jìn)入底層OpenGL字體渲染器工作原理之前,我們先從應(yīng)用層使用的高級別的API開始。這些API對于理解libhwui很重要。

文字API

用于布局和繪制文字主要有4個API:

  • android.widget.TextView:一個可以處理文字布局和渲染的視圖組件。
  • android.text.*:一個可以創(chuàng)建風(fēng)格化文字和布局的類集合。
  • android.graphics.Paint:用于測量文字。
  • android.graphics.Canvas:用于渲染文字。

TextView和android.text的都是在Paint和Canvas上的高級API。Android3.0以后,Paint和Canvas直接被實現(xiàn)在Skia之上,這是一個開源的渲染庫。SKia提供了一個很好的Freetype抽象實現(xiàn),這是一個很熱門的開源字體柵格化程序。

1-BitH26buboQae4iO-FpSyg

對于Android4.4,情況變得有些復(fù)雜。Paint和Canvas都使用了一個內(nèi)部的JNI API,叫做TextLayoutCache。它可以處理復(fù)雜的文字布局(CTL)。這個API依賴Harfbuzz,一個空間開源的字形引擎。TextLayoutCache的輸入是一個字體和一個Java的UTF-16的字符串,輸出是一個帶有x/y坐標(biāo)的字形列表。

TextLayoutCache是支持非拉丁語言的要點,比如阿拉伯語言、希伯來語、泰國語等,本文不會解釋TextLayoutCache和 Harfbuzz的工作原理,但本人強(qiáng)烈建議讀者去學(xué)習(xí)學(xué)習(xí)CTL。如果在開發(fā)應(yīng)用的時候需要支持非拉丁語言環(huán)境,那么就要學(xué)習(xí)它了。如果你曾經(jīng)參與過 OpenGL渲染文字的文章中的討論,就會發(fā)現(xiàn)這種特殊的問題是很少見的。繪制文字比簡單排布字形更復(fù)雜。某些語言中,比如阿拉伯語是從右到左的,還有泰 語甚至需要把字形排布在前一個字形的上面或者下面。

1-VvVj04gAzuTsMC_AN9RGRA

也就是說,當(dāng)直接或間接調(diào)用Canvas.drawText()函數(shù)的時候,OpenGL 渲染器不會收到你發(fā)送的參數(shù),而是收到一串?dāng)?shù)字、符號標(biāo)識,還有x/y 坐標(biāo)集合。

點陣化和緩存

字體渲染器的每一個繪制方法都是和字體相關(guān)的。字體用于緩存?zhèn)€別字形符號,而字形符號又被存儲在緩存結(jié)構(gòu)中(緩存結(jié)構(gòu)可以包含不同字體的字形符 號)。緩存結(jié)構(gòu)是持有多個緩沖區(qū)的一個重要的對象,有block集合、pixel緩沖區(qū)、OpenGL結(jié)構(gòu)處理器,還有點陣緩沖區(qū)(也就是網(wǎng)格)。

1-qK4rIi_HDsEYPQQxFK5uPg

這個對象存儲的數(shù)據(jù)結(jié)構(gòu)比較簡單:

  • 在字體渲染器中字體是存儲在一個LRU緩存中的;
  • 字形符號分別存儲在對應(yīng)的map字體集合中(key就是字形文件的identifier);
  • 緩存結(jié)構(gòu)使用一個塊鏈表集合來記錄空間的大小;
  • 像素緩沖區(qū)是一個uint8_t或者uint32_t類型的數(shù)組(作alpha值和RGBA的緩存);
  • 網(wǎng)格其實就是一個頂點數(shù)組,帶有兩個屬性:x/y位置和u/v坐標(biāo);
  • 一個GLuint的處理器。

字體渲染器對不同類型的緩存結(jié)構(gòu)提供了幾種緩存紋理實例,也就是根據(jù)不同的大小區(qū)分,這個大小可能會根據(jù)不同設(shè)備而有所不同,這里這里說的是默認(rèn)的大小(緩存的數(shù)量是硬編碼的):

  • 1024*512 alpha緩存。
  • 2048*256 alpha緩存。
  • 2028*512alpha緩存。
  • 1024*512alpha緩存。
  • 2048*256alpha緩存。

當(dāng)緩存紋理對象創(chuàng)建之后,其對應(yīng)的緩沖區(qū)不會自動分配空間,除了1024*512的alpha緩存總是自動分配外,其它的都是根據(jù)需要來分配空間。

字形符號以列的形式打包在紋理中,只要字體渲染器遇到?jīng)]有緩存的符號,它就會向緩存紋理請求響應(yīng)的類型(存儲在以上的有序列表中),然后緩存該符號。

這是上述的blocks列表使用到的地方,這個列表包含了當(dāng)前已分配的列和所有未分配的空間。如果字形符號和已經(jīng)存在的列匹配,那該字形符號就會被加到該列的底部。

如果所有列都被占用,從左邊的剩余空間開辟新列。因為所有字體都是等寬的,渲染器會把每個字形的寬度弄成4像素的倍數(shù)(默認(rèn)是4像素)。這是對列的重利用和字形打包的一個折衷,這個打包目前還不是很好,但是實現(xiàn)起來比較快。

所有的字形符號都存儲在一個含有1個像素邊框的結(jié)構(gòu)中,這樣在雙線過濾采樣的時候可以避免偽跡的產(chǎn)生。

在文字帶有縮放變形操作的渲染中,了解文字何時被渲染也是非常重要的。這個變形操作直接到Skia/Freetype來處理,這就意味著字形符號是 在緩存結(jié)構(gòu)中變形存儲的。這樣可以改善渲染的質(zhì)量。幸運的是,文字一般很少做縮放動畫效果,就算是使用了,也只是設(shè)計很少的字形符號。本人做過很多實驗, 也沒有找到一個實際使用的場景。

還有其它關(guān)于paint的屬性會影響字形符號的柵格化和存儲的:粗體、斜體、還有X縮放(在Canvas上做矩陣變換)、字體風(fēng)格以及線條寬度等。

柵格化的可選方案

事實上,還有其它的方式去在GPU上處理文字字形符號??梢灾苯颖讳秩境滔蛄?,但是這樣做開銷很大。我調(diào)查過標(biāo)記距離字段的方法,但是簡單實現(xiàn)的時候遇到了精度的問題(創(chuàng)建曲線的時候會不穩(wěn)定)。

本人建議讀者可以看看Glyphy這個項目。這是一個開源庫,作者是Harfbuzz。項目在標(biāo)記距離字段技術(shù)上進(jìn)行延伸,同時也解決了精度的問題。我暫時沒有花太多時間看這個項目。但是上一次在做著色器的時候,發(fā)現(xiàn)這種技術(shù)在Android上是被禁止使用的。

預(yù)緩存技術(shù)

字形符號緩存是一定要做的。如果做預(yù)緩存的話,效果會更好。因為libhwui是一個延遲的渲染器(和Skia的快速模式正好相反),所有屏幕上出現(xiàn)的字形都是一幀一幀開始的。在一系列的顯示操作(批處理和合并操作)中,字體渲染器需要盡可能多地緩存字形符號。

使用預(yù)緩存技術(shù)的主要優(yōu)勢在于,可以完全或者最小化紋理加載的時間。紋理加載操作是消耗非常大的,它會推延CPU或者GPU。甚至在幀渲染過程中,改變紋理還會在GPU體系結(jié)構(gòu)帶來更多內(nèi)存的壓力。

ImaginationTech的PowerVRml SGX GPUs使用了延遲疊加技術(shù)架構(gòu),可以提供很多有趣的特性。但如果在渲染幀時需要修改紋理,會強(qiáng)制要求驅(qū)動程序?qū)y理進(jìn)行復(fù)制。因為字體結(jié)構(gòu)相當(dāng)大,如果不好好處理紋理加載的話,很容易就內(nèi)存耗盡了。

這樣的場景確實發(fā)生在Google Play的一個應(yīng)用中。這個APP是一個簡單的計算器,僅使用一些數(shù)學(xué)符號和數(shù)字進(jìn)行簡單的繪制按鈕。字體渲染器在某的時候甚至渲染不出***幀。因為按鈕 是連續(xù)進(jìn)行繪制的,每一個按鈕都會觸發(fā)一個紋理加載,然后復(fù)制整個字體緩存。系統(tǒng)根本沒有這么多內(nèi)存去存儲這么多緩存的備份。

清空緩存

因為用作字形緩存的紋理是非常大的,它們有時會被系統(tǒng)回收再利用,以便為其它程序更多的RAM。

當(dāng)用戶隱藏當(dāng)前的應(yīng)用時,系統(tǒng)給應(yīng)用發(fā)送一條消息要求釋放盡可能多的內(nèi)存。很明顯,這就需要銷毀***的字形緩存結(jié)構(gòu)。在Android中,這個大緩存結(jié)構(gòu)就是所有字形的緩存。除了默認(rèn)***個創(chuàng)建的以外(1024*512的默認(rèn)緩存)。

紋理結(jié)構(gòu)在沒有存儲空間的時會被清空。字體渲染器使用LRU算法對素有字體進(jìn)行記錄,僅僅是記錄而已。如果需要,就會根據(jù)最近最少使用的紋理來清除內(nèi)存。目前沒有提供這個操作,但是它確實是一個不錯的優(yōu)化策略。

批處理和合并操作

Android4.3引入的繪制批處理和合并操作是一項重要的優(yōu)化,徹底減少了大量往OpenGL驅(qū)動發(fā)送指令的問題。

為了進(jìn)行合并操作,字體渲染器在進(jìn)行多種繪制調(diào)用的時候會緩存文字,每個緩存紋理都會擁有一個客戶端的2048 quads的數(shù)組(1 quad = 1 glyph)。當(dāng)調(diào)用lilbhwui中的一個文字繪制API時,字體渲染器獲取合適的網(wǎng)格為每個字形符號進(jìn)行位置和u/v坐標(biāo)的繪制。網(wǎng)格在批處理的末 端被發(fā)送到GPU上(由延遲顯示系統(tǒng)決定)。或者當(dāng)一個quad的緩沖區(qū)滿了的時候,可能會出現(xiàn)多網(wǎng)格渲染同一個字符串的情況——一個字符緩存占用一個網(wǎng) 格。

這個優(yōu)化過程很容易實現(xiàn),對顯示效果幫助也很大。因為字體渲染器使用多緩存結(jié)構(gòu),所以在一個字符串的渲染過程匯總,可能字形符號會來自不同的紋理。 如果沒有批處理好合并操作的話,每個繪制調(diào)用都要傳遞給GPU。字體渲染器就需要不斷切換不同的緩存結(jié)構(gòu),這樣會帶來很大的消耗。

在測試字體渲染器的時候,我已經(jīng)在一個測試App中發(fā)現(xiàn)了這個問題。這個App只是簡單地用不同的樣式和大小渲染一句“hello world”。其中字母“o”被存儲在不同的紋理中,和其它的字符不一樣。這種情況導(dǎo)致字體渲染器開始時只繪制了“hell”,然后渲染“o”,然后再渲 染“w”,然后在渲染“o”,接著才是“rld”。這5個繪制調(diào)用和5個紋理進(jìn)行綁定連接后,只有其中兩個是實際需要的,現(xiàn)在渲染器先繪制“hell w rld”,然后在一起繪制兩個“o”,這就是批處理和合并操作的好處了。

優(yōu)化紋理加載

之前提到過字體渲染在更新緩存紋理的時候(記錄每個紋理中的臟數(shù)據(jù)塊)會盡可能加載少一點數(shù)據(jù)。但是很不幸,這個方法還是有兩個限制。

首先,OpenGL ES2.0不允許隨意上傳一個矩形區(qū)域。glTextSubImage2D 會讓你指定矩形的x/y坐標(biāo)和寬高來更新矩形里面的紋理。并且它會把矩形的寬當(dāng)做內(nèi)存里的數(shù)據(jù)幅度,這個可以通過創(chuàng)建一個合適大小的CPU緩沖區(qū)來解決, 但是也需要事先知道這個矩形的到底有多大。

有一個很好的折衷,就是加載包含臟數(shù)據(jù)塊(矩形)的最小像素帶。因為這個像素帶和紋理一樣寬,這樣就可以節(jié)省空間。比每次都要更新整個紋理效果好得多。

第二個問題是紋理加載屬于異步調(diào)用,這樣可能造成相當(dāng)長的CPU延遲(甚至可能會達(dá)到1毫秒,依賴紋理的大小、驅(qū)動程序還有GPU)。像之前說的那 樣,如果使用預(yù)緩存應(yīng)該是沒有問題的。但是如果使用的是“重字體”的場景,或者是區(qū)域化語言的場景的話(較多的使用字形符號比如中文),那么問題就還是會 出現(xiàn)的。

令人欣慰的是,OpenGL3.0為這兩個問題提供了解決方案,這樣就可以直接使用一個像素存儲的屬性來加載數(shù)據(jù)矩形了。GL_UNPACK_ROW_LENGTH這個屬性指定了內(nèi)存源數(shù)據(jù)的寬度。需要注意的是,這個屬性會影響到當(dāng)前OpenGL上下文的全局狀態(tài)。

加載紋理時,CPU延遲可以通過使用像素緩沖對象(PBOs)來避免。就像所有OpenGL里的緩沖區(qū)對象一樣PBO會駐留在GPU中,但也可以映 射到內(nèi)存中。PBOs有很多有趣的屬性,但是我們關(guān)心的是一個在主存中取消映射關(guān)系后還可以進(jìn)行異步加載紋理的屬性,此時操作隊列變成:

glMapBufferRange → write glyphs to buffer → glUnmapBuffer → glPixelStorei(GL_UNPACK_ROW_LENGTH) → glTexSubImage2D

調(diào)用glTexSubImage2D可以立即返回,而不用阻塞渲染器,字體渲染器可以在內(nèi)存中映射整個緩沖區(qū),而且似乎不會出現(xiàn)問題。這對于緩存紋理的更新操作是一個不錯的方案。

這兩種OpenGL ES3.0的優(yōu)化方法會出現(xiàn)在Android4.4中。

陰影效果

一般文字在渲染的時候都會帶有陰影效果,這是一個相當(dāng)耗費資源的操作。在臨近的字形符號可以進(jìn)行相互模糊操作之后,字體渲染器不再進(jìn)行獨立的預(yù)模糊 操作。有很多中方法可以實現(xiàn)模糊化,但是為了在同一幀中把這些調(diào)配操作和紋理采樣操作最小化,陰影效果會被簡單存儲為紋理,在多幀切換的時候可以保存。

因為應(yīng)用程序可以輕易地拖垮GPU,所以我們還是得依靠CPU來對文字進(jìn)行模糊化。最簡單和高效的方式就是使用Renderscript的C++ API,只需要簡單幾行代碼就可以實現(xiàn)核心功能。最簡單的方法是在初始化Renderscript的時候指定RS_INIT_LOW_LATENCY標(biāo)記 來強(qiáng)制運行在CPU上。

未來的優(yōu)化操作

有一個優(yōu)化方法我希望可以在我離開Android團(tuán)隊之前實現(xiàn)。文字預(yù)緩存、異步和部分紋理更新都是一些重要的優(yōu)化操作。但是柵格化文字符號一直都是一個很耗費資源的操作,在systrace可以很容易看到(啟用gfs標(biāo)識然后看precacheText事件)。

對預(yù)緩存的一個簡單的優(yōu)化方式就是,把這個操作放到另一個工作線程去執(zhí)行,把柵格化操作放到后臺。這個技術(shù)已經(jīng)被用到一些復(fù)雜的路徑柵格化操作中,但是沒有添加到OpenGL架構(gòu)之中。

改進(jìn)批處理和合并操作也是一個可能的優(yōu)化方式,用于繪制文字的顏色一般是被發(fā)送到一個fragment陰影統(tǒng)一操作。這樣可以減少發(fā)送到GPU的頂 點數(shù)據(jù),但副作用會產(chǎn)生很多不需要的批處理指令:一個批處理操作只能包含一種文字顏色。如果文字顏色也存儲為頂點屬性,那么就可以網(wǎng)GPU傳遞更少的數(shù) 據(jù)。

源代碼

如果想詳細(xì)地看看字體渲染器的實現(xiàn),可以瀏覽libhwui的GitHub,可以從FontRender.cpp開始,因為很多驚喜都在這里發(fā)生,它的支持類可以在font或者sub目錄找到。對了,PixelBuffer.cpp這個文件也不錯,可以看看。這就是一個像素緩沖區(qū)的抽象實現(xiàn),可以用于CPU(uint8_t類型的數(shù)組)或者GPU緩沖區(qū)(PBO)。

***的話

本文只是對Android的字體渲染器進(jìn)行簡單介紹,還有很多實現(xiàn)的細(xì)節(jié)沒有考慮到,或者很多問題以后會說明,所以有什么問題可以盡管向我提問。

原文鏈接: medium   翻譯: chris

譯文鏈接: http://blog.jobbole.com/70468/

責(zé)任編輯:閆佳明 來源: blog.jobbole
相關(guān)推薦

2010-08-13 11:02:27

Flex渲染器

2009-07-15 13:48:26

Swing模型和渲染器

2009-07-16 10:26:49

渲染器接口Swing

2009-11-23 19:51:48

ibmdwWeb

2009-07-16 10:11:06

渲染器RendererSwing組件

2017-12-26 14:27:24

2010-08-13 11:21:31

Flex渲染器

2017-05-10 14:47:37

Headless Ch頁面 Docker

2019-08-01 15:19:26

前端開發(fā)技術(shù)

2022-04-18 08:09:44

渲染器DOM掛載Vue.js

2010-06-30 13:45:05

ZKZK 5.0.3

2014-04-29 14:16:54

2023-05-24 16:41:41

React前端

2020-11-06 15:20:45

瀏覽器前端架構(gòu)

2012-06-01 10:28:54

Web

2012-06-06 15:57:29

Web

2022-08-14 23:04:54

React前端框架

2024-02-22 13:47:40

2022-07-04 08:29:13

electron通信

2015-09-16 13:54:30

Android性能優(yōu)化渲染
點贊
收藏

51CTO技術(shù)棧公眾號