UIKit Inside: frame bounds position anchorPoint center

chaoguo1234發表於2023-11-06

iOS 中UIView的屬性:frameboundscenter以及CALayer的屬性:positionanchorPoint與檢視的位置與大小相關,理解這些屬性是進行 iOS 檢視編碼的基礎。

下面從彙編角度看一下這些屬性的實現以及相互關係。

1 frame

frame定義了檢視在父檢視座標系下的位置與大小。

UIKit Inside: frame bounds position anchorPoint center

上圖中紅色UIViewframe為 {x: 50, y: 50, width: 100, height: 200}。

如果訪問viewframe屬性,彙編程式碼如下:

;UIKitCore`-[UIView(Geometry) frame]:
...
; 1. x0 暫存器儲存 UIView 的 CALayer 物件指標
0x1bae62384 <+44>: mov    x0, x8
; 2. bl 指令呼叫 objc_msgSend 方法,也就是呼叫 [CALayer frame]
0x1bae62388 <+48>: bl     0x1bc590f00  ; objc_msgSend$frame
...

上面程式碼註釋 1 暫存器x0儲存的是UIView對應的CALayer物件指標,在控制檯輸出的結果如下:

(lldb) po $x0
<CALayer:0x280ea0f40; position = CGPoint (100 150); bounds = CGRect (0 0; 100 200); delegate = <UIView: 0x10380dd70; frame = (50 50; 100 200); backgroundColor = UIExtendedSRGBColorSpace 1 0 0 1; layer = <CALayer: 0x280ea0f40>>; allowsGroupOpacity = YES; backgroundColor = <CGColor 0x282aa9680> [<CGColorSpace 0x282aa0cc0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 )>

註釋 2 的bl指令是函式呼叫指令,相當於x64彙編中的call指令。這個呼叫指令呼叫objc_msgSend方法,而ARM裡面,函式的第一個引數由x0暫存器傳遞,objc_msgSend第一個引數是self,因此這裡相當於呼叫[CALayer frame]

從程式碼可以看到,訪問UIViewframe屬性,實際上就是訪問UIView對應的CALayerframe屬性。CALayerframe彙編程式碼如下:

;QuartzCore`-[CALayer frame]:
...
; 1. x0 暫存器 CALayer 物件指標暫存到暫存器 x21
0x1ba335b6c <+32>:  mov    x21, x0
...
; 2. x21 暫存器儲存著 CALayer 物件指標,ldr 指標是 LOAD 記憶體操作,讀取從 CALayer 物件開始偏移 0x10 位元組的內容,
; 也就是讀取記憶體地址 (x21 + 0x10) 的內容,存到 x20 暫存器
0x1ba335ba4 <+88>:  ldr    x20, [x21, #0x10]
0x1ba335ba8 <+92>:  ldr    w8, [x20, #0x34]
;3. tbnz 是一個測試跳轉指令,如果有設定過 CALayer 的 anchorPoint,就跳轉到當前程式碼偏移 <+112> 位元組處執行,
; 也就是去呼叫 [CALayer anchorPoint] 方法
0x1ba335bac <+96>:  tbnz   w8, #0x18, 0x1ba335bbc    ; <+112>
; 4. 如果沒有設定過 CALayer 物件的 anchorPoint 屬性,那麼就使用預設值 (0.5, 0.5)
0x1ba335bb0 <+100>: fmov   d1, #0.50000000
0x1ba335bb4 <+104>: fmov   d0, #0.50000000
; 5. b 指令是跳轉指令,跳轉到當前程式碼偏移 <+124> 位元組處執行
0x1ba335bb8 <+108>: b      0x1ba335bc8               ; <+124>
; 6. 如果設定過 CALayer 物件的 anchorPoint 屬性,就會呼叫 [CALayer anchorPoint] 方法,
; x21 暫存器儲存著 CALayer 物件指標,這裡傳遞給 x0 暫存器,作為 objc_msgSend 方法第一個引數
0x1ba335bbc <+112>: mov    x0, x21
; 7. bl 指令呼叫 objc_msgSend 方法,方法的返回結果儲存在 d0 與 d1 暫存器中
0x1ba335bc0 <+116>: bl     0x1ba680aa0               ; objc_msgSend$anchorPoint
...
; 8. 讀取記憶體地址 (x20 + 0x68) 處 16 位元組的內容,前 8 位元組存到到暫存器 d8,後 8 位元組存放到暫存器 d9,
; 這裡實際上讀取的是 CALayer 物件 bounds 屬性的 width 與 height
0x1ba335bd0 <+132>: ldp    d8, d9, [x20, #0x68]
...
; 9. 讀取記憶體地址 (x20 + 0x48) 處 16 位元組的內容,前 8 位元組存到暫存器 d2,後 8 位元組存放到暫存器 d3,
; 這裡時機上讀取的是 CALayer 物件的 position 屬性
0x1ba335bdc <+144>: ldp    d2, d3, [x20, #0x48]
; 10. fmsub 指令的操作是: 將暫存器 d0 與 d8 內容相乘,然後使用暫存器 d2 內容減去前面的乘積,最後將結果儲存到暫存器 d10,
; 也就是相當於 d10 = d2 - d0 * d8,
; 也就是相當於 UIView.frame.origin.x = CALayer.position.x - anchorPoint.x * CALayer.bounds.width
0x1ba335be0 <+148>: fmsub  d10, d0, d8, d2
; 11. 同註釋 9,相當於 d11 = d3 - d1 * d9,
; 也相當於 UIView.frame.origin.y = CALayer.position.y - anchorPoint.y * CALayer.bounds.height
0x1ba335be4 <+152>: fmsub  d11, d1, d9, d3
...
; 12. 下面 4 條 fmov 指令將暫存器 d10 d11 d8 d9 的值賦值給暫存器 d0 d1 d2 d3 作為返回值,
; d0 = origin.x d1 = origin.y d2 = size.width d3 = size.height
0x1ba335c48 <+252>: fmov   d0, d10
0x1ba335c4c <+256>: fmov   d1, d11
0x1ba335c50 <+260>: fmov   d2, d8
0x1ba335c54 <+264>: fmov   d3, d9
...
// 13. 函式返回
0x1ba335c70 <+292>: retab

上面程式碼註釋 1 將 x0 暫存器的值暫存到 x21 暫存器,也就是 x21 暫存器也儲存著 CALayer 物件指標。

程式碼註釋 2 ldr 指令是ARM裡面載入記憶體的 LOAD 指令,也就是將記憶體裡面的值載入到對應暫存器,相當於從記憶體地址 (x21 + 0x10) 處載入值。由於暫存器x21儲存的是CALayer物件指標,這裡相當於從CALayer物件首地址偏移0x10位元組,然後將此處記憶體內容載入到暫存器x20:

UIKit Inside: frame bounds position anchorPoint center

CALayer物件偏移0x10位元組處存放的是什麼呢?使用LLDB除錯命令po [$x21 _ivarDescription]輸出CALayer物件的例項變數成員:

(lldb) po [$x21 _ivarDescription]
<CALayer: 0x28065fa60>:
in CALayer:
	_attr (struct _CALayerIvars): {
		refcount (int): 4
		magic (unsigned int): 1279351122
		layer (void*): 0x1031053f0
		_objc_observation_info (void*): 0x0
	}
in NSObject:
	isa (Class): CALayer (isa, 0x10000020f2845e3)

方法_ivarDescriptionNSObject的私有方法,任何一個繼承自NSObject的物件,都可以使用它輸出自己的例項變數成員,包括繼承過來的。從上面的輸出可以看到,CALayer物件首地址偏移0x10位元組處是一個void指標,它指向一個名為layer的物件:

UIKit Inside: frame bounds position anchorPoint center

這個layer物件很重要,從後面可以知道,layer物件偏移0x48處儲存著CALayerposition值,偏移0x58處儲存著CALayer.bounds.origin值,偏移0x68處儲存著CALayer.bounds.size值:

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 3 處tbnz是一個測試跳轉指令,如果CALayer設定過anchorPoint,就跳轉到當前程式碼偏移<+112>位元組處執行。

程式碼註釋 4 處是如果沒有設定過CALayer物件的anchorPoint屬性,就使用預設的值 (0.5, 0.5),預設值被儲存在浮點數暫存器d0d1中。

程式碼註釋 5 b指令是一個跳轉指令,獲取到預設的anchorPoint值之後,就跳轉到當前程式碼偏移<+124>位元組處執行。

程式碼註釋 6 7 處正是註釋 3 處跳轉過來要執行的程式碼,是呼叫[CALayer anchorPoint]方法去獲取設定的anchorPoint值,方法返回的結果會被儲存在浮點數暫存器d0d1中。

總之,經過程式碼註釋 3 4 5 6 7處的程式碼,浮點暫存器d0d1已經儲存了CALayer物件的anchorPoint屬性值,其中d0儲存anchorPoint.xd1儲存anchorPoint.y

程式碼註釋 8 處讀取地址(x20 + 0x68)處的內容。由於暫存器x20儲存著上圖中的layer物件指標,因此這裡讀取的是CALayer物件的bounds.size值。其中,浮點數暫存器d8儲存bounds.size.width,浮點數暫存器d9儲存bounds.size.height

程式碼註釋 9 處讀取地址(x20 + 0x48)處的內容。由於暫存器x20儲存這上圖中的layer物件指標,因此這裡讀取的是CALayer物件的position值。其中,浮點數暫存器d2儲存position.x,浮點數暫存器d3儲存position.y

程式碼註釋 10 處fmsub指令的操作是: \(d10 = d2 - d0 * d8\)。由於d2暫存器儲存著position.x,d0暫存器儲存著anchorPoint.xd8暫存器儲存著bounds.size.width,因此這裡實際上是在計算frame.origin.x:

\[frame.origin.x = position.x - anchorPoint.x * bounds.size.width \]

程式碼註釋 11 同理,相當於:\(d11 = d3 - d1 * d9\)。由於d3暫存器儲存著position.yd1暫存器儲存著anchorPoint.yd9暫存器儲存著bounds.size.height,因此這裡實際上是在計算frame.origin.y:

\[frame.origin.y = position.y - anchorPoint.y * bounds.size.height \]

從上面的公式可以看出,UIViewframe本質上是由CALayer物件的positionanchorPointbounds.size計算而來

程式碼註釋 12 處 4 條fmov指令將浮點暫存器d10 d11 d8 d9的值賦值到浮點數暫存器d0 d1 d2 d3,以變用來符合ARM函式返回值呼叫約定。這樣一來,UIViewframe值為 {x: d0, y: d1, width: d3, height: d4}。

程式碼註釋 13 retab執行函式返回指令。

綜上所述,UIViewframe本質上就是CALayer的`frame,使用虛擬碼表示為:

@interface UIView

@end

@implementation

- (CGRect)frame {
	return [self.layer frame];
}

CALayerframe虛擬碼可以表示為:

@interface CALayer

@end

@implementation

- (CGRect)frame {
	CGPoint anchorPoint = CGPointMake(0.5, 0.5);
	if (設定過 CALayer 物件的 anchorPoint) {
		anchorPoint = [self anchorPoint];
	}
	
	CGFloat x = self.position.x - anchorPoint.x * self.bounds.size.width;
	CGFloat y = self.position.y - anchorPoint.y * self.bounds.size.height;
	return CGRectMake(x, y, self.bounds.size.width, self.bounds.size.height);
}

同理,如果是對UIViewframe進行設定,本質上也是對CALayerframe進行設定:

;UIKitCore`-[UIView(Geometry) setFrame:]:
...
// 1. x0 暫存器存放 UIView 對像指標,浮點數暫存器 d0 d1 d2 d3 組成 CGRect 結構體,作為 [UIView _backing_setFrame:] 的引數
0x1bae60bc4 <+316>: mov    x0, x19
0x1bae60bc8 <+320>: fmov   d0, d12
0x1bae60bcc <+324>: fmov   d1, d13
0x1bae60bd0 <+328>: fmov   d11, d8
0x1bae60bd4 <+332>: fmov   d2, d8
0x1bae60bd8 <+336>: fmov   d3, d9
// 2. bl 指令呼叫 [UIView _backing_setFrame:] 方法
0x1bae60bdc <+340>: bl     0x1bb818844               ; -[UIView _backing_setFrame:]

從上面程式碼可以看到,當設定UIViewframe時,首先呼叫其內部方法[UIView _backing_setFrame:]

程式碼註釋 1 處x0暫存器存放UIView物件指標,作為self引數,浮點數暫存器d0 d1 d2 d3組成CGRect結構體,作為[UIView _backing_setFrame:]方法的引數。

程式碼註釋 2 處bl指令呼叫[UIView _backing_setFrame:]方法。

下面接著看[UIView _backing_setFrame:]的實現:

;UIKitCore`-[UIView _backing_setFrame:]:
...
 0x1bb818874 <+48>: add    x8, x8, #0xaa4    ; UIView._layer
0x1bb818878 <+52>: ldrsw  x8, [x8]
// 1. x0 暫存器儲存 CALayer 物件指標
0x1bb81887c <+56>: ldr    x0, [x19, x8]
// 2. bl 呼叫 [CALayer setFrame:] 方法,浮點數暫存器 d0 d1 d2 d3 沒有發生改變,作為 CGRect 引數
0x1bb818880 <+60>: bl     0x1bc5f9b40        ; objc_msgSend$setFrame:
...

上面程式碼可以看到,[UIView _backing_setFrame:]實際上呼叫的是[CALayer setFrame:]方法。

程式碼註釋 1 處暫存器x0儲存 CALayer 物件指標。

程式碼註釋 2 處bl指令呼叫[CALayer setFrame:]方法,呼叫時浮點數暫存器d0 d1 d2 d3的值沒有發生改變,仍作為CGRect引數傳遞。

下面看下[CALayer setFrame:]方法:

QuartzCore`-[CALayer setFrame:]:
...
; 1. 下面 4 條指令儲存函式引數,其中 self = x0 x = d0 y = d1 width = d2 height = d3,
; 經過 fmov 指令 x19 = self d11 = x d14 = y d8 = width d9 = height
0x1ba339d58 <+40>:  fmov   d9, d3
0x1ba339d5c <+44>:  fmov   d8, d2
0x1ba339d60 <+48>:  fmov   d14, d1
0x1ba339d64 <+52>:  fmov   d11, d0
0x1ba339d68 <+56>:  mov    x19, x0
...
; 2. 下面 2 條指令呼叫 [CALayer anchorPoint] 方法,其中 x0 暫存器儲存 CALayer 物件指標,成為 self
0x1ba339dc8 <+152>: mov    x0, x19
0x1ba339dcc <+156>: bl     0x1ba680aa0    ; objc_msgSend$anchorPoint
; 下面 2 條指令儲存 [CALayer anchorPoint] 的返回值,其中 anchorPoint.x = d0 anchorPoint.y = d1,
; 經過 fmov 指令,d15 = anchorPoint.x d13 = anchorPoint.y
0x1ba339dd0 <+160>: fmov   d15, d0
0x1ba339dd4 <+164>: fmov   d13, d1
...
; 3. fmadd 指令的操作為: d11 = d8 * d15 + d11,其中 width = d8 anchorPoint.x = d15 x = d11,
; 本質上相當於 position.x = x + anchorPoint.x * width
0x1ba339df4 <+196>: fmadd  d11, d8, d15, d11
; 4. 同註釋 3: d10 = d9 * d13 + d14,其中 height = d9 anchorPoint.y = d13 y = d14,
; 本質上相當於 positio.y = y + anchorPoint.y * height
0x1ba339df8 <+200>: fmadd  d10, d9, d13, d14
...
; 5. 下面 4 條指令呼叫 [CALyaer setPosition:] 方法,其中 x0 = self d0 = d11 = position.x d1 = d10 = position.y
0x1ba339eb0 <+384>: mov    x0, x19
0x1ba339eb4 <+388>: fmov   d0, d11
0x1ba339eb8 <+392>: fmov   d1, d10
0x1ba339ebc <+396>: bl     0x1ba687c80   ; objc_msgSend$setPosition:
...
; 6. 下面 5 條指令呼叫 [CALayer setBounds:] 方法,其中 x0 = self d0 = bounds.origin.x d1 = bounds.origin.y d2 = d8 = width d3 = d9 =height
0x1ba339ec0 <+400>: mov    x0, x19
0x1ba339ec4 <+404>: ldp    d0, d1, [sp]
0x1ba339ec8 <+408>: fmov   d2, d8
0x1ba339ecc <+412>: fmov   d3, d9
0x1ba339ed0 <+416>: bl     0x1ba6869e0    ; objc_msgSend$setBounds:
...

從上面程式碼可以看到,[CALayer setFrame]方法的frame引數中的 x y 最終計算出新的position值,設定到CALayer物件中。frame引數中的width height最終被設定為CALayer物件的bounds.size.width bounds.size.height

image

從上圖可以看到,引數frame只會影響到CALayer物件的position屬性和bounds.size屬性,而不會改變CALayeranchorPosition屬性與bounds.origin屬性。

上面程式碼註釋 1 儲存CALayer物件指標以及引數frame的值。

程式碼註釋 2 呼叫[CALayer anchorPoint]方法獲取anchorPoint值,目的是為後面計算新的position值做準備。獲取的anchorPosition值被最終儲存在暫存器d15 d13中,其中d15 = anchorPoint.x d13 = anchorPoint.y

程式碼註釋 3 4 使用anchorPoint 與引數frame算新的position值,用公式表示如下:

\[position.x = x + anchorPoint.x * width \]

\[position.y = y + anchorPoint.y * height \]

程式碼註釋 5 呼叫[CALayer setBounds:]設定CALayer物件的bounds屬性,主要更新bounds屬性的size部分,而不會改變bounds屬性的origin部分。

如果使用虛擬碼表示,[UIView setFrame:]表示為:

@interface UIView 

@end

@implementation

- (void)setFrame:(CGRect)frame {
	[self.layer setFrame:frame];
}

@end

函式[CALayer setFrame:]使用虛擬碼表示為:

@interface CALayer

@end

@implementation CALayer

- (void)setFrame:(CGRect)frame {
    // 獲取 anchorPoint
	CGPoint anchorPoint = [self anchorPoint];
	// 計算新的 position
	CGFloat newPositionX = frame.x + anchorPosition.x * frame.width;
	CGFloat newPositionY = frame.y + anchorPosition.y * frame.height;
	[self setPosition:CGPointMake(newPositionX, newPositionY)];
	// 設定新的 bounds.size
	CGRect oldBounds = [self bounds];
	CGRect newBounds = CGRectMake(oldBounds.origin.x, oldBounds.origin.y, frame.size.width, frame.size.height);
	[self setBounds:newBounds];
}

@end

2 bounds

bounds定義了一個UIView自己的座標系,也就是這個UIViewSubview佈局就是相對於bounds定義的座標系。

bounds定義的座標系原點位於UIView檢視的坐上角,預設為 (0, 0),修改boundsorigin屬性可以更改原點的值:

UIKit Inside: frame bounds position anchorPoint center

上圖 1 紅色檢視bounds為 {0, 0, 100, 200},藍色檢視frame為 {30, 100, 30, 30}。

上圖 2 修改了紅色檢視bounds為 {10, 50, 100, 200},bounds的修改不會改變紅色檢視的位置,也不會改變藍色檢視的frame值,藍色檢視的frame依然是 {30, 100, 30, 30}。但是由於紅色檢視左上角已被修改為 (10, 50),所以藍色檢視現在只需要距離紅色檢視左邊 20,距離紅色檢視上邊 50。在視覺上,就是藍色檢視向上和向做發生了移動。

下面看一下[UIView bounds]方法:

;UIKitCore`-[UIView bounds]:
...
; 1. ldr 指令執行之後,x0 暫存器儲存 CALayer 物件指標
0x1bae6220c <+24>: ldr    x0, [x0, x8]
; 2. bl 指令呼叫 [CALayer bounds] 方法
0x1bae62210 <+28>: bl     0x1bc562480    ; objc_msgSend$bounds
...

從上面程式碼可以看到,[UIView bounds]方法最終呼叫了[CALayer bounds]方法。

程式碼註釋 1 ldr指令執行之後,x0暫存器儲存CALayer物件指標,作為objc_msgSend方法的self引數。

程式碼註釋 2 bl指令呼叫[CALayer bounds]方法。

接著看一下[CALyaer bounds]方法:

QuartzCore`-[CALayer bounds]:
; 1. CALayer 物件首地址偏移 0x10 位元組處是 C++ layer 物件,
; ldr 指令執行之後,暫存器 x8 儲存 CALayer 物件中的 C++ layer 物件。
0x1ba33a120 <+0>:  ldr    x8, [x0, #0x10]
; 2 .C++ layer 物件首地址偏移 0x58 位元組儲存 bounds.origin,
; 指令執行之後 d0 = bounds.origin.x d1 = bounds.origin.y
0x1ba33a124 <+4>:  ldp    d0, d1, [x8, #0x58]
; 3. C++ layer 物件首地址偏移 0x68 位元組儲存 bounds.size,
; 指令執行之後 d2 = bounds.size.width d3 = bounds.size.height
0x1ba33a128 <+8>:  ldp    d2, d3, [x8, #0x68]
; 4. 函式返回
0x1ba33a12c <+12>: ret 

從上面程式碼可以看到[CALayer bounds]方法非常簡短,總共只有 4 條彙編語句。

程式碼註釋 1 載入CALayer物件首地址偏移 0x10 位元組處記憶體內容,也就是前面圖中CALayer物件中的 C++ layer 物件指標到暫存器x8

程式碼註釋 2 載入 C++ layer 物件首地址偏移 0x58 位元組處內容,也就是前面圖中的 bounds.origin,其中d0 = bounds.origin.x d1 = bounds.origin.y

程式碼註釋 3 載入 C++ layer 物件首地址偏移 0x69 位元組處內容,也就是前面圖中的bounds.size,其中d2 = bounds.size.width d3 = bounds.size.height

程式碼註釋 4 函式返回。

設定bounds的方法如下所示:

;UIKitCore`-[UIView(Geometry) setBounds:]:
; 1. 儲存函式引數
; d12 = d3 = bounds.size.height
; d13 = d2 = bounds.size.width
; d14 = d1 = bounds.origin.y
; d15 = d0 = bounds.origin.x
; x19 = x0 = UIView 物件指標
0x1bae8ed50 <+52>:   fmov   d12, d3
0x1bae8ed54 <+56>:   fmov   d13, d2
0x1bae8ed58 <+60>:   fmov   d14, d1
0x1bae8ed5c <+64>:   fmov   d15, d0
0x1bae8ed60 <+68>:   mov    x19, x0
...
; 2. 下面 6 句程式碼呼叫 [UIView _backing_setBounds:],其中前 5 句程式碼準備引數
; x0 = x19 = UIView 物件指標
; d0 = d15 = bounds.origin.x
; d1 = d14 = bounds.origin.y
; d2 = d13 = bounds.size.width
; d3 = d12 = bounds.size.height
0x1bae8eea8 <+396>:  mov    x0, x19
0x1bae8eeac <+400>:  fmov   d0, d15
0x1bae8eeb0 <+404>:  fmov   d1, d14
0x1bae8eeb4 <+408>:  fmov   d2, d13
0x1bae8eeb8 <+412>:  fmov   d3, d12
0x1bae8eebc <+416>:  bl     0x1bb8188a8     ; -[UIView _backing_setBounds:]
...

從上面程式碼可以看到,設定bounds程式碼最終呼叫了方法[UIView _backing_setBuonds:]

程式碼註釋 1 對引數進行儲存。

程式碼註釋 2 是對方法[UIView _backing_setBounds:]的呼叫。

[UIView _backing_setBounds:]方法如下:

;UIKitCore`-[UIView _backing_setBounds:]:
...
; 1. b 指令相當於 x64 彙編中的 jump 指令,跳轉到 [CALayer setBounds:] 執行
0x1bb8189b0 <+264>: b      0x1bc5ec8c0   ; objc_msgSend$setBounds:
...

從上面程式碼可以看到,[UIView _backing_setBounds:]方法最終呼叫[CALayer setBounds:]方法。

[CALayer setBounds:]方法程式碼如下:

; QuartzCore`-[CALayer setBounds:]:
...
; 1. x19 = x0 = CALayer 物件指標,這裡將 CALayer 物件指標暫存到 x19 暫存器
0x1ba339f84 <+20>:  mov    x19, x0
...
; 2. ldr 是記憶體 LOAD 指令,這裡載入 CALayer 物件首地址偏移 0x10 位元組處內容,也就是 C++ layer 物件指標
0x1ba339fac <+60>:  ldr    x0, [x19, #0x10]
; 3. stp 是記憶體 STORE 指令,sp 暫存器指向棧頂,這裡將暫存器 d0 d1 存入棧中,儲存結果如下:
; sp + 0x08 = d0 = bounds.origin.x sp + 0x10 = d1 = bounds.origin.y
0x1ba339fb0 <+64>:  stp    d0, d1, [sp, #0x8]
...
; 4. stp 是記憶體 STORE 指令,sp 暫存器指向棧頂,這裡將暫存器 d2 d3 存入棧中,儲存結果如下:
; sp + 0x18 = d2 = bounds.size.width sp + 0x20 = d3 = bounds.size.height
0x1ba339fb8 <+72>:  stp    d2, d3, [sp, #0x18]
...
; 5. x1 暫存器是下面要呼叫的 C++ 方法 CA::Layer::set_bounds 的第二個引數,它儲存的是棧地址 sp + 0x08,
; 註釋 3 4 的 stp 指令已經將 buonds 儲存到了從 sp + 0x08 開始的棧地址處,這裡 x1 暫存器相當於指標指向這片區域
; C++ 方法和 OC 方法一樣,每個方法也有一個隱藏引數,就是由 x0 暫存器儲存的第一個引數,也就是 this 指標,這裡是 C++ Layer 物件
0x1ba339fe8 <+120>: add    x1, sp, #0x8
; 6. x2 暫存器是 64bit 的,如果引用其低 32bit,就是 w2 暫存器,這裡作為下面方法的第三個引數,也就是傳遞 true
0x1ba339fec <+124>: mov    w2, #0x1
; 7. bl 是函式呼叫指令,這裡呼叫方法 CA::Layer::set_bounds 方法
0x1ba339ff0 <+128>: bl     0x1ba352ba8     ; CA::Layer::set_bounds(CA::Rect const&, bool)
...

從上面程式碼可以看到,[CALayer setBounds:]呼叫了 C++ 方法CA::Layer::set_bounds

程式碼註釋 1 將x0暫存器儲存的CALayer物件指標暫存到暫存器x19,也就是 `x19 = x0 = CALayer 物件指標。

程式碼註釋 2 載入CALayer物件首地址偏移 0x10 位元組的記憶體內容,從前面知道,這個記憶體儲存著 C++ layer 物件指標。這裡將該值載入到暫存器x0

程式碼註釋 3 是記憶體 STORE 指令,指令中使用的sp暫存器是棧頂指標,將暫存器d0 d1的內容儲存到棧地址 sp + 0x08 sp + x010裡面,也就是 sp + 0x8 = bounds.origin.x sp + 0x10 = bounds.origin.y

程式碼註釋 4 是記憶體 STORE 指令,將暫存器d2 d3的內容儲存到棧地址sp + 0x18 sp + 0x20 裡面,也就是sp + 0x18 = bounds.size.width sp + 0x20 = bounds.size.height

經過上面兩步,棧儲存的內容如下:

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 5 為要呼叫的函式準備第 2 個引數,這個引數是對Rect的引用,Rect值已經由註釋 3 4 存入到了棧裡面,這裡將該值地址,也就是sp + 0x08存入到暫存器x1。C++ 方法和 OC 方法一樣,都有隱藏引數,OC 是self_cmd引數,C++ 是this指標。this指標已經存入到了暫存器x0,這裡暫存器x1儲存的就是方法引數CA::Rect const&

程式碼註釋 6 為要呼叫的函式準備第 3 個引數。在 ARM64 裡面,x2暫存器是 64bit 的,如果想引用其低 32bit,就使用w2。這裡w2儲存值 1,也就是傳true

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 7 呼叫函式CA::Layer::set_bounds

C++ 函式CA::Layer::set_bounds程式碼如下:

;QuartzCore`CA::Layer::set_bounds:
...
; 1. x1 = CA::Rect const&,這裡將暫存器 x1 裡面儲存的指標存入到暫存器 x20
0x1ba352bc4 <+28>:  mov    x20, x1
...
; 2. ldr 是記憶體 LOAD 指令,這裡載入 x20 指向的記憶體區域,
; 暫存器 q0 是 128bit,因此這載入連續的 0x10 位元組記憶體內容到暫存器 q0,
; 也就是 q0 高 64bit 存放 bounds.origin.y q0 低 64bit 存放 bounds.origin.x
0x1ba352d04 <+348>: ldr    q0, [x20]
; 3. str 是記憶體 STORE 指令,此時的暫存器 x0 已經指向偏移 C++ layer 物件首地址 0x28 處,
; x0 + 0x30 就是指向了偏移 C++ layer 物件首地址 0x58 處,這裡將暫存器 q0 內容儲存到這個位置,
; 從前面圖可以知道,C++ layer 物件首地址偏移 0x58 處正是 bounds.origin 儲存的地方。
0x1ba352d08 <+352>: str    q0, [x0, #0x30]
; 4. ldr 是記憶體 LOAD 指令,這裡載入 x20 + 0x10 記憶體地址內容,
; 暫存器 q0 是 128bit,因此這裡載入連續的 0x10 位元組記憶體內容到暫存器 q0,
; 也就是 q0 高 64bit 存放 bounds.size.height q0 低 64bit 存放 bounds.size.width
0x1ba352d0c <+356>: ldr    q0, [x20, #0x10]
; 5. str 是記憶體 STORE 指令,x0 + 0x40 就是指向了偏移 C++ layer 物件首地址 0x68 處,這裡將暫存器 q0 內容儲存到這個位置,
; 從前面圖可以知道,C++ layer 物件首地址偏移 0x68 處正是 bounds.size 儲存的地方
0x1ba352d10 <+360>: str    q0, [x0, #0x40]
...

從上面程式碼可以看到,CA::Layer::set_bounds方法將bounds值存放到了 C++ layer 物件對應的地方。

程式碼註釋 1 將x1的值賦值給x20,由於x1指向的是[CALayer setBounds:]方法存入到棧裡面的bounds值,這裡x20也指向同一片區域。

程式碼註釋 2 是記憶體 LOAD 指令,指令中暫存器q0是 128bit,這裡將暫存器x20指向的記憶體連續 0x10 位元組載入到暫存器q0,也就是q0高 64bit 儲存bounds.origin.y,低 64bit 儲存bounds.origin.x。ARM64 裡面總共有 32 個 128bit 的浮點數暫存器,記為v0~v31或者q0~q31,它們的低 64bit 被記做d0~d31

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 3 是記憶體 STORE 指令,此時暫存器x0已經指向偏移 C++ layer 物件首地址 0x28 處,x0 + 0x30就是指向了偏移 C++ layer 物件首地址 0x58 處,這裡正是儲存bounds.origin的地方。

程式碼註釋 4 是記憶體 LOAD 指令,這裡將x20 + 0x10處連續 0x10 位元組載入暫存器q0,也就是q0高 64bit 儲存bounds.size.height,低 64bit 儲存bounds.size.width

程式碼註釋 5 是記憶體 STORE 指令,此時暫存器x0已經指向偏移 C++ layer 物件首地址 0x28 處,x0 + 0x40就是指向了偏移 C++ layer 物件首地址 0x68 處,這裡正是儲存bounds.size的地方。

3 position

position表示檢視在父檢視中的位置,它和anchorPoint一起計算出檢視的frame

讀取position的程式碼如下:

; QuartzCore`-[CALayer position]:
; 1. x0 暫存器是 CALayer 物件指標,首地址偏移 0x10 位元組處就是 C++ layer 物件指標
; C++ layer 物件指標被載入到 x8 暫存器
0x1ba330df8 <+0>: ldr    x8, [x0, #0x10]
; 2. C++ layer 物件偏移 0x48 位元組處正是存放的 position 值,這裡將該值存放到暫存器 d0 d1,
; d0 = position.x d1 = position.y
0x1ba330dfc <+4>: ldp    d0, d1, [x8, #0x48]
0x1ba330e00 <+8>: ret 

從程式碼上可以知道,讀取position的程式碼非常簡單,直接從 C++ layer 物件的對應位置讀取就行。

程式碼註釋 1 處 x0 暫存器是 CALayer 物件指標,首地址偏移 0x10 位元組處就是 C++ layer 物件指標,這裡將 C++ layer 物件指標載入到暫存器x8

程式碼註釋 2 處載入 C++ layer 物件首地址偏移 0x48 處記憶體內容,這裡儲存的就是position值,也就是d0 = position.x d1 = position.y

設定position的程式碼如下:

; QuartzCore`-[CALayer setPosition:]:
...
; 1. x0 暫存器儲存著 CALayer 物件地址指標,首地址偏移 0x10 位元組處是 C++ layer 物件指標,
; 這裡將 C++ layer 物件指標載入到暫存器 x0,x0 儲存的就是方法 CA::Layer::set_positon 的 this 指標。
0x1ba339f34 <+32>: ldr    x0, [x0, #0x10]
; 2. d0 = position.x d1 = position.y 暫存器 sp 是棧頂指標,這裡將 d0 d1 的值儲存到 sp + 0x8 地址處
0x1ba339f38 <+36>: stp    d0, d1, [sp, #0x8]
; 3. x1 儲存地址 sp + 0x8,也就是指向剛存入棧裡的 position 值,作為 CA::Layer::set_position 的弟 2 個引數 CA::Vec2<double> const&
0x1ba339f3c <+40>: add    x1, sp, #0x8
; 4. w2 儲存 CA::Layer::set_position 方法第 3 個引數,這裡就是傳佈爾值 true
0x1ba339f40 <+44>: mov    w2, #0x1
; 5. 呼叫函式 CA::Layer::set_bounds
0x1ba339f44 <+48>: bl     0x1ba352968  ; CA::Layer::set_position(CA::Vec2<double> const&, bool)
...

從程式碼上可以看到,[CALayer setPosition:]呼叫 C++ 函式[CA::Layer::set_position]設定position值。

程式碼註釋 1 x0 暫存器儲存著 CALayer 物件地址指標,首地址偏移 0x10 位元組處是 C++ layer 物件指標,這裡將 C++ layer 物件指標載入到暫存器 x0,x0 儲存的就是方法 CA::Layer::set_positon 的 this 指標。

程式碼註釋 2 將引數position值存放到堆疊中。暫存器 d0 = position.x d1 = position.y,這兩個值被存入到堆疊地址sp + 0x8處。

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 3 將地址sp + 0x8儲存暫存器x1,這樣x1指向了這片棧區域,而x1暫存器會作為第 2 個引數CA::Vec2 const&`傳給下面要呼叫的 C++ 函式。

程式碼註釋 4 給暫存器w2賦值 1,也就是布林值true,作為下面呼叫函式的第 3 個引數。

程式碼註釋 5 呼叫 C++ 函式CA::Layer::set_position

C++ 函式CA::Layer::set_position程式碼如下:

; QuartzCore`CA::Layer::set_position:
...
; x1 指向儲存在棧裡的 position 值,這裡將 x1 的值賦值給 x20,x20 也指向了同樣的區域
0x1ba352984 <+28>:  mov    x20, x1
...
; ldr 是記憶體 LOAD 指令,這裡將 x20 指向的記憶體內容載入到暫存器 q0,
; q0 高 64bit 儲存 position.y 低 64bit 儲存 position.x
0x1ba352a80 <+280>: ldr    q0, [x20]
; str 是記憶體 STORE 指令,這裡 x0 已經指向了偏移 C++ layer 物件首地址 0x28 處,
; x0 + 0x20 就是偏移 C++ layer 物件首地址 0x48 處,這裡正好是儲存 position 值的地方
0x1ba352a84 <+284>: str    q0, [x0, #0x20]
...

從程式碼可以知道,CA::Layer::set_position方法最終將引數position值儲存到了偏移 C++ layer 物件首地址 0x48 位元組處,從前面圖可以知道這裡正好是CALayer儲存position值的地方。

同時,設定position時,並不會改變anchorPoint

4 anchorPoint

anchorPoint使用基於檢視大小的單位座標系(unit coordinate space),預設值是 (0.5, 0.5)。從前面的frame計算可以知道,改變achorPoint的值,將會改變檢視的位置。

UIKit Inside: frame bounds position anchorPoint center

所有幾何操作都是基於anchorPoint的,比如如果旋轉檢視,預設情況下anchorPoint位於檢視中心,此時旋轉會以檢視中心為支點進行。如果更改了anchorPoint,那麼旋轉就會以新的anchorPoint為支點進行。

UIKit Inside: frame bounds position anchorPoint center

[CALayer anchorPoint]程式碼如下:

; QuartzCore`-[CALayer anchorPoint]:
...
; 1. 暫存器 x0 儲存 CALayer 物件指標,偏移首地址 0x10 位元組處儲存的是 C++ layer 物件指標,
; 這個指標值儲存到 x0
0x1ba33a5c8 <+32>:  ldr    x0, [x0, #0x10]
...
; 2. ldr 是記憶體 LOAD 指令,這裡載入 C++ layer 物件首地址偏移 0x40 位元組處內容到暫存器 x8
0x1ba33a5f0 <+72>:  ldr    x8, [x0, #0x40]
...
; 3. ldr 是記憶體 LOAD 指令,繼續載入 x8 指向的記憶體內容,儲存到暫存器 x0,
; 這樣暫存器 x0 就儲存 CA::AttrList::get 的 this 指標,作為第 1 個引數
0x1ba33a5f8 <+80>:  ldr    x0, [x8]
; 4. sp 是棧頂指標,這裡 x3 也指向了棧頂,作為 CA::AttrList::get 函式的第 4 個引數,儲存返回結果
0x1ba33a5fc <+84>:  mov    x3, sp
; 5. w1 儲存 CA::AttrList::get 的第 2 個引數
0x1ba33a600 <+88>:  mov    w1, #0x15
; 6. w2 儲存 CA::AttrList::get 的第 3 個引數
0x1ba33a604 <+92>:  mov    w2, #0x13
; 7. 呼叫 CA::AttrList::get 函式,函式而返回結果寫入 x3 指向的棧頂
0x1ba33a608 <+96>:  bl     0x1ba3b8ee8               ; CA::AttrList::get(unsigned int, _CAValueType, void*) const
; 8. 函式執行完成後,跳轉到當前函式偏移 <+120> 位元組處
0x1ba33a60c <+100>: b      0x1ba33a620               ; <+120>
...
; 9. 將儲存在棧頂的結果載入到暫存器 d0 d1,也就是 d0 = anchorPoint.x  d1 = anchorPoint.y
0x1ba33a620 <+120>: ldp    d0, d1, [sp]
...

從上面函式可以知道,[CALayer anchorPoint]呼叫 C++ 函式CA::AttrList::get讀取出來。

程式碼註釋 1 處暫存器x0一開始儲存著CALayer物件指標,其首地址偏移 0x10 位元組處儲存著 C++ layer 物件指標,這個指標值被載入到x0暫存器。

程式碼註釋 2 載入 C++ layer 物件首地址偏移 0x40 位元組處內容到暫存器x8,從下圖可以知道,此處儲存的是指向CA::AttrList連結串列的指標。

程式碼註釋 3 載入x8指向的記憶體內容到暫存器x0,從下圖可以看到,x8指向CA::AttrList連結串列的哨兵,哨兵的首地址內容儲存下一個節點的地址,因此x0指向的是CA::AttrList連結串列哨兵節點後的第一個節點。

程式碼註釋 4 將棧頂暫存器sp賦值給了x3,這樣x3也指向了棧頂。x3作為函式CA::AttrList::get的第 4 個引數,函式的返回結果會寫入x3指向的棧頂處。

程式碼註釋 5 給w1賦值,作為函式CA::AttrList::get的第 2 個引數。

程式碼註釋 6 給w2賦值,作為函式CA::AttrList::get的第 3 個引數。

程式碼註釋 7 呼叫函式CA::AttrList::get函式,函式返回結果會寫入x3指向的棧頂。

程式碼註釋 8 會挑戰到當前函式偏移<+120>位元組處執行。

程式碼註釋 9 讀取棧頂內容寫入暫存器d0 d1,也就是d0 = anchorPoint.x d1 = anchorPoint.y

CA::AttrList連結串列結構如下圖所示:

image

每一個連結串列節點儲存一個對應的屬性值,節點總共佔用 0x18 個位元組:

第一個位元組儲存指向下一個節點的指標;

第二個位元組儲存型別標識,也就是上面程式碼暫存器w1 w2組成的值,對於anchorPoint屬性,其值為0x130015

第三個位元組儲存指向屬性值物件的指標,對於anchorPoint屬性節點,它指向屬性值物件儲存的是anchorPoint座標值。

CA::AttrList::get獲取anchorPoint值就是遍歷這個連結串列,找到型別匹配的節點,並將其值返回。

[CALayer setAnchorPoint:]程式碼如下:

; QuartzCore`-[CALayer setAnchorPoint:]:
...
; 1. stp 是記憶體 STORE 指令,d0 = anchorPoint.x d1 = anchorPoint.y sp 是棧頂暫存器,
; 這裡將 d0 d1 的值儲存到記憶體地址 sp + 0x18 處
0x1ba34b9a8 <+56>:  stp    d0, d1, [sp, #0x18]
...
; 2. ldr 是記憶體 LOAD 指令,x0 此時指向的是 C++ layer 物件首地址偏移 0x28 位元組處,
; x0 + 0x18 就是指向了偏移 C++ layer 物件首地址 0x40 位元組處,這裡儲存著指向 AttrList 連結串列的指標,
; 指令執行之後,x0 就是 CA::AttrList::set 的 this 指標。
0x1ba34bae4 <+372>: ldr    x0, [x0, #0x18]
 ...
; 3. x3 指向記憶體地址 sp + 0x18 處,這塊棧區域儲存著引數 anchorPoint 值,
; x3 作為 CA::AttrList::set 的第 4 個引數。
0x1ba34bafc <+396>: add    x3, sp, #0x18
; 4. w1 儲存 CA::AttrList::set 的第 2 個引數
0x1ba34bb00 <+400>: mov    w1, #0x15
; 5. w2 儲存 CA::AttrList::set 的第 3 個引數
0x1ba34bb04 <+404>: mov    w2, #0x13
; 6. 呼叫函式 CA::AttrList::set
0x1ba34bb08 <+408>: bl     0x1ba377544     ; CA::AttrList::set(unsigned int, _CAValueType, void const*)
...

從上面程式碼可以知道,[CALayer setAnchorPoint:]是呼叫 C++ CA::AttrList::set來設定anchorPoint值的。

程式碼註釋 1 將引數anchorPoint儲存到棧。也就是將暫存器d0 = anchorPoint.x d1 = anchorPoint.y儲存到記憶體地址sp + 0x18處。

UIKit Inside: frame bounds position anchorPoint center

程式碼註釋 2 載入指向CA::AttrList連結串列的指標。x0暫存器已經指向了偏移 C++ layer 物件首地址 0x28 位元組記憶體處,x0 + 0x18正好指向偏移 C++ layer 物件 0x40 位元組記憶體處,此處正好儲存的是CA::AttrList連結串列地址。指令執行完成之後,x0就是要呼叫的方法CA::AttrList::setthis指標,也是第 1 個引數。

程式碼註釋 3 將x3指向棧地址sp + 0x18處,作為方法CA::AttrList::set的第 4 個引數。

程式碼註釋 4 設定w1的值,作為方法CA::AttrList::set的第 2 個引數。

程式碼註釋 5 設定w2的值,作為方法CA::AttrList::set的第 3 個引數。

程式碼註釋 6 呼叫函式CA::AttrList::set方法。

從上圖CA::AttrList連結串列結構可以知道,CA::AttrList::set從連結串列哨兵開始遍歷。

CA::AttrList::set方法內部使用引數w1 w2匹配連結串列節點型別,如果能找到對應的節點,就修改該節點的屬性值;如果找不到對應型別的節點,就建立一個新的節點,設定好屬性值,並且新節點插入到哨兵節點後面。

從設定anchorPoint的過程可以知道,設定anchorPoint也不會影響position的值,它們之間相互不發生影響。但是從前面frame的計算公式可以知道,anchorPoint的改動會影響檢視的位置。

5 center
center屬性指定了檢視在父檢視中的位置。

[UIView center]程式碼如下:

; UIKitCore`-[UIView center]:
...
; x0 儲存 CALayer 物件指標
0x1bae62688 <+44>: mov    x0, x8
; 呼叫 [CALayer position] 方法
0x1bae6268c <+48>: bl     0x1bc5cd9a0   ; objc_msgSend$position
...

從上面程式碼可以知道,獲取檢視center的程式碼很簡單,就是直接呼叫[CALayer center]方法。

程式碼註釋 1 x0儲存CALayer物件指標。

程式碼註釋 2 呼叫[CALayer position]方法。

[UIView setCenter:]程式碼如下:

; UIKitCore`-[UIView setCenter:]:
...
; 1. d0 = center.x d1 = center.y 下面 2 條程式碼將引數 center 暫存到暫存器 d8 d9
; d8 = d1 = center.y d9 = d0 = center.x
0x1bae8374c <+44>:  fmov   d8, d1
0x1bae83750 <+48>:  fmov   d9, d0
...
; 2. x0 儲存 CALayer 物件指標
0x1bae83830 <+272>: ldr    x0, [x19, x8]
; 3. 下面 2 條語句給暫存器 d0 d1 賦值,為呼叫 [CALayer setPosition:] 準備引數,
; d0 = d9 = center.x  d1 = d8 = center.y
0x1bae83834 <+276>: fmov   d0, d9
0x1bae83838 <+280>: fmov   d1, d8
; 4. 呼叫 [CALayer setPosition:] 方法
0x1bae8383c <+284>: bl     0x1bc60a340  ; objc_msgSend$setPosition:
...

從上面程式碼可以知道,設定檢視的center就是呼叫[CALayer setPosition:]

程式碼註釋 1 暫存引數centerd8 = d1 = center.y d9 = d0 = center.x

程式碼註釋 2 將CALayer物件指標儲存到x0

程式碼註釋 3 為呼叫[CALayer setPosition:]準備引數,d0 = d9 = center.x d1 = d8 = center.y

程式碼註釋 4 呼叫函式[CALayer setPosition:]

綜上所述,UIViewcenter屬性本質上就是CALayerposition屬性。

6 總結

1 檢視frame的位置由CALayerposition anchorPoint bounds.size計算而來:

\[frame.origin.x = position.x - anchorPoint * bounds.size.width \]

\[frame.origin.y = position.y - anchorPoint * bounds.size.height \]

因此,改變positin或者anchorPoint會改變檢視的位置。

2 設定檢視的frame影響position的值,但是不會改變anchorPoint的值:

\[position.x = frame.origin.x + anchorPoint.x * frame.size.width \]

\[posotion.y = frame.origin.y + anchorPoint.y * frame.size.height \]

3 positionanchorPoint的值互相不受影響,設定其中一個,不會影響到另一個。

4 檢視的center屬性本質上就是CALayerposition屬性。

相關文章