iOS 底層探索:類擴展 與 關(guān)聯(lián)對象的底層原理分析

iOS 底層探索: 學(xué)習(xí)大綱 OC篇

前言

  • 上一篇講了分類的本質(zhì)和底層原理,提到分類可以關(guān)聯(lián)對象添加屬性,也講到類擴展和分類的區(qū)別,這篇講一下 深入探索一下,類擴展和關(guān)聯(lián)對象的底層實現(xiàn)原理。

  • 復(fù)習(xí):iOS 底層探索:類的加載下(分類)

  • 準(zhǔn)備:調(diào)試代碼下載

一 锅移、 類擴展extension

1.1. 類擴展的創(chuàng)建方式
  • 通過 command+N 新建 -> Objective-C File -> 選擇Extension
  • 直接在類中書寫:永遠在聲明之后,在實現(xiàn)之前(需要在.m文件中書寫)
1.2. 類擴展的本質(zhì)

Clang 生成cpp文件:xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc AppDelegate.m

查看被編譯后的內(nèi)容如下:

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

  • 本想通過源碼調(diào)試的推沸,結(jié)果電腦升級到最新系統(tǒng)版本之后备绽,源碼不過了,尷尬鬓催。肺素。。宇驾。以后補充吧

總結(jié):

  • 類的擴展 在編譯器 會作為類的一部分倍靡,和類一起編譯進來

  • 類的擴展只是聲明,依賴于當(dāng)前的主類课舍,沒有.m文件塌西,可以理解為一個·h文件

二 、關(guān)聯(lián)對象

之前分析分類的時候筝尾,講過分類添加屬性是無效的捡需,但是可以通過runtime的方式動態(tài)添加關(guān)聯(lián)對象。
舉個例子:

#import <objc/runtime.h>
// 本類
@interface HJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation HJPerson
@end

// 分類
@interface HJPerson (EXT)
@property (nonatomic, copy) NSString * ext_name; // 屬性
@end

@implementation HJPerson(EXT)

- (void)setExt_name:(NSString *)ext_name { 

   /*給屬性`ext_name `忿等,動態(tài)添加set方法
      1 .對象
      2. 標(biāo)識符
      3.value
      4.屬性的策略,即nonatomic崔挖、atomic贸街、assign等
   */
    objc_setAssociatedObject(self, "ext_name", ext_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)ext_name { // 給屬性`ext_name `,動態(tài)添加get方法
    return objc_getAssociatedObject(self, "ext_name");
}
@end

其底層原理的實現(xiàn)狸相,主要分為兩部分:
動態(tài)設(shè)置關(guān)聯(lián)屬性: objc_setAssociatedObject(關(guān)聯(lián)對象薛匪,關(guān)聯(lián)屬性key,關(guān)聯(lián)屬性value脓鹃,策略)
動態(tài)讀取關(guān)聯(lián)屬性:objc_getAssociatedObject(關(guān)聯(lián)對象逸尖,關(guān)聯(lián)屬性key)

復(fù)習(xí):

很明顯我們這里看可以看到這個關(guān)聯(lián)屬性是通過這兩個方法寫入讀取的。那么回顧一下,我們正常寫的屬性是如何寫入讀取的呢娇跟?岩齿?

2.0 正常屬性的寫入

通過Clang 查看編譯代碼如下:

static NSString * _I_LGTeacher_ext_name(LGTeacher * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGTeacher$_ext_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

發(fā)現(xiàn)常規(guī)是調(diào)用objc_setProperty完成set方法,我們在源碼中檢查objc_setProperty的實現(xiàn):

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

進入reallySetProperty:


static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) __attribute__((always_inline));

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset); //根據(jù)讀源碼的經(jīng)驗苞俘,我們就可以想到+ offset 就是在做內(nèi)存偏移計算盹沈,通過類地址和偏移值讀到當(dāng)前屬性的地址,然后讀取到值
    
    if (copy) {
        newValue = [newValue copyWithZone:nil]; //copy 方法
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];//mutableCopy 方法
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);//如果等于新值 就返回新值
    }

    if (!atomic) {//  非原子操作直接賦值
        oldValue = *slot;
        *slot = newValue;
    } else {//  原子操作加鎖之后再賦值
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue); //舊值release
}

總結(jié):正常屬性的讀瘸砸ァ:1. 通過地址讀取屬性 -> 2.新值retain -> 3.屬性賦值 -> 4.舊值release

2.1. objc_setAssociatedObject設(shè)值流程
// 進入源碼
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
//繼續(xù)jump
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};
// 繼續(xù)jump
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

進入_object_set_associative_reference ,查看policy的枚舉類型如下乞封,很明顯定義了屬性的類型assigncopy岗憋,retain

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

進入_object_set_associative_reference源碼實現(xiàn)

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{

    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封裝成一個數(shù)組結(jié)構(gòu)類型肃晚,類型為DisguisedPtr
    DisguisedPtr<objc_object> disguised{(objc_object *)object};//相當(dāng)于包裝了一下 對象object,便于使用
    // 包裝一下 policy - value
     /*
      class ObjcAssociation {
            uintptr_t _policy;
            id _value;
    */
    ObjcAssociation association{policy, value};

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

        if (value) {
            //refs_result :從map表中讀取類的buckets (類對:key:為disguised 類標(biāo)志杂穷;value: 為類中關(guān)聯(lián)對象的信息)
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {//判斷第二個存不存在悍缠,即bool值是否為true ,為什么是second 得從associations.try_emplace 方法內(nèi)部查看
                /* it's the first association we make 第一次建立關(guān)聯(lián)*/
                object->setHasAssociatedObjects();//nonpointerIsa 耐量,標(biāo)記位true 即置isa指針的has_assoc屬性為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();//調(diào)用 objc_release 釋放舊值
}

其整體流程如下:

關(guān)鍵點的分析:
2.1.1 setHasAssociatedObjects()設(shè)置關(guān)聯(lián)

inline void
objc_object::setHasAssociatedObjects()
{
    if (isTaggedPointer()) return; 

 retry:
    isa_t oldisa = LoadExclusive(&isa.bits);
    isa_t newisa = oldisa;
    if (!newisa.nonpointer  ||  newisa.has_assoc) {
        ClearExclusive(&isa.bits);
        return;
    }
    newisa.has_assoc = true; // 這里給isa的has_assoc 做了一個標(biāo)記 趴拧,要考的
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}

這里涉及一個面試題: 問關(guān)聯(lián)對象是否需要手動釋放?從這里就可以看出來山叮,在關(guān)聯(lián)對象的時候著榴,isa中記錄了是否有關(guān)聯(lián)對象。通過dealloc 釋放屁倔。

執(zhí)行流程: objc_object::rootDealloc() -> object_dispose()->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(); //這里如果有關(guān)聯(lián)對象

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj); //就在這里釋放啦
        obj->clearDeallocating();
    }
    return obj;
}

2.1.2 AssociationsHashMap

  • 關(guān)聯(lián)屬性的信息會存在一個AssociationsHashMap類型的map中脑又,map中有很多的關(guān)聯(lián)對象key-value鍵值對,其中key為DisguisedPtr<objc_object>锐借,value類型是ObjectAssociationMap问麸,可以理解為一個類對應(yīng)一個:ObjectAssociationMap ;
  • 每個ObjectAssociationMap钞翔,也有很多key-value鍵值對严卖,其中key的類型為const void *,value的類型為ObjcAssociation 布轿,ObjcAssociation是用于包裝policy和value的一個類;
2.2 objc_getAssociatedObject 取值流程
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

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
}

其整體流程如下:

本來想驗證的启具。結(jié)果我電腦系統(tǒng)升級了钮惠。源碼編譯不了规肴。所以只能等以后再驗證了缺猛。

2.3 補充: objc_removeAssociatedObjects移除關(guān)聯(lián)對象
// 移除關(guān)聯(lián)的對象
// 使用objc_removeAssociatedObjects函數(shù)可以移除某個對象身上的所有關(guān)聯(lián)的對象中姜。
void objc_removeAssociatedObjects(id object)

2.4 總結(jié)關(guān)聯(lián)對象:
    1. 關(guān)聯(lián)對象的主要兩個方法
    • set :objc_setAssociatedObject
    • get:objc_getAssociatedObject
    1. 關(guān)聯(lián)對象管理類 AssociationsManager
    1. 關(guān)聯(lián)對象的數(shù)據(jù)結(jié)構(gòu):AssociationsHashMap ,ObjectAssociationMapObjcAssociation
    1. 不管set還是get方法,都是先通過類信息找到相應(yīng)的類對,再從類對的value中跨扮,通過關(guān)聯(lián)屬性key找到對應(yīng)的關(guān)聯(lián)屬性序无,進行相應(yīng)操作。
    1. 關(guān)聯(lián)對象并不是存儲在被關(guān)聯(lián)對象本身內(nèi)存中衡创,而是存儲在全局的統(tǒng)一的一個AssociationsManager中,如果設(shè)置關(guān)聯(lián)對象為nil,就相當(dāng)于是移除關(guān)聯(lián)對象
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末一也,一起剝皮案震驚了整個濱河市巢寡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椰苟,老刑警劉巖抑月,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異舆蝴,居然都是意外死亡谦絮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門洁仗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來层皱,“玉大人,你說我怎么就攤上這事京痢∧谈剩” “怎么了篷店?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵祭椰,是天一觀的道長臭家。 經(jīng)常有香客問我,道長方淤,這世上最難降的妖魔是什么钉赁? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮携茂,結(jié)果婚禮上你踩,老公的妹妹穿的比我還像新娘。我一直安慰自己讳苦,他們只是感情好带膜,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸳谜,像睡著了一般膝藕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咐扭,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天芭挽,我揣著相機與錄音,去河邊找鬼蝗肪。 笑死袜爪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的薛闪。 我是一名探鬼主播辛馆,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逛绵!你這毒婦竟也來了怀各?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤术浪,失蹤者是張志新(化名)和其女友劉穎瓢对,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胰苏,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡硕蛹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硕并。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片法焰。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖倔毙,靈堂內(nèi)的尸體忽然破棺而出埃仪,到底是詐尸還是另有隱情,我是刑警寧澤陕赃,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布卵蛉,位于F島的核電站颁股,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏傻丝。R本人自食惡果不足惜甘有,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望葡缰。 院中可真熱鬧亏掀,春花似錦、人聲如沸泛释。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宇智。三九已至胰丁,卻和暖如春锦庸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背萝嘁。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工牙言, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咱枉,地道東北人蚕断。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓入挣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親葛假。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容