在平日編程中或閱讀第三方代碼時(shí)芹壕,category可以說是無處不在栏笆。category也可以說是OC作為一門動(dòng)態(tài)語言的一大特色。category為我們動(dòng)態(tài)擴(kuò)展類的功能提供了可能没咙,或者我們也可以把一個(gè)龐大的類進(jìn)行功能分解立膛,按照category進(jìn)行組織。
關(guān)于category的使用無需多言铁孵,今天我們來深入了解一下锭硼,category是如何在runtime中實(shí)現(xiàn)的。
category的數(shù)據(jù)結(jié)構(gòu)
category對(duì)應(yīng)到runtime中的結(jié)構(gòu)體是struct category_t(位于objc-runtime-new.h):
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
category_t的定義很簡(jiǎn)單蜕劝。從定義中看出檀头,category 的可為:添加實(shí)例方法(instanceMethods),類方法(classMethods)岖沛,協(xié)議(protocols)和實(shí)例屬性(instanceProperties)暑始,以及不可為:不能夠添加實(shí)例變量(關(guān)于實(shí)例屬性和實(shí)例變量的區(qū)別,我們將會(huì)在別的章節(jié)中探討)婴削。
category的加載
知道了category的數(shù)據(jù)結(jié)構(gòu)廊镜,我們來深入探究一下category是如何在runtime中實(shí)現(xiàn)的。
原理很簡(jiǎn)單:runtime會(huì)分別將category 結(jié)構(gòu)體中的instanceMethods, protocols唉俗,instanceProperties添加到target class的實(shí)例方法列表嗤朴,協(xié)議列表,屬性列表中虫溜,會(huì)將category結(jié)構(gòu)體中的classMethods添加到target class所對(duì)應(yīng)的元類的實(shí)例方法列表中雹姊。其本質(zhì)就相當(dāng)于runtime在運(yùn)行時(shí)期,修改了target class的結(jié)構(gòu)衡楞。
經(jīng)過這一番修改吱雏,category中的方法,就變成了target class方法列表中的一部分瘾境,其調(diào)用方式也就一模一樣啦~
現(xiàn)在歧杏,就來看一下具體是怎么實(shí)現(xiàn)的。
首先迷守,我們?cè)贛ach-O格式和runtime 介紹過在Mach-O文件中犬绒,category數(shù)據(jù)會(huì)被存放在__DATA段下的__objc_catlist section中。
當(dāng)OC被dyld加載起來時(shí)兑凿,OC進(jìn)入其入口點(diǎn)函數(shù)_objc_init:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
我們忽略一堆init方法懂更,重點(diǎn)來看_dyld_objc_notify_register方法眨业。該方法會(huì)向dyld注冊(cè)監(jiān)聽Mach-O中OC相關(guān)section被加載入\載出內(nèi)存的事件。
具體有三個(gè)事件:
_dyld_objc_notify_mapped(對(duì)應(yīng)&map_images回調(diào)):當(dāng)dyld已將images加載入內(nèi)存時(shí)沮协。
_dyld_objc_notify_init(對(duì)應(yīng)load_images回調(diào)):當(dāng)dyld初始化image后龄捡。OC調(diào)用類的+load方法,就是在這時(shí)進(jìn)行的慷暂。
_dyld_objc_notify_unmapped(對(duì)應(yīng)unmap_image回調(diào)):當(dāng)dyld將images移除內(nèi)存時(shí)聘殖。
而category寫入target class的方法列表,則是在_dyld_objc_notify_mapped行瑞,即將Mach-O相關(guān)sections都加載到內(nèi)存之后所發(fā)生的奸腺。
我們可以看到其對(duì)應(yīng)回調(diào)為map_images方法。
在map_images 最終會(huì)調(diào)用_read_images 方法來讀取OC相關(guān)sections血久,并以此來初始化OC內(nèi)存環(huán)境突照。_read_images 的極簡(jiǎn)實(shí)現(xiàn)版如下,可以看到氧吐,rumtime是如何根據(jù)Mach-O各個(gè)section的信息來初始化其自身的:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
static bool doneOnce;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertWriting();
if (!doneOnce) {
doneOnce = YES;
ts.log("IMAGE TIMES: first time tasks");
}
// Discover classes. Fix up unresolved future classes. Mark bundle classes.
for (EACH_HEADER) {
classref_t *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
}
}
ts.log("IMAGE TIMES: discover classes");
// Fix up remapped classes
// Class list and nonlazy class list remain unremapped.
// Class refs and super refs are remapped for message dispatching.
for (EACH_HEADER) {
Class *classrefs = _getObjc2ClassRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
// fixme why doesn't test future1 catch the absence of this?
classrefs = _getObjc2SuperRefs(hi, &count);
for (i = 0; i < count; i++) {
remapClassRef(&classrefs[i]);
}
}
ts.log("IMAGE TIMES: remap classes");
for (EACH_HEADER) {
if (hi->isPreoptimized()) continue;
bool isBundle = hi->isBundle();
SEL *sels = _getObjc2SelectorRefs(hi, &count);
UnfixedSelectors += count;
for (i = 0; i < count; i++) {
const char *name = sel_cname(sels[i]);
sels[i] = sel_registerNameNoLock(name, isBundle);
}
}
ts.log("IMAGE TIMES: fix up selector references");
// Discover protocols. Fix up protocol refs.
for (EACH_HEADER) {
extern objc_class OBJC_CLASS_$_Protocol;
Class cls = (Class)&OBJC_CLASS_$_Protocol;
assert(cls);
NXMapTable *protocol_map = protocols();
bool isPreoptimized = hi->isPreoptimized();
bool isBundle = hi->isBundle();
protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
for (i = 0; i < count; i++) {
readProtocol(protolist[i], cls, protocol_map,
isPreoptimized, isBundle);
}
}
ts.log("IMAGE TIMES: discover protocols");
// Fix up @protocol references
// Preoptimized images may have the right
// answer already but we don't know for sure.
for (EACH_HEADER) {
protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
for (i = 0; i < count; i++) {
remapProtocolRef(&protolist[i]);
}
}
ts.log("IMAGE TIMES: fix up @protocol references");
// Realize non-lazy classes (for +load methods and static instances)
for (EACH_HEADER) {
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i]);
if (!cls) continue;
realizeClass(cls);
}
}
ts.log("IMAGE TIMES: realize non-lazy classes");
// Realize newly-resolved future classes, in case CF manipulates them
if (resolvedFutureClasses) {
for (i = 0; i < resolvedFutureClassCount; i++) {
realizeClass(resolvedFutureClasses[i]);
resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
}
free(resolvedFutureClasses);
}
ts.log("IMAGE TIMES: realize future classes");
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
}
}
}
ts.log("IMAGE TIMES: discover categories");
}
大致的邏輯是讹蘑,runtime調(diào)用_getObjc2XXX格式的方法,依次來讀取對(duì)應(yīng)的section內(nèi)容筑舅,并根據(jù)其結(jié)果初始化其自身結(jié)構(gòu)座慰。
_getObjc2XXX方法有如下幾種,可以看到他們都一一對(duì)應(yīng)了Mach-O中相關(guān)的OC seciton翠拣。
// function name content type section name
GETSECT(_getObjc2SelectorRefs, SEL, "__objc_selrefs");
GETSECT(_getObjc2MessageRefs, message_ref_t, "__objc_msgrefs");
GETSECT(_getObjc2ClassRefs, Class, "__objc_classrefs");
GETSECT(_getObjc2SuperRefs, Class, "__objc_superrefs");
GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist");
GETSECT(_getObjc2NonlazyClassList, classref_t, "__objc_nlclslist");
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList, protocol_t *, "__objc_protolist");
GETSECT(_getObjc2ProtocolRefs, protocol_t *, "__objc_protorefs");
GETSECT(getLibobjcInitializers, Initializer, "__objc_init_func");
可以看到版仔,我們使用的類,協(xié)議和category误墓,都是在_read_images 方法中讀取出來的蛮粮。
在讀取cateogry的方法 _getObjc2CategoryList(hi, &count)中,讀取的是Mach-O文件的 __objc_catlist 段谜慌。
我們重點(diǎn)關(guān)注和category相關(guān)的代碼:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
bool classExists = NO;
// 如果Category中有實(shí)例方法然想,協(xié)議,實(shí)例屬性畦娄,會(huì)改寫target class的結(jié)構(gòu)
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
// 如果category中有類方法,協(xié)議弊仪,或類屬性(目前OC版本不支持類屬性), 會(huì)改寫target class的元類結(jié)構(gòu)
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
ts.log("IMAGE TIMES: discover categories");
discover categories的邏輯如下:
- 先調(diào)用_getObjc2CategoryList讀取__objc_catlist seciton下所記錄的所有category熙卡。并存放到category_t *數(shù)組中。
- 依次讀取數(shù)組中的category_t * cat
- 對(duì)每一個(gè)cat励饵,先調(diào)用remapClass(cat->cls)驳癌,并返回一個(gè)objc_class *對(duì)象cls。這一步的目的在于找到到category對(duì)應(yīng)的類對(duì)象cls役听。
- 找到category對(duì)應(yīng)的類對(duì)象cls后颓鲜,就開始進(jìn)行對(duì)cls的修改操作了表窘。首先,如果category中有實(shí)例方法甜滨,協(xié)議乐严,和實(shí)例屬性之一的話,則直接對(duì)cls進(jìn)行操作衣摩。如果category中包含了類方法昂验,協(xié)議,類屬性(不支持)之一的話艾扮,還要對(duì)cls所對(duì)應(yīng)的元類(cls->ISA())進(jìn)行操作既琴。
- 不管是對(duì)cls還是cls的元類進(jìn)行操作,都是調(diào)用的方法addUnattachedCategoryForClass泡嘴。但這個(gè)方法并不是category實(shí)現(xiàn)的關(guān)鍵甫恩,其內(nèi)部邏輯只是將class和其對(duì)應(yīng)的category做了一個(gè)映射。這樣酌予,以class為key磺箕,就可以取到所其對(duì)應(yīng)的所有的category。
- 做好class和category的映射后霎终,會(huì)調(diào)用remethodizeClass方法來修改class的method list結(jié)構(gòu)滞磺,這才是runtime實(shí)現(xiàn)category的關(guān)鍵所在。
remethodizeClass
既然remethodizeClass是category的實(shí)現(xiàn)核心莱褒,那么我們就單獨(dú)一節(jié)击困,細(xì)看一下該方法的實(shí)現(xiàn):
/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
該段代碼首先通過unattachedCategoriesForClass取出還未被附加到class上的category list,然后調(diào)用attachCategories將這些category附加到class上广凸。
attachCategories的實(shí)現(xiàn)如下:
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 首先分配method_list_t *阅茶, property_list_t *, protocol_list_t *的數(shù)組空間谅海,數(shù)組大小等于category的個(gè)數(shù)
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) { // 依次讀取每一個(gè)category脸哀,將其methods,property扭吁,protocol添加到mlists撞蜂,proplist,protolist中存儲(chǔ)
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 取出class的data()數(shù)據(jù)侥袜,其實(shí)是class_rw_t * 指針蝌诡,其對(duì)應(yīng)結(jié)構(gòu)體實(shí)例存儲(chǔ)了class的基本信息
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount); // 將category中的method 添加到class中
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls); // 如果需要,同時(shí)刷新class的method list cache
rw->properties.attachLists(proplists, propcount); // 將category的property添加到class中
free(proplists);
rw->protocols.attachLists(protolists, protocount); // 將category的protocol添加到class中
free(protolists);
}
到此為止枫吧,我們就完成了category的加載工作浦旱。可以看到九杂,最終颁湖,cateogry被加入到了對(duì)應(yīng)class的方法宣蠕,協(xié)議以及屬性列表中。
最后我們?cè)倏匆幌耡ttachLists方法是如何將兩個(gè)list合二為一的:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
仔細(xì)看會(huì)發(fā)現(xiàn)甥捺,attachLists方法其實(shí)是使用的‘頭插’的方式將新的list插入原有l(wèi)ist中的抢蚀。即,新的list會(huì)插入到原始list的頭部涎永。
這也就說明了思币,為什么category中的方法,會(huì)‘覆蓋’class的原始方法羡微。其實(shí)并沒有真正的‘覆蓋’谷饿,而是由于cateogry中的方法被排到了原始方法的前面,那么在消息查找流程中妈倔,會(huì)返回首先被查找到的cateogry方法的實(shí)現(xiàn)博投。
category和+load方法
在面試時(shí),可能被問到這樣的問題:
在類的+load方法中盯蝴,可以調(diào)用分類方法嗎毅哗?
要回答這個(gè)問題,其實(shí)要搞清load方法的調(diào)用時(shí)機(jī)和category附加到class上的先后順序捧挺。
如果在load方法被調(diào)用前虑绵,category已經(jīng)完成了附加到class上的流程,則對(duì)于上面的問題闽烙,答案是肯定的翅睛。
我們回到runtime的入口函數(shù)來看一下,
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
runtime在入口點(diǎn)分別向dyld注冊(cè)了三個(gè)事件監(jiān)聽:mapped oc sections黑竞, init oc section 以及 unmapped oc sections捕发。
而這三個(gè)事件的順序是: mapped oc sections -> init oc section -> unmapped oc sections
在mapped oc sections 事件中,我們已經(jīng)看過其源碼很魂,runtime會(huì)依次讀取Mach-O文件中的oc sections扎酷,并根據(jù)這些信息來初始化runtime環(huán)境。這其中就包括cateogry的加載遏匆。
之后法挨,當(dāng)runtime環(huán)境都初始化完畢,在dyld的init oc section 事件中幅聘,runtime會(huì)調(diào)用每一個(gè)加載到內(nèi)存中的類的+load方法凡纳。
這里我們注意到,+load方法的調(diào)用是在cateogry加載之后的喊暖。因此惫企,在+load方法中撕瞧,是可以調(diào)用category方法的陵叽。
調(diào)用已被category‘覆蓋’的方法
前面我們已經(jīng)知道狞尔,類中的方法并不是真正的被category‘覆蓋’,而是被放到了類方法列表的后面巩掺,消息查找時(shí)找不到而已偏序。我們當(dāng)然也可以手動(dòng)來找到并調(diào)用它,代碼如下:
@interface Son : NSObject
- (void)sayHi;
@end
@implementation Son
- (void)sayHi {
NSLog(@"Son say hi!");
}
@end
// son 的分類胖替,覆寫了sayHi方法
@interface Son (Good)
- (void)sayHi;
- (void)saySonHi;
@end
- (void)sayHi {
NSLog(@"Son's category good say hi");
}
- (void)saySonHi {
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList([self class], &methodCount);
SEL sel = @selector(sayHi);
NSString *originalSelName = NSStringFromSelector(sel);
IMP lastIMP = nil;
for (NSInteger i = 0; i < methodCount; ++i) {
Method method = methodList[i];
NSString *selName = NSStringFromSelector(method_getName(method));
if ([originalSelName isEqualToString:selName]) {
lastIMP = method_getImplementation(method);
}
}
if (lastIMP != nil) {
typedef void(*fn)(id, SEL);
fn f = (fn)lastIMP;
f(self, sel);
}
free(methodList);
}
// 分別調(diào)用sayHi 和 saySonHi
Son *mySon1 = [Son new];
[mySon1 sayHi];
[mySon1 saySonHi];
輸出為:
Son's category good say hi
Son say hi
果然研儒,我們調(diào)用到了原始的sayHi方法。
category和關(guān)聯(lián)對(duì)象
眾所周知独令,category是不支持向類添加實(shí)例變量的端朵。這在源碼中也可以看出,cateogry僅支持實(shí)例方法燃箭、類方法冲呢、協(xié)議、和實(shí)例屬性(注意招狸,實(shí)例屬性并不等于實(shí)例變量)敬拓。
但是,runtime也給我提供了一個(gè)折中的方式裙戏,雖然不能夠向類添加實(shí)例變量乘凸,但是runtime為我們提供了方法,可以向類的實(shí)例對(duì)象添加關(guān)聯(lián)對(duì)象累榜。
所謂關(guān)聯(lián)對(duì)象营勤,就是為目標(biāo)對(duì)象添加一個(gè)關(guān)聯(lián)的對(duì)象,并能夠通過key來查找到這個(gè)關(guān)聯(lián)對(duì)象信柿。說的形象一點(diǎn)冀偶,就像我們?nèi)ヌ瑁瑀untime可以給我們分配一個(gè)舞伴一樣渔嚷。
這種關(guān)聯(lián)是對(duì)象和對(duì)象級(jí)別的进鸠,而不是類層次上的。當(dāng)你為一個(gè)類實(shí)例添加一個(gè)關(guān)聯(lián)對(duì)象后形病,如果你再創(chuàng)建另一個(gè)類實(shí)例客年,這個(gè)新建的實(shí)例是沒有關(guān)聯(lián)對(duì)象的。
我們可以通過重寫set/get方法的形式漠吻,來自動(dòng)為我們的實(shí)例添加關(guān)聯(lián)對(duì)象量瓜。
MyClass+Category1.h:
#import "MyClass.h"
@interface MyClass (Category1)
@property(nonatomic,copy) NSString *name;
@end
MyClass+Category1.m:
#import "MyClass+Category1.h"
#import <objc/runtime.h>
@implementation MyClass (Category1)
+ (void)load
{
NSLog(@"%@",@"load in Category1");
}
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self,
"name",
name,
OBJC_ASSOCIATION_COPY);
}
- (NSString*)name
{
NSString *nameObject = objc_getAssociatedObject(self, "name");
return nameObject;
}
@end
代碼很簡(jiǎn)單,我們重點(diǎn)關(guān)注一下其背后的實(shí)現(xiàn)途乃。
objc_setAssociatedObject
我們要設(shè)置關(guān)聯(lián)對(duì)象绍傲,需要調(diào)用objc_setAssociatedObject 方法將對(duì)象關(guān)聯(lián)到目標(biāo)對(duì)象上。我們需要傳入4個(gè)參數(shù):target object, associated key烫饼, associated value, objc_AssociationPolicy猎塞。
objc_AssociationPolicy是一個(gè)枚舉,可以取值為:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
分別和property的屬性定義一一匹配杠纵。
當(dāng)我們?yōu)閷?duì)象設(shè)置關(guān)聯(lián)對(duì)象的時(shí)候荠耽,所關(guān)聯(lián)的對(duì)象到底存在了那里呢?我們看源碼:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
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; // 這是一個(gè)單例比藻,內(nèi)部保存一個(gè)全局的static AssociationsHashMap *_map; 用于保存所有的關(guān)聯(lián)對(duì)象铝量。
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object); // 取反object 地址 作為accociative key
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(); // 將object標(biāo)記為 has AssociatedObjects
}
} else { // 如果傳入的關(guān)聯(lián)對(duì)象值為nil,則斷開關(guān)聯(lián)
// 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); // 釋放掉old關(guān)聯(lián)對(duì)象银亲。(如果多次設(shè)置同一個(gè)key的value,這里會(huì)釋放之前的value)
}
大體流程為:
根據(jù)關(guān)聯(lián)的policy慢叨,調(diào)用id new_value = value ? acquireValue(value, policy) : nil; ,acquireValue 方法會(huì)根據(jù)poilcy是retain或copy务蝠,對(duì)value做引用+1操作或copy操作插爹,并返回對(duì)應(yīng)的new_value。(如果傳入的value為nil请梢,則返回nil赠尾,不做任何操作)
acquireValue實(shí)現(xiàn)代碼是:
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return objc_retain(value);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}
獲取到new_value 后,根據(jù)是否有new_value的值毅弧,進(jìn)入不同流程气嫁。如果 new_value 存在,則對(duì)象與目標(biāo)對(duì)象關(guān)聯(lián)够坐。實(shí)質(zhì)是存入到全局單例 AssociationsManager manager 的對(duì)象關(guān)聯(lián)表中寸宵。 如果new_value 不存在,則釋放掉之前目標(biāo)對(duì)象及關(guān)聯(lián) key所存儲(chǔ)的關(guān)聯(lián)對(duì)象元咙。實(shí)質(zhì)是在 AssociationsManager 中刪除掉關(guān)聯(lián)對(duì)象梯影。
最后,釋放掉之前以同樣key存儲(chǔ)的關(guān)聯(lián)對(duì)象庶香。
其中甲棍,起到關(guān)鍵作用的在于AssociationsManager manager, 它是一個(gè)全局單例赶掖,其成員變量為static AssociationsHashMap *_map感猛,用于存儲(chǔ)目標(biāo)對(duì)象及其關(guān)聯(lián)的對(duì)象。_map中的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)如下圖所示:
仔細(xì)看這一段代碼奢赂,會(huì)發(fā)現(xiàn)有個(gè)問題:當(dāng)我們第一次為目標(biāo)對(duì)象創(chuàng)建關(guān)聯(lián)對(duì)象時(shí)陪白,會(huì)在AssociationsManager manager 的ObjectAssociationMap 中插入一個(gè)以disguised_object為key 的節(jié)點(diǎn),用于存儲(chǔ)該目標(biāo)對(duì)象所關(guān)聯(lián)的對(duì)象膳灶。
但是咱士,上面代碼中,僅有釋放old_association關(guān)聯(lián)對(duì)象的代碼,而沒有釋放保存在AssociationsManager manager 中節(jié)點(diǎn)的代碼序厉。那么拆吆,AssociationsManager manager 中的節(jié)點(diǎn)是什么時(shí)候被釋放的呢?
在對(duì)象的銷毀邏輯里脂矫,會(huì)調(diào)用objc_destructInstance,實(shí)現(xiàn)如下:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj); // 調(diào)用C++析構(gòu)函數(shù)
if (assoc) _object_remove_assocations(obj); // 移除所有的關(guān)聯(lián)對(duì)象霉晕,并將其自身從AssociationsManager的map中移除
obj->clearDeallocating(); // 清理ARC ivar
}
return obj;
}
obj的關(guān)聯(lián)對(duì)象會(huì)在_object_remove_assocations方法中全部移除庭再,同時(shí),會(huì)將obj自身從AssociationsManager的map中移除:
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}