Objc中國 #3 Views
1. 繪制像素到屏幕上
Display 的上一層便是圖形處理單元 GPU灵寺,GPU 是一個專門為圖形高并發(fā)計算而量身定做的處理單元梧乘。這也是為什么它能同時更新所有的像素揖赴,并呈現(xiàn)到顯示器上窖壕。它并發(fā)的本性讓它能高效的將不同紋理合成起來檐迟。我們將有一小塊內(nèi)容來更詳細(xì)的討論圖形合成砍艾。關(guān)鍵的是卖鲤,GPU 是非常專業(yè)的肾扰,因此在某些工作上非常高效。比如蛋逾,GPU 非臣恚快,并且比 CPU 使用更少的電來完成工作区匣。通常 CPU 都有一個普遍的目的偷拔,它可以做很多不同的事情,但是合成圖像在 CPU 上卻顯得比較慢亏钩。
GPU Driver 是直接和 GPU 交流的代碼塊莲绰。不同的GPU是不同的性能怪獸,但是驅(qū)動使他們在下一個層級上顯示的更為統(tǒng)一姑丑,典型的下一層級有 OpenGL/OpenGL ES.
OpenGL(Open Graphics Library) 是一個提供了 2D 和 3D 圖形渲染的 API蛤签。GPU 是一塊非常特殊的硬件,OpenGL 和 GPU 密切的工作以提高GPU的能力栅哀,并實現(xiàn)硬件加速渲染震肮。對大多數(shù)人來說,OpenGL 看起來非常底層留拾,但是當(dāng)它在1992年第一次發(fā)布的時候(20多年前的事了)是第一個和圖形硬件(GPU)交流的標(biāo)準(zhǔn)化方式戳晌,這是一個重大的飛躍,程序員不再需要為每個GPU重寫他們的應(yīng)用了间驮。
OpenGL 之上擴(kuò)展出很多東西躬厌。在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來竞帽,然而在 OS X 上扛施,繞過 Core Animation 直接使用 Core Graphics 繪制的情況并不少見。對于一些專門的應(yīng)用屹篓,尤其是游戲疙渣,程序可能直接和 OpenGL/OpenGL ES 交流。事情變得使人更加困惑堆巧,因為 Core Animation 使用 Core Graphics 來做一些渲染妄荔。像 AVFoundation泼菌,Core Image 框架,和其他一些混合的入口啦租。
要記住一件事情哗伯,GPU 是一個非常強(qiáng)大的圖形硬件,并且在顯示像素方面起著核心作用篷角。它連接到 CPU焊刹。從硬件上講兩者之間存在某種類型的總線,并且有像 OpenGL恳蹲,Core Animation 和 Core Graphics 這樣的框架來在 GPU 和 CPU 之間精心安排數(shù)據(jù)的傳輸虐块。為了將像素顯示到屏幕上,一些處理將在 CPU 上進(jìn)行嘉蕾。然后數(shù)據(jù)將會傳送到 GPU贺奠,這也需要做一些相應(yīng)的操作,最終像素顯示到屏幕上错忱。
JPEG
每個人都知道 JPEG儡率。它是相機(jī)的產(chǎn)物。它代表著照片如何存儲在電腦上航背。甚至你媽媽都聽說過 JPEG喉悴。
一個很好的理由,很多人都認(rèn)為 JPEG 文件僅是另一種像素數(shù)據(jù)的格式玖媚,就像我們剛剛談到的 RGB 像素布局那樣箕肃。這樣理解離真相真是差十萬八千里了。
將 JPEG 數(shù)據(jù)轉(zhuǎn)換成像素數(shù)據(jù)是一個非常復(fù)雜的過程今魔,你通過一個周末的計劃都不能完成勺像,甚至是一個非常漫長的周末(原文的意思好像就是為了表達(dá)這個過程非常復(fù)雜,不過老外的比喻總讓人拎不清)错森。對于每一個二維顏色吟宦,JPEG 使用一種基于離散余弦變換(簡稱 DCT 變換)的算法,將空間信息轉(zhuǎn)變到頻域.這個信息然后被量子化涩维,排好序殃姓,并且用一種哈夫曼編碼的變種來壓縮。很多時候瓦阐,首先數(shù)據(jù)會被從 RGB 轉(zhuǎn)換到二維 YCbCr蜗侈,當(dāng)解碼 JPEG 的時候,這一切都將變得可逆睡蟋。
這也是為什么當(dāng)你通過 JPEG 文件創(chuàng)建一個 UIImage 并且繪制到屏幕上時踏幻,將會有一個延時,因為 CPU 這時候忙于解壓這個 JPEG戳杀。如果你需要為每一個 tableviewcell 解壓 JPEG该面,那么你的滾動當(dāng)然不會平滑(原來 tableviewcell 里面最要不要用 JPEG 的圖片)夭苗。
那究竟為什么我們還要用 JPEG 呢?答案就是 JPEG 可以非常非常好的壓縮圖片隔缀。一個通過 iPhone5 拍攝的题造,未經(jīng)壓縮的圖片占用接近 24M。但是通過默認(rèn)壓縮設(shè)置猾瘸,你的照片通常只會在 2-3M 左右晌梨。JPEG 壓縮這么好是因為它是失真的,它去除了人眼很難察覺的信息须妻,并且這樣做可以超出像 gzip 這樣壓縮算法的限制。但這僅僅在圖片上有效的泛领,因為 JPEG 依賴于圖片上有很多人類不能察覺出的數(shù)據(jù)荒吏。如果你從一個基本顯示文本的網(wǎng)頁上截取一張圖,JPEG 將不會這么高效渊鞋。壓縮效率將會變得低下绰更,你甚至能看出來圖片已經(jīng)壓縮變形了。
PNG
PNG讀作”ping”锡宋。和 JPEG 相反儡湾,它的壓縮對格式是無損的。當(dāng)你將一張圖片保存為 PNG执俩,并且打開它(或解壓)徐钠,所有的像素數(shù)據(jù)會和最初一模一樣,因為這個限制役首,PNG 不能像 JPEG 一樣壓縮圖片尝丐,但是對于像程序中的原圖(如buttons,icons)衡奥,它工作的非常好爹袁。更重要的是,解碼 PNG 數(shù)據(jù)比解碼 JPEG 簡單的多矮固。
在現(xiàn)實世界中失息,事情從來沒有那么簡單,目前存在了大量不同的 PNG 格式档址№锞ぃ可以通過維基百科查看詳情。但是簡言之辰晕,PNG 支持壓縮帶或不帶 alpha 通道的顏色像素(RGB)蛤迎,這也是為什么它在程序原圖中表現(xiàn)良好的另一個原因。
可變尺寸的圖像
類似的含友,你可以使用可變尺寸的圖像來降低繪圖系統(tǒng)的壓力替裆。讓我們假設(shè)你需要一個 300×50 點的按鈕插圖校辩,這將是 600×100=60k 像素或者 60kx4=240kB 內(nèi)存大小需要上傳到 GPU,并且占用 VRAM辆童。如果我們使用所謂的可變尺寸的圖像宜咒,我們只需要一個 54×12 點的圖像,這將占用低于 2.6k 的像素或者 10kB 的內(nèi)存把鉴,這樣就變得更快了故黑。
Core Animation 可以通過 CALayer 的 contentsCenter 屬性來改變圖像,大多數(shù)情況下庭砍,你可能更傾向于使用场晶,-[UIImage resizableImageWithCapInsets:resizingMode:]。
同時注意怠缸,在第一次渲染這個按鈕之前诗轻,我們并不需要從文件系統(tǒng)讀取一個 60k 像素的 PNG 并解碼,解碼一個小的 PNG 將會更快揭北。通過這種方式扳炬,你的程序在每一步的調(diào)用中都將做更少的工作,并且你的視圖將會加載的更快搔体。
2. 理解 Scroll Views
現(xiàn)在恨樟,回想一下,每個視圖都有一個 bounds 和 frame疚俱。當(dāng)布局一個界面時劝术,我們需要處理視圖的 frame。這允許我們放置并設(shè)置視圖的大小计螺。視圖的 frame 和 bounds 的大小總是一樣的夯尽,但是他們的 origin 有可能不同。弄懂這兩個工作原理是理解 UIScrollView 的關(guān)鍵登馒。
在光柵化步驟中匙握,視圖并不關(guān)心即將發(fā)生的組合步驟。也就是說陈轿,它并不關(guān)心自己的 frame (這是用來放置視圖的圖像)或自己在視圖層級中的位置(這是決定組合的順序)圈纺。這時視圖只關(guān)心一件事就是繪制它自己的 content。這個繪制發(fā)生在每個視圖的 drawRect: 方法中麦射。
在 drawRect: 方法被調(diào)用前蛾娶,會為視圖創(chuàng)建一個空白的圖片來繪制 content。這個圖片的坐標(biāo)系統(tǒng)是視圖的 bounds潜秋。幾乎每個視圖 bounds 的 origin 都是 {0蛔琅,0}。因此峻呛,當(dāng)在光柵化圖片左上角繪制一些東西的時候罗售,你都會在 bounds 的 origin {x:0, y:0} 處繪制辜窑。在一個圖片右下角的地方繪制東西的時候,你都會繪制在 {x:width, y:height} 處寨躁。如果你的繪制超出了視圖的 bounds穆碎,那么超出的部分就不屬于光柵化圖片的部分了,并且會被丟棄职恳。
在組合的步驟中所禀,每個視圖將自己光柵化圖片組合到自己父視圖的光柵化圖片上面。視圖的 frame 決定了自己在父視圖中繪制的位置放钦,frame 的 origin 表明了視圖光柵化圖片左上角相對父視圖光柵化圖片左上角的偏移量色徘。所以,一個 origin 為 {x:20, y:15} 的 frame 所繪制的圖片左邊距其父視圖 20 點操禀,上邊距父視圖 15 點贺氓。因為視圖的 frame 和 bounds 矩形的大小總是一樣的,所以光柵化圖片組合的時候是像素對齊的床蜘。這確保了光柵化圖片不會被拉伸或縮小。
2
軟鍵盤遮擋問題:
如何在自己的代碼中使用 content inset蔑水?當(dāng)鍵盤在屏幕上時邢锯,有一個很好的用途:你想要設(shè)置一個緊貼屏幕的用戶界面。當(dāng)鍵盤出現(xiàn)在屏幕上時搀别,你損失了幾百個像素的空間丹擎,鍵盤下面的東西全都被擋住了。
現(xiàn)在歇父,scroll view 的 bounds 并沒有改變蒂培,content size 也并沒有改變(也不需要改變)。但是用戶不能滾動 scroll view榜苫』ご粒考慮一下之前一個公式:content offset 的最大值是 content size 和 bounds 的差。如果他們相等垂睬,現(xiàn)在 content offset 的最大值是 {x:0, y:0}.
現(xiàn)在開始出絕招媳荒,將界面放入一個 scroll view。scroll view 的 content size 仍然和 scroll view 的 bounds 一樣大驹饺。當(dāng)鍵盤出現(xiàn)在屏幕上時钳枕,你設(shè)置 content inset 的底部等于鍵盤的高度。
這允許在 content offset 的最大值下顯示滾動區(qū)域外的區(qū)域赏壹∮愠矗可視區(qū)域的頂部在 scroll view bounds 的外面,因此被截取了(雖然它在屏幕之外了蝌借,但這并沒有什么)昔瞧。
3. 自定義控件
視圖層次概覽
如果你觀察一下 UIView 的子類指蚁,可以發(fā)現(xiàn) 3 個基類: reponders (響應(yīng)者),views (視圖)和 controls (控件)硬爆。我們快速重溫一下它們之間發(fā)生了什么欣舵。
UIResponder
UIResponder 是 UIView 的父類。responder 能夠處理觸摸缀磕、手勢缘圈、遠(yuǎn)程控制等事件。之所以它是一個單獨的類而沒有合并到 UIView 中袜蚕,是因為 UIResponder 有更多的子類糟把,最明顯的就是 UIApplication 和 UIViewController。通過重寫 UIResponder 的方法牲剃,可以決定一個類是否可以成為第一響應(yīng)者 (first responder)遣疯,即當(dāng)前輸入焦點元素。
當(dāng) touches (觸摸) 或 motion (指一系列運動傳感器) 等交互行為發(fā)生時凿傅,它們被發(fā)送給第一響應(yīng)者 (通常是一個視圖)缠犀。如果第一響應(yīng)者沒有處理,則該行為沿著響應(yīng)鏈到達(dá)視圖控制器聪舒,如果行為仍然沒有被處理辨液,則繼續(xù)傳遞給應(yīng)用。如果想監(jiān)測晃動手勢箱残,可以根據(jù)需要在這3層中的任意位置處理滔迈。
UIResponder 還允許自定義輸入方法,從 inputAccessoryView 向鍵盤添加輔助視圖到使用 inputView 提供一個完全自定義的鍵盤被辑。
UIView
UIView 子類處理所有跟內(nèi)容繪制有關(guān)的事情以及觸摸時間燎悍。只要寫過 "Hello, World" 應(yīng)用的人都知道視圖,但我們重申一些技巧點:
一個普遍錯誤的概念:視圖的區(qū)域是由它的 frame 定義的盼理。實際上 frame 是一個派生屬性谈山,是由 center 和 bounds 合成而來。不使用 Auto Layout 時宏怔,大多數(shù)人使用 frame 來改變視圖的位置和大小勾哩。小心些,官方文檔特別詳細(xì)說明了一個注意事項:
如果 transform 屬性不是 identity transform 的話举哟,那么這個屬性的值是未定義的思劳,因此應(yīng)該將其忽略
另一個允許向視圖添加交互的方法是使用手勢識別。注意它們對 responders 并不起作用妨猩,而只對視圖及其子類奏效潜叛。
UIControl
UIControl 建立在視圖上,增加了更多的交互支持。最重要的是威兜,它增加了 target / action 模式销斟。看一下具體的子類椒舵,我們可以看一下按鈕蚂踊,日期選擇器 (Date pickers),文本框等等笔宿。創(chuàng)建交互控件時关贵,你通常想要子類化一個 UIControl限寞。一些常見的像 bar buttons (雖然也支持 target / action) 和 text view (這里需要你使用代理來獲得通知) 的類其實并不是 UIControl雕崩。
使用 Block
另一個選擇是使用 block厦酬。再一次用餅狀圖舉例,代碼看起來大概是這樣:
@interface PieChart : UIControl
@property (nonatomic,copy) void(^selectionHandler)(PieChartSection* selectedSection);
@end
在選取行為的代碼中炬灭,你只需要執(zhí)行它醋粟。在此之前檢查一下block是否被賦值非常重要,因為執(zhí)行一個未被賦值的 block 會使程序崩潰重归。
if (self.selectionHandler != NULL) {
self.selectionHandler(self.selectedSection);
}
這種方法的好處是可以把相關(guān)的代碼整合在視圖控制器中:
- (void)setupPieChart
{
self.pieChart.selectionHandler = ^(PieChartSection* section) {
// 處理區(qū)塊
}
}
就像代理米愿,每個動作通常只有一個 block。另一個重要的限制是不要形成引用循環(huán)鼻吮。如果你的視圖控制器持有餅狀圖的強(qiáng)引用吗货,餅狀圖持有 block,block 又持有視圖控制器狈网,就形成了一個引用循環(huán)。只要在 block 中引用 self 就會造成這個錯誤笨腥。所以通常代碼會寫成這個樣子:
__weak id weakSelf = self;
self.pieChart.selectionHandler = ^(PieChartSection* section) {
MyViewController* strongSelf = weakSelf;
[strongSelf handleSectionChange:section];
}
一旦 block 中的代碼要失去控制 (比如 block 中要處理的事情太多拓哺,導(dǎo)致 block 中的代碼過多),你還應(yīng)該將它們抽離成獨立的方法脖母,這種情況的話可能用代理會更好一些士鸥。