【iOS】老生常談category增加屬性的幾種操作

前言

日常開發(fā)中,為一個(gè)已有的類(比如說不想影響其文件結(jié)構(gòu))沪摄、第三方庫提供的類增加幾個(gè)property砸烦,已經(jīng)是十分常見且需要的操作了,有人會(huì)單獨(dú)起草一份category.m文件口锭,也有人直接繼承,像我一般會(huì)用category介杆,一是能減少類文件的數(shù)量提高編譯速度鹃操,二也是為了代碼結(jié)構(gòu)更加清晰。

這篇文章是用來寫Category的進(jìn)行屬性擴(kuò)展的行為的春哨,所以我還是言歸正傳荆隘,首先,我要闡述一下目前比較主流的幾個(gè)屬性擴(kuò)展形式赴背,再往下進(jìn)行分析:

利用 objc_setAssociatedObject函數(shù)進(jìn)行對(duì)象的聯(lián)合臭胜。

利用 class_addProperty 函數(shù)進(jìn)行類屬性的擴(kuò)展

通過內(nèi)部創(chuàng)建一個(gè)其他對(duì)象(比如字典),通過重寫本對(duì)象set和get或者消息轉(zhuǎn)發(fā)。

下面對(duì)這三種常用方法進(jìn)行分析癞尚,其實(shí)常見的都是前面兩種耸三,第三種也是比較非主流。在分析這三種之前浇揩,我要談一下為什么不能

class_addIvar 函數(shù)仪壮。

class_addIvar 函數(shù)

在蘋果文檔中,對(duì) class_addIvar 函數(shù)有下面一段話:

Thisfunctionmay?only?be?called?after?objc_allocateClassPair(_:_:_:)?and?before?objc_registerClassPair(_:).?Adding?an?instancevariable?to?an?existingclassisnot?supported.

Theclassmust?not?be?a?metaclass.?Adding?an?instancevariable?to?a?metaclassisnot?supported.

這個(gè)功能只能在?objc_allocateClassPair(_:_:_:)?之后和?objc_registerClassPair(_:)?之前調(diào)用胳徽。不支持將實(shí)例變量添加到現(xiàn)有的類积锅。

該類不能是元類。不支持將實(shí)例變量添加到元類养盗。

文檔是說不能將此函數(shù)用于已有的類缚陷,必須是動(dòng)態(tài)創(chuàng)建的類,為了能夠知道為何會(huì)這樣往核,我們需要翻閱一下蘋果開源的 runtime 源碼箫爷。

1.首先看一下關(guān)于 objc_allocateClassPair 函數(shù)的代碼實(shí)現(xiàn):

去除干擾代碼,我們尋找到下面的函數(shù)調(diào)用鏈條:

objc_allocateClassPair?->?objc_initializeClassPair_internal

//?下面的代碼已經(jīng)被我大部分剔除聂儒,只留下我們分析所需要用到的代碼

staticvoidobjc_initializeClassPair_internal(Class?superclass,constchar?*name,?Class?cls,?Class?meta)

{

//?Set?basic?info

cls->data()->flags?=?RW_CONSTRUCTING?|?RW_COPIED_RO?|?RW_REALIZED?|?RW_REALIZING;

meta->data()->flags?=?RW_CONSTRUCTING?|?RW_COPIED_RO?|?RW_REALIZED?|?RW_REALIZING;

cls->data()->version?=0;

meta->data()->version?=7;

//?RW_CONSTRUCTING?類已分配但還未注冊(cè)

//?RW_COPIED_RO?class_rw_t->ro?來自?class_ro_t?結(jié)構(gòu)的復(fù)制

//?RW_REALIZED?//??class_t->data?的結(jié)構(gòu)為?class_rw_t

//?RW_REALIZING?//?類已開始分配虎锚,但并未完成

//?以上幾個(gè)宏都是對(duì)新類的class_rw_t結(jié)構(gòu)設(shè)置基本信息

}

2.下面是class_addIvar的與我分析所需要的實(shí)現(xiàn)代碼

//?無關(guān)代碼已經(jīng)剔除

BOOL

class_addIvar(Class?cls,constchar?*name,?size_t?size,

uint8_t?alignment,constchar?*type)

{

if(!cls)returnNO;

if(!type)?type?="";

if(name??&&0==?strcmp(name,""))?name?=?nil;

rwlock_writer_t?lock(runtimeLock);

assert(cls->isRealized());

//?No?class?variables

if(cls->isMetaClass())?{

returnNO;

}

//?Can?only?add?ivars?to?in-construction?classes.

if(!(cls->data()->flags?&?RW_CONSTRUCTING))?{

returnNO;

}

}

//?重點(diǎn)在這最后一句,前面我們已經(jīng)看到?objc_allocateClassPair?函數(shù)所分配的新類的flags位信息衩婚,在此處?&?RW_CONSTRUCTING窜护,必定為真,取反后跳過大括號(hào)向下執(zhí)行非春。

3.已經(jīng)存在的類柱徙,經(jīng)過測(cè)試缓屠,flag位為 RW_REALIZED|RW_REALIZING,設(shè)置函數(shù)如下:

staticClass?realizeClass(Class?cls)

{

runtimeLock.assertWriting();

constclass_ro_t?*ro;

class_rw_t?*rw;

Class?supercls;

Class?metacls;

bool?isMeta;

if(!cls)returnnil;

if(cls->isRealized())returncls;

assert(cls?==?remapClass(cls));

//?fixme?verify?class?is?not?in?an?un-dlopened?part?of?the?shared?cache?

ro?=?(constclass_ro_t?*)cls->data();

if(ro->flags?&?RO_FUTURE)?{

//?This?was?a?future?class.?rw?data?is?already?allocated.

rw?=?cls->data();

ro?=?cls->data()->ro;

cls->changeInfo(RW_REALIZED|RW_REALIZING,?RW_FUTURE);

}else{

//?Normal?class.?Allocate?writeable?class?data.

rw?=?(class_rw_t?*)calloc(sizeof(class_rw_t),1);

rw->ro?=?ro;

rw->flags?=?RW_REALIZED|RW_REALIZING;

cls->setData(rw);

}

}

所以在經(jīng)過條件?!((RW_REALIZED?|?RW_REALIZING)?&?RW_CONSTRUCTING)?時(shí)返回NO。

以上便是對(duì)已有類不能使用 class_addIvar 函數(shù)的分析

好了护侮,回到真正的話題藏研,對(duì)上面三種操作的分析:

objc_setAssociatedObject

我們都知道,在category中使用property概行,可以生成set和get的方法聲明,原因在此不做分析弧岳,一般為了方便的調(diào)用凳忙,我們都會(huì)寫上property,關(guān)鍵在于沒有set和get的實(shí)現(xiàn)禽炬,于是就會(huì)有下面這樣的代碼:

staticvoid*key?="key";

@implementation?Person?(Extra)

//?此處不考慮讀寫鎖的問題

-?(void)setName:(NSString?*)name{

objc_setAssociatedObject(self,?key,?name,?OBJC_ASSOCIATION_COPY_NONATOMIC);

}

-?(NSString?*)name{

returnobjc_getAssociatedObject(self,?key);

}

@end

上面的 objc_setAssociatedObject 函數(shù)內(nèi)部的調(diào)用鏈條如下:

objc_setAssociatedObject?->?objc_setAssociatedObject_non_gc?->?_object_set_associative_reference

//?其中主要操作都在?_object_set_associative_reference?函數(shù)中涧卵,內(nèi)部實(shí)現(xiàn)類似一般屬性的set實(shí)現(xiàn)(保留新值,釋放舊值)腹尖,在此我們不進(jìn)行深究柳恐,具體可以參考業(yè)內(nèi)大佬的博客文章。

這種操作很直觀的表達(dá)了我們的需要热幔,且API十分友好乐设,僅僅是對(duì)于 weak 策略我們需要自己設(shè)計(jì)一個(gè)。

并且這種操作的好處是我們無需關(guān)系關(guān)聯(lián)對(duì)象的聲明周期绎巨,因?yàn)楹推胀ǖ膶傩砸粯咏校瑫?huì)隨著宿主對(duì)象的釋放而釋放,具體可以看以下代碼:

dealloc?->?_objc_rootDealloc?->?rootDealloc?->?object_dispose?->?objc_destructInstance

//?大部分釋放操作在?objc_destructInstance?函數(shù)中完成

//?下面是?objc_destructInstance?函數(shù)的實(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?=?!UseGC?&&?obj->hasAssociatedObjects();

bool?dealloc?=?!UseGC;

//?This?order?is?important.

//?內(nèi)部通過C++的析構(gòu)函數(shù)進(jìn)行對(duì)象屬性的釋放,具體可看sunny大神的博文

if(cxx)?object_cxxDestruct(obj);

//?此處會(huì)移除所有的關(guān)聯(lián)對(duì)象场勤,也就是objc_setAssociatedObject?函數(shù)所設(shè)置上去的對(duì)象

if(assoc)?_object_remove_assocations(obj);

//?清空引用計(jì)數(shù)與weak表

if(dealloc)?obj->clearDeallocating();

}

returnobj;

}

當(dāng)然也有不足之處戈锻,利用 objc_setAssociatedObject 生成的關(guān)聯(lián)對(duì)象無法直接利用目前主流的Json轉(zhuǎn)Model庫(原因是無法在ivar及property中遍歷出來)。

利用 class_addProperty 函數(shù)進(jìn)行類屬性的擴(kuò)展

class_addProperty 函數(shù)可以為我們生成類的property和媳,@property是編譯器的標(biāo)識(shí)符格遭,在普通類中可生成property、ivar留瞳、setMethod與getMethod拒迅,在我看來property的真實(shí)作用類似于方法的聲明,后面我會(huì)再談為什么她倘。

在分類中使用class_addProperty和普通類一樣坪它, 只能生成set和get方法的聲明,無論有沒有被實(shí)現(xiàn)帝牡,我們都可以用 class_copyMethodList 函數(shù)得到property的list往毡,如果這時(shí)候你想存儲(chǔ)屬性值,你依然必須手動(dòng)或動(dòng)態(tài)實(shí)現(xiàn)那些set和get方法靶溜,并且真實(shí)數(shù)據(jù)的存儲(chǔ)也必須由你自己提供實(shí)現(xiàn)开瞭,比如可以使用前面所說的objc_setAssociatedObject 函數(shù)懒震。

現(xiàn)在說說為啥property只是一個(gè)類似聲明的作用呢,我們可以從蘋果開源的代碼中找到蛛絲馬跡:

Class?是一個(gè)指向結(jié)構(gòu)體?objc_class?的指針嗤详,而此結(jié)構(gòu)體的結(jié)構(gòu)如下所示:

struct?objc_class?:?objc_object?{

//?Class?ISA;

Class?superclass;//?指向父類

cache_t?cache;//?緩存指針與vtable(沒學(xué)過C++,沒了解過虛函數(shù)這些)个扰,加速方法的調(diào)用

class_data_bits_t?bits;//?真正保存對(duì)象的ivar,property與method等信息的地方

}

在源碼中大部分時(shí)候表現(xiàn)為將類的大部分信息保存在?class_rw_t?*rw指針中葱色,不過內(nèi)部也是返回bits中處理后的信息

class_rw_t?*data()?{

returnbits.data();

}

在class_rw_t的結(jié)構(gòu)中递宅,結(jié)構(gòu)如下所示:

struct?class_rw_t?{

//?Be?warned?that?Symbolication?knows?the?layout?of?this?structure.

uint32_t?flags;//?類的信息標(biāo)記

uint32_t?version;//?當(dāng)前運(yùn)行時(shí)版本

constclass_ro_t?*ro;

method_array_t?methods;

property_array_t?properties;

protocol_array_t?protocols;

}

可以看到在class_rw_t的結(jié)構(gòu)中,包含了另一個(gè)十分相似的 const class_ro_t *ro 成員變量苍狰。

這個(gè)成員變量為一個(gè)不可修改內(nèi)容的結(jié)構(gòu)體指針办龄,其中存儲(chǔ)了類在編譯時(shí)就已經(jīng)確定好的ivar、 property淋昭、method俐填、protocol等信息,在類的初始化時(shí)會(huì)通過 methodizeClass 函數(shù)將其大部分內(nèi)容都拷貝到 class_rw_t *rw中翔忽,其中 ivar 不會(huì)被拷貝英融,這也是前面所說的不能在運(yùn)行時(shí)給已有的類增加 ivar的原因。

像property歇式、method驶悟、protocol都是可以在運(yùn)行時(shí)動(dòng)態(tài)添加的,且存儲(chǔ)到 rw 的結(jié)構(gòu)中去材失。

好像說的有點(diǎn)跑題了撩银,咱們還是一起看看property到底存儲(chǔ)了什么信息:

struct?property_t?{

constchar?*name;

constchar?*attributes;

};

可以看到,propperty中并沒有存儲(chǔ)很多信息豺憔,只有name和配置的屬性额获,也沒有實(shí)現(xiàn)函數(shù)的地址,所以前面我說property的作用其實(shí)和方法的聲明是差不多的恭应。

關(guān)于property的好處抄邀,也就是在使用網(wǎng)上json轉(zhuǎn)model庫時(shí)可以被遍歷到了,但是如果你沒有實(shí)現(xiàn)set和get昼榛,那依然會(huì)導(dǎo)致KVC的crash境肾。

通過內(nèi)部創(chuàng)建一個(gè)其他對(duì)象(比如字典),通過重寫本對(duì)象set和get或者消息轉(zhuǎn)發(fā)。

最后一種方法胆屿,也是比較少用的方式奥喻,說起來也比較簡(jiǎn)單,比如定義一個(gè)靜態(tài)的字典變量非迹,然后通過實(shí)現(xiàn)interface中聲明的set和get的實(shí)現(xiàn)對(duì)這個(gè)字典變量做存取操作环鲤,或者通過消息轉(zhuǎn)發(fā)中的 (id)forwardingTargetForSelector:(SEL)aSelector 方法返回這個(gè)字典變量,但是要注意本類中沒有對(duì)轉(zhuǎn)發(fā)做過什么事憎兽,不然這種方法也是不適用的冷离。

對(duì)上文的總結(jié)

其實(shí)剛剛所描述的三種分類策略并不是很嚴(yán)謹(jǐn)吵冒,因?yàn)槠渲袔追N總是會(huì)搭配著使用,所以在此也要選擇一個(gè)比較均衡的策略來實(shí)現(xiàn)Category屬性的綁定西剥。

建議的策略:

由于我們肯定會(huì)在interface 中提供生的property(由于沒有合成實(shí)現(xiàn)與ivar痹栖,在此稱為生的),所以這樣對(duì)于在外部訪問時(shí)和普通property相同瞭空。

由于缺乏的是實(shí)現(xiàn)以及可以存取的數(shù)據(jù)量揪阿,這里我們可以直接實(shí)現(xiàn)這些set與get。

set與get的實(shí)現(xiàn)可以通過 associatedObject 進(jìn)行對(duì)對(duì)象的存取操作咆畏。

好處: 這種操作由于提供了生的property南捂,所以在第三方的json轉(zhuǎn)model庫遍歷property時(shí)可以直接遍歷到,由于你手動(dòng)實(shí)現(xiàn)了set與get鳖眼,所以在遍歷后的KVC賦值時(shí)也能起到作用,保證了和普通成員變量的操作一致性嚼摩。

估計(jì)會(huì)有人看完結(jié)論后覺得:“ 我本來就是這么寫的啊钦讳,你寫這么多字到頭來得出的結(jié)論和我平時(shí)寫的也一樣≌砻妫”是的愿卒,我只能略表抱歉啦!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末潮秘,一起剝皮案震驚了整個(gè)濱河市琼开,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枕荞,老刑警劉巖柜候,帶你破解...
    沈念sama閱讀 222,865評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異躏精,居然都是意外死亡渣刷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門矗烛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辅柴,“玉大人,你說我怎么就攤上這事瞭吃÷掂郑” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵歪架,是天一觀的道長(zhǎng)股冗。 經(jīng)常有香客問我,道長(zhǎng)和蚪,這世上最難降的妖魔是什么魁瞪? 我笑而不...
    開封第一講書人閱讀 60,199評(píng)論 1 300
  • 正文 為了忘掉前任穆律,我火速辦了婚禮,結(jié)果婚禮上导俘,老公的妹妹穿的比我還像新娘峦耘。我一直安慰自己,他們只是感情好旅薄,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,196評(píng)論 6 398
  • 文/花漫 我一把揭開白布辅髓。 她就那樣靜靜地躺著,像睡著了一般少梁。 火紅的嫁衣襯著肌膚如雪洛口。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,793評(píng)論 1 314
  • 那天凯沪,我揣著相機(jī)與錄音第焰,去河邊找鬼。 笑死妨马,一個(gè)胖子當(dāng)著我的面吹牛挺举,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播烘跺,決...
    沈念sama閱讀 41,221評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼湘纵,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了滤淳?” 一聲冷哼從身側(cè)響起梧喷,我...
    開封第一講書人閱讀 40,174評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脖咐,沒想到半個(gè)月后铺敌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屁擅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,770評(píng)論 3 343
  • 正文 我和宋清朗相戀三年适刀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煤蹭。...
    茶點(diǎn)故事閱讀 40,918評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笔喉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出硝皂,到底是詐尸還是另有隱情常挚,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評(píng)論 5 351
  • 正文 年R本政府宣布稽物,位于F島的核電站奄毡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏贝或。R本人自食惡果不足惜吼过,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,255評(píng)論 3 336
  • 文/蒙蒙 一锐秦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盗忱,春花似錦酱床、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至闲昭,卻和暖如春罐寨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背序矩。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工鸯绿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人簸淀。 一個(gè)月前我還...
    沈念sama閱讀 49,364評(píng)論 3 379
  • 正文 我出身青樓瓶蝴,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親啃擦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子囊蓝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,926評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉饿悬,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,734評(píng)論 0 9
  • 前言 日常開發(fā)中令蛉,為一個(gè)已有的類(比如說不想影響其文件結(jié)構(gòu))、第三方庫提供的類增加幾個(gè)property狡恬,已經(jīng)是十分...
    JUNGHSU閱讀 604評(píng)論 0 1
  • 我們常常會(huì)聽說 Objective-C 是一門動(dòng)態(tài)語言珠叔,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,199評(píng)論 0 7
  • 冬天汇鞭,就是一個(gè)吃嘎嘎、長(zhǎng)嘎嘎的季節(jié)庸追。 提到貴州的肉嘎嘎霍骄,是不是就口水滴答? 高能預(yù)警 千萬別在晚上把這條帖子發(fā)給好...
    貴州新目標(biāo)閱讀 565評(píng)論 0 0
  • 借一雙冬的眼睛 一枚葉子妄圖抓住一根陽光 野草還在風(fēng)中糾纏 一截枯枝淡溯,在地上走走停停 前面已是江岸 水流還在蕩漾读整,...
    丁海鵬閱讀 189評(píng)論 1 6