什麼是category?
category是 Objective-C 2.0 之後新增的語言特性,主要作用是為已經存在的類新增方法。除此之外,Apple 還推薦了category 的另外兩個使用場景。
- 可以把類的實現分開在幾個不同的檔案裡面。這樣做有幾個顯而易見的好處
- 可以減少單個檔案的體積
- 可以把不同的功能組織到不同的 category 裡
- 可以由多個開發者共同完成一個類
- 可以按需載入想要的 category 等等
- 宣告私有方法
不過除了apple推薦的使用場景,還衍生出了 category 的其他幾個使用場景:
- 模擬多繼承
- 把framework的私有方法公開
category特點
- category 只能給某個已有的類擴充方法,不能擴充成員變數
- category 中也可以新增屬性,只不過
@property
只會生成setter
和getter
的宣告,不會生成setter
和getter
的實現以及成員變數 - 如果 category 中的方法和類中原有方法同名,category 中的方法會覆蓋掉類中原有的方法
- 如果多個 category 中存在同名的方法,執行時到底呼叫哪個方法由編譯器決定,後面參與編譯的方法會覆蓋前面同名的方法,所以最後一個參與編譯的方法會被呼叫
這裡說的是覆蓋而不是替換,是因為後編譯的方法被放在了方法列表的前面而已,runtime機制先找到前面的方法來執行
Category
VS Extension
category 常常拿來與 extension 做比較,extension 一樣可以新增屬性和方法,extension 看起來很像一個匿名的 category。但實際上兩者幾乎完全是兩個東西
- extension 執行在編譯期,它就是類的一部分,擴充的方法,屬性和變數一起形成一個完整的類。category 是執行期決議的,此時物件的記憶體佈局已經確定,無法再新增例項變數
- extension 一般用來隱藏類的私有資訊,你必須有一個類的原始碼才能為一個類新增 extension
- extension 和 category 都可以新增屬性,但是 category 的屬性不能生成成員變數和 getter、setter 方法的實現
category原理
講了一堆category的作用和特點,我們來看一下category的定義
typedef struct category_t *Category;
struct category_t {
const char *name; //category名稱
classref_t cls; //要擴充的類
struct method_list_t *instanceMethods; //給類新增的例項方法的列表
struct method_list_t *classMethods; //給類新增的類方法的列表
struct protocol_list_t *protocols; //給類新增的協議的列表
struct property_list_t *instanceProperties; //給類新增的屬性的列表
};
複製程式碼
實際上 Category
是一個 category_t
的結構體,裡面維護著類的資訊和category的名稱,以及類方法列表,例項方法列表,協議的列表和屬性的列表
那麼Category是怎麼載入的呢?
我們知道,Objective-C 的執行是依賴 OC 的 runtime 的, 而 OC 的 runtime 和其他系統庫一樣,是OS X和iOS通過dyld動態載入的
我們從OC執行時,入口方法出發
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);
}
複製程式碼
到真正完成繫結 category 的函式attachCategories
中間的函式呼叫棧是
void _objc_init(void);
└── void map_images(...);
└── void map_images_nolock(...);
└── void _read_images(...);
└── void _read_images(...);
└── static void remethodizeClass(Class cls);
└──attachCategories(Class cls, category_list *cats, bool flush_caches);
複製程式碼
我們來看一下 attachCategories
原始碼的簡易版:
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
bool isMeta = cls->isMetaClass();
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
int mcount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
}
複製程式碼
從上面的程式碼可以看出,增加方法的操作實際是分配一個大的例項方法列表
method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists));
再通過 for 迴圈將category中的方法列表填入這個大的列表,最後交給 rw->methods.attachLists(mlists, mcount);
將方法列表增加到類的方法列表上去。其他的屬性新增與此類似
我們再看一段 attachLists
的原始碼
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (hasArray()) {
//舊的方法列表的長度
uint32_t oldCount = array()->count;
//新的方法列表的長度
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//從addedCount的偏移量新增舊的方法列表
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//從開始新增新方法列表
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
複製程式碼
從上面的程式碼也驗證了我們上面所說的,同名的方法是覆蓋而不是替換,category的方法被放到了新方法列表的前面
category和關聯物件
如上所述,category 的屬性不能生成成員變數和 getter
、setter
方法的實現,我們要自己實現 getter
和 setter
方法,需藉助關聯物件來實現
關聯物件來實現提供三個介面 objc_setAssociatedObject
,objc_getAssociatedObject
,objc_removeAssociatedObjects
,他們分別呼叫的是
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
複製程式碼
他們呼叫的介面都位於 objc-references.mm
檔案中,
_object_get_associative_reference
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
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()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
複製程式碼
這段程式碼引用的型別有
- AssociationsManager
- AssociationsHashMap
- ObjcAssociationMap
- ObjcAssociation
AssociationsManager原始碼
spinlock_t AssociationsManagerLock;
class AssociationsManager {
static AssociationsHashMap *_map;
public:
// 初始化時候
AssociationsManager() { AssociationsManagerLock.lock(); }
// 析構的時候
~AssociationsManager() { AssociationsManagerLock.unlock(); }
// associations 方法用於取得一個全域性的 AssociationsHashMap 單例
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
複製程式碼
AssociationsManager 初始化一個 AssociationsHashMap
的單例,用自旋鎖 AssociationsManagerLock
保證執行緒安全
AssociationsHashMap原始碼
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
複製程式碼
AssociationsHashMap
是一個map型別,用於儲存物件的物件的 disguised_ptr_t
到 ObjectAssociationMap
的對映
ObjectAssociationMap原始碼
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
複製程式碼
ObjectAssociationMap
則儲存了從 key 到關聯物件 ObjcAssociation
的對映,這個資料結構儲存了當前物件對應的所有關聯物件
ObjcAssociation原始碼
class ObjcAssociation {
uintptr_t _policy;
id _value;
public:
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
ObjcAssociation() : _policy(0), _value(nil) {}
uintptr_t policy() const { return _policy; }
id value() const { return _value; }
bool hasValue() { return _value != nil; }
};
複製程式碼
ObjcAssociation
就是真正的關聯物件的類,上面的所有資料結構只是為了更好的儲存它。
最關鍵的 ObjcAssociation
包含了 policy
以及 value
用一張圖解釋他們的關係就是:
從上圖我們不難看出 _object_get_associative_reference
獲取關聯物件的步驟是:
AssociationsHashMap &associations(manager.associations())
獲取AssociationsHashMap
的單例物件associations
disguised_ptr_t disguised_object = DISGUISE(object)
獲取物件的地址- 通過物件的地址在
associations
中獲取AssociationsHashMap
迭代器 - 通過
key
獲取到ObjectAssociationMap
的迭代器 - 最後得出關聯物件類
ObjcAssociation
的例項entry
,再獲取到value
和policy
的值
_object_set_associative_reference
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
uintptr_t old_policy = 0; // NOTE: old_policy is always assigned to when old_value is non-nil.
id new_value = value ? acquireValue(value, policy) : nil, old_value = nil; // 呼叫 acquireValue 對 value 進行 retain 或者 copy
{
// & 取地址 *是指標,就是地址的內容
AssociationsManager manager; // 初始化一個 AssociationsManager 型別的變數 manager
AssociationsHashMap &associations(manager.associations()); // 取得一個全域性的 AssociationsHashMap 單例
if (new_value) {
// 如果new_value不為空,開始遍歷associations指向的map,查詢object物件是否存在儲存聯合儲存資料的ObjectAssociationMap物件
// 查詢map中是否包含某個關鍵字條目,用 find() 方法,傳入的引數是要查詢的key(被關聯物件的記憶體地址),在這裡需要提到的是begin()和end()兩個成員,分別代表map物件中第一個條目和最後一個條目,這兩個資料的型別是iterator.
// 定義一個條目變數 i (實際是指標)
AssociationsHashMap::iterator i = associations.find(object); // AssociationsHashMap 是一個無序的雜湊表,維護了從物件地址到 ObjectAssociationMap 的對映;
// iterator是 C++ 中的迭代器 , 這句話是定義一個 AssociationsHashMap::iterator 型別的變數 i,初始化為 associations.find(object) , associations是AssociationsHashMap型別物件。
// 通過map物件的方法獲取的iterator資料型別 是一個std::pair物件
// 根據物件地址獲取起對應的 ObjectAssociationMap物件
if (i != associations.end()) {
// 存在
// object物件在associations指向的map中存在一個ObjectAssociationMap物件refs
// ObjectAssociationMap 是一個 C++ 中的 map ,維護了從 key(就是外界傳入的key) 到 ObjcAssociation 的對映,即關聯記錄
ObjectAssociationMap *refs = i->second; // 指標 呼叫方法 需要用 -> i 是 AssociationsHashMap i->second 表示ObjectAssociationMap i->first 表示物件的地址
ObjectAssociationMap::iterator j = refs->find(key); // 根據傳入的關聯物件的key(一個地址)獲取其對應的關聯物件 ObjectAssociationMap
// 關聯物件是否存在
if (j != refs->end()) {
// 使用過該key儲存value,用新的value和policy替換掉原來的值
// 如果存在 持有舊的關聯物件
ObjcAssociation &old_entry = j->second;
old_policy = old_entry.policy;
old_value = old_entry.value;
// 存入新的關聯物件
old_entry.policy = policy;
old_entry.value = new_value;
} else {
// 沒用使用過該key儲存value,將value和policy儲存到key對映的map中
// 如果不存在 直接存入新的關聯物件
(*refs)[key] = ObjcAssociation(policy, new_value); // 對map 插入元素
}
}
else {
// 不存在
// 沒有object就建立
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
_class_setInstancesHaveAssociatedObjects(_object_getClass(object));
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &old_entry = j->second;
old_policy = old_entry.policy;
old_value = (id) old_entry.value;
// 從 map中刪除該項
refs->erase(j);
}
}
}
}
// 舊的關聯物件是否存在,如果存在,釋放舊的關聯物件。
// release the old value (outside of the lock).
if (old_value) releaseValue(old_value, old_policy);
}
複製程式碼
_object_set_associative_reference
設定關聯物件的流程參照圖片:
關聯策略
在給一個物件新增關聯物件時有五種關聯策略可供選擇:
關聯策略 | 等價屬性 | 說明 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) or @property (unsafe_unretained) | 弱引用關聯物件 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (strong, nonatomic) | 強引用關聯物件,且為非原子操作 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (copy, nonatomic) | 複製關聯物件,且為非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (strong, atomic) | 強引用關聯物件,且為原子操作 |
OBJC_ASSOCIATION_COPY | @property (copy, atomic) | 複製關聯物件,且為原子操作 |
_object_remove_assocations
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()) {
// 獲取到所有的關聯物件的associations例項
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
delete refs; //刪除ObjectAssociationMap
associations.erase(i);//刪除AssociationsHashMap
}
}
//刪除elements集合中的所有ObjcAssociation元素
for_each(elements.begin(), elements.end(), ReleaseValue());
}
複製程式碼
刪除關聯物件的流程相對就比較簡單了,將獲取到的關聯物件ObjcAssociation的例項放入一個 vector
中,刪除對應的 ObjectAssociationMap
和 AssociationsHashMap
,最後對 vector
中每個 ObjcAssociation
例項做release操作
總結
Category在iOS開發中是比較常見的,用於給現有的類擴充新的方法和屬性。本文從底層分析了Category的原理,以及關聯物件實現,使大家對Category能有一個更深的認識,在以後的開發工作中能更好的使用這一特性。