從原始碼解讀Category實現原理

西木中堂發表於2018-03-06

什麼是category?

category是 Objective-C 2.0 之後新增的語言特性,主要作用是為已經存在的類新增方法。除此之外,Apple 還推薦了category 的另外兩個使用場景。

  1. 可以把類的實現分開在幾個不同的檔案裡面。這樣做有幾個顯而易見的好處
    • 可以減少單個檔案的體積
    • 可以把不同的功能組織到不同的 category 裡
    • 可以由多個開發者共同完成一個類
    • 可以按需載入想要的 category 等等
  2. 宣告私有方法

不過除了apple推薦的使用場景,還衍生出了 category 的其他幾個使用場景:

  • 模擬多繼承
  • 把framework的私有方法公開

category特點

  • category 只能給某個已有的類擴充方法,不能擴充成員變數
  • category 中也可以新增屬性,只不過 @property 只會生成 settergetter 的宣告,不會生成 settergetter 的實現以及成員變數
  • 如果 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 的屬性不能生成成員變數和 gettersetter 方法的實現,我們要自己實現 gettersetter 方法,需藉助關聯物件來實現

關聯物件來實現提供三個介面 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_tObjectAssociationMap 的對映

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

用一張圖解釋他們的關係就是:

從原始碼解讀Category實現原理

從上圖我們不難看出 _object_get_associative_reference 獲取關聯物件的步驟是:

  1. AssociationsHashMap &associations(manager.associations()) 獲取 AssociationsHashMap 的單例物件 associations
  2. disguised_ptr_t disguised_object = DISGUISE(object) 獲取物件的地址
  3. 通過物件的地址在 associations 中獲取 AssociationsHashMap迭代器
  4. 通過 key獲取到 ObjectAssociationMap的迭代器
  5. 最後得出關聯物件類 ObjcAssociation 的例項 entry,再獲取到 valuepolicy 的值
_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設定關聯物件的流程參照圖片:

從原始碼解讀Category實現原理

關聯策略

在給一個物件新增關聯物件時有五種關聯策略可供選擇:

關聯策略 等價屬性 說明
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中,刪除對應的 ObjectAssociationMapAssociationsHashMap,最後對 vector 中每個 ObjcAssociation 例項做release操作

總結

Category在iOS開發中是比較常見的,用於給現有的類擴充新的方法和屬性。本文從底層分析了Category的原理,以及關聯物件實現,使大家對Category能有一個更深的認識,在以後的開發工作中能更好的使用這一特性。

相關文章