我原本以為這兩個東西沒啥好寫的,結(jié)果是property確實(shí)沒啥好寫的侨歉,但是ivar就不少了膘壶。
本文不探討何時該選擇property掰吕,何時該選擇ivar
我會把我研究這兩東西的過程原原本本的展示出來。
試探期
在Runtime源碼 —— 方法加載的過程這篇文章中呜笑,我提到過兩個結(jié)構(gòu)體:
- class_ro_t
記錄編譯期就已經(jīng)確定的信息 - class_rw_t
運(yùn)行期拷貝class_ro_t中的部分信息存入此結(jié)構(gòu)體中夫否,并存放運(yùn)行期添加的信息
細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn)在ro和rw結(jié)構(gòu)體中,method叫胁、protocol和property都是存在的凰慈,拷貝也就是拷貝的這部分信息,但是ro中還存在一個字段叫做:
const ivar_list_t * ivars;
這玩意兒就是本文研究的重點(diǎn)了驼鹅。
例子
在寫代碼之前微谓,我心里是這樣想的:
ivar都應(yīng)該存在ro的ivars字段中,property存在ro的baseProperties字段中输钩,在運(yùn)行期豺型,將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驗(yàn)證一下:
// 獲取ZNObjectFather class的內(nèi)存地址
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的內(nèi)容顯示count = 3,但是實(shí)際只聲明了兩個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的結(jié)構(gòu)體后面會分析
(ivar_t) $6 = {
offset = 0x0000000100002d90
name = 0x0000000100001450 "ivarBool"
type = 0x0000000100001af0 "c"
alignment_raw = 0
size = 1
}
(lldb) p $5.get(2)
// 發(fā)現(xiàn)聲明的屬性自動生成了一個_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
// 結(jié)果符合預(yù)期
(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的測試結(jié)果很正常剪验,ivar不完全相同肴焊,如果直接聲明的2個之外,屬性也自動生成了一個ivar功戚。
另外$3的baseMethodList也不為空娶眷,存的就是property自動生成的get/set方法,感興趣自己打印一下啸臀。
看到這里也就不難理解為什么:
property = ivar + get + set
但也并不總是這樣届宠,如果重寫了屬性的get/set方法,就不會生成_propertyName這樣的ivar了,本文不做深入席揽。
再看看$3里面的這么兩個屬性:
instanceStart = 8
instanceSize = 32
- instanceStart之所以等于8顽馋,是因?yàn)槊總€對象的isa占用了前8個字節(jié)。
- instanceSize = isa + 3個ivar幌羞,$6的size只有1寸谜,但是為了對齊,也占用了8属桦,對齊是怎么計(jì)算的后面再講熊痴。
到這里對ivar和property已經(jīng)有一個大概的理解了,下面繼續(xù)深入聂宾。
深入期
根據(jù)上半部分的分析果善,我們已經(jīng)知道了
- ivars在編譯期就已經(jīng)確定了
- 屬性會生成 _propertyName格式的ivar,也在編譯期確定
- 對象的大小是由 isa + ivars決定的
但是這就引出了如下幾個問題:
- 帶有繼承體系的對象是怎么表示的系谐?
- 繼承體系中對象的 instanceStart和 instanceSize是怎么計(jì)算的巾陕?
- ivar_t中的 alignment_raw和 offset是什么意思?
- class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思纪他?
現(xiàn)在我們都知道class_ro_t中的ivar鄙煤,property,protocol和method都是在編譯期就確定的茶袒,在運(yùn)行期時梯刚,通過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);
注釋里面講了亡资,這一步會調(diào)整ivar的offset值植康,并且更新ro的信息,看起來這一步就是關(guān)鍵,看看方法是怎么實(shí)現(xiàn)的:
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的相關(guān)代碼
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);
}
}
只保留了最關(guān)鍵的代碼,關(guān)注一下其中的if判斷,比較的是當(dāng)前類的instanceStart和父類的instanceSize宪卿,當(dāng)start < size的時候調(diào)整了一下當(dāng)前類ro的相關(guān)信息。
這給了我一個信息,也就是在這一步之前禾进,ro中的instanceStart和instanceSize其實(shí)并不是最終值宠纯。
具體調(diào)整的過程在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;
}
這個方法做了這些事情:
- 更新當(dāng)前類ivar中的offset字段
- 更新當(dāng)前類ro的instanceStart和instanceSize
先按照源代碼分析,最后寫代碼驗(yàn)證。
part1
diff = superSize - ro->instanceStart;
獲取了當(dāng)前類的instanceStart和父類的instanceSize的偏移量珍逸,但這并不是最終的結(jié)果摹量,因?yàn)榇嬖趯R的問題祝迂。這就是后面這個if判斷內(nèi)部做的事情睦尽。
part2
先看第一個for循環(huán):
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結(jié)構(gòu)體中的方法:
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当凡,這個字段據(jù)我的理解,應(yīng)該是在編譯期確定的纠俭,但是是按照什么規(guī)則確定的就不清楚了沿量。根據(jù)測試的結(jié)果來看,一般都是0或者3冤荆。
通過ivar.alignment()得到的結(jié)果是1 << 3朴则,也就是8。
part3
uint32_t alignMask = maxAlignment - 1;
diff = (diff + alignMask) & ~alignMask;
這一步確定了diff的值钓简,那個&運(yùn)算的結(jié)果就是把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;
}
這一步調(diào)整ivar的offset字段撤蚊,調(diào)整的過程就是用原來的offset加上上一步得到的diff。說白了就是當(dāng)前類的ivar是在父類的ivar之后的损话。
part5
*(uint32_t *)&ro->instanceStart += diff;
*(uint32_t *)&ro->instanceSize += diff;
最后更新了當(dāng)前類的instanceStart和instanceSize侦啸,過程也是加上diff。其實(shí)就是把父類的instanceSize給空出來了丧枪。
到這里的時候光涂,已經(jīng)回答了這部分最開始提出的4個問題中的前3個。先來驗(yàn)證一下豪诲。
例子
為了驗(yàn)證前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()方法中添加一個條件斷點(diǎn)服赎,進(jìn)入斷點(diǎn)后葵蒂,通過lldb獲取一下相關(guān)值,請看圖:
條件斷點(diǎn)設(shè)置的是ZNObjectFather的地址重虑,所以:
- ro的信息就是ZNObjectFather的ro
ZNObjectFather繼承于NSObject践付,所以:
- super_ro是NSObject的ro
根據(jù)控制臺打印的信息,這一步的if判斷結(jié)果為true缺厉,所以直接return了永高,調(diào)整一下條件斷點(diǎn)的內(nèi)容,把地址設(shè)置為ZNObjectSon的地址再試一下:
可以看到father的start和size沒有發(fā)生變化提针,因?yàn)樯弦徊阶鲞^說明直接return了命爬。
再來看看son的start值,說實(shí)話看到這個24我是無法理解的辐脖。在這之前我預(yù)期start = 8饲宛,這多出來的16是怎么回事?
我做了一個猜測:instanceStart的值在編譯期已經(jīng)計(jì)算了父類直接聲明的ivar嗜价,由property生成的沒有計(jì)算艇抠。
我做了一些驗(yàn)證,先把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
跟預(yù)期的一樣,因?yàn)橹挥幸粋€ivar错妖,所以son的start只多了8绿鸣,那是不是可以證明上面的猜測是對的呢?
回到上面的截圖暂氯,這個時候那一步if判斷是沒法通過的,因?yàn)?4 < 32亮蛔,這個時候就進(jìn)到了moveIvars()方法了痴施,再進(jìn)這個方法之前,先把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偷仿,結(jié)果很清晰哩簿。
moveIvars()方法前面已經(jīng)分析過源碼了宵蕉,這里不再贅述,直接看看方法結(jié)束之后的結(jié)果节榜,在moveIvars()方法之后加一個斷點(diǎn):
這個時候ro的start和size已經(jīng)是這樣的了:
// ZNObjectSon
instanceStart = 32
instanceSize = 56
調(diào)整結(jié)果符合預(yù)期羡玛,繼續(xù)打印出ivar的offset也是沒問題的,這里就不截圖了宗苍。
到這里稼稿,前3個問題基本驗(yàn)證完畢了,還剩最后一個問題:
class_ro_t中的 ivarLayout和 weakIvarLayout是什么意思讳窟?
這個問題之所以單獨(dú)講让歼,是因?yàn)樵趯ふ掖鸢傅倪^程中,出現(xiàn)了一些有趣的結(jié)果丽啡,怎么個有趣法是越,一起來看看。
首先依然是一個猜測碌上,weakIvarLayout名字中有個weak倚评,是不是統(tǒng)計(jì)weak類型的ivar用的。又因?yàn)閕var默認(rèn)類型是strong馏予,所以ivarLayout是不是用于統(tǒng)計(jì)strong類型的ivar呢天梧?
當(dāng)然這里默認(rèn)strong是不針對基本類型的
這時候又要修改一下測試的代碼了,son類已經(jīng)不需要了霞丧,只用一個father類就可以了:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
NSInteger ivarInt;
BOOL ivarBool;
__strong NSArray *ivarArray;
}
@end
// .m文件就不寫了呢岗,因?yàn)槭裁匆矝]有
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;
}
其實(shí)就是返回ro的那兩個值,直接用這兩個方法就不需要用lldb慢慢打印了蛹尝,測試的代碼是這樣的:
const uint8_t *ivarLayout = class_getIvarLayout([ZNObjectFather class]);
const uint8_t *weakIvarLayout = class_getWeakIvarLayout([ZNObjectFather class]);
使用上面修改之后的father代碼測試一下后豫,有趣的事情就發(fā)生了:
ivarLayout = "!"
weakIvarLayout = NULL
說實(shí)話,看到這個結(jié)果的時候突那,我的第一反應(yīng)是: 臥槽挫酿,這個!是什么鬼
第二行為空我裝作可以理解,因?yàn)闆]有weak類型的ivar愕难。
我在想早龟,是不是因?yàn)樵趕trong之前有兩個基本類型,去掉那兩個基本類型再試試:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__strong NSArray *ivarArray;
}
@end
結(jié)果:
ivarLayout = "\x01"
weakIvarLayout = NULL
這個結(jié)果看起來還像點(diǎn)樣子猫缭,那個01中的1應(yīng)該就表示有一個strong類型的ivar吧葱弟,接著做測試:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__strong NSArray *ivarArray;
}
@property (nonatomic, weak) NSArray *propertyArrayWeak;
@end
結(jié)果:
ivarLayout = "\x01"
weakIvarLayout = "\x11"
看到這里我又不能理解了,這個"\x11"怎么解釋呢猜丹?
沒辦法芝加,只能搜索一下,發(fā)現(xiàn)了Objective-C Class Ivar Layout 探索
這篇文章里面的結(jié)果輸出并不完全正確射窒,可能作者并沒有真正寫代碼測試吧藏杖,但是關(guān)于layout編碼的規(guī)則猜測看起來是沒問題的:
layout 就是一系列的字符往湿,每兩個一組狭魂,比如 \xmn,每一組 ivarLayout 中第一位表示有 m 個非強(qiáng)屬性,第二位表示接下來有 n 個強(qiáng)屬性荡短。
再回過去看之前的結(jié)果:
- ivarLayout = "\x01"嚷硫,表示在先有0個弱屬性类垫,接著有1個連續(xù)的強(qiáng)屬性培廓。若之后沒有強(qiáng)屬性了,則忽略后面的弱屬性误褪,對weakIvarLayout也是同理责鳍。
- weakIvarLayout = "\x11",表示先有1個強(qiáng)屬性兽间,然后才有1個連續(xù)的弱屬性历葛。
但是文章中并沒有出現(xiàn)過那個神奇的"!",我繼續(xù)做測試嘀略。
中間過程比較艱辛恤溶,省略無數(shù)次結(jié)果
直到發(fā)現(xiàn)下面這兩次結(jié)果:
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__weak NSArray *ivarArrayWeak;
__weak NSArray *ivarArrayWeak2;
__strong NSArray *ivarArray;
}
結(jié)果:
ivarLayout = "!"
weakIvarLayout = "\x02"
這個感嘆號又來了,這個時候根據(jù)上面的規(guī)則帜羊,ivarLayout = "\x21" 才對咒程。
// ZNObjectFather.h
@interface ZNObjectFather : NSObject {
__weak NSArray *ivarArrayWeak;
__weak NSArray *ivarArrayWeak2;
__strong NSArray *ivarArray;
__strong NSArray *ivarArray2;
}
結(jié)果:
ivarLayout = "\""
weakIvarLayout = "\x02"
居然輸出了一個引號("),結(jié)果難道不應(yīng)該是:ivarLayout = "\x22" 嗎讼育?
這個時候我靈光一閃帐姻!
當(dāng)然在閃之前已經(jīng)搜索了好久,但沒有找到答案奶段,不過這個時候真的是一閃饥瓷!
我去搜索了ASCII碼表,結(jié)果真讓我猜中了:
所以結(jié)果其實(shí)是正確的痹籍,只是被轉(zhuǎn)成了ASCII碼呢铆,至于xcode為什么要這么做,我就不得而知了...
總結(jié)
原本以為很簡單的property和ivar词裤,其實(shí)一點(diǎn)也不簡單刺洒,特別是ivar,真的是花了很多時間吼砂。順便把class_ro_t中幾個之前沒有分析的屬性也一并理解了一下,還是很不錯的鼎文。
- property在編譯期會生成_propertyName的ivar渔肩,和相應(yīng)的get/set屬性
- ivars在編譯期確定,但不完全確定拇惋,offset屬性在運(yùn)行時會修改
- 對象的大小是由ivars決定的周偎,當(dāng)有繼承體系時抹剩,父類的ivars永遠(yuǎn)放在子類之前
- class_ro_t的instanceStart和instanceSize會在運(yùn)行時調(diào)整
- class_ro_t的ivarLayout和weakIvarLayout存放的是強(qiáng)ivar和弱ivar的存儲規(guī)則