iOS之UIKit的布局和繪制

整理對(duì)于iOS繪制和布局的知識(shí)赢赊。

一.iOS的主RunLoop

iOS 的主 RunLoop 負(fù)責(zé)處理所有的用戶輸入事件并觸發(fā)相應(yīng)的響應(yīng)辽狈。所有的用戶交互都會(huì)被加入到一個(gè)事件隊(duì)列中着茸。UIApplication對(duì)象會(huì)從隊(duì)列中取出事件并將它們分發(fā)到應(yīng)用中的其他對(duì)象上敬特。當(dāng)視圖響應(yīng)之后(視圖響應(yīng)者)掰伸,控制流回到主 RunLoop 上歧蕉,然后開(kāi)始 update cycle(更新周期),Update cycle 負(fù)責(zé)布局并且重新渲染視圖們 views。

main_event_loop.jpg

二.Update Cycle

Update cycle 是當(dāng)應(yīng)用完成了你的所有事件處理代碼后逃呼,控制流回到主 RunLoop 時(shí)的那個(gè)時(shí)間點(diǎn)推姻。正是在這個(gè)時(shí)間點(diǎn)上系統(tǒng)開(kāi)始更新布局拧晕、顯示和設(shè)置約束饲嗽。

如果你在處理事件的代碼中請(qǐng)求修改了一個(gè) view,那么系統(tǒng)就會(huì)把這個(gè) view 標(biāo)記為需要重畫(redraw)。在接下來(lái)的 Update cycle 中,系統(tǒng)就會(huì)執(zhí)行這些 view 上的更改揖盘。

用戶交互和布局更新間的延遲幾乎不會(huì)被用戶察覺(jué)到。iOS 應(yīng)用一般以 60 fps 的速度展示動(dòng)畫,就是說(shuō)每個(gè)更新周期只需要 1/60 秒鹿蜀。這個(gè)更新的過(guò)程很快婉商,所以用戶在和應(yīng)用交互時(shí)感覺(jué)不到 UI 中的更新延遲。但是由于在處理事件和對(duì)應(yīng) view 重畫間存在著一個(gè)間隔箫攀,RunLoop 中的某時(shí)刻的 view 更新可能不是你想要的那樣缀雳。如果你的代碼中的某些計(jì)算依賴于當(dāng)下的 view 內(nèi)容或者是布局,那么就有在過(guò)時(shí)的(錯(cuò)誤的) view 信息上操作的風(fēng)險(xiǎn)梢睛。對(duì)此肥印,需要理解UIView中幾個(gè)重要的布局方法來(lái)避免這類問(wèn)題。

下面的圖展示出了 update cycle 發(fā)生在 RunLoop 的尾部绝葡。

tech-blog-loop.png

三.Layout

一個(gè)視圖的布局指的是它在屏幕上的的大小和位置深碱。每個(gè) view 都有一個(gè) frame 屬性,用來(lái)表示在父 view 坐標(biāo)系中的位置和具體的大小藏畅。UIView給你提供了用來(lái)通知系統(tǒng)某個(gè) view 布局發(fā)生變化的方法敷硅,也提供了在 view 布局重新計(jì)算后調(diào)用的回調(diào)方法。

layoutSubviews()

這個(gè) UIView 方法處理對(duì)視圖(view)及其所有子視圖(subview)的重新定位和大小調(diào)整。它負(fù)責(zé)給出當(dāng)前 view 和每個(gè)子 view 的位置和大小竞膳。這個(gè)方法很開(kāi)銷很大航瞭,因?yàn)樗鼤?huì)在每個(gè)子視圖上起作用并且調(diào)用它們相應(yīng)的 layoutSubviews 方法。系統(tǒng)會(huì)在任何它需要重新計(jì)算視圖的 frame 的時(shí)候自動(dòng)調(diào)用這個(gè)方法坦辟,所以你應(yīng)該在需要更新 frame 來(lái)重新定位或更改大小時(shí)重載它刊侯。

但是你不應(yīng)該顯式調(diào)用這個(gè)方法。相反锉走,有許多可以在 runloop 的不同時(shí)間點(diǎn)觸發(fā) layoutSubviews 調(diào)用的機(jī)制滨彻,這些觸發(fā)機(jī)制比直接調(diào)用 layoutSubviews 的資源消耗要小得多。

當(dāng) layoutSubviews 完成后挪蹭,在 view 的所有者 viewController 上亭饵,會(huì)觸發(fā) viewDidLayoutSubviews 調(diào)用。因?yàn)?viewDidLayoutSubviews 是 view 布局更新后會(huì)被唯一可靠調(diào)用的方法梁厉,所以你應(yīng)該把所有依賴于布局或者大小的代碼放在 viewDidLayoutSubviews 中辜羊,而不是放在 viewDidLoad 或者 viewDidAppear 中。這是避免使用過(guò)時(shí)的布局或者位置變量的唯一方法词顾。

Automatic refresh

有許多操作會(huì)自動(dòng)給視圖打上 “update layout” 標(biāo)記八秃,因此 layoutSubviews 會(huì)在下一個(gè)周期中被調(diào)用,而不需要開(kāi)發(fā)者手動(dòng)操作肉盹。這些自動(dòng)通知系統(tǒng) view 的布局發(fā)生變化的方式有:

  • 修改 view 的大小
  • 新增 subview
  • 用戶在 UIScrollView 上滾動(dòng)(layoutSubviews 會(huì)在 UIScrollView 和它的父 view 上被調(diào)用)
  • 用戶旋轉(zhuǎn)設(shè)備
  • 更新視圖的 constraints

setNeedsLayout()

觸發(fā) layoutSubviews 調(diào)用的最省資源的方法就是在你的視圖上調(diào)用 setNeedsLaylout 方法昔驱。調(diào)用這個(gè)方法代表向系統(tǒng)表示視圖的布局需要重新計(jì)算。setNeedsLayout 方法會(huì)立刻執(zhí)行并返回上忍,但在返回前不會(huì)真正更新視圖骤肛。視圖會(huì)在下一個(gè) update cycle 中更新.

layoutIfNeeded()

layoutIfNeeded 是另一個(gè)會(huì)讓 UIView 觸發(fā) layoutSubviews 的方法。 當(dāng)視圖需要更新的時(shí)候窍蓝,與 setNeedsLayout() 會(huì)讓視圖在下一周期調(diào)用 layoutSubviews 更新視圖不同腋颠,layoutIfNeeded 會(huì)立即調(diào)用 layoutSubviews 方法。但是如果你調(diào)用了 layoutIfNeeded 之后吓笙,并且沒(méi)有任何操作向系統(tǒng)表明需要刷新視圖淑玫,那么就不會(huì)調(diào)用 layoutSubview。如果你在同一個(gè) runLoop 內(nèi)調(diào)用兩次 layoutIfNeeded观蓄,并且兩次之間沒(méi)有更新視圖混移,第二個(gè)調(diào)用同樣不會(huì)觸發(fā) layoutSubviews 方法。

使用 layoutIfNeeded侮穿,則布局和重繪會(huì)立即發(fā)生并在函數(shù)返回之前完成(除非有正在運(yùn)行中的動(dòng)畫)歌径。這個(gè)方法在你需要依賴新布局,無(wú)法等到下一次 update cycle 的時(shí)候會(huì)比 setNeedsLayout 有用亲茅。除非是這種情況回铛,否則你更應(yīng)該使用 setNeedsLayout狗准,這樣在每次 runLoop 中都只會(huì)更新一次布局。

當(dāng)對(duì)希望通過(guò)修改 constraint 進(jìn)行動(dòng)畫時(shí)茵肃,這個(gè)方法特別有用腔长。你需要在 animation block 之前對(duì) self.view 調(diào)用 layoutIfNeeded,以確保在動(dòng)畫開(kāi)始之前傳播所有的布局更新验残。在 animation block 中設(shè)置新 constraint 后捞附,需要再次調(diào)用 layoutIfNeeded 來(lái)動(dòng)畫到新的狀態(tài)。

四.Display

一個(gè)視圖的顯示包含了顏色您没、文本鸟召、圖片和 Core Graphics 繪制等視圖屬性,不包括其本身和子視圖的大小和位置氨鹏。和布局的方法類似欧募,顯示也有觸發(fā)更新的方法,它們由系統(tǒng)在檢測(cè)到更新時(shí)被自動(dòng)調(diào)用仆抵,或者我們可以手動(dòng)調(diào)用直接刷新跟继。

draw:

UIViewdraw 方法是對(duì)視圖內(nèi)容顯示的操作,類似于視圖布局的 layoutSubviews镣丑。但是不同于 layoutSubviews``舔糖,draw 方法不會(huì)觸發(fā)后續(xù)對(duì)視圖的子視圖方法的調(diào)用。主要注意的是:你不應(yīng)該直接調(diào)用 draw 方法传轰,而應(yīng)該通過(guò)調(diào)用觸發(fā)方法剩盒,讓系統(tǒng)在 runLoop 中的不同節(jié)點(diǎn)自動(dòng)調(diào)用谷婆。

setNeedsDisplay()

這個(gè)方法類似于布局中的 setNeedsLayout 慨蛙。它會(huì)給有內(nèi)容更新的視圖設(shè)置一個(gè)臟標(biāo)記,但在視圖重繪之前就會(huì)返回纪挎。然后在下一個(gè) update cycle 中期贫,系統(tǒng)會(huì)遍歷所有已標(biāo)標(biāo)記的視圖,并調(diào)用它們的 draw 方法异袄。

大部分時(shí)候通砍,在視圖中更新任何 UI 組件都會(huì)自動(dòng)把相應(yīng)的視圖標(biāo)記為“dirty”,通過(guò)設(shè)置視圖“內(nèi)部更新標(biāo)記”烤蜕,在下一次 update cycle 中就會(huì)重繪封孙,而不需要顯式的 setNeedsDisplay 調(diào)用。

下面的代碼例子中讽营,通過(guò)設(shè)置drawType的值虎忌,進(jìn)行自定義繪制,并在didSet中調(diào)用 setNeedsLayout.

class MyView: UIView {
    var drawType = 0 {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        switch self.drawType {
            case 0: return
            case 1: drawPoint(rect)
            case 2: drawLine(rect)
            case 3: drawRectangle(rect)
            default: drawEllipse(rect)
        }
    }
}

視圖的顯示方法里沒(méi)有類似布局中的 layoutIfNeeded 這樣可以觸發(fā)立即更新的方法橱鹏。

五.Constraints 約束

自動(dòng)布局包含三步來(lái)布局和重繪視圖膜蠢。第一步是更新約束堪藐,系統(tǒng)會(huì)計(jì)算并給視圖設(shè)置所有要求的約束。第二步是布局階段挑围,布局引擎計(jì)算視圖和子視圖的 frame 并且將它們布局礁竞。第三步是顯示階段,重繪視圖的內(nèi)容杉辙,如實(shí)現(xiàn)了 draw方法則調(diào)用 draw模捂。

updateConstraints()

這個(gè)方法用來(lái)在自動(dòng)布局中動(dòng)態(tài)改變視圖約束。和布局中的 layoutSubviews() 方法或者顯示中的 draw 方法類似蜘矢,updateConstraints() 只應(yīng)該被重載枫绅,絕不要在代碼中顯式地調(diào)用。

通常你只應(yīng)該在 updateConstraints 方法中實(shí)現(xiàn)必須要更新的約束硼端。靜態(tài)的約束應(yīng)該在 interface builder并淋、視圖的初始化方法或者 viewDidLoad() 方法中指定。

通常情況下珍昨,設(shè)置或者解除約束县耽、更改約束的優(yōu)先級(jí)或者常量值,或者從視圖層級(jí)中移除一個(gè)視圖時(shí)都會(huì)設(shè)置一個(gè)內(nèi)部的標(biāo)記 “update constarints”镣典,這個(gè)標(biāo)記會(huì)在下一個(gè)更新周期中觸發(fā)調(diào)用 updateConstrains()兔毙。當(dāng)然,也有手動(dòng)給視圖打上“update constarints” 標(biāo)記的方法兄春,如下澎剥。

setNeedsUpdateConstraints()

調(diào)用 setNeedsUpdateConstraints() 會(huì)保證在下一次更新周期中更新約束。它通過(guò)標(biāo)記“update constraints”來(lái)觸發(fā) updateConstraints()赶舆。這個(gè)方法和 setNeedsDisplay() 以及 setNeedsLayout() 方法的工作機(jī)制類似哑姚。

updateConstraintsIfNeeded()

對(duì)于使用自動(dòng)布局的視圖來(lái)說(shuō),這個(gè)方法與 layoutIfNeeded 等價(jià)芜茵。它會(huì)檢查 “update constraints”標(biāo)記(可以被 setNeedsUpdateConstraints 或者 invalidateInstrinsicContentSize方法自動(dòng)設(shè)置)叙量。如果它認(rèn)為這些約束需要被更新,它會(huì)立即觸發(fā) updateConstraints() 九串,而不會(huì)等到 runLoop 的末尾绞佩。

invalidateIntrinsicContentSize()

自動(dòng)布局中某些視圖擁有 intrinsicContentSize 屬性,這是視圖根據(jù)它的內(nèi)容得到的自然尺寸猪钮。一個(gè)視圖的 intrinsicContentSize 通常由所包含的元素的約束決定品山,但也可以通過(guò)重載提供自定義行為。調(diào)用 invalidateIntrinsicContentSize() 會(huì)設(shè)置一個(gè)標(biāo)記表示這個(gè)視圖的 intrinsicContentSize 已經(jīng)過(guò)期烤低,需要在下一個(gè)布局階段重新計(jì)算肘交。

比如UILableUIImageView等都有intrinsicContentSize 屬性拂玻,可以不用設(shè)置它的大小酸些,而通過(guò)intrinsicContentSize自動(dòng)算出來(lái)宰译。

六.總結(jié)

布局、顯示和約束都遵循著相似的模式魄懂,例如他們更新的方式以及如何在 run loop 的不同時(shí)間點(diǎn)上強(qiáng)制更新沿侈。

任一組件都有一個(gè)實(shí)際去更新的方法(layoutSubviews, draw, 和 updateConstraints),你可以重寫來(lái)手動(dòng)操作視圖市栗,但是任何情況下都不要顯式調(diào)用缀拭。如果視圖被標(biāo)記了需要被更新的話,則這個(gè)方法會(huì)在runLoop的末端被調(diào)用填帽。(有一些操作會(huì)自動(dòng)設(shè)置這個(gè)標(biāo)志蛛淋,但是也有一些方法允許您顯式地設(shè)置它。)

connects.png

下面的流程圖總結(jié)了 update cycle 和 event loop 之間的交互篡腌,并指出了上文提到的方法在 run loop 運(yùn)行期間的位置褐荷。

你可以在 run loop 中的任意一點(diǎn)顯式地調(diào)用 layoutIfNeeded 或者 updateConstraintsIfNeeded,需要記住嘹悼,這開(kāi)銷會(huì)很大叛甫。

在循環(huán)的末端是 update cycle 時(shí)期,如果視圖被設(shè)置了特定的 “update constraints”杨伙,“update layout” 或者 “needs display” 標(biāo)記其监,在這節(jié)點(diǎn)會(huì)更新約束、布局以及展示限匣。一旦這些更新結(jié)束抖苦,runloop 會(huì)重新啟動(dòng)。

runloop_update.png

七.概括

1.setNeedsDisplay或者setNeedsDisplay(rect:CGRect)

  • 標(biāo)記相應(yīng)的視圖區(qū)域需要重繪
  • 調(diào)用之后不會(huì)立即重繪米死,而是在下一個(gè)繪制周期里繪制
  • 會(huì)調(diào)用View的draw(_ rect: CGRect)方法
  • 不會(huì)調(diào)用layoutSubviews()方法

2.setNeedsLayout方法

  • 不會(huì)立即更新界面锌历,會(huì)在下一個(gè)刷新周期里更新
  • 需要在主線程調(diào)用此方法
  • 不管尺寸有沒(méi)有更改都會(huì)會(huì)調(diào)用layoutSubviews()方法

3.layoutIfNeeded方法

  • 會(huì)立即更新視圖
  • 使用自動(dòng)布局的視圖會(huì)默認(rèn)更新改變的尺寸
  • 可在動(dòng)畫里使用該屬性
  • 有需要刷新的標(biāo)記會(huì)立即調(diào)用,沒(méi)有則不會(huì)調(diào)用

4.layoutSubviews調(diào)用時(shí)機(jī)

  • 初始化時(shí)設(shè)置frame不為Zero會(huì)觸發(fā)
  • 直接調(diào)用[self setNeedsLayout]
  • addSubview時(shí)
  • 當(dāng)view的size發(fā)送改變的時(shí)候哲身,前提是frame的值前后發(fā)生了變化
  • 滑動(dòng)UIScrollView的時(shí)候
  • 旋轉(zhuǎn)屏幕 可能會(huì)觸發(fā)
  • 更新視圖的 constraint

5.如果要立即刷新

  • 先調(diào)用[view setNeedsLayout]辩涝,標(biāo)記為需要布局贸伐,然后調(diào)用[view layoutIfNeeded]勘天,實(shí)現(xiàn)布局

本文是整理學(xué)習(xí)文章,大部分非原創(chuàng)捉邢,參考鏈接:
1.Demystifying iOS Layout
2.帥氣的軍大王

END脯丝。
我是小侯爺。
在帝都艱苦奮斗伏伐,白天是上班族宠进,晚上是知識(shí)服務(wù)工作者。
如果讀完覺(jué)得有收獲的話藐翎,記得關(guān)注和點(diǎn)贊哦材蹬。
非要打賞的話实幕,我也是不會(huì)拒絕的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末堤器,一起剝皮案震驚了整個(gè)濱河市昆庇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闸溃,老刑警劉巖整吆,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辉川,居然都是意外死亡表蝙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門乓旗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)府蛇,“玉大人,你說(shuō)我怎么就攤上這事屿愚∮担” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵渺鹦,是天一觀的道長(zhǎng)扰法。 經(jīng)常有香客問(wèn)我,道長(zhǎng)毅厚,這世上最難降的妖魔是什么塞颁? 我笑而不...
    開(kāi)封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮吸耿,結(jié)果婚禮上祠锣,老公的妹妹穿的比我還像新娘。我一直安慰自己咽安,他們只是感情好伴网,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著妆棒,像睡著了一般澡腾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糕珊,一...
    開(kāi)封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天动分,我揣著相機(jī)與錄音,去河邊找鬼红选。 笑死澜公,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喇肋。 我是一名探鬼主播坟乾,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼迹辐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嫩与!你這毒婦竟也來(lái)了跋破?” 一聲冷哼從身側(cè)響起柒爵,我...
    開(kāi)封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贷痪,失蹤者是張志新(化名)和其女友劉穎鲤孵,沒(méi)想到半個(gè)月后歹叮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體寓涨,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡澎蛛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年宗兼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躏鱼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殷绍,死狀恐怖染苛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情主到,我是刑警寧澤茶行,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站登钥,受9級(jí)特大地震影響畔师,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牧牢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一看锉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧塔鳍,春花似錦伯铣、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至掌唾,卻和暖如春放前,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背郑兴。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工犀斋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人情连。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像览效,于是被迫代替她去往敵國(guó)和親却舀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虫几,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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

  • 翻譯自:Demystifying iOS Layout 在你剛開(kāi)始開(kāi)發(fā) iOS 應(yīng)用時(shí),最難避免或者是調(diào)試的就是和...
    Mr大喵喵閱讀 402評(píng)論 0 3
  • 在你剛開(kāi)始開(kāi)發(fā) iOS 應(yīng)用時(shí)挽拔,最難避免或者是調(diào)試的就是和布局相關(guān)的問(wèn)題辆脸。通常這種問(wèn)題發(fā)生的原因就是對(duì)于 view...
    MccReeee閱讀 731評(píng)論 0 2
  • 理解Update Cycle摘自《[譯] 揭秘 iOS 布局》 UPdate Cycle是當(dāng)應(yīng)用完成了你所有的事件...
    我是繁星閱讀 1,836評(píng)論 0 1
  • 1术裸、前言 首先倘是,我認(rèn)為學(xué)習(xí)總結(jié),要有所總袭艺,所結(jié)搀崭,就是有歸納后,能用自己的話告訴別人猾编!有所結(jié)瘤睹,就是有所收獲輸出,一般...
    iHTCboy閱讀 687評(píng)論 0 1
  • 表情是什么答倡,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒轰传。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了瘪撇,難過(guò)就哭了绸吸。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,915評(píng)論 2 7