Category的本質(zhì)<一>
Category的本質(zhì)<二>load温眉,initialize方法
面試題:Category能否添加成員變量忠藤?如果可以掉冶,如何給Category添加成員變量?
我們首先創(chuàng)建一個(gè)類Person類繼承自NSObject糙及,給這個(gè)類聲明一個(gè)屬性name:
@property (nonatomic, strong)NSString *name;
我們聲明了這句話之后详幽,實(shí)際是做了三件事:
- 1.聲明了一個(gè)成員變量_name。
NSString *_name;
- 2.聲明了set和get方法:
- (void)setName:(NSString *)name;
- (NSString *)name;
- 3.在.m文件中實(shí)現(xiàn)set和get方法:
- (void)setName:(NSString *)name{
_name = name;
}
- (NSString *)name{
return _name;
}
以上是給一個(gè)類添加屬性。下面給一個(gè)分類添加屬性:
我們創(chuàng)建一個(gè)Person類的分類Test1唇聘,然后給這個(gè)分類添加一個(gè)height屬性:
@property (nonatomic, assign)int height;
這樣只會(huì)申明set和get方法版姑,而不會(huì)申明成員變量和實(shí)現(xiàn)set,get方法:
- (void)setHeight:(int)height;
- (int)height;
既然系統(tǒng)沒(méi)有幫我們聲明成員變量和實(shí)現(xiàn)set和get方法迟郎,那么我們能不能自己去聲明一下呢剥险?我們嘗試一下:
出現(xiàn)了報(bào)錯(cuò)
Instance variables may not be placed in categories
,意思就是成員變量不能聲明在分類中宪肖。所以我們得出結(jié)論表制,分類中不能添加成員變量。
我們從分類的結(jié)構(gòu)的角度來(lái)考慮一下分類中為什么不能添加成員變量:
通過(guò)分類的底層結(jié)構(gòu)我們可以看到控乾,分類中可以存放實(shí)例方法么介,類方法,協(xié)議蜕衡,屬性夭拌,但是沒(méi)有存放成員變量的地方。
既然分類中不能添加成員變量衷咽,那么我們給分類添加屬性時(shí),它的功能不是完整的蒜绽,比如說(shuō)我們分別給Person類的name和height這兩個(gè)屬性賦值镶骗,然后打印讀出這兩個(gè)屬性:
Person *person = [[Person alloc] init];
person.name = @"dongdong";
person.height = 180;
NSLog(@"name: %@, height : %d", person.name, person.height);
程序崩潰了,崩潰原因是:-[Person setHeight:]: unrecognized selector sent to instance 0x60400020a0d0
躲雅,意思就是給這個(gè)person對(duì)象發(fā)送了沒(méi)有實(shí)現(xiàn)的消息:setHeight:
,這應(yīng)該是在我們的預(yù)料之中鼎姊,為什么呢?因?yàn)槲覀冊(cè)诜诸愔新暶?code>height這個(gè)屬性的時(shí)候相赁,不像在類中聲明屬性一樣相寇,系統(tǒng)只會(huì)聲明set和get方法,而不會(huì)在.m中去實(shí)現(xiàn)set和get方法钮科,因此導(dǎo)致了程序崩潰唤衫。因此我們?cè)诜诸惖?m文件中去實(shí)現(xiàn)set和get方法:
//Person+Test1.m文件
- (void)setHeight:(int)height{
}
- (int)height{
return 0;
}
再次運(yùn)行代碼,這次程序不崩潰了绵脯,打印結(jié)果是:
Category[9030:308848] name: dongdong, height : 0
我們看到name屬性賦值成功了佳励,而height屬性顯然沒(méi)有賦值成功。
person.height = 180;
這句代碼顯然是調(diào)用了set方法蛆挫,但是在分類中的set方法什么也沒(méi)有實(shí)現(xiàn)赃承,沒(méi)有存儲(chǔ)下這個(gè)設(shè)置的值180。
person.height
實(shí)則是調(diào)用了get方法悴侵,由于不能保存?zhèn)鬟f過(guò)來(lái)的height值瞧剖,所以上面的代碼中我們返回固定值0。
而name屬性能夠賦值和讀取成功,是因?yàn)樵谄鋝et方法中用_name這個(gè)成員變量保存的賦的值:
- (void)setName:(NSString *)name{
_name = name;
}
在其get方法中利用_name成員變量返回存儲(chǔ)的值:
- (NSString *)name{
return _name;
}
所以如果我們?cè)诜诸惖?m文件中保存?zhèn)鬟f過(guò)來(lái)的值抓于,然后在取值的時(shí)候返回存儲(chǔ)的值做粤,那么應(yīng)該也能實(shí)現(xiàn)屬性的完整功能。
方法一 全局變量
第一種方法是使用全局變量來(lái)存儲(chǔ)傳遞進(jìn)來(lái)的值:
int height_;
- (void)setHeight:(int)height{
height_ = height;
}
- (int)height{
return height_;
}
然后我們運(yùn)行一下程序:
Category[9497:328996] name: dongdong, height : 180
這次好像是賦值成功了毡咏,返回也對(duì)驮宴。我們?cè)侔裩eight改成190試試:
Category[9533:330381] name: dongdong, height : 190
這次打印的也是對(duì)的,那么這樣是不是就真的可以完全實(shí)現(xiàn)屬性的功能呢呕缭?
問(wèn)題在于堵泽,height_是全局變量,所有的對(duì)象共用這一個(gè)全局變量恢总,如果有個(gè)對(duì)象的height值變了迎罗,其他的對(duì)象的height值也會(huì)跟著改變,也是不符合我們的需求的片仿,我們可以測(cè)試一下:
Person *person1 = [[Person alloc] init];
person1.height = 180;
Person *person2 = [[Person alloc] init];
person2.height = 190;
NSLog(@"person1: %d, person2 : %d", person1.height, person2.height);
打印結(jié)果:
Category[9648:335004] person1: 190, person2 : 190
所以這種方法就被pass掉了纹安。
方法二 字典
第一種方法全局變量失敗的原因就是不能做到每個(gè)對(duì)象和自己的height值一一對(duì)應(yīng)。這就讓我們想到了一個(gè)數(shù)據(jù)結(jié)構(gòu)-字典砂豌。假如我們通過(guò)鍵值對(duì)的形式存放height值厢岂,這樣是否可以呢?我們使用person對(duì)象指向的地址作為鍵阳距,將height值作為值存儲(chǔ)在字典中:
NSMutableDictionary *heights_;
//由于load方法只初始化一次塔粒,所以我們可以在這個(gè)方法里做一些初始化操作
+ (void)load{
heights_ = [NSMutableDictionary dictionary];
}
- (void)setHeight:(int)height{
NSString *key = [NSString stringWithFormat:@"%p", self];
heights_[key] = @(height);
}
- (int)height{
NSString *key = [NSString stringWithFormat:@"%p", self];
return [heights_[key] intValue];
}
Person *person1 = [[Person alloc] init];
person1.height = 180;
Person *person2 = [[Person alloc] init];
person2.height = 190;
NSLog(@"person1: %d, person2 : %d", person1.height, person2.height);
打印結(jié)果:
Category[10166:350395] person1: 180, person2 : 190
所以采用字典這種方式是完全可行的。
使用字典存在的問(wèn)題:
- 1.非線程安全
由于這個(gè)字典是全局的筐摘,所有的對(duì)象的height屬性值都是存儲(chǔ)在這個(gè)全局字典里面卒茬,當(dāng)不同的對(duì)象在不同的線程同時(shí)訪問(wèn)這個(gè)全局字典時(shí),這個(gè)時(shí)候就容易產(chǎn)生線程安全問(wèn)題咖熟,需要去加線程鎖圃酵,有些復(fù)雜。 - 2.需要?jiǎng)?chuàng)建多個(gè)全局字典
剛才已經(jīng)看到了馍管,我們需要為分類中的每一個(gè)屬性值創(chuàng)建一個(gè)全局字典郭赐,這是非常麻煩又復(fù)雜的事。
方法三 關(guān)聯(lián)對(duì)象
關(guān)聯(lián)對(duì)象使用的是runtime的API:
/****
//這個(gè)方法是在set方法中使用确沸,目的是把傳遞進(jìn)來(lái)的value值和object這個(gè)對(duì)象關(guān)聯(lián)起來(lái)
@object:這個(gè)參數(shù)是要關(guān)聯(lián)的對(duì)象
@key:在這里設(shè)置了key值堪置,那么在get方法里面就可以根據(jù)這個(gè)key取得值
@value:傳遞進(jìn)來(lái)的值
@policy:它是個(gè)一個(gè)枚舉值,用來(lái)修飾value
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
***/
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
/***
//這個(gè)方法是在get方法中使用张惹,獲得關(guān)聯(lián)對(duì)象的值舀锨。
@object:關(guān)聯(lián)的對(duì)象
@key:set方法中設(shè)置的key值
***/
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
我們?cè)俳oPerson類的分類聲明一個(gè)屬性:
@property (nonatomic, copy)NSString *sex;
然后我們使用關(guān)聯(lián)對(duì)象的方法給sex
這個(gè)屬性賦值和取值:
//由于key的類型是`void *`類型,也就是一個(gè)指針類型宛逗,所以這里聲明了一個(gè)指針類型的sexKey
const void *sexKey;
- (void)setSex:(NSString *)sex{
objc_setAssociatedObject(self, sexKey, sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)sex{
return objc_getAssociatedObject(self, sexKey);
}
Person *person1 = [[Person alloc] init];
person1.sex = @"man";
Person *person2 = [[Person alloc] init];
person2.sex = @"women";
NSLog(@"person1: %@, person2 : %@", person1.sex, person2.sex);
打印結(jié)果:
Category[11243:396207] person1: man, person2 : women
我們發(fā)現(xiàn)打印結(jié)果是正確的坎匿。
但是這里存在一個(gè)問(wèn)題就是我們?cè)O(shè)置的key沒(méi)有賦值,也即是sexKey相當(dāng)于NULL,假如我們?cè)俳oheight屬性設(shè)置一個(gè)key為heightKey替蔬,那么這個(gè)heightKey也是NULL告私,那么在get方法中通過(guò)key值來(lái)取得值時(shí),由于屬性的key都是一樣的承桥,所以就很容易出錯(cuò)驻粟。
- 方法一
因此我們需要給這個(gè)sexKey賦值一個(gè)獨(dú)一無(wú)二的值:
const void *sexKey = &sexKey;
這句話就是直接將sexKey這個(gè)指針的地址值賦給自己。對(duì)于height:
const void *heightKey = &heightKey;
由于這兩個(gè)指針?lè)诸愒诓煌膬?nèi)存地址中凶异,所以heightKey和sexKey可以保證是不相同的蜀撑,這樣就能在get方法中取出正確的值。
- 方法二
上面這種方式實(shí)在是非常啰嗦又累贅剩彬,我們要聲明指針酷麦,初始化指針,下面介紹一種更簡(jiǎn)單的方法:
- (void)setSex:(NSString *)sex{
objc_setAssociatedObject(self, @"sex", sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)sex{
return objc_getAssociatedObject(self, @"sex");
}
我們直接把@"sex"這個(gè)字符串傳進(jìn)去作為key喉恋,這樣就不用聲明指針又初始化了沃饶。有人就有疑問(wèn)了,這里的key明明要求是指針類型的轻黑,我們傳進(jìn)一個(gè)字符串可以嗎糊肤?我們分析一下下面這句代碼:
NSString *name = @"dongdong";
這里name變量是一個(gè)指針變量。那么我們?yōu)槭裁茨苡靡粋€(gè)字符串去初始化一個(gè)指針變量呢氓鄙?原因就是這里傳進(jìn)去的是@"dongdong"這個(gè)字符串的地址轩褐。這樣我們就能明白,上面@"sex"其實(shí)傳進(jìn)去的也是這個(gè)字符串的地址玖详。
為了防止誤寫(xiě),我們還可以把字符串抽成宏:
#define SEX @"sex"
- (void)setSex:(NSString *)sex{
objc_setAssociatedObject(self, SEX, sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)sex{
return objc_getAssociatedObject(self, SEX);
}
方法三
第二種方法已經(jīng)非常簡(jiǎn)便了勤讽,但是為了方便準(zhǔn)確我們還要把字符串抽成宏蟋座。有沒(méi)有更加簡(jiǎn)便的方法呢?我們可以嘗試傳進(jìn)一個(gè)方法的地址作為key脚牍,比如說(shuō)set或get方法:
- (void)setSex:(NSString *)sex{
objc_setAssociatedObject(self, @selector(sex), sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)sex{
return objc_getAssociatedObject(self, @selector(sex));
}
這里傳進(jìn)去的key是@selector(sex)
向臀,也就是sex
這個(gè)get方法的地址。當(dāng)然我們也可以傳進(jìn)set方法的地址作為key诸狭。最后我們還可以更進(jìn)一步的簡(jiǎn)化:
- (void)setSex:(NSString *)sex{
objc_setAssociatedObject(self, @selector(sex), sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)sex{
return objc_getAssociatedObject(self, _cmd);
}
這里在get方法里把@selector(sex)
換成了_cmd
,這是因?yàn)槲覀兪褂玫膋ey是sex
這個(gè)方法的地址券膀,在這個(gè)方法內(nèi)部,我們可以直接使用_cmd
獲取本方法驯遇。那這樣就非常方便簡(jiǎn)潔了芹彬。
關(guān)聯(lián)對(duì)象的原理
set方法
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
方法
我們直接去runtime的源碼中去查看關(guān)聯(lián)對(duì)象的具體實(shí)現(xiàn),直接搜索objc_setA
,
- 1.選擇objc-runtime.mm這個(gè)文件中的實(shí)現(xiàn):
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
- 2.點(diǎn)進(jìn)
_object_set_associative_reference(object, (void *)key, value, policy);
這個(gè)真實(shí)的實(shí)現(xiàn)函數(shù):
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
這個(gè)函數(shù)的實(shí)現(xiàn)看起來(lái)非常復(fù)雜叉庐,都是C++的語(yǔ)法舒帮,對(duì)于不了解C++的人來(lái)說(shuō)非常困難,不過(guò)沒(méi)關(guān)系,即便我們看不懂上面的代碼玩郊,通過(guò)下面的分析肢执,我們也能明白關(guān)聯(lián)對(duì)象的原理:
實(shí)現(xiàn)關(guān)聯(lián)對(duì)象技術(shù)的核心對(duì)象有:
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjectAssociation
這里面經(jīng)常出現(xiàn)Map這個(gè)東西,這其實(shí)和我們Objective-c中的字典是一樣的译红,我們可以把它當(dāng)字典來(lái)看待预茄。在第二種方法里面我們是用字典去實(shí)現(xiàn)的,這里又出現(xiàn)了和字典相似的結(jié)構(gòu)侦厚,那它們的實(shí)現(xiàn)會(huì)不會(huì)相似呢耻陕?
在上面的一大段源碼中,我們?cè)陂_(kāi)頭的位置找到這一句:
AssociationsManager manager;
我們點(diǎn)進(jìn)AssociationsManager
查看其結(jié)構(gòu):
前面講了Map類型是字典假夺,那么什么是key淮蜈,什么是value呢?然后我們繼續(xù)點(diǎn)進(jìn)
AssociationsHashMap
:我們前面也講了已卷,
ObjectAssociationMap
這個(gè)結(jié)構(gòu)也是字典梧田,那么這個(gè)字典里面裝的是什么呢?我們點(diǎn)進(jìn)去看看:那這個(gè)
ObjcAssociation
又是什么東西呢侧蘸?我們進(jìn)去看看:總結(jié)一下上面四個(gè)核心對(duì)象的結(jié)構(gòu):
下面這張圖總結(jié)的是這四個(gè)核心對(duì)象之間的聯(lián)系:
那么問(wèn)題來(lái)了裁眯,
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
中的四個(gè)參數(shù)分別對(duì)應(yīng)上面結(jié)構(gòu)中的哪個(gè)結(jié)構(gòu)呢?下圖就展示了它們的對(duì)應(yīng)關(guān)系:
拿我們之前寫(xiě)的作為例子:
objc_setAssociatedObject(self, @selector(sex), sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
這句代碼中讳癌,self
也就是person對(duì)象被賦給了AssociationHashMap
的key穿稳,而@selector(sex)
的地址被賦給了AssociationMap
的key,策略OBJC_ASSOCIATION_COPY_NONATOMIC
被賦值給了ObjectAssociation
的policy晌坤,傳遞進(jìn)來(lái)的值sex
被賦值給了ObjectAssociation
的value逢艘。
這種設(shè)計(jì)的巧妙之處就在于:
當(dāng)一個(gè)person對(duì)象不光有一個(gè)屬性值要關(guān)聯(lián)時(shí),比如我們要關(guān)聯(lián)height和sex這兩個(gè)屬性時(shí)骤菠,我們以person對(duì)象作為key它改,然后值是AssociationMap
這個(gè)字典類型,在這個(gè)字典類型中商乎,分別使用@selector(sex)
和@selector(height)
作為key央拖,然后分別利用sex屬性的policy和傳遞進(jìn)來(lái)的value和height屬性的policy和傳遞進(jìn)來(lái)的value生成ObjectAssociation
作為value。而如果有多個(gè)person對(duì)象需要關(guān)聯(lián)時(shí)鹉戚,我們只需要在AssociationHashMap
中創(chuàng)造更多的鍵值對(duì)就可以解決這個(gè)問(wèn)題鲜戒。
通過(guò)這個(gè)過(guò)程我們也能明白:
關(guān)聯(lián)對(duì)象的值它不是存儲(chǔ)在自己的實(shí)例對(duì)象的結(jié)構(gòu)中,而是維護(hù)了一個(gè)全局的結(jié)構(gòu)AssociationManager
get方法
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
方法
經(jīng)過(guò)了上面的分析抹凳,基本上就對(duì)set方法的原理比較清楚了遏餐,下面我們直接看一下get方法的源碼:
- 1.在runtime的源碼中找到這個(gè)函數(shù):
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
- 2.點(diǎn)進(jìn)
_object_get_associative_reference(object, (void *)key);
3EFE0EEF-7D66-44FF-AC39-55386E6AE3BB.png
回答面試題
Category能否添加成員變量?如果可以赢底,如何給Category添加成員變量境输?
答:不能直接給Category添加成員變量蔗牡,但是可以間接實(shí)現(xiàn)Category有成員變量的效果。我們可以使用runtime的API嗅剖,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
和objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
這兩個(gè)來(lái)實(shí)現(xiàn)辩越。