扒一扒NSView和CALayer

概述

iOS UIKit的UIView從出生開始便有了一個CALayer盖桥,而真正在屏幕上負(fù)責(zé)顯示任務(wù)的是UIView的layer露泊。
而Mac AppKit的NSView最初不是由 Core Animation Layer 驅(qū)動的杨拐,是因?yàn)槟莻€時候還米有GPU,所有視圖的繪制由CPU完成,后來有了GPU,就順便整合了UIKit的特性褥符,也使得NSView也可以有一個layer鹃操,然后把這個有l(wèi)ayer的NSView稱作layer-backed view韭寸。不過一個NSView想成為layer-backed view還需要設(shè)置 wantsLayer屬性 = true。
比如想要設(shè)置NSView背景顏色的時候荆隘,因NSView不帶有backgroundColor屬性恩伺,想要讓其具有一個layer,通過layer的backgroundColor屬性來設(shè)置顏色:

view.wantsLayer = true
view.layer?.backgroundColor = NSColor.red.cgColor

注意必須要有view.wantsLayer = true臭胜,使得NSView成為一個layer-backed view莫其,才能操作它的layer屬性使設(shè)置的顏色生效癞尚。

那么有l(wèi)ayer-backed view之前耸三,設(shè)置背景色方案是重寫draw(_ dirtyRect: NSRect)方法

override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)
    NSColor.red.setFill()
    dirtyRect.fill()
}

那么從這兒開始便產(chǎn)生了兩種NSView的繪制:
把重寫draw(_ dirtyRect: NSRect)方法設(shè)置背景色叫做traditional AppKit drawing(傳統(tǒng)繪制)乱陡,把設(shè)置wantsLayer=true,繼而操作layer?.backgroundColor設(shè)置背景色的方式成為layer-backed drawing
按照蘋果官方的建議是推薦使用layer-backed NSViews 進(jìn)行繪制仪壮。

利用layer-backed NSViews 繪制的優(yōu)勢

一. Drawing

1.在有l(wèi)ayer之前的traditional AppKit drawing:
看看一個小小的示例:

custom drawRect .png

邊框色憨颠,文字,圖片都是在drawRect方法里面繪制积锅,傳入的dirtyRect代表當(dāng)前繪制的區(qū)域爽彤,下面在介紹這個繪制區(qū)域的時候都把它稱作dirty region
每個被window管理的view及其子view都有一個dirtyRect缚陷, 再看一個圖示:(左邊一藍(lán)色view适篙,右邊紅色view,里面一個子view有文字有圖片)
dirtyRegionDraw.png

NSWindow會遞歸遍歷每個view的dirty region箫爷,先繪制最上層的父view嚷节,然后是其子view。整體的繪制過程中涉及的方法調(diào)用圖如下:

DrawingFlow.png

在這種傳統(tǒng)的繪制機(jī)制中虎锚,一旦設(shè)置了NSView setNeedsDisplay為YES硫痰,這個view的區(qū)域就會標(biāo)記為
dirty region,NSWindow會記錄這個區(qū)域窜护,它會死死地認(rèn)定這一塊區(qū)域就是要重新繪制效斑,這樣會導(dǎo)致一個結(jié)果,所有與這個區(qū)域有交合的view都會被重新繪制

dirtyRegionRedraw.png

如圖中黃色的view某一塊區(qū)域與左邊被標(biāo)記dirty region重繪區(qū)有重疊柱徙,因此黃色view也會重繪缓屠。

2.使用 Core Animation layers以及它是如何工作
我們讓一個NSView成為layer-backed view就設(shè)置其view.wantsLayer = true,還有一個重要的體現(xiàn)是它的子view也擁有了一個layer护侮,如下圖

layer-backed.png

那么layer-backed view是怎么進(jìn)行繪制的:
layerDrawing.png

圖上的介紹已經(jīng)很清晰:當(dāng)一個layer需要被繪制的時候藏研,系統(tǒng)會創(chuàng)建一個CGContextRef的對象,用于存儲用于繪制像素的Data概行,然后調(diào)用drawLayer:inContext:最終調(diào)用到了NSView的drawRect方法,最后的結(jié)果就是layer的content有了繪制并暫存的數(shù)據(jù)蠢挡,最終把像素體現(xiàn)在屏幕上,并有一份緩存凳忙。

每一個layer-backed view都會對應(yīng)一個dirty region业踏,設(shè)置setNeedsDisplay為YES后只會觸發(fā)它本身layer的繪制,也就意味著涧卵,不會導(dǎo)致跟這個view有區(qū)域交集的其他view觸發(fā)重繪:

layerRedraw.png

同樣是兩個view有重合的情況勤家,只因他們是layer-backed view,因此黃色view不會被牽連而重新繪制

二. Animating

1.還是先看traditional Animating AppKit
做一個簡單的調(diào)整frame的動畫:

var frame = customView.frame
frame.size = CGSize(width: 300, height: 300)
customView.animator().frame = frame // NSAnimationContext.current.duration=0.25柳恐,默認(rèn)動畫時間是0.25秒

還可以開啟隱式動畫模式伐脖,這樣直接改變view的frame屬性就可以有動畫

NSAnimationContext.current.allowsImplicitAnimation = true
var frame = customView.frame
frame.size = CGSize(width: 300, height: 300)
customView.frame = frame

這種傳統(tǒng)模式下動畫執(zhí)行的過程中每一步都會更新view的frame热幔,整個過程是在主線程里面進(jìn)行。動畫的每一步都會調(diào)用drawRect方法讼庇。

2.Layer-backed view的Core Animation

customView.superView.wantsLayer = true 
let anim = CABasicAnimation(keyPath: "bounds.size")
anim.duration = 1.0
anim.fromValue = rightView.layer?.bounds.size
anim.toValue = CGSize(width: 300, height: 300)
customView.layer?.add(anim, forKey: "animation")

注意使用這種layer add Animation方式绎巨,必須要保證customView的superView是Layer-backed view,即
customView.superView.wantsLayer = true

也可以通過直接改變layer的屬性蠕啄,開啟layer的隱式動畫:

customView.superView.wantsLayer = true // 保證superView是Layer-backed view
NSAnimationContext.current.allowsImplicitAnimation = true // 開啟隱式動畫模式
var layerBounds = rightView.layer?.bounds
layerBounds?.size = CGSize(width: 300, height: 300)
customView.layer?.bounds = layerBounds!

同樣可以直接改customView的frame做隱式動畫:

customView.wantsLayer = true 
NSAnimationContext.current.allowsImplicitAnimation = true
var frame = rightView.frame
frame.size = CGSize(width: 300, height: 300)
customView.frame = frame

所不同的是:內(nèi)部用Layer Core Animation做動畫场勤,同時又真實(shí)改變了view的frame,可以設(shè)置customView.layerContentsRedrawPolicy = .onSetNeedsDisplay歼跟,控制動畫過程中的不進(jìn)行重新繪制(不會實(shí)時調(diào)用drawRect)和媳。關(guān)于layerContentsRedrawPolicy下面會詳細(xì)介紹。

layer-backed view的animator()還有用嗎哈街?
運(yùn)行下面的代碼同樣可以有動畫:

rightView.wantsLayer = true
var frame = rightView.frame
frame.size = CGSize(width: 300, height: 300)
rightView.animator().frame = frame

它內(nèi)部機(jī)制是會開啟隱式動畫模式留瞳,同時用Layer Core Animation做動畫,又真實(shí)改變了view的frame:


animator.png

注意:Layer Core Animation執(zhí)行動畫的過程是在一個新的線程骚秦。

三. Best Practices

1.合理設(shè)置layer-backed view的重繪策略
針對有l(wèi)ayer的NSView她倘,有一個layerContentsRedrawPolicy屬性,用于設(shè)置重繪策略骤竹,有以下枚舉值:

  • NSViewLayerContentsRedrawDuringViewResize 當(dāng)尺寸拉伸時候進(jìn)行重繪制
  • NSViewLayerContentsRedrawOnSetNeedsDisplay 當(dāng)設(shè)置setNeedsDisplay為YES時候進(jìn)行重繪制
  • NSViewLayerContentsRedrawBeforeViewResize 當(dāng)尺寸拉伸前進(jìn)行重繪制
  • NSViewLayerContentsRedrawNever 永遠(yuǎn)不會重新繪制

其中帝牡,NSViewLayerContentsRedrawDuringViewResize是默認(rèn)值,只要view 尺寸改變就會重新繪制layer蒙揣,雖然作為默認(rèn)值靶溜,但是蘋果不推薦使用,推薦使用NSViewLayerContentsRedrawOnSetNeedsDisplay懒震,當(dāng)你需要重新繪制就手動設(shè)置setNeedsDisplay為YES罩息。

2.節(jié)省內(nèi)存
當(dāng)內(nèi)容完全一樣的多個layer-backed view同時顯示在屏幕上的時候,不要使用drawRect畫邊框个扰,文字瓷炮,圖片,著色递宅,這樣會導(dǎo)致他們各自layer的content都產(chǎn)生同一份內(nèi)容娘香,這樣會產(chǎn)生多份同樣內(nèi)存:

memoryuse.png

使用layer的屬性,borderColor,backGroundColor,對于圖片可以直接賦值image到layer.content,看看官方介紹:
The default value of this property is nil. If you are using the layer to display a static image, you can set this property to the CGImage containing the image you want to display. (In macOS 10.6 and later, you can also set the property to an NSImage object.) Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.
使用layer.content可以在多個重復(fù)的view之間共享數(shù)據(jù)瘟檩,節(jié)省內(nèi)存,此外如果content是image的話安接,會自動拉伸圖片自適應(yīng)
值得注意的是官方的介紹有一個點(diǎn)英融,layer-backed view不應(yīng)該直接去設(shè)置view.layer的屬性盏檐,應(yīng)該在系統(tǒng)的視圖更新的某個生命周期去設(shè)置歇式,具體就是下面的兩個方法:

@property (readonly) BOOL wantsUpdateLayer NS_AVAILABLE_MAC(10_8);
- (void)updateLayer NS_AVAILABLE_MAC(10_8);

調(diào)用過程示意圖:


layerUpdating.png

為了深化這一塊的認(rèn)識,我們拿Mac系統(tǒng)NSButton類的實(shí)現(xiàn)舉例子:


nsbutton.png

背景是純色拉伸的圖片胡野,然后一個TextField控件用于顯示文字材失,點(diǎn)擊后背景圖片換成藍(lán)色,button尺寸改變而拉伸的時候给涕,保持背景圖片拉伸不失真豺憔,TextField保持居中额获。
直接上關(guān)鍵代碼:
- (BOOL)wantsUpdateLayer {
   return YES; // 告訴系統(tǒng)想要使用updateLayer更新content
}
- (void)updateLayer {
    if (self.pressed) {
         self.layer.contents = [NSImage imageNamed:@"pureBlueImage"];
    } else {
         self.layer.contents = [NSImage imageNamed:@"pureGrayImage"];
    }
    // 設(shè)置layer拉伸取最中間的像素.
    self.layer.contentsCenter = CGRectMake(0.5, 0.5, 1e-5, 1e-5);
}
- (void)mouseDown:(NSEvent *)event {
     self.pressed = YES;
     [self setNeedsDisplay:YES];
}
// view尺寸改變或者重新布局時候會調(diào)用layout够庙,類似于UIView的layoutSubviews方法
- (void)layout {
    if (_textField == nil) {
        _textField = [[NSTextField alloc] initWithFrame:frame];
        _textField.title = @”Button”;
    } else {
       _textField.frame = // Update the location
    }
    [super layout];
}
- (void)setTitle:(NSString *)title {
    //  NSTextField賦值title的時候由它自己重繪制layer content
    _textField.title = title;
    // 重新布局,調(diào)用layout抄邀,調(diào)整_textField的位置保持居中
    // 不需要設(shè)置button的setNeedsDisplay為YES重新繪制耘眨,
    // 因?yàn)閎utton layer contentsCenter屬性設(shè)置拉伸中間的像素,當(dāng)尺寸改變的時候境肾,
    // layer自己拉伸至合適的大小
    [self setNeedsLayout:YES];
}

總結(jié):

1. 推薦使用layer-backed view剔难,并設(shè)置layerContentsRedrawPolicy=NSViewLayerContentsRedrawOnSetNeedsDisplay
2.盡量避免在drawRect畫邊框,文字奥喻,圖片
3.盡可能使用-wantsUpdateLayer and -updateLayer改變layer-backed view視圖屬性

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末偶宫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子环鲤,更是在濱河造成了極大的恐慌纯趋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冷离,死亡現(xiàn)場離奇詭異吵冒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)西剥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門痹栖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞭空,你說我怎么就攤上這事揪阿。” “怎么了咆畏?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵南捂,是天一觀的道長。 經(jīng)常有香客問我鳖眼,道長黑毅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任钦讳,我火速辦了婚禮矿瘦,結(jié)果婚禮上枕面,老公的妹妹穿的比我還像新娘。我一直安慰自己缚去,他們只是感情好潮秘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著易结,像睡著了一般枕荞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搞动,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天躏精,我揣著相機(jī)與錄音,去河邊找鬼鹦肿。 笑死矗烛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的箩溃。 我是一名探鬼主播瞭吃,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涣旨!你這毒婦竟也來了歪架?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤霹陡,失蹤者是張志新(化名)和其女友劉穎和蚪,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體穆律,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惠呼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了峦耘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剔蹋。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辅髓,靈堂內(nèi)的尸體忽然破棺而出泣崩,到底是詐尸還是另有隱情,我是刑警寧澤洛口,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布矫付,位于F島的核電站,受9級特大地震影響第焰,放射性物質(zhì)發(fā)生泄漏买优。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望杀赢。 院中可真熱鬧烘跺,春花似錦、人聲如沸脂崔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砌左。三九已至脖咐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汇歹,已是汗流浹背屁擅。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秤朗,地道東北人煤蹭。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓笔喉,卻偏偏與公主長得像取视,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子常挚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容