前言
日常開發中,為一個已有的類(比如說不想影響其檔案結構)、第三方庫提供的類增加幾個property,已經是十分常見且需要的操作了,有人會單獨起草一份category.m檔案,也有人直接繼承,像我一般會用category,一是能減少類檔案的數量提高編譯速度,二也是為了程式碼結構更加清晰。
這篇文章是用來寫Category的進行屬性擴充套件的行為的,所以我還是言歸正傳,首先,我要闡述一下目前比較主流的幾個屬性擴充套件形式,再往下進行分析:
- 利用 objc_setAssociatedObject函式進行物件的聯合。
- 利用 class_addProperty 函式進行類屬性的擴充套件
- 通過內部建立一個其他物件(比如字典),通過重寫本物件set和get或者訊息轉發。
下面對這三種常用方法進行分析,其實常見的都是前面兩種,第三種也是比較非主流。在分析這三種之前,我要談一下為什麼不能用 class_addIvar 函式。
- class_addIvar 函式
在蘋果文件中,對 class_addIvar 函式有下面一段話:
This function may only be called after objc_allocateClassPair(_:_:_:) and before objc_registerClassPair(_:). Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.
這個功能只能在 objc_allocateClassPair(_:_:_:) 之後和 objc_registerClassPair(_:) 之前呼叫。不支援將例項變數新增到現有的類。
該類不能是元類。不支援將例項變數新增到元類。
複製程式碼
文件是說不能將此函式用於已有的類,必須是動態建立的類,為了能夠知道為何會這樣,我們需要翻閱一下蘋果開源的 runtime 原始碼。
- 首先看一下關於 objc_allocateClassPair 函式的程式碼實現:
去除干擾程式碼,我們尋找到下面的函式呼叫鏈條:
objc_allocateClassPair -> objc_initializeClassPair_internal
// 下面的程式碼已經被我大部分剔除,只留下我們分析所需要用到的程式碼
static void objc_initializeClassPair_internal(Class superclass, const char *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 類已分配但還未註冊
// RW_COPIED_RO class_rw_t->ro 來自 class_ro_t 結構的複製
// RW_REALIZED // class_t->data 的結構為 class_rw_t
// RW_REALIZING // 類已開始分配,但並未完成
// 以上幾個巨集都是對新類的class_rw_t結構設定基本資訊
}
複製程式碼
- 下面是class_addIvar的與我分析所需要的實現程式碼
// 無關程式碼已經剔除
BOOL
class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *type)
{
if (!cls) return NO;
if (!type) type = "";
if (name && 0 == strcmp(name, "")) name = nil;
rwlock_writer_t lock(runtimeLock);
assert(cls->isRealized());
// No class variables
if (cls->isMetaClass()) {
return NO;
}
// Can only add ivars to in-construction classes.
if (!(cls->data()->flags & RW_CONSTRUCTING)) {
return NO;
}
}
// 重點在這最後一句,前面我們已經看到 objc_allocateClassPair 函式所分配的新類的flags位資訊,在此處 & RW_CONSTRUCTING,必定為真,取反後跳過大括號向下執行。
複製程式碼
- 已經存在的類,經過測試,flag位為 RW_REALIZED|RW_REALIZING,設定函式如下:
static Class realizeClass(Class cls)
{
runtimeLock.assertWriting();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// fixme verify class is not in an un-dlopened part of the shared cache?
ro = (const class_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);
}
}
所以在經過條件 !((RW_REALIZED | RW_REALIZING) & RW_CONSTRUCTING) 時返回NO。
複製程式碼
以上便是對已有類不能使用 class_addIvar 函式的分析
好了,回到真正的話題,對上面三種操作的分析:
- objc_setAssociatedObject
我們都知道,在category中使用property,可以生成set和get的方法宣告,原因在此不做分析,一般為了方便的呼叫,我們都會寫上property,關鍵在於沒有set和get的實現,於是就會有下面這樣的程式碼:
static void *key = "key";
@implementation Person (Extra)
// 此處不考慮讀寫鎖的問題
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, key);
}
@end
複製程式碼
上面的 objc_setAssociatedObject 函式內部的呼叫鏈條如下:
objc_setAssociatedObject -> objc_setAssociatedObject_non_gc -> _object_set_associative_reference
// 其中主要操作都在 _object_set_associative_reference 函式中,內部實現類似一般屬性的set實現(保留新值,釋放舊值),在此我們不進行深究,具體可以參考業內大佬的部落格文章。
複製程式碼
這種操作很直觀的表達了我們的需要,且API十分友好,僅僅是對於 weak 策略我們需要自己設計一個。
並且這種操作的好處是我們無需關係關聯物件的宣告週期,因為和普通的屬性一樣,會隨著宿主物件的釋放而釋放,具體可以看以下程式碼:
dealloc -> _objc_rootDealloc -> rootDealloc -> object_dispose -> objc_destructInstance
// 大部分釋放操作在 objc_destructInstance 函式中完成
// 下面是 objc_destructInstance 函式的實現程式碼
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.
// 內部通過C++的解構函式進行物件屬性的釋放,具體可看sunny大神的博文
if (cxx) object_cxxDestruct(obj);
// 此處會移除所有的關聯物件,也就是objc_setAssociatedObject 函式所設定上去的物件
if (assoc) _object_remove_assocations(obj);
// 清空引用計數與weak表
if (dealloc) obj->clearDeallocating();
}
return obj;
}
複製程式碼
當然也有不足之處,利用 objc_setAssociatedObject 生成的關聯物件無法直接利用目前主流的Json轉Model庫(原因是無法在ivar及property中遍歷出來)。
- 利用 class_addProperty 函式進行類屬性的擴充套件
class_addProperty 函式可以為我們生成類的property,@property是編譯器的識別符號,在普通類中可生成property、ivar、setMethod與getMethod,在我看來property的真實作用類似於方法的宣告,後面我會再談為什麼。
在分類中使用class_addProperty和普通類一樣, 只能生成set和get方法的宣告,無論有沒有被實現,我們都可以用 class_copyMethodList 函式得到property的list,如果這時候你想儲存屬性值,你依然必須手動或動態實現那些set和get方法,並且真實資料的儲存也必須由你自己提供實現,比如可以使用前面所說的objc_setAssociatedObject 函式。
現在說說為啥property只是一個類似宣告的作用呢,我們可以從蘋果開源的程式碼中找到蛛絲馬跡:
Class 是一個指向結構體 objc_class 的指標,而此結構體的結構如下所示:
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 指向父類
cache_t cache; // 快取指標與vtable(沒學過C++,沒了解過虛擬函式這些),加速方法的呼叫
class_data_bits_t bits; // 真正儲存物件的ivar,property與method等資訊的地方
}
在原始碼中大部分時候表現為將類的大部分資訊儲存在 class_rw_t *rw指標中,不過內部也是返回bits中處理後的資訊
class_rw_t *data() {
return bits.data();
}
在class_rw_t的結構中,結構如下所示:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags; // 類的資訊標記
uint32_t version; // 當前執行時版本
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
}
複製程式碼
可以看到在class_rw_t的結構中,包含了另一個十分相似的 const class_ro_t *ro 成員變數。
這個成員變數為一個不可修改內容的結構體指標,其中儲存了類在編譯時就已經確定好的ivar、 property、method、protocol等資訊,在類的初始化時會通過 methodizeClass 函式將其大部分內容都拷貝到 class_rw_t *rw中,其中 ivar 不會被拷貝,這也是前面所說的不能在執行時給已有的類增加 ivar的原因。
像property、method、protocol都是可以在執行時動態新增的,且儲存到 rw 的結構中去。
好像說的有點跑題了,我們們還是一起看看property到底儲存了什麼資訊:
struct property_t {
const char *name;
const char *attributes;
};
複製程式碼
可以看到,propperty中並沒有儲存很多資訊,只有name和配置的屬性,也沒有實現函式的地址,所以前面我說property的作用其實和方法的宣告是差不多的。
關於property的好處,也就是在使用網上json轉model庫時可以被遍歷到了,但是如果你沒有實現set和get,那依然會導致KVC的crash。
- 通過內部建立一個其他物件(比如字典),通過重寫本物件set和get或者訊息轉發。
最後一種方法,也是比較少用的方式,說起來也比較簡單,比如定義一個靜態的字典變數,然後通過實現interface中宣告的set和get的實現對這個字典變數做存取操作,或者通過訊息轉發中的 (id)forwardingTargetForSelector:(SEL)aSelector 方法返回這個字典變數,但是要注意本類中沒有對轉發做過什麼事,不然這種方法也是不適用的。
對上文的總結
其實剛剛所描述的三種分類策略並不是很嚴謹,因為其中幾種總是會搭配著使用,所以在此也要選擇一個比較均衡的策略來實現Category屬性的繫結。
建議的策略:
- 由於我們肯定會在interface 中提供生的property(由於沒有合成實現與ivar,在此稱為生的),所以這樣對於在外部訪問時和普通property相同。
- 由於缺乏的是實現以及可以存取的資料量,這裡我們可以直接實現這些set與get。
- set與get的實現可以通過 associatedObject 進行對物件的存取操作。
好處: 這種操作由於提供了生的property,所以在第三方的json轉model庫遍歷property時可以直接遍歷到,由於你手動實現了set與get,所以在遍歷後的KVC賦值時也能起到作用,保證了和普通成員變數的操作一致性。
估計會有人看完結論後覺得:“ 我本來就是這麼寫的啊,你寫這麼多字到頭來得出的結論和我平時寫的也一樣。”是的,我只能略表抱歉啦?!