像素是如何繪製到屏幕上面的?把數據輸出到屏幕的方法有不少,經過調用不少不一樣的framework和不一樣的函數。這裏咱們講一下這個過程背後的東西。但願可以幫助你們瞭解何時該使用什麼API,特別是當遇到性能問題須要調試的時候。固然,咱們這裏主要講iOS,可是事實上,不少東西也是能夠應用到OSX上面的。html
繪製屏幕的過程當中又不少都是不被人瞭解的。可是一旦像素被繪製到屏幕上面,那麼像素就是有3種顏色組成:紅綠藍。這3個顏色單元經過特定的強弱組合造成一個特定的顏色。對於iPhone5 IPS_LCD 的分辨率是1,136×640 = 727,040個像素,也就是有2,181,120個顏色單元。對於一個15寸高清屏幕的MacBook Pro來講,這個數字差很少是1500萬。Graphics Stack 就是確保每個單元的強弱都正確。當滑動整個屏幕的時候,上百萬的顏色單元須要在每秒60次的更新。nginx
下面是一個簡單的例子,整個軟件看起來是這個樣子:程序員
顯示器上面的就是GPU,圖像處理單元。GPU是一個高度併發計算的硬件單元,特別是處理圖形圖像的並行計算。這就是爲何能夠這麼快的更新像素並輸出到屏幕的緣由。並行計算的設計讓GPU能夠高效的混合圖像紋理。咱們會在後面詳細解釋混合圖像紋理這個過程。如今須要知道的就是GPU是被高度優化設計的,所以很是適合計算圖像這種類型的工做。他比CPU計算的更快,更節約能耗。由於CPU是爲了更通常的計算設計的硬件。CPU雖然能夠作不少事情,可是在圖像這方面仍是遠遠慢於GPU。算法
GPU驅動是一些直接操做GPU的代碼,因爲各個GPU是不一樣的,驅動在他們之上建立一個層,這個層一般是OpenGL/OpenGL ES。數組
OpenGL(Open Graphics Library)是用來作2D和3G圖形圖像渲染的API。因爲GPU是一個很是定製化的硬件,OpenGL和GPU緊密合做充分發揮GPU的能力來實現圖形圖像渲染硬件加速。對大多數狀況,OpenGL太底層了。可是當1992年第一個版本發佈後(20多年前),它就成爲主流的操做GPU的方式,而且前進了一大步。由於程序員不再用爲了每個GPU編寫不一樣的應用程序。緩存
在OpenGL上面,分開了幾個。iOS設備幾乎全部的東西變成了Core Animation,可是在OSX,繞過Core Animation而使用Core Graphic 並非不常見。有一些特別的應用程序,特別是遊戲,可能直接使用OpenGL/OpenGL ES. 而後事情變得讓人疑惑起來,由於有些渲染Core Animation 使用 Core Graphic。相似AVFoundation, Core Image 這樣的框架,或是其餘的一些混合的方式。安全
這裏提醒一件事情, GPU是一個強有力的圖形圖像硬件,在顯示像素方面起着核心做用。它也鏈接着CPU。從硬件方面講就是有一些總線把他們鏈接了起來。也有一些框架好比 OpenGL, Core Animation。Core Graphic控制GPU和CPU之間的數據傳輸。爲了讓像素可以顯示到屏幕上面,有一些工做是須要CPU的。而後數據會被傳給GPU,而後數據再被處理,最後顯示到屏幕上面。bash
每個過程當中都有本身的挑戰,在這個過程當中也存在不少權衡。markdown
這是一個很簡單的圖表用來描述一個挑戰。GPU有紋理(位圖)合成爲一幀(好比1秒60幀)每個紋理佔用VRAM(顯卡)所以GPU一次處理的紋理有大小限制。GPU處理合成方面很是高效,可是有一些合成任務比其餘要複雜,因此GPU對處理能力有一個不能超過16.7ms的限制(1秒60幀)。session
另外一個挑戰是把數據傳給GPU。爲了讓GPU可以訪問數據,咱們須要把數據從內存複製到顯存。這個過程叫作上傳到GPU。這個可能看上去不重要,可是對於一個大的紋理來講,會很是耗時。
最後CPU運行程序。你可能告訴CPU從資源文件夾中加載一個PNG圖片,並解壓。這些過程都發生在CPU。當須要顯示這些解壓的圖片時,就須要上傳數據到GPU。一些事情看似很是簡單,好比顯示一段文字,對CPU來講是一個很是複雜的任務。須要調用Core Text 和 Core Graphic框架去根據文字生成一個位圖。完成後,以紋理的方式上傳到GPU,而後準備顯示。當你滑動或是移動一段屏幕上面的文字時,一樣的紋理會被重用,CPU會簡單的告訴GPU只是須要一個新的位置,因此GPU能夠從新利用現有的紋理。CPU不須要從新繪製文字,位圖也不須要從新上傳到GPU。
上面的有一點複雜,在有一個總體概念以後,咱們會開始解釋裏面的技術細節。
圖像合成的字面意思就是把不一樣的位圖放到一塊兒建立成最後的圖像而後顯示到屏幕上面。在不少方面來看,這個過程都是顯而易見的,因此很容易忽視其中的複雜性和運算量。
讓咱們忽視一些特殊狀況,假設屏幕上面都是紋理。紋理就是一個RGBA值的矩形區域。每個像素包括紅,綠,藍,透明度。在Core Animation世界裏面,基本上至關於CALayer。
在這個簡單的假設中,每個層是一個紋理,全部的紋理經過棧的方式排列起來。屏幕上的每個像素,CPU都須要明白應該如何混合這些紋理,從而獲得相對應的RGB值。這就是合成的過程。
若是咱們只有一個紋理,並且這個紋理和屏幕大小一致。每個像素就和紋理中得一個像素對應起來。也就是說這個紋理的像素就是最後屏幕顯示的樣子。
若是咱們有另外一個紋理,這個紋理覆蓋在以前的紋理上面。GPU須要首先把第二個紋理和第一個紋理合成。這裏面有不一樣的覆蓋模式,可是若是咱們假設全部的紋理都是像素對齊且咱們使用普通的覆蓋模式。那麼最後的顏色就是經過下面的公式計算出來的。
R = S + D * (1 - Sa)
最後的結果是經過源的顏色(最上面的紋理)加目標顏色(下面的紋理) 乘以(1 – 源顏色的透明度)公式裏面全部的顏色就假定已經預先乘以了他們的透明度。
很顯然,這裏面很麻煩。讓咱們再假設全部的顏色都是不透明的,也就是alpha = 1. 若是目標紋理(下面的紋理)是藍色的(RGB = 0,0,1)源紋理(上面的紋理)是紅色(RGB = 1,0,0)。由於Sa = 1, 那麼這個公式就簡化爲
R = S
結果就是源的紅色,這個和你預期一致。
若是源(上面的)層50%透明,好比 alpha = 0,5. 那麼 S 的RGB值須要乘以alpha會變成 (0.5,0,0)。這個公式會變成這個樣子
0.5 0 0.5 R = S + D * (1 - Sa) = 0 + 0 * (1 - 0.5) = 0 0 1 0.5
咱們最後獲得的RGB顏色是紫色(0.5, 0, 0.5) 。這個和咱們的直覺預期一致。透明和紅色和藍色背景混合後成爲紫色。
要記住,這個只是把一個紋理中的一個像素和另外一個紋理中的一個像素合成起來。GPU須要把2個紋理之間覆蓋的部分中的像素都合成起來。你們都知道,大多數的app都有多層,所以不少紋理須要被合成起來。這個對GPU的開銷很大,即使GPU已是被高度硬件優化的設備。
當源紋理是徹底不透明,最終的顏色和源紋理同樣。這就能夠節省GPU的不少工做,由於GPU能夠簡單的複製源紋理而不用合成全部像素值。可是GPU沒有辦法區別紋理中的像素是不透明的仍是透明。只有程序員才能知道CALayer裏面的究竟是什麼。這也就是CAlayer有opaque屬性的緣由。若是opaque = YES, 那麼GPU將不會作任何合成計算,而是直接直接簡單的複製顏色,無論下面還有什麼東西。GPU能夠減小大量的工做。這就是Instruments(Xcode 的性能測試工具)中 color blended layers 選項作的事情。(這個選項也在模擬器菜單裏面)。它可讓你瞭解哪個層(紋理)被標記成透明,也就是說,GPU須要作合成工做。合成不透明層要比透明的層工做量少不少,由於沒有那麼多的數學運算在裏面。
若是你知道哪個層是不透明的,那麼必定確保opaque = YES。若是你載入一個沒有alpha通道的image,並且在UIImageView顯示,那麼UIImageView會自動幫你設置opaque = YES。可是須要注意一個沒有alpha通道的圖片和每一個地方的alpha都是100%的圖片區別很大。後面的狀況,Core Animation 須要假定全部像素的alpha都不是100%。在Finder中,你可使用Get Info而且檢查More Info部分。它將告訴你這張圖片是否擁有alpha通道。
到目前爲止,咱們考慮的層都是完美的像素對齊的。當全部的像素都對齊時,咱們有一個相對簡單的公式。當GPU判斷屏幕上面的一個像素應該是什麼時,只須要看一下覆蓋在屏幕上面的全部層中的單個像素,而後把這些像素合成起來,或者若是最上面的紋理是不透明的,GPU只須要簡單的複製最上面的像素就行了。
當一個層上面的全部像素和屏幕上面的像素完美對應,咱們就說這個層是像素對齊的。主要有2個緣由致使可能不對齊。第一個是放大縮小;當放大或是縮小是,紋理的像素和屏幕像素不對齊。另外一個緣由是當紋理的起點不在一個像素邊界上。
這2種狀況,GPU不得不作額外的計算。這個須要從源紋理中混合不少像素來建立一個像素用來合成。當全部像素對齊時,GPU就能夠少作不少工做。
注意,Core Animation Instrument和模擬器都有color misaligned images 選項,當CALayer中存在像素不對齊的時候,把問題顯示出來。
一個層能夠有一個和它相關聯的遮罩。遮罩是一個有alpha值的位圖,並且在合成像素以前須要被應用到層的contents屬性上。當你這頂一個層爲圓角時,一就在設置一個遮罩在這個層上面。然而,咱們也能夠指定一個任意的遮罩。好比咱們有一個形狀像字母A的遮罩。只有CALayer的contents中的和字母A重合的一部分被會被繪製到屏幕。
離屏渲染能夠被Core Animation 自動觸發或是應用程序手動觸發。離屏渲染繪製layer tree中的一部分到一個新的緩存裏面(這個緩存不是屏幕,是另外一個地方),而後再把這個緩存渲染到屏幕上面。
你可能但願強制離屏渲染,特別是計算很複雜的時候。這是一種緩存合成好的紋理或是層的方式。若是你的呈現樹(render tree)是複雜的。那麼就但願強制離屏渲染到緩存這些層,而後再使用緩存合成到屏幕。
若是你的APP有不少層,並且但願增長動畫。GPU通常來講不得不從新合成全部的層在1秒60幀的速度下。當使用離屏渲染時,GPU須要合成這些層到一個新的位圖紋理緩存裏面,而後再用這個紋理繪製到屏幕上面。當這些層一塊兒移動時,GPU能夠重複利用這個位圖緩存,這樣就能夠提升效率。固然,若是這些層沒有修改的化,纔能有效。若是這些層被修改了,GPU就不得不從新建立這個位圖緩存。你能夠觸發這個行爲,經過設置shouldRasterize = YES
這是一個權衡,若是隻是繪製一次,那麼這樣作反而會更慢。建立一個額外的緩存對GPU來講是一個額外的工做,特別是若是這個位圖永遠沒有被複用。這個實在是太浪費了。然而,若是這個位圖緩存能夠被重用,GPU也可能把緩存刪掉了。因此你須要計算GPU的利用率和幀的速率來判斷這個位圖是否有用
離屏渲染也能夠在一些其餘場景發生。若是你直接或是間接的給一個層增長了遮罩。Core Animation 會爲了實現遮罩強制作離屏渲染。這個增長了GPU的負擔,由於通常上來,這些都是直接在屏幕上面渲染的。
Instrument的Core Animation 有一個叫作Color Offscreen-Rendered Yellow的選項。它會將已經被渲染到屏幕外緩衝區的區域標註爲黃色(這個選項在模擬器中也能夠用)。同時確保勾選Color Hits Green and Misses Red選項。綠色表明不管什麼時候一個屏幕外緩衝區被複用,而紅色表明當緩衝區被從新建立。
通常來講,你須要避免離屏渲染。由於這個開銷很大。在屏幕上面直接合成層要比先建立一個離屏緩存而後在緩存上面繪製,最後再繪製緩存到屏幕上面快不少。這裏面有2個上下文環境的切換(切換到屏幕外緩存環境,和屏幕環境)。
因此當你打開Color Offscreen-Rendered Yellow後看到黃色,這即是一個警告,但這不必定是很差的。若是Core Animation可以複用屏幕外渲染的結果,這便可以提高性能,當繪製到緩存上面的層沒有被修改的時候,就能夠被複用了。
注意,緩存位圖的尺寸大小是有限制的。Apple 提示大約是2倍屏幕的大小。
若是你使用的層引起了離屏渲染,那麼你最好避免這種方式。增長遮罩,設置圓角,設置陰影都形成離屏渲染。
對於遮罩來講,圓角只是一個特殊的遮罩。clipsToBounds 和 masksToBounds 2個屬性而已。你能夠簡單的建立一個已經設置好遮罩的層建立內容。好比,使用已經設置了遮罩的圖片。固然,這個也是一種權衡。若是你但願在層的contents屬性這隻一個矩形的遮罩,那你更應該使用contentsRect而不是使用遮罩。
若是你最後這是shouldRasterize = YES,記住還要設置rasterizationScale = contentsScale
一般,維基百科上面有許多關於圖像合成的背景知識。咱們這裏簡單的拓展一下像素中的紅、綠、藍以及alpha是如何呈如今內存中的。
若是你在OSX上面工做,你會發現大部分的這些調試選項在一個獨立的叫作「Quartz Debug」的程序裏面。而並不在 Instruments 中。Quartz Debug是Graphics Tools中的一部分,這能夠在蘋果的developer portal中下載到。
就像名字所建議的那樣,Core Animation 讓咱們能夠建立屏幕動畫。咱們將跳過大部分的動畫,關注於繪製部分。重要的是,Core Animation容許你坐高效的渲染。這就是爲何你能夠經過Core Animation 實現每秒60幀的動畫。
Core Animation 的核心就是基於OpenGL ES的抽象。簡單說,它讓你使用OpenGL ES的強大能力而不須要知道OpenGL ES的複雜性。當我討論像素合成的時候,咱們提到的層(layer)和 紋理(texture)是等價的。他們準確來講不是一個東西,可是缺很是相似。
Core Animation的層能夠有多個子層。因此最後造成了一個layer tree。Core Animation作的最複雜的事情就是判斷出那些層須要被繪製或從新繪製,那些層須要OpenGL ES 去合成到屏幕上面。
例如,當你這是一個layer的contents屬性是一個CGImageRef時,Core Animation建立一個OpenGL 紋理,而後確保這個圖片中的位圖上傳到指定的紋理中。或者,你重寫了-drawInContext方法,Core Animation 會分配一個紋理,確保你的Core Graphics的調用將會被做用到這個紋理中。層的 性質和CALayer的子類會影響OpenGL渲染方式的效率。不少底層的OpenGL ES行爲被簡單的封裝到容易理解的CALayer的概念中去。
Core Animation經過Core Graphics和OpenGL ES,精心策劃基於CPU的位圖繪製。由於Core Animation在渲染過程當中處於很是重要的地位,因此如何使用Core Animation,將會對性能產生極大影響。
當在屏幕上面顯示的時候,有不少組件都參與其中。這裏面有2個主要的硬件分別是CPU和GPU。P和U的意思就是處理單元。當東西被顯示到屏幕上面是,CPU和GPU都須要處理計算。他們也都受到限制。
爲了可以達到每秒60幀的效果,你須要確保CPU和GPU都不能過載。也就是說,即便你當前能達到60fps,你仍是要儘量多的繪製工做交給GPU作。CPU須要作其餘的應用程序代碼,而不是渲染。一般,GPU的渲染性能要比CPU高效不少,同時對系統的負載和消耗也更低一些。
由於繪製的性能是基於GPU和CPU的。你須要去分辨哪個是你繪製的瓶頸。若是你用盡的GPU的資源,GPU是性能的瓶頸,也就是繪製是GPU的瓶頸,反之就是CPU的瓶頸。
若是你是GPU的瓶頸,你須要爲GPU減負(好比把一些工做交給CPU),反之亦然。
若是是GPU瓶頸,可使用OpenGL ES Driver instrument,而後點擊 i 按鈕。配置一下,同時注意查看Device Utilization % 是否被選中。而後運行app。你會看到GPU的負荷。若是這個數字接近100%,那麼你交給GPU的工做太多了。
CPU瓶頸是更加一般的問題。能夠經過Time Profiler instrument,找到問題所在。
經過Core Graphics這個框架名字,Quartz 2D更被人所知。
Quartz 2D 有不少小功能,咱們不會在這裏說起。咱們不會講有關PDF建立,繪製,解析或打印。只須要了解答應PDF和建立PDF和在屏幕上面繪製位圖原理幾乎一致,由於他們都是基於Quartz 2D。
讓咱們簡單瞭解一下Quartz 2D的概念。更多細節能夠參考 Apple 的 官方文檔。
Quartz 2D是一個處理2D繪製的很是強大的工具。有基於路徑的繪製,反鋸齒渲染,透明圖層,分辨率,而且設備獨立等不少特性。由於是更爲底層的基於C的API,因此看上去會有一點讓人恐懼。
主要概念是很是簡單的。UIKit和AppKit都封裝了Quartz 2D的一些簡單API,一旦你熟練了,一些簡單C的API也是很容易理解的。最後你能夠作一個引擎,它的功能和Photoshop同樣。Apple提到的一個 APP,就是一個很好的Quartz 2D例子。
當你的程序進行位圖繪製時,無論使用哪一種方式,都是基於Quartz 2D的。也就是說,CPU經過Quartz 2D繪製。儘管Quartz能夠作其餘事情,可是咱們這裏仍是集中於位圖繪製,好比在緩存(一塊內存)繪製位圖會包括RGBA數據。
比方說,咱們要畫一個八角形,咱們經過UIKit能作到這一點
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)]; [path addLineToPoint:CGPointMake(3.29, 20.83)]; [path addLineToPoint:CGPointMake(0.4, 18.05)]; [path addLineToPoint:CGPointMake(18.8, -0.47)]; [path addLineToPoint:CGPointMake(37.21, 18.05)]; [path addLineToPoint:CGPointMake(34.31, 20.83)]; [path addLineToPoint:CGPointMake(20.88, 7.22)]; [path addLineToPoint:CGPointMake(20.88, 42.18)]; [path addLineToPoint:CGPointMake(16.72, 42.18)]; [path addLineToPoint:CGPointMake(16.72, 7.22)]; [path closePath]; path.lineWidth = 1; [[UIColor redColor] setStroke]; [path stroke];
Core Graphics 的代碼差很少:
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22); CGContextAddLineToPoint(ctx, 3.29, 20.83); CGContextAddLineToPoint(ctx, 0.4, 18.05); CGContextAddLineToPoint(ctx, 18.8, -0.47); CGContextAddLineToPoint(ctx, 37.21, 18.05); CGContextAddLineToPoint(ctx, 34.31, 20.83); CGContextAddLineToPoint(ctx, 20.88, 7.22); CGContextAddLineToPoint(ctx, 20.88, 42.18); CGContextAddLineToPoint(ctx, 16.72, 42.18); CGContextAddLineToPoint(ctx, 16.72, 7.22); CGContextClosePath(ctx); CGContextSetLineWidth(ctx, 1); CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextStrokePath(ctx);
問題就是,繪製到哪裏呢? 這就是CGContext作的事情。咱們傳遞的ctx這個參數。這個context定義了咱們繪製的地方。若是咱們實現了CALayer的-drawInContext:方法。咱們傳遞了一個參數context。在context上面繪製,最後會在layer的一個緩存裏面。咱們也能夠建立咱們本身的context,好比 CGBitmapContextCreate()。這個函數返回一個context,而後咱們能夠傳遞這個context,而後在剛剛建立的這個context上面繪製。
這裏咱們發現,UIKit的代碼並無傳遞context。這是由於UIKit或AppKit的context是隱形的。UIKit和UIKit維護着一個context棧。這些UIKit的方法始終在最上面的context繪製。你可使用UIGraphicsPushContext()和 UIGraphicsPopContext()來push和pop對應的context。
UIKit有一個簡單的方式,經過 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext()來建立一個位圖context,和 CGBitmapContextCreate()同樣。混合UIKit和 Core Graphics調用很簡單。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2); CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextBeginPath(ctx); CGContextMoveToPoint(ctx, 16.72, 7.22); CGContextAddLineToPoint(ctx, 3.29, 20.83); ... CGContextStrokePath(ctx); UIGraphicsEndImageContext();
或其餘方式
CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo); CGContextScaleCTM(ctx, 0.5, 0.5); UIGraphicsPushContext(ctx); UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(16.72, 7.22)]; [path addLineToPoint:CGPointMake(3.29, 20.83)]; ... [path stroke]; UIGraphicsPopContext(ctx); CGContextRelease(ctx);
經過 Core Graphics能夠作不少有趣的事情。蘋果的文檔有不少例子,咱們這裏就不太細說他們了。可是Core Graphics有一個很是接近Adobe Illustrator和Adobe Photoshop如何工做的繪圖模型,而且大多數工具的理念翻譯成Core Graphics了。畢竟這就是NextStep一開始作的。
一件很是值得提起的事,即是CGLayer。它常常被忽視,而且它的名字有時會形成困惑。他不是Photoshop中的圖層的意思,也不是Core Animation中的層的意思。
把CGLayer想象成一個子context。它共用父context的全部特性。你能夠獨立於父context,在它本身的緩存中繪製。而且由於它跟context緊密的聯繫在一塊兒,CGLayer能夠被高效的繪製到context中。
何時這將變得有用呢?若是你用Core Graphics來繪製一些至關複雜的,而且部份內容須要被從新繪製的,你只需將那部份內容繪製到CGLayer一次,而後即可繪製這個CGLayer到父context中。這是一個很是優雅的性能竅門。這和咱們前面提到的離屏繪製概念有點相似。你須要作出權衡,是否須要爲CGLayer的緩存申請額外的內存,肯定這是否對你有所幫助。
屏幕上面的像素是經過3個顏色組成的:紅,綠,藍。所以位圖數據有時候也被成爲RGB數據。你可能想知道這個數據在內存中是什麼樣子。可是實際上,有很是很是多的方式。
後面咱們會提到壓縮,這個和下面講得徹底不同。如今咱們看一下RGB位圖數據。RGB位圖數據的每個值有3個組成部分,紅,綠,藍。更多的時候,咱們有4個組成部分,紅,綠,藍,alpha。這裏咱們講4個組成部分的狀況。
iOS和OS X上面的最一般的文件格式是32 bits-per-pixel (bpp),8 bits-per-component (bpc),alpha會被預先計算進去。在內存裏面像這個樣子
A R G B A R G B A R G B | pixel 0 | pixel 1 | pixel 2 0 1 2 3 4 5 6 7 8 9 10 11 ...
這個格式常常被叫作ARGB。每個像素使用4個字節,每個顏色組件1個字節。每個像素有一個alpha值在R,G,B前面。最後RGB分別預先乘以alpha。若是咱們有一個橘黃的顏色。那麼看上去就是 240,99,24. ARGB就是 255,240,99,24 若是咱們有一個一樣的顏色,可是alpha是0.33,那麼最後的就是 ARGB就是 84,80,33,8
另外一個常見的格式是32bpp,8bpc,alpha被跳過了:
x R G B x R G B x R G B | pixel 0 | pixel 1 | pixel 2 0 1 2 3 4 5 6 7 8 9 10 11 ...
這個也被稱爲xRGB。像素並無alpha(也就是100%不透明),可是內存結構是相同的。你可能奇怪爲何這個格式很流行。由於若是咱們把這個沒用的字節從像素中去掉,咱們能夠節省25%的空間。實際上,這個格式更適合現代的CPU和圖像算法。由於每個獨立的像素和32字節對齊。現代CPU不喜歡讀取不對其的數據。算法會處理大量的位移,特別是這個格式和ARGB混合在一塊兒的時候。
當處理xRGB時,Core Graphic也須要支持把alpha放到最後的格式,好比RGBA,RGBx(RGB已經預先乘以alpha的格式)
大多數時候,當咱們處理位圖數據時,咱們就在使用Core Graphic 或是 Quartz 2D。有一個列表包括了全部的支持的文件格式。讓咱們先看一下剩餘的RGB格式。
有16bpp,5bpc,不包括alpha。這個格式比以前節省50%空間(2個字節一個像素)。可是若是解壓成RGB數據在內存裏面或是磁盤上面就有用了。可是,由於只有5個字節一個像素,圖像特別是一些平滑的漸變,可能就混合到一塊兒了。(圖像質量降低)。
還有一個是64bpp,16bpc,最終爲128bpp,32bpc,浮點數組件(有或沒有alpha值)。它們分別使用8字節和16字節,而且容許更高的精度。固然,這會形成更多的內存和更復雜的計算。
最後,Core Graphics 也支持一些其餘格式,好比CMYK,還有一些只有alpha的格式,好比以前提到的遮罩。
大多數的框架(包括 Core Graphics)使用的像素格式是混合起來的。這就是所謂的 planar components, or component planes。每個顏色組件都在內存中的一個區域。好比,對於RGB數據。咱們有3個獨立的內存空間,分別保存紅色,綠色,和藍色的數值。
在某些狀況下,一些視頻框架會使用 Planar Data。
YCbCr 是一個常見的視頻格式。一樣有3個部分組成(Y,Cb,Cr)。可是它更傾向於人眼識別的顏色。人眼是很難精確識別出來Cb和Cr的色彩度。可是卻能很容易識別出來Y的亮度。在相同的質量下,Cb和Cr要比Y壓縮的更多。
JPEG有時候把RGB格式轉換爲YCbCr格式。JPEG單獨壓縮每個color plane。當壓縮YCbCr格式時,Cb和Cr比Y壓縮得更好。
iOS和OSX上面的大多數圖片都是JPEG和PNG格式。下面咱們再瞭解一下。
每一個人都知道JPEG,他來自相機。他表明了圖片是圖和存儲在電腦裏,即時是你的媽媽也聽過JPEG。
你們都認爲JPEG就是一個像素格式。就像咱們以前提到的RGB格式同樣,可是實際上並非這樣。
真正的JPEG數據變成像素是一個很是複雜的過程。一個星期都沒有辦法講清楚,或是更久。對於一個color plane, JPEG使用一種離散餘弦變換的算法。講空間信息轉換爲頻率(convert spatial information into the frequency domain)。而後經過哈夫曼編碼的變種來壓縮。一開始會把RGB轉換成YCbCr,解壓縮的時候,再反過來。
這就是爲何從一個JPEG文件建立一個UIImage而後會知道屏幕上面會有一點點延遲的緣由。由於CPU正在忙於解壓圖片。若是每一個TableViewCell都須要解壓圖片的話,那麼你的滾動效果就不會平滑。
那麼,爲何使用JPEG文件呢?由於JPEG能夠把圖片壓縮的很是很是好。一個沒有壓縮過的IPhone5拍照的圖片差很少24MB。使用默認的壓縮設置,這個只有2-3MB。JPEG壓縮效果很是好,由於幾乎沒有損失。他把那些人眼不能識別的部分去掉了。這樣作能夠遠遠的超過gzip這樣的壓縮算法。可是,這個僅僅在圖片上面有效。由於,JPEG依賴於丟掉那些人眼沒法識別的數據。若是你從一個基本是文本的網頁截取一張圖片,JPEG就不會那麼高效,壓縮效率會變得低下。你甚至均可以看出圖片已經變形了。
PNG讀做「ping」,和JPEG相反,他是無損壓縮的。當你保存圖片成PNG時,而後再打開。全部的像素數據和以前的徹底同樣。由於有這個限制,全部PNG壓縮圖片的效果沒有JPEG那麼好。可是對於app中的設計來講,好比按鈕,icon,PNG就很是適合。並且PNG的解碼工做要比JPEG簡單不少。
在真實的世界裏面,事情沒有這麼簡單。有不少不一樣的PNG格式。維基百科上面有不少細節。可是簡單說,PNG支持壓縮有alpha或是沒有alpha通道的RGB像素,這也就是爲何他適合app上面的緣由。
當在app中使用顏色是,你須要使用者2種格式中得一個,PNG和JPEG。他們的解碼和壓縮算法都是被高度硬件優化的。有些狀況甚至支持並行計算。同時Apple也在不斷地提升解碼的能力在將來的操做系統版本中。若是使用其餘格式,這可能會對你的程序性能產生影響,並且可能會產生漏洞,由於圖像解碼的算法是黑客們最喜歡攻擊的目標。
已經講了好多有關PNG的優化了,你能夠在互聯網上面本身查找。這裏須要注意一點,Xcode的壓縮算法和大部分的壓縮引擎不同。
當Xcode壓縮png時,技術上來講,已經不是一個有效的PNG文件了。可是iOS系統能夠讀取這個文件,而後比一般的PNG圖片處理速度更快。Xcode這樣作,是爲了更好地利用解碼算法,而這些解碼算法不能在通常的PNG文件上面適用。就像上面提到的,有很是多的方法去表示RGB數據。並且若是這個格式不是iOS圖形圖像系統須要的,那麼就須要增長額外的計算。這樣就不會有性能上的提升了。
再搶到一次,若是你能夠,你須要設置 resizable images。你的文件會變得更小,所以,這樣就會有更小的文件須要從文件系統裏面讀取,而後在解碼。
UIKit中得每個view都有本身的CALayer,通常都有一個緩存,也就是位圖,有一點相似圖片。這個緩存最後會被繪製到屏幕上面。
若是你的自定義view的類實現了-drawRest:,那麼就是這樣子工做的:
當你調用-setNeedsDisplay時,UIKit會調用這個view的層的 -setNeedsDisplay方法。這個設置一個標記,代表這個層已經髒了(dirty,被修改了)。實際上,並無作任何事情,因此,調用屢次-setNeedsDisplay 沒有任何問題。
當渲染系統準備好後,會調用層的-display方法。這時,層會設置緩存。而後設置緩存的Core Graphics的上下文環境(CGContextRef)。後面的繪製會經過這個CGContextRef繪製到緩存中。
當你調用UIKit中的函數,好比UIRectFill() 或者 -[UIBezierPath fill]時,會經過這個CGContextRef調用你的drawRect方法。他們是經過把上面的CGContextRef push 到 圖形圖像堆棧中,也就是設置成當前的上下文環境。UIGraphicsGetCurrent()會返回剛纔push的那個context。因爲UIKit繪製方法使用UIGraphicsGetCurrent(),因此這些繪製會被繪製到緩存中。若是你但願直接使用 Core Graphics 方法,那麼你須要調用UIGraphicsGetCurrent()方法,而後本身手動傳遞context參數到Core Graphics的繪製函數中去。
那麼,一個個層的緩存都會被繪製到屏幕上面,知道下一次設置-setNeedsDisplay,而後再從新更新緩存,再重複上面的過程。
當你使用UIImageView的時候,有一點點的不一樣。這個view依然包含一個CALayer,可是這個層並不會分配一個緩存空間。而是使用CGImageRef做爲CALayer的contents屬性,渲染系統會把這個圖片繪製到幀的緩存,好比屏幕。
這個狀況下,就沒有繼續繪製的過程了。咱們就是簡單的經過傳遞位圖這種方式把圖片傳遞給UIImageView,而後傳遞給Core Animation,而後傳遞給渲染系統。
聽上去不怎麼樣,可是,最快速的方法,就是不使用。
大多數狀況,你能夠經過自定義view或是組合其餘層來實現。能夠看一下Chris的文章,有關自定義控件。這個方法是推薦的,由於UIKit很是高效。
當你須要自定義繪製的時候 WWDC2012 session 506 Optimizing 2D Graphics and Animation Performance是一個很是好的例子 。
另外一個地方須要自定義繪製的是iOS的股票軟件。這個股票圖是經過Core Graphics實現的。注意,這個只是你須要自定義繪製,並非必定要實現drawRect函數,有時候經過UIGraphicsBeginImageContextWithOptions()或是 CGBitmapContextCreate()建立一個額外的位圖,而後再上面繪製圖片,而後傳遞給CALayer的contents會更容易。下面有一個測試例子
這是一個簡單的例子
// Don't do this - (void)drawRect:(CGRect)rect { [[UIColor redColor] setFill]; UIRectFill([self bounds]); }
咱們知道爲何這樣作很爛,咱們讓Core Animation建立了一個額外的緩存,而後咱們讓Core Graphics 在緩存上面填充了一個顏色。而後上傳給了GPU。
咱們能夠不實現-drawRect:函數來省去這些步驟。只是簡單的設置view的backgroundColor就行了。若是這個view有CAGradientLayer,那麼一樣的方法也能夠設置成漸變的顏色。
你能夠簡單的經過可變大小的圖片減小圖形系統的工做壓力。若是你原圖上面的按鈕大小是300*50。那麼就有 600 * 100 = 60k 像素 * 4 = 240KB的內存數據須要傳遞給GPU。傳遞給顯存。若是咱們使用resizable image。咱們可使用一個 52 * 12 大小的圖片,這樣能夠節省10kb的內存。這樣會更快。
Core Animation 經過 contentsCenter 來resize圖片,可是,更簡單的是經過 -[UIImage resizableImageWithCapInsets:resizingMode:]。
並且,在第一次繪製的時候,咱們並不須要從文件系統讀取60K像素的PNG文件,而後解碼。越小的圖片解碼越快。這樣,咱們的app就能夠啓動的更快。
上一個咱們講到了併發。UIKit的線程模型很是簡單,你只能在主線程使用UIKit。因此,這裏面還能有併發的概念?
若是你不得不實現-drawRect:,而且你必須繪製大量的東西,而這個會花費很多時間。並且你但願動畫變得更平滑,除了在主線程中,你還但願在其餘線程中作一些工做。併發的繪圖是複雜的,可是除了幾個警告,併發的繪圖仍是比較容易實現的。
你不能在CAlayer的緩存裏面作任何事情出了主線程,不然很差的事情會發生。可是你能夠在一個獨立的位圖上面繪製。
全部的Core Graphics的繪製方法須要一個context參數,指定這個繪製到那裏去。UIKit有一個概念是繪製到當前的context上。而這個當前的context是線程獨立的。
爲了實現異步繪製,咱們作下面的事情。咱們在其餘隊列(queue,GCD中的概念)中建立一個圖片,而後咱們切換到主隊列中把結果傳遞給UIImageView。這個技術被 WWDC 2012 session 211中提到
- (UIImage *)renderInImageOfSize:(CGSize)size; { UIGraphicsBeginImageContextWithOptions(size, NO, 0); // do drawing here UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return result; }
這個函數經過UIGraphicsBeginImageContextWithOptions 建立一個新的CGContextRef。這個函數也修改了當前的UIKit的context。而後能夠銅鼓UIKit的方法繪製,而後經過UIGraphicsGetImageFromCurrentImageContext()根據位圖數據生成一個UIImage。而後關閉掉建立的這個context。
保證線程安全是很是重要的,好比你訪問UIKit的屬性,必須線程安全。若是你在其餘隊列調用這個方法,而這個方法在你的view類裏面,這個事情就可能古怪了。更簡單的方法是建立一個獨立的渲染類,而後當觸發繪製這個圖片的時候才設置這些必須得屬性。
可是UIKit的繪製函數是能夠在其餘隊列中調用的,只須要保證這些操做在 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext ()以前就好。
你能夠經過下面的方法觸發繪製
UIImageView *view; // assume we have this NSOperationQueue *renderQueue; // assume we have this CGSize size = view.bounds.size; [renderQueue addOperationWithBlock:^(){ UIImage *image = [renderer renderInImageOfSize:size]; [[NSOperationQueue mainQueue] addOperationWithBlock:^(){ view.image = image; }]; }];
注意view.image = image; 必須在主隊列調用。這是很是重要的細節。你不能在其餘隊列中調用。
一般來講,異步繪製會帶來不少複雜度。你須要實現取消繪製的過程。你還須要限制異步操做的最大數目。
因此,最簡單的就是經過NSOperation的子類來實現renderInImageOfSize方法。
最後,有一點很是重要的就是異步設置UITableViewCell 的content有時候很詭異。由於當異步繪製結束的時候,這個Cell極可能已經被重用到其餘地方了。
如今你是到了CALayer某種程度上很像GPU中的紋理。層有本身的緩存,緩存就是一個會被繪製到屏幕上的位圖。 大多數狀況,當你使用CALayer時,你會設置contents屬性給一個圖片。這個意思就是告訴 Core Animation,使用這個圖片的位圖數據做爲紋理。 若是這個圖片是PNG或JPEG,Core Animation 會解碼,而後上傳到GPU。
固然,還有其餘種類的層,若是你使用CALayer,不設置contents,而是這事background color, Core Animation不會上傳任何數據給GPU,固然這些工做仍是要被GPU運算的,只是不須要具體的像素數據,同理,漸變也是一個道理,不須要把像素上傳給GPU。
若是CALayer或是子類實現了 -drawInContext 或是-drawLayer:inContext delegate。Core Animation會爲這個layer建立一個緩存,用來保存這些函數中繪製的結果。這些代碼是在CPU上面運行的,結果會被傳遞給GPU。
形狀和文本層會有一點不一樣。首先,Core Animation 會爲每個層生成一個位圖文件用來保存這些數據。而後Core Animation 會繪製到layer的緩存上面。若是你實現了-drawInContext方法,結果和上面提到的同樣。最後性能會受到很大影響。
當你修改形狀層或是文本層致使須要更新layer的緩存時,Core Animation會從新渲染緩存,好比。當實現shape layer的大小動畫時,Core Animation會在動畫的每一幀中從新繪製形狀。
CALayer 有一個屬性是 drawsAsynchronously。這個彷佛看上去很不錯,能夠解決全部問題。實際上雖然可能會提升效率,可是可能會讓事情更慢。
當你設置 drawsAsynchronously = YES 後,-drawRect: 和 -drawInContext: 函數依然實在主線程調用的。可是全部的Core Graphics函數(包括UIKit的繪製API,最後其實仍是Core Graphics的調用)不會作任何事情,而是全部的繪製命令會被在後臺線程處理。
這種方式就是先記錄繪製命令,而後在後臺線程執行。爲了實現這個過程,更多的事情不得不作,更多的內存開銷。最後只是把一些工做從主線程移動出來。這個過程是須要權衡,測試的。
這個多是代價最昂貴的的提升繪製性能的方法,也不會節省不少資源。