[iOS] 類擴展和關(guān)聯(lián)對象

本文主要是針對類的加載的擴展,探索下分類的底層實現(xiàn)原理。

1. 類擴展和分類介紹

1.1 category 類別、分類
  • 專門用來給類添加新的方法
  • 不能給類添加成員變量
  • 可以利用runtime 實現(xiàn)關(guān)聯(lián)屬性,重寫setter、getter方法
  • 分類中用@property 定義的屬性,只有 setter、getter 方法的聲明,不能生成方法實現(xiàn)和帶下劃線的成員變量
1.2 extension 類擴展
  • 可以說成時特殊的分類,也可稱作匿名分類
  • 可以給類添加成員屬性,但是是私有變量
  • 可以給類添加方法,也是私有方法

2. 類擴展底層探索

類的擴展有兩種創(chuàng)建方式:

  • 直接在類中書寫:永遠(yuǎn)在聲明之后,在實現(xiàn)之前(寫在.m文件中)
  • 通過 command + N 新建 ->Objective-C File -> 選擇Extension
    image.jpeg
2.1 通過clang 底層編譯
  • 寫一個類擴展


    image.jpeg
  • 通過clang -rewrite-objc main.mm -o main.cpp 命令生成 cpp 文件,打開 cpp 文件,搜索 ext_name屬性:

    image.jpeg

  • 查看 LGTeacher 類擴展的方法,在編譯過程中,方法就直接添加到了methodlist中,作為類的一部分,即編譯時期直接添加到本類里面

    image.jpeg

2.2 總結(jié)
  • 類的擴展在編譯期會作為類的一部分,和類一起編譯進(jìn)來
  • 類的擴展只是聲明,依賴于當(dāng)前的主類,沒有.m文件,可以理解為一個.h文件

3. 分類關(guān)聯(lián)對象底層探索

其底層原理的實現(xiàn),主要分為兩個部分:

  • 通過objc_setAssociatedObject設(shè)值流程
  • 通過objc_getAssociatedObject取值流程
3.1 關(guān)聯(lián)對象 - 設(shè)值流程
  • 在分類 LG 中重寫 cate_namesetter、getter方法,通過 runtime的屬性關(guān)聯(lián)方法實現(xiàn)

    image.jpeg

  • 運行程序,斷點端在maincate_name賦值處

    image.jpeg

  • 繼續(xù)往下運行,斷在 setCate_name方法中:

    image.jpeg

其中objc_setAssociatedObject方法有四個參數(shù),分別表示:

  • 參數(shù) 1:要關(guān)聯(lián)的對象,即給誰添加關(guān)聯(lián)屬性

  • 參數(shù) 2:標(biāo)識符,方便取值

  • 參數(shù) 3:value

  • 參數(shù) 4:屬性的策略,即 nonatomic、atomic、assign等,如下所示:

    image.jpeg

  • 進(jìn)入objc_setAssociatedObject源碼實現(xiàn)
    這種設(shè)計模式是接口模式,對外的接口不變,內(nèi)部的邏輯變化不影響外部的調(diào)用,類似于set 方法的底層源碼實現(xiàn):

    image.jpeg

  • 進(jìn)入 SetAssocHook.get()方法實現(xiàn),其中ChainedHookFunction是一個函數(shù)指針

    image.jpeg

  • 進(jìn)入SetAssocHook,其底層實現(xiàn)是_base_objc_setAssociatedObject,類型是ChainedHookFunction

    image.jpeg

  • 所以可以理解為SetAssocHook.get()等價于_base_objc_setAssociatedObject

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);//接口模式,對外接口始終不變
}

??等價于

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _base_objc_setAssociatedObject(object, key, value, policy);//接口模式,對外接口始終不變
}
  • 進(jìn)入_base_objc_setAssociatedObject,通過斷點調(diào)試,確實會來到這里:

    image.jpeg

  • 進(jìn)入_object_set_associative_reference方法
    關(guān)鍵的方法,關(guān)于關(guān)聯(lián)對象底層原理的探索,主要是看value存到了哪里,以及如何取出 value,下面是源碼:

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;

    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));
    //object封裝一層,類型為DisguisedPtr
    DisguisedPtr<objc_object> disguised{(objc_object *)object};//相當(dāng)于包裝了一下 對象object,便于使用
    // 包裝一下 policy - value
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    // 根據(jù)策略類型對value進(jìn)行處理
    association.acquireValue();//根據(jù)策略類型進(jìn)行處理
    //局部作用域空間
    {
        //初始化manager變量,相當(dāng)于自動調(diào)用AssociationsManager的析構(gòu)函數(shù)進(jìn)行初始化
        AssociationsManager manager;//并不是全場唯一,構(gòu)造函數(shù)中加鎖只是為了避免重復(fù)創(chuàng)建,在這里是可以初始化多個AssociationsManager變量的
    
        AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全場唯一

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回的結(jié)果是一個類對
            if (refs_result.second) {//判斷第二個存不存在,即bool值是否為true
                /* it's the first association we make 第一次建立關(guān)聯(lián)*/
                object->setHasAssociatedObjects();//nonpointerIsa ,標(biāo)記位true
            }

            /* establish or replace the association 建立或者替換關(guān)聯(lián)*/
            auto &refs = refs_result.first->second; //得到一個空的桶子,找到引用對象類型,即第一個元素的second值
            auto result = refs.try_emplace(key, std::move(association));//查找當(dāng)前的key是否有association關(guān)聯(lián)對象
            if (!result.second) {//如果結(jié)果不存在
                association.swap(result.first->second);
            }
        } else {//如果傳的是空值,則移除關(guān)聯(lián),相當(dāng)于移除
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

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

通過源碼可以知道,主要分為以下幾部分:
1.創(chuàng)建一個 AssociationsManager 管理類
2.獲取唯一的全局靜態(tài)哈希表:AssociationsHashMap
3.判斷插入的關(guān)聯(lián)值 value 是否存在
. 3.1 存在則繼續(xù)向下走
. 3.2 不存在就走:關(guān)聯(lián)對象-插入空流程,也就是移除關(guān)聯(lián)對象流程
4.通過try_emplace方法,并創(chuàng)建一個空的ObjectAssociationMap去取查詢的鍵值對
5.如果發(fā)現(xiàn)沒有這個key,就插入一個空的BuckeT 進(jìn)去并返回 true
6.通過setHasAssociatedObjects方法標(biāo)記對象存在關(guān)聯(lián)對象即置isa指針的has_assoc屬性為true
7.用當(dāng)前policyvalue 組成了一個ObjcAssociation 替換原來 BucketT 中的空
8.標(biāo)記一下ObjectAssociationMap的第一次為 false

下面進(jìn)行源碼調(diào)試:

  • 定義AssociationsManager類型的變量,相當(dāng)于自動調(diào)用 AssociationsManager的構(gòu)造函數(shù)進(jìn)行初始化
    加鎖 lock,并不代表唯一,只是為了避免多線程重復(fù)創(chuàng)建,其實在外面是可以定義多個AssociationsManager manager;

  • 定義AssociationsHashMap類型的HashMap,這個是唯一的,從哪里可以體現(xiàn)呢?
    通過_mapStorage.get()生成HashMap,其中_mapStorage 是一個靜態(tài)變量,所以永遠(yuǎn)是唯一的

    image.jpeg

可以看到這個 hashMapkey 是前面對于 object的封裝:DisguisedPtr<objc_object>,valueObjectAssociationMap類型。

  • 經(jīng)過調(diào)試,可以查看當(dāng)前的數(shù)據(jù)結(jié)構(gòu):
    p disguised : 其中的 value 是來自 object 還原出來的
    p association
    p manager
    p associations: 目前的associations0x0,表示還沒有查找到相應(yīng)的遞歸查找域中

    image.jpeg

  • 走到局部作用域的if判斷,此時value 是有值的,為 KC

    image.jpeg

  • 如果傳入的value是空值,則會走到else 流程,通過源碼得知,相當(dāng)于解除關(guān)聯(lián)

    image.jpeg

  • 繼續(xù)往下執(zhí)行,查看 refs_result,通過p refs_result,其中的類型很多,可以進(jìn)行拆解查看
    associations 調(diào)用try_emplace方法,傳入一個對象disguised 和一個空的關(guān)聯(lián) map : ObjectAssociationMap{}

    image.jpeg

//pair -- 表示有鍵值對
(std::__1::pair<
 objc::DenseMapIterator<DisguisedPtr<objc_object>,
 
 objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> >,
 
 objc::DenseMapValueInfo<objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
 
 objc::DenseMapInfo<DisguisedPtr<objc_object> >,
 
 objc::detail::DenseMapPair<DisguisedPtr<objc_object>, objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
 
 false>,
 
 bool>)

//可以簡寫為

(std::__1::pair<
 
 objc
 
bool>)
  • 進(jìn)入 try_emplace方法的源碼實現(xiàn)
    有兩個返回值,都是通過 std::make_pair生成相應(yīng)的鍵值對;
    通過LookupBucketFor 方法查找桶子,如果 map中已經(jīng)存在,則直接返回,其中make_pair的第二個參數(shù)bool值為false;
    如果沒有找到,則通過InsertIntoBucket插入map,其中make_pair的第二個參數(shù)bool值為true

std:: make_pair主要作用是將兩個數(shù)據(jù)合成一個數(shù)據(jù),然后通過返回值的.first.second進(jìn)行使用

image.jpeg
  • 進(jìn)入LookupBucketFor 源碼,有兩個同名方法,其中第二個方法屬于重載函數(shù),區(qū)別于第一個的是第二個參數(shù)沒有const修飾,通過調(diào)試可知,外部的調(diào)用是調(diào)用的第二個重載函數(shù),而第二個LookupBucketFor方法,內(nèi)部的實現(xiàn)是調(diào)用第一個LookupBucketFor方法:

    image.jpeg

  • 第一個LookupBucketFor 方法實現(xiàn):

    image.jpeg

  • 斷點運行值try_emplace 方法中,獲取bucket 部分TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);:
    然后p TheBucket

    image.jpeg

其中 TheBucket 的類型與refs_result中屬性的類型是一致的:

image.jpeg

  • 進(jìn)入if(refs_result.second)的流程,通過setHasAssociatedObjectsnonpointerIsahas_assoc標(biāo)記為 true:

    image.jpeg

  • 繼續(xù)往下執(zhí)行,查看 refs
    p refs,執(zhí)行try_emplace前查看
    p refs,執(zhí)行 try_emplace后查看

    image.jpeg

第一次執(zhí)行try_emplace插入的是一個空桶,還沒有值,第二次執(zhí)行 try_emplace才插入值,即往空桶中插入ObjectAssociationMap(value,policy),返回true,可以通過調(diào)試驗證:

image.jpeg

p result.second,返回的true,到此就將屬性和 value 關(guān)聯(lián)上了:

image.jpeg

3.1.1 全部流程

所以,關(guān)聯(lián)對象的設(shè)值流程,有點類似于cache_t中的insert方法插入sel-imp的邏輯,如下圖所示:

image.jpeg

屬性關(guān)聯(lián)涉及的hashMap 結(jié)構(gòu):

image.png

  • AssociationsManager可以有多個,通過AssociationsManagerLock鎖可以得到一個AssociationsHashMap類型的map
    -map中有很多關(guān)聯(lián)對象 map,類型是ObjectAssociationMap,其中 key 為DisguisedPtr<objc_object>
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

  • ObjectAssociationMap哈希表中有很多key-value鍵值對,其中 key 的類型為const void *,這個key也就是我們關(guān)聯(lián)屬性時設(shè)置的字符串,value 的類型為 ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
  • 其中ObjcAssociation是用于包裝policy和value的一個類


    image.jpeg
3.1.2 對象插入空流程

根據(jù)源碼可知,主要是局部作用域中的else流程,其實這個流程可以通俗的理解為當(dāng)傳入的value為nil時,則移除關(guān)聯(lián),主要分為以下幾步:

image.png

  • 根據(jù)DisguisedPtr找到 AssociationsHashMap 中的iterator迭代查詢器
  • 清理迭代器
  • 其實如果插入空值,相當(dāng)于清除
3.2 關(guān)聯(lián)對象-取值流程
  • main中 打印person.cate_name的值,斷點來到分類中重寫的屬性get方法

    image.jpeg

  • 進(jìn)入objc_getAssociatedObject源碼實現(xiàn)

    image.jpeg

  • _object_get_associative_reference源碼如下:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};//創(chuàng)建空的關(guān)聯(lián)對象

    {
        AssociationsManager manager;//創(chuàng)建一個AssociationsManager管理類
        AssociationsHashMap &associations(manager.get());//獲取全局唯一的靜態(tài)哈希map
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即獲取buckets
        if (i != associations.end()) {//如果這個迭代查詢器不是最后一個 獲取
            ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查詢器獲取一個經(jīng)過屬性修飾符修飾的value
            ObjectAssociationMap::iterator j = refs.find(key);//根據(jù)key查找ObjectAssociationMap,即獲取bucket
            if (j != refs.end()) {
                association = j->second;//獲取ObjcAssociation
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();//返回value
}

通過源碼可知,主要分為以下幾部分:
1.創(chuàng)建一個AssociationsManager 管理類
2.獲取唯一的全局靜態(tài)哈希Map:AssociationsHashMap
3.通過find方法根據(jù) DisguisedPtr 找到 AssociationsHashMap中的 iterator 迭代查詢器
4.如果這個迭代查詢器不是最后一個 獲取 :ObjectAssociationMap(policy和value)
5.通過find方法找到ObjectAssociationMap的迭代查詢器獲取一個經(jīng)過屬性修飾符修飾的value
6.返回 value

3.2.1 調(diào)試取值流程
  • 接著上一步調(diào)試,進(jìn)入_object_get_associative_reference源碼實現(xiàn)
    進(jìn)入find方法:根據(jù)關(guān)聯(lián)對象迭代查找AssociationsHashMap,即buckets
    image.jpeg
image.jpeg
  • 再次通過find方法,在buckets中查找與key配對的bucket
    find方法執(zhí)行之前,j的打印,此時的value為nil
    image.jpeg

find方法查詢之后,j的打印,此時的valueKC

image.jpeg

3.3 總結(jié)

所以,綜上所述,所以關(guān)聯(lián)對象的底層調(diào)用流程如下圖所示


image.png

總的來說,關(guān)聯(lián)對象主要就是兩層哈希map的處理,即存取時都是兩層處理。

4. 關(guān)聯(lián)對象的釋放

上面介紹了關(guān)聯(lián)對象的取值流程和設(shè)值流程,那么關(guān)聯(lián)對象是在什么時候釋放的呢?
我們知道對象的銷毀會調(diào)用dealloc方法,先來看下這個方法:

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

其中調(diào)用了_objc_rootDealloc方法,源碼如下:

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

繼續(xù)調(diào)用了obj->rootDealloc(),源碼如下:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

這里發(fā)現(xiàn)了一個關(guān)鍵點:isa.hass_assoc,我們在設(shè)值流程中,就是給 isa.hass_assoc進(jìn)行賦值了,標(biāo)志這個對象有關(guān)聯(lián)的屬性,所以這里會走else 流程,也就是object_dispose((id)this);,這個方法源碼如下:

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

free(obj)是釋放對象,那么關(guān)聯(lián)對象的移除就是在objc_destructInstance(obj);里面了,我們看下objc_destructInstance這個方法,源碼如下:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

這個方法中發(fā)現(xiàn)if (assoc) _object_remove_assocations(obj);是移除關(guān)聯(lián)屬性的方法,我們繼續(xù)探索_object_remove_assocations方法,源碼如下:

void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

可以看到這里是將對象 object 關(guān)聯(lián)的表全部移除了,從全局HashMap 找到object 的迭代器,將對象關(guān)聯(lián)的ObjectAssociationMap全部移除。

調(diào)用鏈為:dealloc -> _objc_rootDealloc -> object_dispose() ->
objc_destructInstance() -> _object_remove_assocations。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容