擼面試題中,文中內容基本上都是搬運自大佬博客及自我理解,可能有點亂,不喜勿噴!!!html
原文題目來自:阿里、字節:一套高效的iOS面試題ios
iOS 6,Apple 引入了佈局算法 Cassowary 並實現了本身的佈局引擎 Auto Layout
。git
Cassowary 經過約束來描述視圖之間的關係,所以,Auto Layout
再也不關注 frame,而是關注視圖之間的關係,咱們只須要描述出表示視圖間佈局關係的約束集合。Auto Layout
不止包含了 Cassowary 算法,還包含了佈局在運行時的生命週期等一整套引擎系統,用來統一管理佈局的建立、更新和銷燬。github
這一整套佈局引擎系統被稱爲 Layout Engine
,它是 Auto Layout
的核心,主導整個界面佈局。每一個視圖在獲得本身的佈局以前,Layout Engine 將約束經過計算轉化爲最終的 frame。每當約束改變, Layout Engine 都會從新計算,而後進入 Deferred Layout Pass
去更新 frame,完成以後再次進入監聽狀態。面試
view1.attribute1 = view2.attribute * multiplier + 8。約束就是一個方程式。開發者將視圖之間的關係描述成一個約束集合,Layout Engine
經過優先級等將這些約束方程式求解,得出的結果就是每一個視圖的 frame,而後利用 layoutSubviews
一層一層佈局。算法
iOS 12 以後不用擔憂性能問題。在以前主要是由於更新約束時建立新的 NSISEnginer
(AutoLayout 的線性規劃求解器)從新求解,嵌套視圖在佈局過程當中須要更新約束,因此簡直沒眼看。。。查看 從 Auto Layout 的佈局算法談性能。數組
每個 view 中都有一個 layer,view 持有並管理這個 layer,且這個 view 是 layer 的代理。緩存
UIView
負責響應事件,參與響應鏈,爲 layer 提供內容。bash
CALayer
負責繪製內容,動畫。微信
Hit-testing
Hit-testing
尋找 hit-test view
,也就是觸摸事件所在的 view。
發生觸摸事件後,系統將事件放到一個由 UIApplicaiton 管理的隊列中。UIAplication 將事件發送給 keyWindow,keyWindow 在視圖樹種尋找一個最合適的 view 來響應事件。
可是須要保證視圖 isUserInteractionEnabled 爲 YES,isHidden 爲 NO,alpha 大於 0.01。
兩個重要方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
複製代碼
經過 Hit-testing
找到了觸摸 view 以後。調用這個 view 的 touches 方法來作具體處理:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
複製代碼
若是這個 view 調用了 [super touches]
方法,則事件順着響應者鏈向上傳遞(子視圖向父視圖)。
其順序爲 view -> superview -> viewController -> rootViewController -> window -> UIApplication。若是到最後的 UIApplication
都沒有處理,則丟棄。
drawrect
initWithFrame:
,但 frame 不能爲 CGRectZero;
調用setNeedsDisplay
or setNeedsDisplayInRect:
,在下一次 drawing cycle 繪製;
當 contentMode 爲 UIViewContentModeRedraw 時,修改 frame。
layoutSubviews
initWithFrame:
,但 frame 不能爲 CGRectZero;
setNeedsLayout
標記爲須要佈局,下次 layout cycle 調用;
layoutIfNeeded
立馬布局;
自身 frame 發生變化;
添加子視圖、子視圖 frame 發生變化;
視圖被添加到 UIScrollView、滾動 UIScrollView;
旋轉屏幕(使用自動佈局);
updateConstraints
initWithFrame:,可是須要重寫屬性
requiresConstraintBasedLayout` 並返回 YES;
setNeedsUpdateConstraint
標記爲須要更新約束,下次 layout cycle 調用;
setUpdateConstraintsIfNeeded
,立馬更新約束。
App 在處理交互事件時,會建立新的 UIView,也會修改現有的 View,而後就會更新約束,調整佈局,再渲染並顯示
setNeedsUpdateConstraints
-> updateConstraints
setNeedsLayout
-> layoutSubviews
UIView.setNeedsDisplay
-> CALayer.setNeedsDisplay
-> 等待下一繪製週期
CALayer.display
-> 系統繪製流程 / 異步繪製流程
4.1 系統繪製路程:
建立 backing store + CGContextRef
->Layer.drawInContext:
->delegate.drawInContext
->UIViewAccessibility.drawLayer:inContext:
->UIView(CALayerDelegate).drawLayer:inContext:
->UIView.drawRect
4.2 異步繪製流程:
delegate.displayLayer:
-> 其餘線程,本身建立位圖,本身繪圖 -> 最後在主線程 setContents
修改獨立 layer 的可動畫屬性時,會有一個從當前值到目標值的動畫,這個就是隱式動畫。隱式動畫是由系統框架完成的。
建立 CAAnimation 對象再提交到 layer 執行的動畫就是顯式動畫。
隱式動畫一直存在,按需關閉;顯示動畫須要手動建立。
GPU 在當前屏幕緩衝區以外另開一片內存空間進行渲染操做。
GPU 在繪製時沒有回頭路,某一個 layer 在被繪製以後就已經與以前的若干層造成一個總體了,沒法再次修改。
對於一些有特殊效果的 layer,GPU 沒法經過單次遍歷就能完成渲染,只能另外申請一片內存區域,藉助這個臨時區域來完成更加複雜、屢次的修改與裁減操做。
須要建立新的渲染緩衝區,會存在不小的內存開銷
且須要切換渲染上下文 Context Switch,會浪費不少事件。
imageNamed
會將使用過的圖片緩存到內存中。即便生成的對象被 AutoReleasepool
釋放了,這份緩存依然存在。imageNamed
會先嚐試從緩存中讀取,效率更高,可是會額外增長開銷 CPU 的時間。
imageWithContentsOfFile
直接從文件中加載圖片,圖片不會緩存,加載速度較慢,可是不會浪費內存。
除非某個圖片常用,不然使用 imageWithContentsOfFile
這種經濟的方式。
視加載方式而定:
imageNamed:
會對加載的圖片進行緩存。使用時先嚐試從緩存加載。
imageWithContentsOfFile:
不會緩存,因此會重複加載。
從磁盤拷貝數據到內核緩衝區(系統調用);
從內核緩衝區拷貝數據到用戶控件(進程所在)
生成 UIImageView,把圖像數據賦值給 UIImageView;
如圖像未解碼,解碼爲位圖數據;
CATransaction
捕獲到 UIImageView 圖層樹的變化;
主線程 Runloop 在最後的 drawing cycle 提交 CATransaction
;
GPU 處理位圖數據,進行渲染。
也就是說,當圖片須要顯示時,纔會被解碼。
經常使用的圖片格式 PNG、JPEG 等都是壓縮格式,而屏幕顯示的是位圖 bitmap。
Data Buffer :存儲在內存中的原始數據。圖像能夠以不一樣格式存儲,如 JPEG、PNG。Data Buffer 數據不能用來描述圖像的位圖像素信息。
Image Buffer :圖像在內存中的存儲方式,每個元素描述一個像素點。其存儲方式與位圖相同,存儲在內存中。
Frame Buffer :幀緩存,用於顯示到顯示器上的。存儲在 vRAM(video RAM)中。
將 Data Buffer 轉換爲 Image Buffer 的過程,就能夠稱爲解碼。或者說,將未解碼的 CGImage 轉換爲位圖。核心方法爲:
CGContextRef
CGBitmapContextCreate(
void * __nullable data,
size_t width, size_t height,
size_t bitsPerComponent,
size_t bytesPerRow,
CGColorSpaceRef cg_nullable space,
uint32_t bitmapInfo);
複製代碼
data :若是爲 NULL,系統會自動分配和釋放所須要的內存;若是不爲 NULL,它應該指向一片 bytesPerRow * height
字節的內存空間;
width / height:圖像的高度與寬度;
bitsPerComponent :像素的每一個顏色份量只用的 bit 數,RGB 空間爲一個字節,也就是 8 bits;
bytesPerRow :圖像每一行使用的字節數,至少爲 width * bytes per pixel
。指定爲 0 / NULL 時,系統不但自動計算該值,還會進行緩存行對齊 cache line alignment 的優化操做;
space :顏色空間,通常使用 RGB;
bitmapInfo :位圖的佈局信息,差很少理解爲 ARGB(kCGImageAlphaPremultipliedFirst
) 仍是 RGBA(kCGImageAlphaPremultipliedLast
)。若存在透明通道,傳入 kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst
,不然傳入 kCGBitmapByteOrder32Host | kCGImageAlphaNoneSkipFirst
。
請查看 談談 iOS 中圖片的解壓縮 、 Which CGImageAlphaInfo should we use?、 UIGraphicsBeginImageContextWithOptions - Apple,或者學習下 SDWebImage 和 YYImage。
圖片在被設置到 UIImageView.image 或 layer.contents 中以後,在 layer 被提交到 GPU 以前,CGImage 數據纔會被解碼。這一步發生在主線程中,無可避免。
提早強制解碼。
將圖片用 CGContextDrawImage()
繪製到畫布上,而後把畫布的數據取出來看成圖片。
if (!cgImage) return NULL;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
size_t bytesPerRow = width * 4;
size_t bytesTotal = bytesPerRow * height;
/// 在內存中哪裏進行繪製
void *bitmapdata = calloc(bytesTotal, sizeof(uint8_t));
if (!bitmapdata) {
fprintf(stderr, "Failed to calloc 'bitmapdata'");
return NULL;
}
/// 顏色空間
/// 此處實際上是固定的
static CGColorSpaceRef colorSpaceRef;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/// 應該是不須要支持 iOS 8了。。。
colorSpaceRef = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
});
/**
*
* You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).
* From https://developer.apple.com/documentation/uikit/1623912-uigraphicsbeginimagecontextwitho?language=occ
*
*/
/// 移動設備爲小端存儲。
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
BOOL hasAlpha = CGImageHasAlpha(cgImage);
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
/// 這裏其實與 [CALayer drawInContext:] 和 UIGraphicsBeginImageContext() 相同
CGContextRef context = CGBitmapContextCreate(bitmapdata, /* 在內存中哪裏進行繪製。若傳入 NULL,系統會自動爲咱們分配空間 */
width, /* 位圖寬度 */
height, /* 位圖高度 */
8, /* 一個像素每一個顏色份量所佔位數 bits,固定 8 */
bytesPerRow, /* 位圖每行的字節數 bytes,若傳入 0,系統會自動計算 */
colorSpaceRef, /* 顏色空間 */
bitmapInfo /* 位圖佈局信息 */
);
if (!context) return NULL;
/// 解碼
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
/// 從上下文取出解碼的圖片
CGImageRef rstImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return rstImage;
複製代碼
如小標題
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage);
size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage);
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
/// 解碼
CFDataRef data = CGDataProviderCopyData(dataProvider);
///
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
/// 使用解壓後的數據建立新的 CGImageRef
CGImageRef newImage = CGImageCreate(width,
height,
bitsPerComponent, /* 像素的顏色份量所佔 bits */
bitsPerPixel, /* 每一個像素所佔 bits */
bytesPerRow, /* 每行所佔 bytes */
colorSpace, /* 顏色空間 */
bitmapInfo, /* 位圖佈局信息 */
newProvider, /* 圖片數據 */
NULL, /* 圖像解碼數組。根據提供的上下限來重映射每一個顏色份量 */
false, /* 是否插值。是對應像素更平滑 */
kCGRenderingIntentDefault /* 如何處理不在圖形上下文目標顏色空間色域的顏色 */
);
/// 釋放中間變量
CFRelease(data);
CFRelease(newProvider);
return newImage;
複製代碼
優化無非從兩個方向着手:空間、時間。
而圖像須要被傳輸到 GPU,因此空間小了時間也就小了。
限制圖片文件大小;
限制圖片的緩存。除非使用頻繁,不然不要緩存;
對網絡圖片進行降採樣(本地圖片誰會放那麼大的,降採樣的同時進行解碼);
使用 Image Asset Catalogs,壓縮率較高(缺點是隻能經過 imageNamed:
來加載);
使用 mmap 將文件直接映射到內存中,從磁盤直接訪問。NSData 能夠假設一塊磁盤空間是在內容中的。
限制渲染格式爲 SRGB(每一個像素 4 字節),display p3 寬色域爲每一個像素 8 字節(適用於 iPhone 7 之後的設備);
使用 UIGraphicsImageRenderer 代替 UIGraphicsBEginImageContextWithOptions
。
提早強制解碼;
使用符合尺寸的圖片:字節對齊。
若是緩衝區容許覆蓋,則必定會存在某些幀尚未悖視頻控制器讀取就已經被徹底覆蓋或者部分覆蓋,因此會出現掉幀或者畫面撕裂;若是不容許覆蓋,直接掉幀。
雙緩衝機制 + 垂直同步信號 VSync。
Auto Layout
自動佈局Auto Layout
,自動佈局。本質上就是一個線性方程解析引擎。
使用 Auto Layout
開發界面,咱們須要關注的再也不是 frame 的位置與 尺寸,而是各個視圖之間的關係。當咱們描述出視圖與視圖之間佈局關係的約束集合時,Auto Layout
會解析出每一個視圖最終的 frame 並應用到各個視圖上。
Auto Layout
的前世此生2012 年,Apple 推出了 4 英寸的 iPhone 5,尺寸比例的變化讓本來就費時費力的手動佈局更加難了。因而,Apple 在同期的 iOS 6 中引入了源於 佈局算法 Cassowary 的自動佈局引擎 Auto Layout
。
1997 年,Alan Borning,Kim Marriott,Peter Stuckey 等人發佈了論文 《Solving Linear Arithmetic Constraints for User Interface Applications》,文中提出瞭解決佈局問題的 Cassowary constraint-solving 算法,而且將代碼發佈在 Cassowary 網站 上。
可是,使用 Auto Layout
的語法極其噁心,一個小部件的約束可能要寫好幾個屏幕,再加上性能問題,就算是 VFL(Visual Format Language)也沒有讓 Auto Layout
火起來。直到 Masonry (Swift 爲 SnapKit)的出現,其簡單的鏈式語法才讓 Auto Layout
開始普遍使用。
Auto Layout
的本質Auto Layout
不止包含了 Cassowary 算法,還包含了佈局在運行時的生命週期等一整套佈局引擎關係,用來統一管理佈局的的建立、更新和銷燬。
這一整套佈局引擎系統被稱爲 Layout Engine
,是 Auto Layout
的核心,主導整個界面佈局。
Auto Layout
的佈局是有延遲的,並不是已有約束變化就立馬進行佈局。這樣作的目的是實現批量更新約束和繪製視圖,避免頻繁遍歷視圖層級,優化性能。固然,咱們能夠使用 layoutIfNeeded
來強制立馬更新。
在 2020 年的 WWDC 上,Apple 給出這張圖片:
Layout Cycle
,佈局循環,在應用 Runloop 下循環執行的流程。
Application Run Loop
Runloop 自動收集已經改變的約束,發送給 Layout Engine
。
而後 Runloop 從 Layout Engine
計算好的數據,在經過 GPU 將對應的視圖渲染並展現。
Constraints Change
其過程爲了兩個步驟:更改表達式、Layout Engine
從新計算佈局。
約束,以線性表達式的形式存放於 Layout Engine
中,任何約束的更改,都屬於 Constraints Change
。
這些更改包括:
activate / deactivate
;constant
;priority
;add / remove
(移除視圖會自動移除相關的約束)。Layout Engine
從新計算佈局約束表達式表達的是與視圖位置、尺寸相關的變量。
當約束更新後,Layout Engine
從新計算佈局,這些變量極可能被更新。
從新計算以後,因爲約束更新致使位置、尺寸發生變化的視圖的父視圖會被標記爲 needing layout
,也就是調用 superview.setNeedsLayout
來進行標記。
這些操做完成以後,視圖的新的 frame 已經在 Layout Engine
中。可是視圖尚未更新位置、尺寸。該 Deferred Layout Pass
表演了。
Deferred Layout Pass
如圖所示:Deferred Layout Pass
會在視圖樹上作兩步操做:更新約束、從新賦值視圖的 frame。
與 Constraints Change
不一樣,這裏的更新約束是指 「從下到上(子視圖到父視圖),依次遍歷視圖樹,對全部被 needing layout
標記的視圖調用 updateConstraints
(UIViewController 對應方法爲 updateViewConstraints
)來更新視圖的約束」。
咱們能夠重寫 updateCnstraints
方法來監聽此過程。
爲何要自下而上? 由於子視圖的約束會影響到父視圖的約束。
咱們能夠調用 setNeedsUpdateConstraints
來手動觸發這個過程。
Apple 建議在如下兩種狀況能夠手動觸發這個過程:
layoutSubviews
(UIViewController 對應方法爲 layoutWillLayoutSubviews
) ,以便讓視圖佈局其子視圖。咱們能夠重寫 layoutSubviews
來監聽此過程,可是除非 Auto Layout
搞定不了才考慮重寫。(重寫時會發現,剛方法調用以前,視圖自己已經有了新的 frame,而子視圖的 frame 此時仍是舊值)
爲何要自上而下? 由於只有先肯定了父視圖,才能肯定子視圖。
Layout Engine
中拷貝出子視圖的位置、尺寸信息並設置到對應的視圖上。以下圖所示,iOS 爲 setCenter:
和 setBounds
;而 MacOS 爲 setFrame:
。
Render Loop
關於界面渲染,這裏不作涉及,可是得說一下 Render Loop
。我姑且將其翻譯爲 渲染循環。
Render Loop
是一個每秒執行 120 次的過程,用來確保全部的視圖能爲每一幀作好準備。其執行分三步:
更新/修改約束:從子視圖向上逐層更新約束,一直到 window;
調整佈局: 從父視圖鄉下逐層調整 layout;
渲染與顯示: 從父視圖鄉下逐層渲染繪製。
也就是下面這張從 WWDC 2018:高性能 Auto Layout 偷來的圖:
Layout Engine
與 Render Loop
當某個視圖發生 Constrain Change
時,Layout Engine
將這個視圖上的多個約束方程式求解,其結果即是這個視圖的 frame。(對應上邊 2.2 Auto Layout
的佈局週期 - Applicaiton Run Loop
)
方程求解完成以後,Layout Engine
通知對應的視圖調用父視圖的 setNeedsLayout
方法來更新約束。(對應上邊 2.2 Auto Layout
的佈局週期 - Constraints Change
- Layout Engine
從新計算佈局)
當更新約束完成以後,進步佈局階段。每一個視圖都會從 Layout Engine
中讀取器子視圖的 frame,而後調用 layoutSubviews
來調整子視圖的佈局。(對應上邊 **2.2 Auto Layout
的佈局流程 - Deferred Layout Pass
- 從新賦值視圖的 frame)
Render Loop
的具體操做Render Loop
每一步對應的方法以下:
幹什麼 | 由誰來幹 | 誰可讓它幹 |
---|---|---|
更新約束 | updateConstraints |
setNeedsUpdateConstraints 、updateConstraintsIfNeeded |
調整佈局 | layoutSubviews |
setNeendsLayout 、layoutIfNeeded |
渲染顯示 | drawRect: |
setNeedsDisplayInRect: |
- (void)updateConstraints
什麼時候觸發?
initWithFrame:
時調用:
可是須要重寫屬性
requiresConstraintBasedLayout
並返回 YES。
被標記爲須要更新時,下次 layouty cycle
自動調用:
調用
setNeedsUpdateConstraints
;當約束改變時,下次
render loop
還會自動調用layoutsubviews
來佈局。
有須要更新的標記,當即觸發(手動觸發):
調用
updateConstraintsIfNeeded
。當約束改變時,下次
render loop
還會自動調用layoutsubviews
來佈局。
- (void)layoutSubviews
什麼時候觸發?
initWithFrame:
時調用:
可是rect的值不能爲CGRectZero。
標記爲須要佈局,下次 Layout cycle
自動調用:
調用
setNeedsLayout
。
有須要佈局的標記,當即觸發(手動觸發):
調用
layoutIfNeeded
自身的 frame 發生改變時,約束會致使 frame 改變;
添加子視圖、子視圖 frame 發生改變,約束會致使 frame 改變;
視圖被添加到 UIScrollView
,滾動 UIScrollView
。
- (void)drawRect:(CGRect)rect
什麼時候觸發?
initWithFrame:
時觸發:
但 frame 值不能爲 CGRectZero。
被標記爲須要顯示,下次 render loop
自動調用:
調用
setNeedsDisplay
。
Auto Layout
思惟在使用 Auto Layout
以前,咱們須要先養成一種習慣:按照視圖關係來思考,而不是本來的位置與尺寸。
NSLayoutConstraint
來描述兩個視圖之間的關係,它本質上是一個方程式。不過這種方程式既能夠是線性等式,也能夠是不等式。這個關係能夠表示爲 view1.attribute1 (relationship) view2.attribute2 * multiplier + constant
,也就是 Apple 官方教程上的這張圖:
上圖的意思就是:RedView 的頭等於( 1 倍的 BlueView 的尾,再向右偏移 8)。再解釋一下,就是 RedView 在 BlueView 右邊,且二者之間的水平間隔是 8。
NSLayoutAttribute
才能互相約束。先列一個經常使用的佈局屬性表格:
屬於同一分類的屬性能夠做用於上邊的公式來互相約束(接下來我就寫簡稱了)。
【Leading
和 Trailing
】對應着【Left
和 Right
】,可是【Left
和 Right
】不可跟 【Leading
和 Trailing
】作約束關係。並且,在閱讀習慣不一樣的語言中,其對應關係也不一樣:
從左向右 |
向右向左 |
|
---|---|---|
Leading |
Left |
Right |
Trailing |
Right |
Left |
基於以上原則,推薦在開發中使用 Leading
和 Trailing
,而不是 Left
和 Right
,畢竟國際化也是個問題。
剛纔說到,這個方程式既能夠是等式,也能夠是不等式。這也就意味着,這個方程式中間的 【等號 =】 能夠是別的關係。準確來講,能夠是 等於 =,也能夠是 小於等於 ≤ 或者 大於等於 ≥。
Apple 提供了這個了這樣一個枚舉:
typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1, /// 小於等於
NSLayoutRelationEqual = 0, /// 等於
NSLayoutRelationGreaterThanOrEqual = 1, /// 大於等於
};
複製代碼
咱們前邊說到,存在一個 容錯處理機制 Deffered Layout Pass。若是咱們添加的全部約束關係中,存在缺失、或者衝突。程序筆記是程序,不是人,它沒法主觀決定哪一個更重要。
因而,Apple 提供了佈局約束的優先級,這個優先級本質上是一個 float 值(Swift 是一個由 Float 初始化的結構體)。
系統提供瞭如下幾個默認的優先級:
/// 本質是一個 float 數值。
typedef float UILayoutPriority NS_TYPED_EXTENSIBLE_ENUM;
/// 必須約束,表明這個約束必須知足。
/// 最高優先級,也是默認優先級。
/// 默認優先級一旦衝突,直接崩潰。官方提示:不要指定超過這個值的優先級
UILayoutPriority UILayoutPriorityRequired = 1000;
/// 按鈕 UIButton 內容擴張約束的優先級
/// 也就是向外擴張
UILayoutPriority UILayoutPriorityDefaultHigh = 750;
/// 按鈕 UIButton 內容壓縮約束的優先級
/// 也就是牢牢貼着其文字
UILayoutPriority UILayoutPriorityDefaultLow = 250;
/// 視圖以參數 target size 爲基準,經過計算儘可能符合。
/// 不要用它來肯定一個約束的優先級。使用 -[UIView systemLayoutSizeFittingSize:]。
/// 當咱們調用 [view systemLayoutSizeFittingSize:CGSizeMake(50, 50)] 時,這個 view 會經過計算獲得一個最符合 CGSizeMake(50, 50) 的尺寸。
UILayoutPriority UILayoutPriorityFittingSizeLevel = 50;
/// 拖動控件的推薦優先級,它可能會調整窗口尺寸
UILayoutPriority UILayoutPriorityDragThatCanResizeScene = 510;
/// 窗口但願保持相同大小的優先級。
/// 一般狀況下,不要使用它來肯定約束的優先級。
UILayoutPriority UILayoutPrioritySceneSizeStayPut = 500;
/// 拖動分割視圖分隔符的優先級。不會調整窗口的尺寸
UILayoutPriority UILayoutPriorityDragThatCannotResizeScene = 490;
複製代碼
爲了防止由於佈局而崩潰,建議 將必須約束的優先級數值設置爲 999。如此一來,既保證了其優先級,也避免了崩潰的風險。來自於 How do you set UILayoutPriority? - stack overflow
instrinsicContentSize
/ Compression Resistance Size Priority / Hugging Priority某些視圖具備 instrinsicContentSize
的屬性,好比 UIlabel、UIButton、UITextField、UIImageView、選擇控件、進度條、分段等。他們能夠經過其內部 content 計算本身的大小,好比 UILabel 在設置 text 和 font 以後其大小能夠經過計算獲得。
基於這類控件的 content,Apple 提供了兩個特定的約束:收縮約束 Content Hugging
和 擴張約束 Content Compression Resistance
。這兩個約束簡稱爲 CHCR
。能夠經過這張圖來理解這兩個約束。
此時,能夠經過這兩個函數來完成:
/*
* @abstract 設置指定軸向上的壓縮優先級
*
* @param priority
* 優先級,值越大越抱緊視圖裏的內容。
* 也就是,不會隨着父視圖變大而變大。
*
* @param axis
* 軸向,分爲水平與垂直。有 UILayoutConstraintAxisHorizontal 和 UILayoutConstraintAxisVertical
*/
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis;
/*
* @abstract 設置指定軸向上的擴張優先級
*
* @param priority
* 優先級,值越大越不容易被壓縮
* 當總體空間沒法徹底顯示全部子視圖的時候,Content Compression Resistance 越大的子視圖內容顯示越完整
*
* @param axis
* 軸向,分爲水平與垂直。有 UILayoutConstraintAxisHorizontal 和 UILayoutConstraintAxisVertical
*/
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis;
複製代碼
這裏有一個測試實例:
測試代碼爲:
[self.label1 setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
[self.label2 setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
複製代碼
最好使用有直接關係的兩個視圖來作約束關係,好比父子、兄弟。最好不要使用 view1.subview1 與 view2.subview2 來作。
這張圖裏,view1 與 view2 是兄弟。subview1 是 view1 的子視圖,subview2 是 view2 的子視圖。可是 subview1 與 subview2 毫無關係。
因此,按照上邊說的。subview1 與 subview2 最好不要用來相互約束。
Auto Layout
得益於廣大開發者,現現在使用 Auto Layout
的方式有不少,咱們一種一種來介紹。
不管使用任何一種方式來佈局,只要是 Auto Layout
,到最後都會轉換成 NSLayoutConstraint
這個對象的實例。咱們先來認識一下幾個重要的屬性與方法:
/// 當前須要約束的對象視圖
@property (nullable, readonly, assign) id firstItem;
/// 做爲關係的依賴視圖。換句話說,用哪一個視圖來約束對象視圖
@property (nullable, readonly, assign) id secondItem;
/// 當前對象視圖須要約束的某個屬性
@property (readonly) NSLayoutAttribute firstAttribute;
/// 使用依賴視圖的哪一個屬性來約束對象視圖的指定屬性
@property (readonly) NSLayoutAttribute secondAttribute;
/// 方程式的 相等關係 或者是 不等關係
@property (readonly) NSLayoutRelation relation;
/// 方程式中的 倍數
@property (readonly) CGFloat multiplier;
/// 方程式中的 常數
@property CGFloat constant;
/// NSLayoutConstraint 對象是否啓用
@property (getter=isActive) BOOL active;
/// NSLayoutConstraint 對象的標識
@property (nullable, copy) NSString *identifier;
/// 類方法:啓用傳入參數中的全部約束
+ (void)activateConstraints:(NSArray<NSLayoutConstraint *> *)constraints;
/// 類方法:停用傳入參數中的全部約束
+ (void)deactivateConstraints:(NSArray<NSLayoutConstraint *> *)constraints
複製代碼
好了,我開始表演了。。。
NSLayoutConstraint
若是非要我用這種方式來開發的話,可能我仍是會選擇用手動佈局,或者是本身去封裝一種簡潔的使用方案了吧。。。。
話很少說,直接開始!!!
目的:
theView.x = 50,
theView.y = 100,
theView.width = superview.width * 0.5 + 50
theView.height = superview.height * 0.5 + 100
UIView *theView = [UIView new];
[self.view addSubview:theView];
theView.backgroundColor = [UIColor systemBlueColor];
theView.translatesAutoresizingMaskIntoConstraints = NO;
/// theView.leading = 1 * superview.centerX + 50
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:theView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1
constant:50];
/// theView.top = 1 * superview.top + 100
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:theView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1
constant:100];
/// theView.width = 0.5 * superview.width + 50
NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:theView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:50];
/// theView.height = 0.5 * superview.height + 100
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:theView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:100];
[self.view addConstraints:@[centerX, centerY, width, height]];
[NSLayoutConstraint activateConstraints:@[leading, top, width, height]];
複製代碼
在沒怎麼換行的狀況下,這麼長。。。
NSLayoutConstraint
的優缺點優勢:惟一的優勢就是它確實是 Auto Layout
。
缺點:代碼太過冗餘,可讀性也不好;約束的更新、刪除很不方便,須要使用變量來記錄約束對象、或者經過匹配來查找對應約束。
VFL
VFL
,即 Visual Format Language
,也就是可視化語言。
這種佈局方式,一樣建立 NSLayoutConstraint 對象來建立約束。剛說完的 一.4.2 原生 NSLayoutConstraint
每建立一個對象,就是一個約束關係。
而 VFL
使用字符串編碼的方式來建立約束,它能夠傳入任意多個視圖、任意多個佈局關係。
所以能夠採用 VFL
能夠一次性建立多個約束關係,其方法返回一個 NSLayoutConstraint
對象的集合。
UIView *blueView = [UIView new];
[self.view addSubview:blueView];
blueView.backgroundColor = [UIColor systemBlueColor];
blueView.translatesAutoresizingMaskIntoConstraints = NO;
UIView *redView = [UIView new];
[self.view addSubview:redView];
redView.backgroundColor = [UIColor systemRedColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
/// VFL 須要獲取對象的對應的字典 key,key 是字符串,在 VFL 中能夠直接使用
// NSDictionary *views = @{
// @"blueView": blueView,
// @"redView": redView,
// };
/// Apple 提供這樣一個快捷宏來建立這樣的字典
NSDictionary *views = NSDictionaryOfVariableBindings(blueView, redView);
/// 能夠建立這樣一個字典 傳入 metrics ,做爲 VFL 語句中用到的具體數值
/// 若是將這個字典傳入 metrics,那麼咱們能夠將下邊水平方向佈局 VFL 語句中的 50 所有換成 space
// NSDictionary *spaceMetrics = @{@"space": @50};
/*
* 解釋一下
* @"H:|-50-[blueView(100)]-50-[redView(==blueView)]"
* 邊界 - 寬度爲 50 的空白 - 寬度爲 100 的 blueView - 寬度爲 50 的空白 - 與 blueView 寬高都相同的 redView
*
* 若是語句最右邊加上 -50-|,而且去掉 blueView 的寬度指明,也就是 H:|-50-[blueView]-50-[redView(==blueView)]-50-|
* 邊界 - 寬度爲 50 的空白 - blueView - 寬度爲 50 的空白 - 與 blueView 寬高都相同的 redView - 寬度爲 50 的空白 - 邊界
* 而且此時,blueView 與 redView 的寬度將自適應爲 (superView.width - 50 - 50 - 50) / 2。
*
*
* NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
* 使全部視圖根據他們的頂部邊緣和底部邊緣對其,也就是垂直方向在同一位置
*
*
* @"V:|-100-[blueView(200)]"
* 邊界 - 高度爲 100 的空白 - 高度爲 200 的 blueView
*
*/
/// 水平方向
NSArray *horizontalConstraints = \
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[blueView(100)]-50-[redView(==blueView)]"
options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
metrics:nil
views:views];
/// 垂直方向
NSArray *verticalConstraints = \
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[blueView(200)]"
options:kNilOptions
metrics:nil
views:views];
[NSLayoutConstraint activateConstraints:horizontalConstraints];
[NSLayoutConstraint activateConstraints:verticalConstraints];
複製代碼
我根本就不會這種方式,想要具體學習的朋友們請移步 IOS開發之自動佈局--VFL語言 、iOS Auto Layout 中的對齊選項、 Apple 官方教程 VFL
VFL
的優缺點優勢:代碼簡潔,可一次性佈局多個視圖
缺點:
雖然我我的不推薦這種佈局方式,但最好能看懂
Interface Builder
Apple 建議使用 Interface Builder
進行佈局。如此開發速度確實很快。
我的平時都是純代碼開發,就不在這裏演示了。並且就算演示了,也看不到演示過程,只是一個拖好的也沒啥好看的!!!
優勢:
缺點:
NSLayoutAnchor
iOS 9,Apple 提供了一種新的 Auto Layout
開發方式:錨點 NSLayoutAnchor
。
相比於 NSLayoutConstraint
來講,這種方案的開發速度提高了很多。
NSLayoutAnchor
存在多個對象直接做爲 UIView 的屬性,它們與 NSLayoutConstraint
的佈局屬性 NSLayoutAttribute
一一對應。
NSLayoutAnchor
有三個子類,別對應不一樣的佈局屬性:
子類 | 錨點類型 | 錨點屬性 |
---|---|---|
NSLayoutXAxisAnchor |
X 軸方向 | leadingAnchor 、trailingAnchor 、 leftAnchor 、rightAnchor 、centerXAnchor |
NSLayoutYAxisAnchor |
Y 軸方向 | topAnchor 、bottomAnchor 、centerYAnchor 、firstBaselineAnchor 、lastBaselineAnchor |
NSLayoutDimension |
尺寸 | widthAnchor 、heightAnchor |
上代碼!!!
UIView *blueView = [UIView new];
[self.view addSubview:blueView];
blueView.backgroundColor = [UIColor systemBlueColor];
blueView.translatesAutoresizingMaskIntoConstraints = NO;
UIView *redView = [UIView new];
[self.view addSubview:redView];
redView.backgroundColor = [UIColor systemRedColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
/// blueView.leading = 1 * superview.leading + 0
[blueView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
/// blueView.top = 1 * superview.top + 100
[blueView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100].active = YES;
/// blueView.width = 0.3 * superview.width + 20
[blueView.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:0.3 constant:20].active = YES;
/// blueView.height = 0 * nil + 50
[blueView.heightAnchor constraintEqualToConstant:50].active = YES;
/// redView.leading = 1 * blueView.trailing + 20
[redView.leadingAnchor constraintEqualToAnchor:blueView.trailingAnchor constant:20].active = YES;
/// redView.trailing = 1 * superview.trailing - 10
[redView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-10].active = YES;
/// redView.top = 1 * blueView.bottom + 30
[redView.topAnchor constraintEqualToAnchor:blueView.bottomAnchor constant:30].active = YES;
/// redView.bottom = 1 * superview.bottom - 50
[redView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-50].active = YES;
複製代碼
我這裏直接將 返回的 NSLayoutConstraint 對象的 active 屬性
設置爲 YES,也就是激活了。固然也能夠將返回的對象賦值給變量,而後使用 +[NSLayoutConstraint activateConstraints:]
來啓用約束。
另外,與 NSLayoutAttribute
相同, leadingAnchor / trailingAnchor
與 leftAnchor / rightAnchor
不可相互約束。
NSLayoutAnchor
的優缺點優勢:
NSLayoutConstraint
;缺點:
NSLayoutConstraint
同樣;UIStackView
iOS 9,Apple 不止推出了 NSLayoutAnchor
,還推出了這個。高產似??
說實話,我雖然知道這東西,但確實沒用過。。。。
UIStackView
很是適合這種佈局:
也就是,在某一個方向上平鋪着一堆視圖,一個接一個出現。可是,遇到這種的就必需要嵌套使用了:
最左邊的兩個視圖,也就是深綠色與紫色的兩塊。只靠單獨的一個 UIStackView
是沒法實現的。
UIStackView
經常使用的屬性與方法:
/// 佈局軸向
/// 分爲兩種:水平 UILayoutConstraintAxisHorizontal 和垂直 UILayoutConstraintAxisVertical
@property(nonatomic) UILayoutConstraintAxis axis;
/// arrangeSubiviews 沿着佈局軸向的佈局方式
@property(nonatomic) UIStackViewDistribution distribution;
/// arrangedSubviews 沿着垂直於佈局軸向的佈局方式
@property(nonatomic) UIStackViewAlignment alignment;
/// arrangeSubiviews 之間的間隔
@property(nonatomic) CGFloat spacing;
/// 向 arrangeSubiviews 添加 view,添加在其列表尾部
/// 同時將 view 添加爲 UIStackView 的 subview
- (void)addArrangedSubview:(UIView *)view;
/// 從 arrangeSubiviews 移除 view
/// 可是 view 依然是 UIStackView 的 subview
- (void)removeArrangedSubview:(UIView *)view;
/// 向 arrangeSubiviews 指定位置插入 view
/// 注意 stackIndex 的越界問題,不然將形成崩潰
/// 若 stackIndex 爲 [UIStackView subviews].count,則至關於 -addArrangedSubview:
- (void)insertArrangedSubview:(UIView *)view atIndex:(NSUInteger)stackIndex;
複製代碼
關於 distribution
與 alignment
這兩個屬性的具體做用,請查看 官網 或 UIStackView學習分享, 純代碼實現
上代碼!!!
- (void)didClickButton:(UIButton *)sender {
switch (sender.tag) {
case 100: {
UILabel *theView = [UILabel new];
theView.textAlignment = NSTextAlignmentCenter;
theView.backgroundColor = [UIColor colorWithRed:arc4random() % 256 / 255.0
green:arc4random() % 256 / 255.0
blue:arc4random() % 256 / 255.0
alpha:1];
NSMutableString *title = [NSMutableString stringWithString:@"Test"];
int length = arc4random() % 3;
for (int i = 0; i < length; ++i) {
[title appendFormat:@"Test"];
}
theView.text = @"TestTest";
[stackView addArrangedSubview:theView];
[UIView animateWithDuration:1 animations:^{
[self->stackView layoutIfNeeded];
}];
break;
}
case 101: {
UIView *theView = [stackView subviews].lastObject;
if (nil == theView) return;
[stackView removeArrangedSubview:theView];
[theView removeFromSuperview];
[UIView animateWithDuration:0.5 animations:^{
[self->stackView layoutIfNeeded];
}];
break;
}
default:
break;
}
}
- (void)display_UIStackView {
stackView = [[UIStackView alloc] initWithFrame:CGRectMake(0, 100, self.view.bounds.size.width, 300)];
[self.view addSubview:stackView];
stackView.backgroundColor = [UIColor systemBlueColor];
/// 佈局方向
stackView.axis = UILayoutConstraintAxisHorizontal;
/// 子視圖之間的間距
stackView.spacing = 10;
/// 子控件依據何種規則佈局
stackView.distribution = UIStackViewDistributionFill;
UIButton *addButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 500, 100, 40)];
[self.view addSubview:addButton];
addButton.tag = 100;
[addButton setTitle:@"添加" forState:UIControlStateNormal];
[addButton setTitleColor:[UIColor systemGreenColor] forState:UIControlStateNormal];
[addButton addTarget:self action:@selector(didClickButton:) forControlEvents:UIControlEventTouchUpInside];
UIButton *removeButton = [[UIButton alloc] initWithFrame:CGRectMake(200, 500, 100, 40)];
[self.view addSubview:removeButton];
removeButton.tag = 101;
[removeButton setTitle:@"移除" forState:UIControlStateNormal];
[removeButton setTitleColor:[UIColor systemRedColor] forState:UIControlStateNormal];
[removeButton addTarget:self action:@selector(didClickButton:) forControlEvents:UIControlEventTouchUpInside];
}
複製代碼
UIStackView
的特殊性只有經過 addArrangedSubview:
到 UIStackView
的視圖才具備正確的佈局;使用 addSubview:
添加的視圖,確實在 UIStackView.subviews
中,可是其 frame 爲 (0, 0, 0, 0)。
UIStackView
只參與佈局,不參與渲染。也就是說,給 UIStackView
設置 backgroundColor
沒有任何效果,圓角等效果也是如此;
若將 arrangedSubviews
中某個視圖的 isHidden
狀態設置爲 YES,這個視圖依然存在於 arrangedSubviews
,且還在正常佈局的位置,可是不會展現出來,也不會影響其餘視圖的佈局。而若是不是 UIStackView
,就算 view.isHidden
爲 YES,其約束依然存在,依然會影響佈局。
UIView.isHidden
是不可動畫的屬性,可是添加到 UIStackView.arrangedSubviews
中的視圖,其 isHidden
已是可動畫的了。
使用 UIStackView
的佈局,一般須要設置視圖的 instrinsicContentSize
和 CHCR
,也就是 1、三、Auto Layout
思惟 中提到的 instrinsicContentSize / Compression Resistance Size Priority / Hugging Priority
!
UIStackView
的優缺點優勢:
原生,且百度團隊維護的 FDStackView
,能夠作到向下兼容到 iOS 6,代碼無侵入,無學習成本,直接使用 UIStackView
就好;
添加、移除自帶動畫。
缺點:
稍微複雜一點的佈局就須要嵌套(我的以爲這種簡單的佈局還好比直接用 VFL
);
學習成本。
接下來要吹的 Masonry
也提供了 UIStackView
相似的功能!!!
Masonry
Auto Layout
第三方開源框架,代碼地址:Masonry。Swift 版本爲 SnapKit。
很少吹了。就一句話:Auto Layout
首選方案!!!
先來搞一搞 UIStackView
的功能:
/// 將 五個 views 當作一個總體 redviews
NSMutableArray<UIView *> *redviews = [NSMutableArray array];
for (int i = 0; i < 5; ++i) {
UIView *theView = [UIView new];
theView.backgroundColor = [UIColor systemRedColor];
[theBackView addSubview:theView];
[redviews addObject:theView];
}
/// 肯定最左邊的間隔,最右邊的間隔,每一個 view 之間的間隔爲 10,redView 的寬度自適應
/// 設置佈局軸向爲 水平軸向
/// redviews 中各個 view 之間的間隔爲 10(可調整)
/// redviews.leading = 1 * superview.leading + 10
/// redviews.trailing = 1 * superview.trailing + 10
[redviews mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:10 leadSpacing:10 tailSpacing:10];
/// 補齊缺失的約束
[redviews mas_makeConstraints:^(MASConstraintMaker *make) {
/// redviews.top = 1 * superview.top + 80
make.top.equalTo(theBackView).offset(20);
/// redviews.height = 1 * redView.width + 0
make.height.equalTo(@200);
}];
複製代碼
效果圖:
升級一下,實現下邊的綠色色塊。要求:
固然,確定能夠經過不一樣的寫法實現,我就負責拋磚引玉了~
NSMutableArray *greenviews = [NSMutableArray array];
CGFloat greenNum = 3;
CGFloat greenBaseWidth = 30;
/// (greenNum * (1 + greenNum) / 2) 等差數列前 N 項只和
CGFloat greenPadding = (theBackView.bounds.size.width - greenBaseWidth * (greenNum * (1 + greenNum) / 2) ) / (greenNum + 1);
for (int i = 0; i < greenNum; ++i) {
UIView *theView = [UIView new];
theView.backgroundColor = [UIColor systemGreenColor];
[theBackView addSubview:theView];
[greenviews addObject:theView];
[theView mas_makeConstraints:^(MASConstraintMaker *make) {
/// theView.leading = 1 * theBackView.mas_leading + ( i+1個greenPadding + 以前全部blueView的寬度之和)
make.leading.equalTo(theBackView.mas_leading).offset(greenPadding * (i + 1) + greenBaseWidth * (i * (1 + i) / 2));
/// theView.bottom = 1 * theBackView.bottom - 20
make.bottom.equalTo(theBackView.mas_bottom).offset(-80);
/// theView.width = greenBaseWidth * (i + 1)
/// theView.height = greenBaseWidth * (i + 1)
make.width.height.mas_equalTo(greenBaseWidth * (i + 1));
}];
}
複製代碼
來一個很實用的。。。讓子視圖撐開父視圖:
指定其部分屬性:
leading = 1 * grayview.leading + 20;
top = 1 * grayview.top + 10;
width = 60;
height = 60。
leading = 1 * 頭像.leading + 20;
top = 1 * 頭像.top
height = 1 * 頭像.height;
width:自適應。
leading = 1 * grayview.leading + 10;
top = 1 * 頭像.bottom + 10;
trailing = 1 * grayview.trailing - 10
bottom = 1 * grayview.bottom - 10
top = 1 * superview.top + 70;
centerX = 1 * superview.centerX;
width = 1 * superview.width - 20;
height = 1 * superview.height - 100;
從上邊的圖,和約束的說明也能看出來。能直接肯定 frame 只有 頭像一個。連 grayview 都沒法肯定其 frame。
UIView *grayview = [UIView new];
[self.view addSubview:grayview];
grayview.backgroundColor = [UIColor systemBlueColor];
CGFloat avatarWidth = 60;
self.avatarImageView = [UIImageView new];
self.avatarImageView.image = [UIImage imageNamed:@"theFox.jpg"];
self.avatarImageView.clipsToBounds = YES;
self.avatarImageView.layer.cornerRadius = avatarWidth / 2;
self.nameLabel = [UILabel new];
self.nameLabel.text = @"Title";
self.descLabel = [UILabel new];
self.descLabel.text = @"Desc";
self.descLabel.numberOfLines = 0;
for (UIView *theView in @[self.avatarImageView, self.nameLabel, self.descLabel]) {
[grayview addSubview:theView];
}
[self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) {
/// avatarImageView.leading = 1 * containerView.leading + 20
make.leading.equalTo(grayview).offset(20);
/// avatarImageView.top = 1 * containerView.top + 10
make.top.equalTo(grayview).offset(10);
/// avatarImageView.width = 1 * 0 + avatarWidth
/// avatarImageView.height = 1 * 0 + avatarWidth
make.width.height.mas_equalTo(avatarWidth);
}];
[self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
/// nameLabel.leading = 1 * avatarImageView.trailing + 20
make.leading.equalTo(self.avatarImageView.mas_trailing).offset(20);
/// nameLabel.trailing = 1 * containerView.trailing - 20
make.trailing.equalTo(grayview).offset(-20);
/// nameLabel.top = 1 * avatarImageView.top + 0
make.top.equalTo(self.avatarImageView);
/// nameLabel.height = 1 * avatarImageView.height + 0
make.height.equalTo(self.avatarImageView);
}];
[self.descLabel mas_makeConstraints:^(MASConstraintMaker *make) {
/// descLabel.leading = 1 * containerView.leading + 10
make.leading.equalTo(grayview).offset(10);
/// descLabel.trailing = 1 * containerView.trailing - 10
make.trailing.equalTo(grayview).offset(-10);
/// descLabel.top = 1 * avatarImageView.bottom + 10
make.top.equalTo(self.avatarImageView.mas_bottom).offset(10);
/// descLabel.bottom = 1 * containerView.bottom - 10
make.bottom.equalTo(grayview).offset(-10);
}];
[grayview mas_makeConstraints:^(MASConstraintMaker *make) {
/// containerView.centerX = 1 * self.view.centerX + 0
make.centerX.equalTo(self.view);
/// containerView.top = 1 * self.view.top + 70
make.top.equalTo(self.view).offset(70);
/// containerView.width ≤ 1 * self.view.width -20;
make.width.lessThanOrEqualTo(self.view).offset(-20);
/// containerView.height ≤ 1 * self.view.height - 100
make.height.lessThanOrEqualTo(self.view).offset(-100);
}];
複製代碼
如今長這個樣子:
我在左下角加一個 UIButton
,來隨機改變 暱稱
和 描述
。
UIButton *theButton = [[UIButton alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height - 60, 100, 50)];
[self.view addSubview:theButton];
[theButton setTitle:@"Just Do It" forState:UIControlStateNormal];
[theButton addTarget:self action:@selector(justDoIt) forControlEvents:UIControlEventTouchUpInside];
- (void)justDoIt {
int numTitle = arc4random() % 10;
int numDesc = arc4random() % 300;
NSMutableString *theTitle = [NSMutableString stringWithString:@"Title"];
NSMutableString *theDesc = [NSMutableString stringWithString:@"Desc"];
for (int i = 0; i < numTitle; ++i) {
[theTitle appendString:@"Title"];
}
for (int i = 0; i < numDesc; ++i) {
[theDesc appendString:@"Desc"];
}
dispatch_async(dispatch_get_main_queue(), ^{
self.nameLabel.text = theTitle;
self.descLabel.text = theDesc;
});
}
複製代碼
UIView
與 CALayer
UIView
本節內容絕大部分翻譯自 UIView | Apple Developer Documentation
An object that manages the content for a rectangular area on the screen.
一個管理屏幕上矩形區域內容的對象。
UIView
是構建程序用戶界面的基本單元。視圖對象在其邊界區域內呈現內容,並處理與內容相關的交互事件。
視圖對象是應用中與用戶交互的主要方式,其承擔了諸多責任:
視圖使用
UIKit
或Core Graphics
來繪製內容。某些屬性在被設置爲新值時,能夠呈現動畫。
視圖能夠包含零個或多個子視圖。
視圖能夠調整其子視圖的尺寸與位置。
使用
Auto Layout
來定義視圖樹的改變來調整視圖大小和位置的規則。
UIView
繼承自UIResponder
,能夠響應觸摸和其餘類型的事件。視圖能夠添加
UIGestureRecognizer
來處理經常使用手勢。
視圖能夠被嵌套在其餘視圖中,以此建立視圖樹,這是一種組織有關內容的便捷方式。嵌套視圖將建立子視圖 subview
和父視圖 superview
的關係。
默認狀況下,子視圖超出父視圖的部分不會被裁減,能夠使用 clipsToBounds
屬性來改變這種行爲。
每個視圖的幾何形態由 frame 和 bounds 定義。frame 定義了視圖在父視圖座標系中的位置與尺寸,bounds 則定義了其以自身爲座標系的尺寸。center 屬性提供了一種無需修改 frame 和 bounds 即可從新擺放其位置的便捷方式。
UIView 類使用按需繪製模式來來呈現內容。當視圖第一次顯示,或因爲佈局變化致使視圖的所有或部分可見時,系統會要求視圖繪製其內容。
系統會捕獲 view 顯示內容的快照,並把這個快照做爲 view 的視覺顯示。只要不更改 view 的內容,這個 view 的繪製代碼就不會被再次調用。這份快照能夠用於大多數與 view 有關的操做(好比拉伸,平移等)。
一旦 view 的內容改變了,咱們不須要直接從新繪製這些改變。咱們使用 setNeedsDisplay
或 setNeedsDisplayInRect:
方法來給 view 打上 dirty 的標記。這兩個方法告訴系統 view 的內容已經改變,須要在下一個 drawing cycle 進行重繪。
系統在當前 runloop 的最後進行繪製操做。正是這個延遲機制,讓咱們能夠一次性調整多個 view (包括但不限於重繪、添加 / 刪除視圖、隱藏視圖、調整大小、調整位置等),這些具體調整會在同一幀畫面呈現出來。
更改 view 的集合形狀並不會自動引發系統對 view 的重繪操做。view 的
contentMode
決定了如何解釋視圖形狀的改變。絕大多數 contentMode 都不會建立新的快照,而是在 view 的邊界內拉伸或從新擺放現有的快照。試試將 contentMode 設置爲
UIViewContentModeRedraw
。【記得子類化 UIView 並重寫drawRect:
】
某些屬性的改變時可動畫的。經過改變更畫來建立的動畫以當前值開始,到指定值結束。
如下屬性可動畫:
使用 + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
便可完成以上屬性的動畫:
- (void)doAnimation:(NSNumber *)flag {
int theFlag = [flag intValue];
switch (theFlag) {
case 1: {
[UIView animateWithDuration:0.25 animations:^{
self.theView.frame = CGRectMake(50, 200, 200, 150);
}];
break;
}
case 2: {
[UIView animateWithDuration:0.25 animations:^{
self.theView.bounds = CGRectMake(0, 0, 300, 300);
}];
break;
}
case 3: {
[UIView animateWithDuration:0.25 animations:^{
self.theView.center = self.view.center;
}];
break;
}
case 4: {
[UIView animateWithDuration:0.25 animations:^{
self.theView.transform = CGAffineTransformMakeRotation(M_PI_4);
}];
break;
}
case 5: {
self.theView.alpha = 0.1;
[UIView animateWithDuration:0.25 animations:^{
self.theView.alpha = 1;
}];
break;
}
case 6: {
[UIView animateWithDuration:0.25 animations:^{
self.theView.backgroundColor = [UIColor blackColor];
}];
break;
}
default: {
break;
}
}
if (theFlag < 6) {
[self performSelector:@selector(doAnimation:) withObject:@(theFlag + 1) afterDelay:1];
}
}
複製代碼
固然,Apple 在 iOS 10 提供了 UIViewPorpertyAnimator 這個類讓咱們在作交互式動畫時更加方便了。這裏就不作延伸了,感興趣的朋友請谷歌。
Auto Layout
聯動一下若存在須要更新的約束,但不着急,調用 [self setNeedsUpdateConstraints]
來標記爲在下次 layout cycle
更新;
若存在須要立馬更新的約束,調用 [self updateConstraintsIfNeeded]
。
若存在須要調整的佈局,但不着急,調用 [self setNeedsLayout]
來標記爲在下次 layout cycle
調整佈局;
若存在須要立馬調整的佈局,調用 [self layoutIfNeeded]
。
若須要從新繪製整個 layer,調用 [self setNeedsDisplay]
來在下一次 drawing cycle
繪製所有內容;
若只須要從新繪製部份內容,調用 [self setNeedsDisplayInRect:rect]
來在下一次 drawing cycle
繪製指定矩形框內的內容。
CALayer
本節內容絕大部分翻譯自 CALayer | Apple Developer Documentation
An object that manages image-based content and allows you to perform animations on that content.
一個管理基於圖形內容且容許在其內容上執行動畫的對象。
CALayer
一般用來給 UIView
提供後備存儲,但即使沒有 UIView
,CALayer
也能夠正常展現內容。咱們能夠把 CALayer
稱之爲 「層」。
layer 的主要工做是管理咱們提供的視覺內容,可是 layer 自己也含有能夠被設置的視覺屬性,例如背景色、邊框、陰影等。
除了管理視覺內容, layer 還維護其內容在屏幕上展現的幾何形態(如位置、尺寸、變換)。改變 layer 屬性也就是啓動 layer 內容或幾何形態動畫的手段。 layer 經過實現協議 CAMediaTiming
的方式來管理其動畫的時長和步調。
若 layer 對象由視圖建立,則這個 view 就自動成爲這個 layer 的代理,不要改變這種關係。若是 layer 是由咱們本身建立的,咱們能夠爲這個 layer 提供一個代理,並由這個代理來動態爲 layer 提供內容以及執行任務。
layer 內部維護着三份 layer tree,分別是:presentationLayer tree(動畫樹)、modelLayer tree(模型樹)、render tree(渲染樹)。在作動畫時,咱們修改的動畫屬性是 presentationLayer tree 的。而最終顯示在界面上的內容實際上是 modelLayer 提供的。本段內容來自詳解CALayer 和 UIView的區別和聯繫
layer.contents
咱們都知道,若是要給一個 view 設置一張背景圖片,既能夠給這個 view 添加一個類型爲 UIImageView 的 subview,也能夠這樣給 view.layer 添加一個 sublayer:
CALayer *theLayer = [CALayer layer];
[self.myView.layer addSublayer:theLayer];
theLayer.frame = self.myView.bounds;
theLayer.contents = (__bridge id)[UIImage imageNamed:@"theFox.jpg"].CGImage;
複製代碼
甚至能夠一句代碼搞定:
self.myView.layer.contents = (__bridge id)[UIImage imageNamed:@"theFox.jpg"].CGImage;
複製代碼
不過這是會拉伸的,拉伸選項來源於屬性 contentsGravity
。
contents
是一個類型爲 id 的屬性。給此屬性賦值一個 CGImage 就能夠在 layer 上顯示給定的圖片。
其實 layer 的顯示也是靠 contents 的。但是爲何這個屬性時 id 類型呢?其實,這個屬性在早期的 macOS 時代就已經存在,那時但是顯示 CGImage 與 NSImage,但在 iOS 上是不適用 NSImage 的,因而就只剩下 CGImage。但這個屬性的類型仍是保留下來了,我認爲是爲了庫原理的統一性。
contentsGravity : 指定 contents 的對齊方式
contentsScale : 指定 contentes 的縮放比例
contentsRect : 指定 contents 的顯示區域
contentsCenter : 指定 contents 的拉伸區域
這是一個 NSString *
!!!指定 layer 的 contents 如何映射到它的矩形區域,也就是對齊方式。對應 UIView.contentMode
。
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
複製代碼
默認值爲 kCAGravityResize
。
這跟 UIView.contentMode 基本是兄弟,沒啥好說的,說了也 記不住。
不過記住一點:有 Resize 的會重設尺寸,此時不會在意屏幕的分辨率問題。
這個屬性指定了 contents 的縮放比例。它決定了 layer 的實際繪圖與物理屏幕顯示的映射比例。對應 UIView.contentScaleFactor
。
若是物理尺寸爲 (w, h),那麼邏輯尺寸就是 (w / contentsScale, h / contentsScale)。它的值不但影響由 CGImage 提供的 contents,還會影響由 drawInContext:
繪製的 contents(若是值爲 2,drawInContext:
在繪製時會繪製 layer.bounds 的兩倍)。
默認值爲 1.0。可是,若是這個 layer 是 UIView 直屬的(建立 view 時建立的),這個值會自動被設置爲 [UIScreen mainScreen].scale
;可是對於咱們自行建立的 layer ,值爲 1.0。
NOTE 測試的時候記得把 contentsGravity
設置爲不帶 Resize 的。
這個值容許咱們設置 layer 顯示 contents 的區域。
默認值爲 {0, 0, 1, 1},覺得整個 contents。與 frame / bounds 一點計數不一樣,這個屬性使用單位座標,每一個小參數取值爲 [0, 1]。
這個屬性很是適合使用拼接圖。某些時候,會須要使用大量的小圖標,例如功能列表(請按一下操做執行:微信 -> 我,功能列表的小圖標)。這樣作有許多好處:內存使用、加載時間、渲染性能等。
搞起來~~~
int hNum = 2;
int vNum = 3;
int totalCount = hNum * vNum;
CGFloat margin = 10;
CGFloat vStart = 80;
CGFloat totalHeight = self.view.bounds.size.height - vStart - 110;
CGFloat viewWidth = (self.view.bounds.size.width - margin * (hNum + 1)) / hNum;
CGFloat viewHeight = (totalHeight - margin * (vNum + 1)) / vNum;
for (int row = 0; row < vNum; ++row) { /// 一行一行來
for (int col = 0; col < hNum; ++col) { /// 一列一列來
UIView *theView = [[UIView alloc] initWithFrame:
CGRectMake(margin * (col + 1) + viewWidth * col,
vStart + margin * (row + 1) + viewHeight * row,
viewWidth,
viewHeight)];
[self.view addSubview:theView];
theView.layer.contents = (__bridge id)theImage;
theView.layer.contentsRect = CGRectMake((hNum * row + col) * 1.0 / totalCount, 0, 1.0 / totalCount, 1);
}
}
UIView *wholeImage = [[UIView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height - 105, self.view.bounds.size.width, 100)];
[self.view addSubview:wholeImage];
wholeImage.layer.contents = (__bridge id)theImage;
複製代碼
這個一個 CGRect ,CGRect, CGRect。用來肯定被拉伸區域。跟 UIImage.resizableImageWithCapInsets:
類似。
先把網上流傳的圖片拿過來:
而後解釋一下:
設置了 contentsCenter 這個 CGRect 以後,咱們把這個 rect 的邊界延伸出去,這個 contents 就被分紅了 9 塊。
首先是這個 rect 以內的區域,也就是綠色區域;
而後是 rect 上下的兩塊區域,也就是藍色區域;
接下來是 rect 左右的兩塊區域,也就是紅色區域;
最後是在 rect 四個角的四塊區域,也就是黃色區域。
藍色區域只在水平方向拉伸;
垂直方向兩端在垂直方向不拉伸
藍色區域在 rect 垂直方向,垂直方向不拉伸
紅色區域只在垂直方向拉伸;
水平方向兩端在水平方向不拉伸
紅色區域在 rect 水平方向,水平方向不拉伸
綠色區域同時在水平方向與垂直方向拉伸;
在 rect 內部,兩個方向都拉伸。
黃色區域不拉伸;
在四個角,不拉伸。
說實話,看了那麼多網上的解釋,我想。。。
想了好久,找了一張旋渦圖,我的認爲很適合演示這個屬性的。
NOTE 並非非要 x * 2 + width = 1,有興趣的朋友能夠嘗試一下其餘的組合。
類 | 用途 |
---|---|
CAEmitterLayer |
實現基於 Core Animation 的粒子發射系統。CAEmitterLayer 控制粒子的產生和位置。 |
CAGradientLayer |
繪製填滿 layer 的漸變色。 |
CAMetalLayer |
創建和使用可繪製紋理以使用 Metal 渲染 layer 內容。 |
CAEAGLLayer / CAOpenGLLayer |
創建後備存儲和圖形上下文,並使用 OpenGL ES 來渲染 layer 內容(後者爲 macOS,使用 OpenGL) |
CAReplicatorLayer |
當須要製做一個或多個 sublalyer 的副本時使用。複製器製造副本,並使用咱們指定的屬性來更改副本的外觀與屬性。 |
CAScrollLayer |
管理由多個 sublayer 組成大的滑動區域 |
CAShapeLayer |
繪製立體的貝塞爾曲線。CAShapeLayer 在繪製基於路徑的形狀上很是優秀,由於它永遠輸出清晰的路徑,這與咱們繪製到 layer 後備存儲相反,後者在縮放時效果並很差。然而,清晰的結果須要在主線程上繪製並緩存。 |
CATextLayer |
渲染純文本或屬性字符串 |
CATiledLayer |
管理可分離爲小塊的大圖,支持放大和縮小內容,從而分別渲染 |
CATransformLayer |
渲染真正的 3D 圖層結構 |
Auto Layout
聯動一下若須要從新繪製整個 layer,調用 [self setNeedsDisplay]
來在下一次 drawing cycle
繪製所有內容;
若只須要從新繪製部份內容,調用 [self setNeedsDisplayInRect:rect]
來在下一次 drawing cycle
繪製指定矩形框內的內容。
UIView
與 CALayer
的聯繫UIView
負責響應事件,並管理 CALayer
的生命週期。CALayer
負責繪製內容。
UIView
是 Layer
的代理(CALayerDelegate
)。
類比一下 PhotoShop。咱們將 UIView 比喻成 PSD,而 CALayer 即是 圖層了。一個 view 是由多個 layer 疊加而成。
view 是 layer 的管理器,給 layer 提供了發揮的空間;
一個 view 至少有一個 layer;
view 能繪製到屏幕上,這就要歸功於 layer 了。layer 有一個 graphics context,view 其實就將這個 context 繪製到屏幕上,layer 會緩存這個繪製結果,view 能夠經過 layer 來訪問 context。當 view 須要被繪製到屏幕上時,會調用 layer 的繪製方法,固然這個過程也能夠利用 drawRect:
來繪製自定義的內容,這個方法內繪製的內容就存在與 context 中。
view 的層級決定 layer 的層級。view 的層級能夠改變 layer 的層級,反之不行。(給 view 添加 subview ,其 layer 也會添加響應的 sublayer;可是直接給 layer 添加 sublayer 並不會影響 view 的 subviews);
接下來一段照抄。。。查看原文請前往 View-Layer Synergy
全部的 view 內部都存在一個 layer,而且 view 從這個 layer 直接得到了大多數數據。所以,對 layer 的修改也將反應到 view 中。這意味着,使用 Core Animation 或 UIKit 均可以達到修改界面的目的。
不過,也有一些單獨存在的 layer,好比 AVCaptureVideoPreviewLayer
和 CAShapeLayer
,它們不須要附加到 view 就能在屏幕上顯示內容。不管是哪一種狀況,都是 layer 在起決定性做用。然而,被附加到 view 的 layer 與獨立的 layer 在行爲上仍是稍有不一樣的。
若是咱們修改獨立 layer 的任何屬性(幾乎是任何屬性),都會看到一個從舊值過渡到新值的動畫。然而,咱們修改 view 中 layer 的同一個屬性,它會直接從這一幀調到下一幀。儘管這兩種狀況主角都是 layer,但 layer 一旦被附加到 view 中,其默認隱式動畫的行爲就消失了。
layer 幾乎全部的屬性都是隱式可動畫的。在文檔中能夠看到他們的簡介以
animatable
結尾。這適用於絕大多數數字屬性,例如 position、size、color、opacity,甚至是 isHidden 和 doubleSided。
屬性 paths 也是可動畫的,可是不支持隱式動畫。
在文章 Core Animation Programming Guide 第四章節 「Animating Layer Content」 的小節 「How to Animate Layer-Backed Views」 中,解釋了爲什麼 view 中的 layer 沒有隱式動畫的能力。
The UIView class disables layer animations by default but reenables them inside animation blocks.
默認狀況下,
UIView
類禁止了 layer 的動畫,可是在 animation block 中又從新啓用了。
這正是咱們所看到的行爲:在 animation block 外修改屬性並無動畫;可是在 animation block 中修改屬性時動畫便出現了。
不管什麼時候,一個可動畫的屬性改變時,layer 老是會老是尋找合適的 action
來實行這個改變。用 Core Animation 的專業術語來說,這樣的動畫被稱爲 action
(CAAction
)。
從技術上來說,
CAAction
是一個能夠,他能夠用來作不少事情。可是在實際使用中,咱們通常用來處理動畫。
layer 以 文檔 中所說的方式來查找 action,這包括五個步驟。當咱們研究 view 與 layer 的交互時,第一步是最有趣的。
layer 向它的代理(若是是附加到 view 的 layer,代理就是這個 view;獨立的 layer,其代理使咱們自行制定的 )發送消息 actionForLayer:forKey:
來詢問一個屬性改變的 action。代理能夠經過如下三種方式來響應:
有趣的點在於:對於一個附加到 view 中的 layer,這個 view 就是這個 layer 的代理。
在 iOS 中,若是一個 layer 被附加到 UIView 對象中,layer 的 delegate 屬性必須設置爲這個 UIView 對象。
以前如此晦澀難懂的行爲,如今已經很明瞭了:任什麼時候候,layer 詢問 action ,view 老是返回 NSNull
對象,除非在 animation block 中修改屬性。可是,不要輕信,咱們能夠驗證一下。只須要以一個能夠動畫的 layer 屬性來詢問 view 便可,好比 position:
NSLog(@"在 animtion block 以外:%@", [self.view actionForLayer:self.view.layer forKey:@"position"]);
[UIView animateWithDuration:0.25 animations:^{
NSLog(@"在 animtion block 之中:%@", [self.view actionForLayer:self.view.layer forKey:@"position"]);
}];
複製代碼
從結果來看,在 animation block 以外返回的是一個 <null>
,這正是 NSNull 對象,而在 animtion block 之中返回的是一個 _UIViewAdditiveAnimationAction
對象。
打印 nil 的 log 是
(null)
,NSNull 是<null>
。
對於附加到 view 的 layer,對於 action 的尋找只會到第一步。對於獨立的 layer,更多的四個步驟能夠查看 actionForKey: - Apple Developer Documentation。
UIView
與 CALayer
的區別UIView
負責響應事件,參與響應鏈,爲 layer 提供內容。
CALayer
負責繪製內容,動畫。
在開發中,UI 界面是不可或缺的一部分,對於用戶來講,這部分甚至高過於其餘任何東西。iOS 提供了很是豐富且性能優越的 UI 工具庫,直接使用 UIKit 和 Core Animation 已經能夠知足絕大部分的工做學了。
可是,咱們仍然會遇到顯示的一些問題,其中以卡頓爲首。想要解決問題,就必須得了解問題的本質。只要搞懂了 UI 是如何顯示到屏幕上的,其中通過了哪些步驟,再集合一些輔助工具,就能從根源解決問題。
這是老式的 CRT 顯示器的原理圖。CRT 電子槍按照上圖方式,從上到下一行一行掃描,掃描完成後顯示器就呈現一幀畫面,隨後電子槍回到初始位置繼續下一次掃描。
爲了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或其餘硬件)會用硬件時鐘產生一系列的定時信號。
當電子槍換到新的一行,準備進行掃描時,顯示器會發出一個水平同步信號(horizontal synchronization),簡稱 HSync;當一幀畫面繪製完成以後,電子槍恢復到初始位置,準備繪製下一幀以前,顯示器會發出一個垂直同步信號(vertical synchronization),簡稱 VSync。
一般顯示器以固定的頻率進行刷新,這個固定的刷新頻率就是 VSync 信號產生的頻率。
屏幕上一幀畫面的顯示是由 CPU、GPU 和顯示器按照上圖的方式協同工做完成的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成以後將渲染結果放入幀緩存區,隨後視頻控制器會按照 VSync 信號 逐行 讀取幀緩衝區 frame buffer 的數據,通過數模轉換傳遞給顯示器顯示。
在最簡單的狀況下,幀緩衝區只有一個,這種狀況下幀緩衝區的讀取與刷新都會存在比較大的效率問題。爲了解決效率問題,顯示系統會引入兩個幀緩衝區,也就是 雙緩衝機制。在這種狀況下,GPU 會預先渲染好一幀放入一個緩衝區內,讓視頻控制器讀取,當下一幀渲染好後,GPU 會直接把視頻控制器的指針指向第二個緩衝區。
雙緩衝這樣的處理確實解決了效率問題,可是這也引入了新的問題。當視頻控制器對當前緩衝區的讀取還未完成時,即屏幕內容剛顯示一部分時,GPU 將新的一幀提交到幀緩衝區並將兩個緩衝區指針交換後,視頻控制器就會重新一幀畫面數據中來讀取還未讀取的部分,形成畫面上下撕裂。就像這樣:
視頻控制器是 逐行 讀取幀緩衝區的數據的。
上半部分數據來自於交換以前的幀緩衝區,下半部分數據來自原交換以後的幀緩衝區。
爲了解決這個問題,GPU 一般有一個叫 VSync 的機制。開啓 VSync 以後,GPU 會等待顯示器上一幀的 VSync 信號發出以後,才進行當前幀的緩衝區更新和下一幀的渲染。這就能解決上邊的畫面撕裂問題,也增長了畫面流暢度。可是這須要更多的計算資源,也會帶來部分延遲。
GPU 等待 VSync 信號是爲了等待視頻控制器讀取完上一幀的數據而後 交換兩個緩衝區的指針 並 渲染下一幀。並非爲了渲染當前幀,當前幀已經在另外一個緩衝區了。GPU 會在視頻控制器剛剛讀完數據的哪一個緩衝區渲染下一幀。若是是渲染當前幀,那就失去了雙緩衝區的意義了。
可能有點繞:等待第一幀畫面顯示完成的 VSync 信號,將視頻控制器的指針指向已經渲染好的第二幀畫面的緩衝區,而後開始渲染第三幀(在第一幀畫面的緩衝區內)。
目前 iOS 設備有雙緩衝機制,也有 三緩衝機制。
總結來講,屏幕成像流程以下:
CPU 計算好須要顯示的內容,提交給 GPU;
GPU 進行紋理合成,將渲染結果提交到幀緩衝區;
視頻控制器總幀緩衝區逐行讀取輸入,進行數據轉換以後傳遞給顯示器;
顯示器成像。
在 iOS 中,視圖的渲染工做實際上是由一個 Render Server
的獨立進程來完成的。它在 iOS 5 及以前交 SpringBoard,以後則被叫作 BackBoard。
咱們的全部視圖、動畫的都是由 Core Animation 的 CALayer 實現的,這是 Core Animation 的繪製管線圖:
Core Animation 經過 Core Animation Pipeline 實現繪製,它以流水線的形式進行渲染工做:
Commit Transaction
Layout :構建 UI, 佈局,文本計算等;
Display :視圖繪製,主要就是 drawRect
;
Prepare :附加步驟、通常作圖片解碼;
Commit :將 layer 遞歸打包,提交給 Render Server。
這一步發生在 Runloop 在
BeforeWaiting
與Exit
以前的 drawing cycle 中。
Render Server
將數據反序列化獲得圖層數;
根據圖層樹的圖層順序、RGBA值、圖層 frame 等過濾掉圖層中被遮擋的部分;
將圖層樹轉爲渲染樹;
將渲染樹提交給 OpenGL / Metal
;
OpenGL / Metal
生成繪製命令,等待 VSync 信號到來,隨後提交到命令緩衝區 Command Buffer 供 GPU 讀取執行。
GPU
等待 VSync 信號到來,隨後從命令緩衝區讀取指令並執行。
Vertext Shader :頂點着色
Shape Assembly :形狀裝配,又稱圖元裝配
Geometry Shader : 幾何着色
Rasterization :光柵化
Fragment Shader :片斷着色
Tests and Blending : 測試與混合
Display
VSync 信號到來時,視頻控制器從幀緩衝區中逐行讀取數據,控制屏幕顯示內容
衆所周知,iOS 設備的屏幕刷新率爲 60hz(iOS 14 以後能夠開啓高刷新率 120hz。。。)。也就是說,一秒鐘須要更新 60 幀畫面,這至關於一幀畫面的渲染時間是 16.67ms。
VSync 信號每隔 16.67ms 產生一次。等 VSync 信號到來以後,系統圖形服務會經過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,這包括視圖建立、佈局計算、圖片解碼、文本繪製等。隨後 CPU 將計算好的內容提交到 GPU ,GPU 進行變換、合成、渲染,而後將結果提交到幀緩衝區。等待下一次 VSync 信號到來時顯示到屏幕上。
因爲垂直同步的機制,若是在一個 VSync 的時間內,CPU 或 GPU 沒有完成其負責的工做,這就致使渲染結果提交到幀緩衝區的時間晚於下一個 VSync 信號到達的時間,那這一幀就被丟棄,等待下一次機會再顯示,此時顯示器保持以前的內容不變。也就是說,本該存在於第一幀與第三幀之間的第二幀沒有被現實,就形成了掉幀。這也就是界面卡頓的緣由。
總結下來: **CPU 與 GPU 在下一次 VSync 信號到達時還未完成下一幀畫面的渲染,就會形成掉幀卡頓。