iOS八股文(十)分類和關聯物件原始碼解析

語言: CN / TW / HK

highlight: xcode theme: orange


我們平時在開發的時候經常會使用分類來新增方法、協議、屬性,但在新增屬性的時候屬性是不會自動生成成員變數的,這時候我們就需要關聯物件來動態儲存屬性值。

```c++ @interface NSObject (Study) @property (nonatomic, strong) NSObject obj1; @property (nonatomic, strong) NSObject obj2; - (void)instanceMethod; + (void)classMethod; @end

static const void *NSObjectObj1Name = "NSOBJECT_OBJ1";

@implementation NSObject (Study) @dynamic obj2;

  • (void)setObj1:(NSObject *)obj1 { objc_setAssociatedObject(self, &NSObjectObj1Name, obj1, OBJC_ASSOCIATION_RETAIN); }

  • (NSObject *)obj1 { return objc_getAssociatedObject(self, &NSObjectObj1Name); }

  • (void)instanceMethod { NSLog(@"-類名:%@,方法名:%s,行數:%d",NSStringFromClass(self.class),func,LINE); }

  • (void)classMethod { NSLog(@"+類名:%@,方法名:%s,行數:%d",NSStringFromClass(self.class),func,LINE); }

@end `` 這段程式碼包括Object-C的兩個知識點,分別是分類關聯物件`,本文主要圍繞這兩個知識點來進行探究。

分類

可以使用clang重寫將上面的程式碼成c++程式碼。

image.png

重寫後的關鍵程式碼:

c++ static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) = { "NSObject", 0, // &OBJC_CLASS_$_NSObject, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Study, (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_Study, 0, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_Study, }; 可以看到其根本的實現是_category_t這個結構,那麼我們可以藉助objc4(828.2)原始碼來查詢關於category_t的定義:

```cpp struct category_t { const char name; classref_t cls; WrappedPtr instanceMethods; WrappedPtr 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);

protocol_list_t *protocolsForMeta(bool isMeta) {
    if (isMeta) return nullptr;
    else return protocols;
}

}; ``` 根據原始碼的定義,我們可以總結: - 分類裡面即有例項方法列表又有類方法列表 - 分類沒有成員變數列表

分類的載入

分類的載入是在objc中實現的。 在原始碼attachCategories的實現中:

``` c++ // 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, const locstamped_category_t cats_list, uint32_t cats_count, int flags) { //。。。縮簡// bool fromBundle = NO; bool isMeta = (flags & ATTACH_METACLASS); //新建rwe auto rwe = cls->data()->extAllocIfNeeded(); //debug程式碼可以放這裡 //遍歷每個分類 for (uint32_t i = 0; i < cats_count; i++) { auto& entry = cats_list[i]; //獲取分類裡面的方法 method_list_t mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { if (mcount == ATTACH_BUFSIZ) { prepareMethodLists(cls, mlists, mcount, NO, fromBundle, func); rwe->methods.attachLists(mlists, mcount); mcount = 0; } mlists[ATTACH_BUFSIZ - ++mcount] = mlist; fromBundle |= entry.hi->isBundle(); }

   //。。縮簡了協議和屬性的內容

if (mcount > 0) {
    prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                       NO, fromBundle, __func__);
    //新增分類的方法到rwe中
    rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
    if (flags & ATTACH_EXISTING) {
        flushCaches(cls, __func__, [](Class c){
            // constant caches have been dealt with in prepareMethodLists
            // if the class still is constant here, it's fine to keep
            return !c->cache.isConstantOptimizedCache();
        });
    }
}

rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);

} `` 可以在這段程式碼裡面加上類名判斷,然後打上斷點。使用lldb除錯`檢視新增方法的過程。objc原始碼除錯可參考這裡

c++ const char *mangledName = cls->nonlazyMangledName(); //你新增分類的類名 const char *className = "OSObject"; if (strcmp(mangledName, className) == 0 && !isMeta) { printf("debug find it"); } 注意:這裡面,分類和本類都需要實現+load方法才可以。 我們先看斷點的堆疊資訊

image.png 可以看到是load_images中呼叫的。前面的文章已經講解過load_images的呼叫時機。 這裡可以再複習一遍。

接下來我們通過lldb來除錯。在使用lldb的時候,要多看原始碼,在原始碼中尋找可以使用的方法。如果是地址就用*來去處地址裡面的內容。如果是內容看原始碼中的定義,使用其方法獲取有效資訊。

image.png

image.png 這裡需要注意⚠️,使用了2種獲取列表數量的方式,其中一種是不準確的,但是在載入完分類的時候,不準確的方式就正確了,暫時沒找到原因。我的本類中有一個例項方法,分類裡面也有一個例項方法,在沒有載入分類的時候,我的方法列表裡面的數量是1(第2中方式檢視得到)。我們繼續過斷點,再設定完分類後,我們同樣方式再來看效果:

image.png 可以看到載入完分類之後,方法列表的數量是2。

糾正

這裡的lldb指令有些過於複雜,在我們獲取到method_array_t的時候,該結構體有一個count()方法的,該方法可直接獲取數量。

使用count()方法的除錯如下:

image.png 在沒有載入分類的時候count為1,在看看載入完分類之後的:

image.png 可以看到count為2,得到了和上面同樣的結論。

關聯物件

回到我們一開始的程式碼,還有一個關聯物件。我們先在objc原始碼中找到關聯物件api的實現部分:

```c++ void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {     _object_set_associative_reference(object, key, value, policy); }

void objc_removeAssociatedObjects(id object)  {     if (object && object->hasAssociatedObjects()) {         _object_remove_assocations(object, /deallocating/false);     } } `` 可以看到是呼叫了內部函式_object_set_associative_reference`,解析註解如下:

```c++ void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) { // This code used to work when nil was passed for object and key. Some code // probably relies on that to not crash. Check and handle it explicitly. // rdar://problem/44094390 if (!object && !value) return; //isa有一位資訊為禁止關聯物件,如果設定了,直接報錯 if (object->getIsa()->forbidsAssociatedObjects()) _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

//包裝物件,轉換型別
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//包裝值和屬性資訊
ObjcAssociation association{policy, value};

// retain the new value (if any) outside the lock.
//設定屬性資訊
association.acquireValue();

bool isFirstAssociation = false;
{
    //呼叫建構函式,建構函式內加鎖操作
    AssociationsManager manager;
    //獲取全域性的HasMap
    AssociationsHashMap &associations(manager.get());
    //如果值不為空
    if (value) {
        //去關聯物件表中找物件對應的二級表,如果沒有內部會重新生成一個
        auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
        //如果沒有找到
        if (refs_result.second) {
            /* it's the first association we make */
            //說明是第一次設定關聯物件,把是否關聯物件設定為YES
            isFirstAssociation = true;
        }

        /* establish or replace the association */
        auto &refs = refs_result.first->second;
        //在二級表中找key對應的內容,
        auto result = refs.try_emplace(key, std::move(association));
        //如果已經有內容了,沒有內容上面根據association已經插入了值,所以啥也不用幹
        if (!result.second) {
            //替換掉
            association.swap(result.first->second);
        }
     //如果value為空
    } else {
        //通過object找對應的二級表
        auto refs_it = associations.find(disguised);
        // 如果有
        if (refs_it != associations.end()) {
            auto &refs = refs_it->second;
            //通過key再在二級表裡面找對應的內容
            auto it = refs.find(key);
            //如果有
            if (it != refs.end()) {
                //刪除掉
                association.swap(it->second);
                refs.erase(it);
                if (refs.size() == 0) {
                    associations.erase(refs_it);

                }
            }
        }
    }
}

// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
    object->setHasAssociatedObjects();

// release the old value (outside of the lock).
association.releaseHeldValue();

} `` 其中需要注意try_emplace`這個方法。

```c++ // Inserts key,value pair into the map if the key isn't already in the map. // The value is constructed in-place if the key is not in the map, otherwise // it is not moved. template std::pair try_emplace(KeyT &&Key, Ts &&... Args) { BucketT *TheBucket; //如果已經存在了 if (LookupBucketFor(Key, TheBucket)) return std::make_pair( makeIterator(TheBucket, getBucketsEnd(), true), false); // Already in map.

// Otherwise, insert the new element.
//不存在就插入一個新的物件
TheBucket =
    InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
return std::make_pair(
         makeIterator(TheBucket, getBucketsEnd(), true),
         true);

} ``` 這裡返回的是一個迭代器,如果有內容返回對應的迭代器,如果沒有的話,新增一個,並返回迭代器。

可以看到使用了兩次try_emplace方法,可以得知他是巢狀兩層的HashMap結構,根據上面程式碼的理解,可以得到以下結構圖:

image.png 下面我們在看看get_associtiond的原始碼:

```c++ id _object_get_associative_reference(id object, const void *key) { ObjcAssociation association{};

{   //加鎖
    AssociationsManager manager;
    //全域性的表
    AssociationsHashMap &associations(manager.get());
    //通過object找對應的二級表
    AssociationsHashMap::iterator i = associations.find((objc_object *)object);
    if (i != associations.end()) {
        ObjectAssociationMap &refs = i->second;
        //在二級表內通過key在找對應的值
        ObjectAssociationMap::iterator j = refs.find(key);
        if (j != refs.end()) {
            association = j->second;
            association.retainReturnedValue();
        }
    }
}
//取值並返回然後放到自動釋放池中
return association.autoreleaseReturnedValue();

} `` 除了getset方法,在物件被銷燬的時候還會呼叫remove`方法,從全域性的關聯物件表中把物件對應的關聯表刪除。後面在整理物件銷燬流程的時候會涉及。

參考連結

以上便是我們經常在分類裡面編碼對應原始碼的解析。希望各位看官有什麼不同見解可以一起溝通學習。

到這裡,已經通過十篇文章對Object-C的動態性,從多角度進行了分析。雖然起名為八股文系列,但個人認為除了面試裝X之外,認證探究後,對底層也有更深一步的理解,能為以後編碼提供一定的幫助,尤其是通過對原始碼的分析,可以效仿優秀程式碼的邏輯構思和編碼風格。這些部落格的目的,首先是希望自己能通過寫部落格,增強記憶,在以後面試複習的時候有一份不錯的複習資料,其二是記錄學習探究過程,能對外傳播自己微薄的能量,能和優秀的從業者溝通交流,從而進一步提升自己。在寫部落格的過程中檢視借鑑了很多優秀的部落格,大多已經註明了參考連結,也有部分遺漏的,如有雷同,可聯絡作者,作者會第一時間補全出處。寫部落格不易,轉載註明出處。