前言
前幾天有人問(wèn)我一個(gè)問(wèn)題:為什么分類不能自動(dòng)創(chuàng)建get set方法。老實(shí)說(shuō),筆者從來(lái)沒(méi)有去思考過(guò)這個(gè)問(wèn)題。于是這次通過(guò)代碼實(shí)踐跟runtime源碼來(lái)探究這個(gè)問(wèn)題。
準(zhǔn)備工作
為了能減少輸出類數(shù)據(jù)的代碼工作,筆者基于NSObject的分類封裝了一套代碼

其中輸出類實(shí)例變量的具體代碼:
- (void)logIvarsWithExpReg: (NSString *)expReg customed: (BOOL)customed {
[NSObject kRecordOBJ];
unsigned int ivarCount;
Ivar * ivars = class_copyIvarList([self class], &ivarCount);
for (int idx = 0; idx < ivarCount; idx++) {
Ivar ivar = ivars[idx];
NSString * ivarName = [NSString stringWithUTF8String: ivar_getName(ivar)];
if (customed && [kOBJIvarNames containsObject: ivarName]) {
continue;
}
if (expReg && !kValidExpReg(ivarName, expReg)) {
continue;
}
printf("ivar: %s --- %s\n", NSStringFromClass([self class]).UTF8String, ivarName.UTF8String);
}
free(ivars);
}
+(void)kRecordOBJ采用dispatch_once的方式將NSObject存在的數(shù)據(jù)存儲(chǔ)到三個(gè)數(shù)組中,用來(lái)排除父類的數(shù)據(jù)輸出
類的屬性
-
正常創(chuàng)建類
@interface Person: NSObject {
int _pId;
}@property (nonatomic, copy) NSString * name; @property (nonatomic, assign) NSUInteger age; @end int main(int argc, char * argv[]) { @autoreleasepool { Person * p = [[Person alloc] init]; [p logCustomIvars]; [p logCustomMethods]; [p logCustomProperties]; return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
運(yùn)行結(jié)果:屬性name和age生成了對(duì)應(yīng)的_propertyName的實(shí)例變量以及setter和getter

-
動(dòng)態(tài)生成屬性
age
@implementation Person
@dynamic age;@end
運(yùn)行結(jié)果:缺少了_age變量以及對(duì)應(yīng)的setAge:和age方法

-
手動(dòng)實(shí)現(xiàn)
setter/getter
@implemetation Person
@dynamic age;- (void)setAge: (NSUInteger)age {} - (NSUInteger)age { return 18; } @end
輸出結(jié)果:未生成_age實(shí)例變量

-
手動(dòng)實(shí)現(xiàn)
_pId的setter/getter
@implemetation Person
@dynamic age;- (void)setAge: (NSUInteger)age {} - (NSUInteger)age { return 18; } - (void)setPId: (int)pId { _pId = pId; } - (int)pId { return _pId; } @end [p setValueForKey: @"pId"];
運(yùn)行結(jié)果:KVC的訪問(wèn)會(huì)觸發(fā)setter方法,_pId除了無(wú)法通過(guò)點(diǎn)語(yǔ)法訪問(wèn)外,其他表現(xiàn)與@property無(wú)異

通過(guò)上面的幾段試驗(yàn),可以得出@property的公式:

分類屬性
-
分類中添加
weigh和height屬性
@interface Person (category)@property (nonatomic, assign) CGFloat weigh; @property (nonatomic, assign) CGFloat height; @end
運(yùn)行結(jié)果:weigh和height未生成實(shí)例變量以及對(duì)應(yīng)的setter/getter,與@dynamic修飾的age表現(xiàn)一致

-
使用
@synthesize自動(dòng)合成setter/getter方法時(shí)編譯報(bào)錯(cuò)
-
手動(dòng)實(shí)現(xiàn)
setter/getter
@implemetation Person (category)- (void)setWeigh: (CGFloat)weigh {} - (CGFloat)weigh { return 150; } @end
運(yùn)行結(jié)果:與@dynamic age后重寫其setter/getter表現(xiàn)一致
-
動(dòng)態(tài)綁定屬性來(lái)實(shí)現(xiàn)
setter/getter
void * kHeightKey = &kHeightKey;
@implemetation Person (category)- (void)setWeigh: (CGFloat)weigh {} - (CGFloat)weigh { return 150; } - (void)setHeight: (CGFloat)height { objc_setAssociatedObject(self, kHeightKey, @(height), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (CGFloat)height { return return [objc_getAssociatedObject(self, kHeightKey) doubleValue];; } @end [p logCustomIvars] [p logCustomMethods]; [p logCustomProperties]; CGFloat height = 180; p.height = 180; height = p.height; [p logCustomIvars] [p logCustomMethods]; [p logCustomProperties];
運(yùn)行結(jié)果:動(dòng)態(tài)綁定前后ivar沒(méi)有發(fā)生任何變化

通過(guò)代碼實(shí)驗(yàn),可以得出下面兩個(gè)結(jié)論:
- 分類屬性相當(dāng)于
@dynamic property - 缺少
ivar的情況下無(wú)法使用@synthesize自動(dòng)合成屬性
以及一個(gè)猜想:
- 在類完成加載后無(wú)法繼續(xù)添加
ivar
通過(guò)runtime動(dòng)態(tài)創(chuàng)建類驗(yàn)證猜想:
int main(int argc, char * argv[]) {
NSString * className = @"Custom";
Class customClass = objc_allocateClassPair([NSObject class], className.UTF8String, 0);
class_addIvar(customClass, @"ivar1".UTF8String, sizeof(NSString *), 0, "@");
objc_property_attribute_t type1 = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership1 = { "C", "N" };
objc_property_attribute_t atts1[] = { type1, ownership1 };
class_addProperty(customClass, "property1", atts1, 2);
objc_registerClassPair(customClass);
id instance = [[customClass alloc] init];
NSLog(@"\nLog Ivars ===================");
[instance logCustomIvars];
NSLog(@"\nLog methods ===================");
[instance logCustomMethods];
NSLog(@"\nLog properties ===================");
[instance logCustomProperties];
class_addIvar(customClass, @"ivar2".UTF8String, sizeof(NSString *), 0, "@");
objc_property_attribute_t type2 = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership2 = { "C", "N" };
objc_property_attribute_t atts2[] = { type2, ownership2 };
class_addProperty(customClass, "property2", atts2, 2);
instance = [[customClass alloc] init];
NSLog(@"\nLog Ivars ===================");
[instance logCustomIvars];
NSLog(@"\nLog methods ===================");
[instance logCustomMethods];
NSLog(@"\nLog properties ===================");
[instance logCustomProperties];
}
運(yùn)行結(jié)果:在調(diào)用class_registerClassPair后,添加ivar失敗

從源碼解析
objc_class的結(jié)構(gòu)體定義如下:
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
}
ps: 在新版本中結(jié)構(gòu)體內(nèi)部已經(jīng)發(fā)生了大改,但是內(nèi)部的屬性大致上仍是這些
這里面有個(gè)重要的屬性ivar_layout,顧名思義存放的是變量的位置屬性,與之對(duì)應(yīng)的還有一個(gè)weakIvarLayout變量,不過(guò)在默認(rèn)結(jié)構(gòu)中沒(méi)有出現(xiàn)。這兩個(gè)屬性用來(lái)記錄ivar哪些是strong或者weak,而這個(gè)記錄操作在runtime階段已經(jīng)被確定好。正由于如此,這極有可能是ivar無(wú)法在類被加載后繼續(xù)添加的原因之一。ivar_layout的更多了解可以參照Objective-C Class Ivar layout一文
import操作幫助編譯檢查和鏈接過(guò)程,但是在category的加載過(guò)程中,不會(huì)將擴(kuò)展的內(nèi)容添加到原始的類結(jié)構(gòu)中。runtime對(duì)于category的加載過(guò)程可以簡(jiǎn)單的分成下面幾步(摘自objc category的密碼):
-
objc runtime的加載入口是一個(gè)叫_objc_init的方法,在library加載前由libSystem dyld調(diào)用,進(jìn)行初始化操作 - 調(diào)用
map_images方法將文件中的imagemap到內(nèi)存 - 調(diào)用
_read_images方法初始化map后的image,這里面干了很多的事情,像load所有的類、協(xié)議和category,著名的+ load方法就是這一步調(diào)用的
-仔細(xì)看category的初始化,循環(huán)調(diào)用了_getObjc2CategoryList方法,這個(gè)方法拿出來(lái)看看: - .…
這一切的過(guò)程發(fā)生在_objc_init函數(shù)中,函數(shù)實(shí)現(xiàn)如下

簡(jiǎn)單來(lái)說(shuō)在
load_images函數(shù)中最終會(huì)走到下面的代碼調(diào)用來(lái)加載所有的類以及類的分類
根據(jù)上面的代碼加上
runtime的加載順序,可以繼續(xù)推出:
-
@dynamic實(shí)際上是將屬性的加載推遲到類加載完成后
另外,前面也說(shuō)過(guò)在缺少ivar的情況下無(wú)法自動(dòng)合成setter/getter,除了category本身是不被添加到類結(jié)構(gòu)中的,所以無(wú)法使用類結(jié)構(gòu)的ivar合成屬性外,還有分類自身結(jié)構(gòu)的問(wèn)題
struct category_t {
const char *name; /// 類名
classref_t cls; /// 類指針
struct method_list_t *instanceMethods; /// 實(shí)例方法
struct method_list_t *classMethods; /// 類方法
struct protocol_list_t *protocols; /// 擴(kuò)展的協(xié)議
struct property_list_t *instanceProperties; /// 擴(kuò)展屬性
method_list_t *methodsForMeta(bool isMeta) { ... }
property_list_t *propertiesForMeta(bool isMeta) { ... }
};
可以看到分類結(jié)構(gòu)本身是不存在ivar的容器的,因此缺少了自動(dòng)合成屬性的條件。最后還有一個(gè)問(wèn)題,我們?cè)谑褂?code>objc_associate系列函數(shù)綁定屬性的時(shí)候這些變量存儲(chǔ)在了哪里?

總結(jié)
首先,iOS的分類在runtime實(shí)現(xiàn)的結(jié)構(gòu)體中并不存在Ivar類型的容器,缺少了自動(dòng)合成setter以及getter的必要條件,因此在分類中聲明的屬性默認(rèn)為@dynamic修飾。
其次,OC本身是一門原型語(yǔ)言,對(duì)象和類原型很像。類對(duì)象執(zhí)行alloc方法就像是原型模式中的copy操作一樣,類保存了copy所需的實(shí)例信息,這些信息內(nèi)存信息在runtime加載時(shí)就被固定了,沒(méi)有擴(kuò)充Ivar的條件。(感謝大表哥的科普)
最后,在runtime中存在一個(gè)類型為AssociationHashMap的哈希映射表保存著對(duì)象動(dòng)態(tài)添加的屬性,每個(gè)對(duì)象以自身地址為key維護(hù)著一個(gè)綁定屬性表,我們動(dòng)態(tài)添加的屬性就都存儲(chǔ)在這個(gè)表里,這也是動(dòng)態(tài)添加property能成功的基礎(chǔ)。
上一篇:閑聊內(nèi)存管理
轉(zhuǎn)載請(qǐng)注明原文地址及作者
