我原本以為這兩個東西沒啥好寫的,結果是property確實沒啥好寫的,但是ivar就不少了。
本文不探討何時該選擇property,何時該選擇ivar
我會把我研究這兩東西的過程原原本本的展示出來。
試探期
在Runtime源碼 —— 方法加載的過程這篇文章中,我提到過兩個結構體:
- class_ro_t
記錄編譯期就已經確定的信息 - class_rw_t
運行期拷貝class_ro_t中的部分信息存入此結構體中,并存放運行期添加的信息
細心的同學應該發現在ro和rw結構體中,method、protocol和property都是存在的,拷貝也就是拷貝的這部分信息,但是ro中還存在一個字段叫做:
const ivar_list_t * ivars;
這玩意兒就是本文研究的重點了。
例子
在寫代碼之前,我心里是這樣想的:
ivar都應該存在ro的ivars字段中,property存在ro的baseProperties字段中,在運行期,將property拷貝到rw中。獲取property的時候直接從rw中獲取,獲取ivar則從ro中獲取。
寫代碼測試一下:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
NSInteger ivarInt;
BOOL ivarBool;
}
@property (nonatomic, assign) NSInteger propertyInt;
@end
// ZNObjectFather.m
#import "ZNObjectFather.h"
@implementation ZNObjectFather
@end
還是通過lldb驗證一下:
// 獲取ZNObjectFather class的內存地址
2017-02-21 10:06:12.772188 TestOSX[6560:288465] 0x100002e10
(lldb) p (class_data_bits_t *)0x100002e30
(class_data_bits_t *) $0 = 0x0000000100002e30
(lldb) p $0->data()
(class_rw_t *) $1 = 0x000060800007e140
(lldb) p (*$1).ro
(const class_ro_t *) $2 = 0x0000000100002318
(lldb) p *$2
(const class_ro_t) $3 = {
flags = 128
instanceStart = 8
instanceSize = 32
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x000000010000139a "ZNObjectFather"
baseMethodList = 0x0000000100002260
baseProtocols = 0x0000000000000000
ivars = 0x0000000100002298
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000100002300
}
// 獲取ivars
(lldb) p $3.ivars
(const ivar_list_t *const) $4 = 0x0000000100002298
(lldb) p *$4
// $5的內容顯示count = 3,但是實際只聲明了兩個ivar
(const ivar_list_t) $5 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 3
first = {
offset = 0x0000000100002d88
name = 0x0000000100001448 "ivarInt"
type = 0x0000000100001aee "q"
alignment_raw = 3
size = 8
}
}
}
(lldb) p $5.get(1)
// ivar_t的結構體后面會分析
(ivar_t) $6 = {
offset = 0x0000000100002d90
name = 0x0000000100001450 "ivarBool"
type = 0x0000000100001af0 "c"
alignment_raw = 0
size = 1
}
(lldb) p $5.get(2)
// 發現聲明的屬性自動生成了一個_propertyName的ivar
(ivar_t) $7 = {
offset = 0x0000000100002d80
name = 0x0000000100001459 "_propertyInt"
type = 0x0000000100001aee "q"
alignment_raw = 3
size = 8
}
// 獲取property
(lldb) p $3.baseProperties
(property_list_t *const) $8 = 0x0000000100002300
(lldb) p *$8
// 結果符合預期
(property_list_t) $9 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "propertyInt", attributes = "Tq,N,V_propertyInt")
}
}
property的測試結果很正常,ivar不完全相同,如果直接聲明的2個之外,屬性也自動生成了一個ivar。
另外$3的baseMethodList也不為空,存的就是property自動生成的get/set方法,感興趣自己打印一下。
看到這里也就不難理解為什么:
property = ivar + get + set
但也并不總是這樣,如果重寫了屬性的get/set方法,就不會生成_propertyName這樣的ivar了,本文不做深入。
再看看$3里面的這么兩個屬性:
instanceStart = 8
instanceSize = 32
- instanceStart之所以等于8,是因為每個對象的isa占用了前8個字節。
- instanceSize = isa + 3個ivar,$6的size只有1,但是為了對齊,也占用了8,對齊是怎么計算的后面再講。
到這里對ivar和property已經有一個大概的理解了,下面繼續深入。
深入期
根據上半部分的分析,我們已經知道了
- ivars在編譯期就已經確定了
- 屬性會生成 _propertyName格式的ivar,也在編譯期確定
- 對象的大小是由 isa + ivars決定的
但是這就引出了如下幾個問題:
- 帶有繼承體系的對象是怎么表示的?
- 繼承體系中對象的 instanceStart和 instanceSize是怎么計算的?
- ivar_t中的 alignment_raw和 offset是什么意思?
- class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思?
現在我們都知道class_ro_t中的ivar,property,protocol和method都是在編譯期就確定的,在運行期時,通過realizeClass()方法將部分信息拷貝到class_rw_t中。
在realizeClass()方法中有這么一段代碼:
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
注釋里面講了,這一步會調整ivar的offset值,并且更新ro的信息,看起來這一步就是關鍵,看看方法是怎么實現的:
static void reconcileInstanceVariables(Class cls, Class supercls, const class_ro_t*& ro)
{
class_rw_t *rw = cls->data();
...
const class_ro_t *super_ro = supercls->data()->ro;
...// 省略了用于debug的相關代碼
if (ro->instanceStart >= super_ro->instanceSize) {
// Superclass has not overgrown its space. We're done here.
return;
}
if (ro->instanceStart < super_ro->instanceSize) {
...
class_ro_t *ro_w = make_ro_writeable(rw);
ro = rw->ro;
moveIvars(ro_w, super_ro->instanceSize);
gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
}
}
只保留了最關鍵的代碼,關注一下其中的if判斷,比較的是當前類的instanceStart和父類的instanceSize,當start < size的時候調整了一下當前類ro的相關信息。
這給了我一個信息,也就是在這一步之前,ro中的instanceStart和instanceSize其實并不是最終值。
具體調整的過程在moveIvars(ro_w, super_ro->instanceSize)這個方法中完成:
static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
...
uint32_t diff;
...
diff = superSize - ro->instanceStart;
if (ro->ivars) {
uint32_t maxAlignment = 1;
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t alignment = ivar.alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}
uint32_t alignMask = maxAlignment - 1;
diff = (diff + alignMask) & ~alignMask;
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t oldOffset = (uint32_t)*ivar.offset;
uint32_t newOffset = oldOffset + diff;
*ivar.offset = newOffset;
...
}
}
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
}
這個方法做了這些事情:
- 更新當前類ivar中的offset字段
- 更新當前類ro的instanceStart和instanceSize
先按照源代碼分析,最后寫代碼驗證。
part1
diff = superSize - ro->instanceStart;
獲取了當前類的instanceStart和父類的instanceSize的偏移量,但這并不是最終的結果,因為存在對齊的問題。這就是后面這個if判斷內部做的事情。
part2
先看第一個for循環:
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t alignment = ivar.alignment();
if (alignment > maxAlignment) maxAlignment = alignment;
}
遍歷了ivars,獲取了最大得alignment。這個ivar.alignment()是ivar_t結構體中的方法:
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;
uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
}
# define WORD_SHIFT 3UL
備注:這里有這么一個字段:alignment_raw,這個字段據我的理解,應該是在編譯期確定的,但是是按照什么規則確定的就不清楚了。根據測試的結果來看,一般都是0或者3。
通過ivar.alignment()得到的結果是1 << 3,也就是8。
part3
uint32_t alignMask = maxAlignment - 1;
diff = (diff + alignMask) & ~alignMask;
這一步確定了diff的值,那個&運算的結果就是把diff按8對齊,比如本來diff = 9,這一步之后diff = 16。
part4
for (const auto& ivar : *ro->ivars) {
if (!ivar.offset) continue; // anonymous bitfield
uint32_t oldOffset = (uint32_t)*ivar.offset;
uint32_t newOffset = oldOffset + diff;
*ivar.offset = newOffset;
}
這一步調整ivar的offset字段,調整的過程就是用原來的offset加上上一步得到的diff。說白了就是當前類的ivar是在父類的ivar之后的。
part5
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
最后更新了當前類的instanceStart和instanceSize,過程也是加上diff。其實就是把父類的instanceSize給空出來了。
到這里的時候,已經回答了這部分最開始提出的4個問題中的前3個。先來驗證一下。
例子
為了驗證前3個問題,需要給增加一個類:
// ZNObjectSon.h
#import "ZNObjectFather.h"
@interface ZNObjectSon : ZNObjectFather {
NSInteger ivarIntSon;
BOOL ivarBoolSon;
}
@property (nonatomic, assign) NSInteger propertyIntSon;
@end
// ZNObjectSon.m
#import "ZNObjectSon.h"
@implementation ZNObjectSon
@end
此類繼承于ZNObjectFather,按照老套路,還是先獲取一下類的地址:
2017-02-21 11:43:49.962750 TestOSX[6743:331148] father address: 0x100002e30
2017-02-21 11:43:49.962803 TestOSX[6743:331148] son address: 0x100002de0
接著在reconcileInstanceVariables()方法中添加一個條件斷點,進入斷點后,通過lldb獲取一下相關值,請看圖:
條件斷點設置的是ZNObjectFather的地址,所以:
- ro的信息就是ZNObjectFather的ro
ZNObjectFather繼承于NSObject,所以:
- super_ro是NSObject的ro
根據控制臺打印的信息,這一步的if判斷結果為true,所以直接return了,調整一下條件斷點的內容,把地址設置為ZNObjectSon的地址再試一下:
可以看到father的start和size沒有發生變化,因為上一步做過說明直接return了。
再來看看son的start值,說實話看到這個24我是無法理解的。在這之前我預期start = 8,這多出來的16是怎么回事?
我做了一個猜測:instanceStart的值在編譯期已經計算了父類直接聲明的ivar,由property生成的沒有計算。
我做了一些驗證,先把father類中的屬性注釋掉了:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
NSInteger ivarInt;
BOOL ivarBool;
}
//@property (nonatomic, assign) NSInteger propertyInt;
@end
這時候打印出來的start和size如下:
// ZNObjectFather
instanceStart = 8
instanceSize = 24
// ZNObjectSon
instanceStart = 24
instanceSize = 48
沒有問題,father的size少了8,son沒有變化,這個時候son的start >= father的size,所以直接return。
如果把father中的一個ivar注釋掉:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
NSInteger ivarInt;
// BOOL ivarBool;
}
@property (nonatomic, assign) NSInteger propertyInt;
@end
這時候打印出來的start和size如下:
// ZNObjectFather
instanceStart = 8
instanceSize = 24
// ZNObjectSon
instanceStart = 16
instanceSize = 40
跟預期的一樣,因為只有一個ivar,所以son的start只多了8,那是不是可以證明上面的猜測是對的呢?
回到上面的截圖,這個時候那一步if判斷是沒法通過的,因為24 < 32,這個時候就進到了moveIvars()方法了,再進這個方法之前,先把son的ivars全打印出來,看看offset的原始值:
(lldb) p $2.ivars
(const ivar_list_t *const) $4 = 0x0000000100002170
(lldb) p *$4
(const ivar_list_t) $5 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 3
first = {
offset = 0x0000000100002d90
name = 0x00000001000013e5 "ivarIntSon"
type = 0x0000000100001ace "q"
alignment_raw = 3
size = 8
}
}
}
(lldb) p $5.get(0)
(ivar_t) $6 = {
offset = 0x0000000100002d90
name = 0x00000001000013e5 "ivarIntSon"
type = 0x0000000100001ace "q"
alignment_raw = 3
size = 8
}
(lldb) p $5.get(1)
(ivar_t) $7 = {
offset = 0x0000000100002d98
name = 0x00000001000013f0 "ivarBoolSon"
type = 0x0000000100001ad0 "c"
alignment_raw = 0
size = 1
}
(lldb) p $5.get(2)
(ivar_t) $8 = {
offset = 0x0000000100002d88
name = 0x00000001000013fc "_propertyIntSon"
type = 0x0000000100001ace "q"
alignment_raw = 3
size = 8
}
(lldb) p $6.offset
(int32_t *) $9 = 0x0000000100002d90
(lldb) p *$9
(int32_t) $10 = 24
(lldb) p $7.offset
(int32_t *) $11 = 0x0000000100002d98
(lldb) p *$11
(int32_t) $12 = 32
(lldb) p $8.offset
(int32_t *) $13 = 0x0000000100002d88
(lldb) p *$13
(int32_t) $14 = 40
$6和$7是直接聲明的ivar,排在前2位,屬性生成的$8排在后面,打印出各自的offset,第一個ivar的offset即$10就是instanceStart,最后一個offset即$14加上ivar的size就是instanceSize,結果很清晰。
moveIvars()方法前面已經分析過源碼了,這里不再贅述,直接看看方法結束之后的結果,在moveIvars()方法之后加一個斷點:
這個時候ro的start和size已經是這樣的了:
// ZNObjectSon
instanceStart = 32
instanceSize = 56
調整結果符合預期,繼續打印出ivar的offset也是沒問題的,這里就不截圖了。
到這里,前3個問題基本驗證完畢了,還剩最后一個問題:
class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思?
這個問題之所以單獨講,是因為在尋找答案的過程中,出現了一些有趣的結果,怎么個有趣法,一起來看看。
首先依然是一個猜測,weakIvarLayout名字中有個weak,是不是統計weak類型的ivar用的。又因為ivar默認類型是strong,所以ivarLayout是不是用于統計strong類型的ivar呢?
當然這里默認strong是不針對基本類型的
這時候又要修改一下測試的代碼了,son類已經不需要了,只用一個father類就可以了:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
NSInteger ivarInt;
BOOL ivarBool;
__strong NSArray *ivarArray;
}
@end
// .m文件就不寫了,因為什么也沒有
runtime也提供了方法用于獲取 ivarLayout和 weakIvarLayout
const uint8_t *
class_getIvarLayout(Class cls)
{
if (cls) return cls->data()->ro->ivarLayout;
else return nil;
}
const uint8_t *
class_getWeakIvarLayout(Class cls)
{
if (cls) return cls->data()->ro->weakIvarLayout;
else return nil;
}
其實就是返回ro的那兩個值,直接用這兩個方法就不需要用lldb慢慢打印了,測試的代碼是這樣的:
const uint8_t *ivarLayout = class_getIvarLayout([ZNObjectFather class]);
const uint8_t *weakIvarLayout = class_getWeakIvarLayout([ZNObjectFather class]);
使用上面修改之后的father代碼測試一下,有趣的事情就發生了:
ivarLayout = "!"
weakIvarLayout = NULL
說實話,看到這個結果的時候,我的第一反應是: 臥槽,這個!是什么鬼
第二行為空我裝作可以理解,因為沒有weak類型的ivar。
我在想,是不是因為在strong之前有兩個基本類型,去掉那兩個基本類型再試試:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__strong NSArray *ivarArray;
}
@end
結果:
ivarLayout = "\x01"
weakIvarLayout = NULL
這個結果看起來還像點樣子,那個01中的1應該就表示有一個strong類型的ivar吧,接著做測試:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__strong NSArray *ivarArray;
}
@property (nonatomic, weak) NSArray *propertyArrayWeak;
@end
結果:
ivarLayout = "\x01"
weakIvarLayout = "\x11"
看到這里我又不能理解了,這個"\x11"怎么解釋呢?
沒辦法,只能搜索一下,發現了Objective-C Class Ivar Layout 探索
這篇文章里面的結果輸出并不完全正確,可能作者并沒有真正寫代碼測試吧,但是關于layout編碼的規則猜測看起來是沒問題的:
layout 就是一系列的字符,每兩個一組,比如 \xmn,每一組 ivarLayout 中第一位表示有 m 個非強屬性,第二位表示接下來有 n 個強屬性。
再回過去看之前的結果:
- ivarLayout = "\x01",表示在先有0個弱屬性,接著有1個連續的強屬性。若之后沒有強屬性了,則忽略后面的弱屬性,對weakIvarLayout也是同理。
- weakIvarLayout = "\x11",表示先有1個強屬性,然后才有1個連續的弱屬性。
但是文章中并沒有出現過那個神奇的"!",我繼續做測試。
中間過程比較艱辛,省略無數次結果
直到發現下面這兩次結果:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__weak NSArray *ivarArrayWeak;
__weak NSArray *ivarArrayWeak2;
__strong NSArray *ivarArray;
}
結果:
ivarLayout = "!"
weakIvarLayout = "\x02"
這個感嘆號又來了,這個時候根據上面的規則,ivarLayout = "\x21" 才對。
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__weak NSArray *ivarArrayWeak;
__weak NSArray *ivarArrayWeak2;
__strong NSArray *ivarArray;
__strong NSArray *ivarArray2;
}
結果:
ivarLayout = "\""
weakIvarLayout = "\x02"
居然輸出了一個引號("),結果難道不應該是:ivarLayout = "\x22" 嗎?
這個時候我靈光一閃!
當然在閃之前已經搜索了好久,但沒有找到答案,不過這個時候真的是一閃!
我去搜索了ASCII碼表,結果真讓我猜中了:
所以結果其實是正確的,只是被轉成了ASCII碼,至于xcode為什么要這么做,我就不得而知了...
總結
原本以為很簡單的property和ivar,其實一點也不簡單,特別是ivar,真的是花了很多時間。順便把class_ro_t中幾個之前沒有分析的屬性也一并理解了一下,還是很不錯的。
- property在編譯期會生成_propertyName的ivar,和相應的get/set屬性
- ivars在編譯期確定,但不完全確定,offset屬性在運行時會修改
- 對象的大小是由ivars決定的,當有繼承體系時,父類的ivars永遠放在子類之前
- class_ro_t的instanceStart和instanceSize會在運行時調整
- class_ro_t的ivarLayout和weakIvarLayout存放的是強ivar和弱ivar的存儲規則