OC運行時機制Runtime(三):關聯對象Associated Object和分類Category

Runtime最全總結

本系列詳細講解Runtime知識點,由于運行時的內容較多,所以將內容拆分成以下幾個方面屋剑,可以自行選擇想要查看的部分

Category

分類是我們開發(fā)過程中必不可少的一個重要技術手段然痊,包括動態(tài)添加方法,更換原有方法等屉符,那么首先常規(guī)套路分析一下Category的結構剧浸。

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}  

這里看到結構體里分別有分類名,類名矗钟,實例方法列表唆香,類方法列表,協議列表這幾項吨艇,所以我們通炒恚可以在分類中動態(tài)添加方法而不能添加實例變量,當然我們也可以更詳細的分析一下秸应,方法列表和實例變量列表的區(qū)別虑凛。

struct objc_class {
    Class isa;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;


struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

這里看到,objc_ivar_list結構體里面有ivar_count以及一個變長結構體ivar_list软啼,這個結構體里面存儲了ivar的各種屬性桑谍,包括name、type等祸挪,同樣objc_method_list里面也存儲了method_count變長結構體method_list锣披,方法結構體包括了方法名和方法實現,這里SEL類型表示的是選擇子的名字,IMP類型表示的方法的具體實現雹仿,這里有一個問題*ivars和**methodLists分別是一級指針和二級指針增热,二者的區(qū)別是使用一級指針做參數傳遞時,如果函數改變傳入參數的值胧辽,原參數指針指向的值不會改變峻仇,而使用二級指針做參數傳遞時,原參數指針指向的值是可以改變的邑商,所以當使用添加方法時摄咆,*methodList值可以改變,所以可以添加方法人断。那如果添加屬性是否可以呢吭从,我們寫一個分類嘗試一下

//UIImage+Detection.h
@interface UIImage (Detection)
@property (nonatomic, copy) NSString * remarkName;
@end


//ViewController.m
- (void)viewDidLoad {
    UIImage * image = [UIImage imageNamed:@"image_name"];
    image.imageName = @"image_name";
    NSLog(@"%@", image.imageName);
}

可以看到控制臺報出-[UIImage setImageName:]: unrecognized selector sent to instance 0x6000000b1040找不到setter的錯,是因為Category沒有給屬性自動添加setter和getter方法恶迈,但是我們如果使用_remarkName = remarkName;這種方式重寫setter方法涩金,會因為Category不能添加變量而報錯,所以這里引出一個概念Associated Object暇仲。

Associated Object——關聯對象

當我們給一個系統(tǒng)類添加方法步做,我們常用的是使用類別來進行擴展,但是如果我們想添加一個系統(tǒng)類的屬性熔吗,我們通常是使用繼承的方式辆床,但是只是添加一個屬性就使用繼承有些小題大做,這里我們可以使用Associated Object桅狠,下面繼續(xù)之前的例子讼载,將setter和getter用關聯對象的方式實現,首先請出兩個當事人中跌,哦不當事函數咨堤。

//為一個實例對象添加一個關聯對象,用鍵來區(qū)分漩符,由于是C函數只能使用C字符串一喘,這個key就是關聯對象的名稱,value為具體的關聯對象的值嗜暴,policy為存儲策略凸克,用以維護相應的內存管理語義
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通過key和實例對象獲取關聯對象的值
id objc_getAssociatedObject(id object, const void *key);
//刪除實例對象的關聯對象
void objc_removeAssociatedObjects(id object);

由于都是c函數,所以oc的內存管理語義在這里也受到了影響闷沥,需要做相應的改變萎战,詳細的objc_AssociationPolicy如下

/**
 * 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. */
};

這里語義內容很清晰了,就不多做解釋了舆逃,那針對上面的案例蚂维,如何用關聯對象完成setter和getter

- (void)setImageName:(NSString *)imageName {
    objc_setAssociatedObject(self, @selector(imageName), imageName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)imageName {
    return objc_getAssociatedObject(self, _cmd);
}

這里的key戳粒,可以使用c字符串等形式,我這里放入表示方法名的SEL類型也可以虫啥,_cmd 關鍵字表示當前方法選擇子蔚约,也就是@selector(imageName),當然這里也可以用靜態(tài)指針static void *但是為了保證key的唯一性涂籽,我們還是用當前的方法苹祟,可以看到成功打印出結果image_name,下面對個函數進行詳細分析又活。

objc_setAssociatedObject

我們在ojbc-runtime.m文件中找到以下代碼片段

#if SUPPORT_GC
PRIVATE_EXTERN void objc_setAssociatedObject_gc(id object, const void *key, id value, objc_AssociationPolicy policy) {
    if ((policy & OBJC_ASSOCIATION_COPY_NONATOMIC) == OBJC_ASSOCIATION_COPY_NONATOMIC) {
        value = objc_msgSend(value, SEL_copy);
    }
    auto_zone_set_associative_ref(gc_zone, object, (void *)key, value);
}
#endif

PRIVATE_EXTERN void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
#if SUPPORT_GC
    if (UseGC) {
        if ((policy & OBJC_ASSOCIATION_COPY_NONATOMIC) == OBJC_ASSOCIATION_COPY_NONATOMIC) {
            value = objc_msgSend(value, SEL_copy);
        }
        auto_zone_set_associative_ref(gc_zone, object, (void *)key, value);
    } else 
#endif
    {
        // Note, creates a retained reference in non-GC.
        _object_set_associative_reference(object, (void *)key, value, policy);
    }
}

在這里我們確定方法調用棧的內部方法為_objc_set_associative_reference這個方法苔咪,那么我們跳轉到這個方法內部看其具體實現锰悼。

PRIVATE_EXTERN 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;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(object);
            if (i != associations.end()) {
                // secondary table exists
                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 = old_entry.value;
                    old_entry.policy = policy;
                    old_entry.value = new_value;
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 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;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_value) releaseValue(old_value, old_policy);
}

由于代碼量較多柳骄,我們忽略大部分的邏輯部分,看到產生作用的有如下幾個類:AssociationsManager,AssociationsHashMap,ObjectAssociationMap,ObjcAssociation

下面一個個看這幾個類的作用

AssociationsManager關聯對象管理類
class AssociationsManager {
    static OSSpinLock _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { OSSpinLockLock(&_lock); }
    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new(::_malloc_internal(sizeof(AssociationsHashMap))) AssociationsHashMap();
        return *_map;
    }
};

OSSpinLock AssociationsManager::_lock = OS_SPINLOCK_INIT;
AssociationsHashMap *AssociationsManager::_map = NULL;

這里實現了OSSpinLockAssociationsHashMap這兩個單例箕般,在構造這個方法的時候耐薯,會調用OSSpinLockLock,而在析構的時候會調用OSSpinLockUnlock丝里,associations方法可以取得一個AssociationsHashMap單例曲初,很明顯這個管理類通過持有一個自旋鎖保證了操作AssociationsHashMap是線程安全的,所以每次只有一個線程可以對AssociationsHashMap進行操作杯聚。

ObjcAssociation關聯對象實際存儲的方式

ObjectAssociation臼婆,ObjectAssociationMap結構體名定義來看,這兩個分別是關聯對象結構體和關聯對象結構體映射表幌绍,下面上源碼

struct ObjcAssociation {
        uintptr_t policy;
        id value;
        ObjcAssociation(uintptr_t newPolicy, id newValue) : policy(newPolicy), value(newValue) { }
        ObjcAssociation() : policy(0), value(0) { }
    };

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjcAllocator<std::pair<void * const, ObjcAssociation> > > {
    public:
        void *operator new(size_t n) { return ::_malloc_internal(n); }
        void operator delete(void *ptr) { ::_free_internal(ptr); }
    };
    typedef hash_map<void *, ObjectAssociationMap *, ObjcPointerHash, ObjcPointerEqual, ObjcAllocator<void *> > AssociationsHashMap;

這里ObjcAssociation關聯對象結構體存儲了policy存儲策略value關聯對象值這兩個重要屬性颁褂,ObjcAssociationMap這里關聯了keyObjcAssociation的映射,這個類存儲了所有這個對象所關聯對象的信息傀广。
拿出我們的經典demo案例颁独,

- (void)setImageName:(NSString *)imageName {
    objc_setAssociatedObject(self, @selector(imageName), imageName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

用圖來表示一下這段代碼的具體實現


關聯對象的底層實現

下面接著分析_objc_set_associative_reference這個方法,根據new_value出現了第一次邏輯判斷

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;

用個demo看一下這個new_value的作用

UIImage * image = [UIImage imageNamed:@"image_name"];
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
    
objc_setAssociatedObject(image, @selector(imageName), @"image_name", OBJC_ASSOCIATION_COPY_NONATOMIC);
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
    
objc_setAssociatedObject(image, @selector(imageName), nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
NSLog(@"%@", objc_getAssociatedObject(image, @selector(imageName)));
2019-03-19 18:21:08.667240+0800 Demo[5301:197907] (null)
2019-03-19 18:21:08.667557+0800 Demo[5301:197907] image_name
2019-03-19 18:21:08.667789+0800 Demo[5301:197907] (null)

這里看得出伪冰,如果我們在value字段設置為nil誓酒,相當于清除了這個關聯對象的key,所以我們初步可以認為贮聂,如果new_value為真靠柑,那么邏輯中應該是創(chuàng)建一個關聯對象或者修改一個關聯對象的值,如果new_value為假吓懈,那么應該是清除這個關聯對象歼冰。為了驗證,我們找到邏輯為假這部分代碼骄瓣。

if (j != refs->end()) {
    ObjcAssociation &old_entry = j->second;
    old_policy = old_entry.policy;
    old_value = (id) old_entry.value;
    refs->erase(j);
}

這里明顯看到調用了erase函數擦出了這個關聯對象的key停巷。

所以_objc_set_associative_reference方法流程如下
1.從AssociationsManager單例中耍攘,在線程安全的狀態(tài)下,取得全局關聯對象哈希表AssociationsHashMap
2.根據關聯對象所屬類畔勤,從AssociationsHashMap取得這個類的關聯對象哈希表ObjectAssociationMap蕾各,如果ObjectAssociationMap這個表不存在則創(chuàng)建一個新的表。
3.根據void * key庆揪,從ObjectAssociationMap中查找到ObjcAssociation結構體式曲,如果沒有這個結構體則新創(chuàng)建這個結構體。
4.如果new_value為空缸榛,ObjectAssociationMap會調用erase函數擦除這個key吝羞。
4.ObjcAssociation結構體中應存有policy存儲策略value值

以上就是objc_setAssociatedObject方法的實現流程内颗,另外兩個方法原理雷同這里不加以多贅述钧排。

總結

Category結構體中不可以添加實例變量可以添加方法均澳,添加屬性時不會自動生成setter和getter方法恨溜,需要我們手動實現,由于不能添加實例變量所以實現這兩個方法需要用到關聯對象找前,它的實質是ObjcAssociation這個結構體糟袁,里面主要存儲了存儲策略和具體值,這個結構體存在于ObjectAssociationMap哈希表中躺盛,這個表存儲了每個對象具體的關聯對象项戴,鍵為void *類型的一段字符串,這個哈希表存在于AssociationsHashMap這個表中槽惫,這個表實際根據對象的不同為key存儲了全部關聯對象周叮,這個表被AssociationsManager這個單例所持有,每次調用方法時會以單例的形式創(chuàng)建躯枢,它是線程安全的则吟。

后續(xù)

到這里已經將最重要的三個部分分析好了,分別是運行時結構和消息機制以及關聯對象锄蹂,感興趣的朋友們可以移步下一篇文章 OC運行時機制Runtime(四):嘗試使用黑魔法 Method Swizzling氓仲,如果覺得本文對您有些作用,請在下方點個贊再走哈~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末得糜,一起剝皮案震驚了整個濱河市敬扛,隨后出現的幾起案子,更是在濱河造成了極大的恐慌朝抖,老刑警劉巖啥箭,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異治宣,居然都是意外死亡急侥,警方通過查閱死者的電腦和手機砌滞,發(fā)現死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坏怪,“玉大人贝润,你說我怎么就攤上這事÷料” “怎么了打掘?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鹏秋。 經常有香客問我尊蚁,道長,這世上最難降的妖魔是什么侣夷? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任横朋,我火速辦了婚禮,結果婚禮上惜纸,老公的妹妹穿的比我還像新娘叶撒。我一直安慰自己绝骚,他們只是感情好耐版,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著压汪,像睡著了一般粪牲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上止剖,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天腺阳,我揣著相機與錄音,去河邊找鬼穿香。 笑死亭引,一個胖子當著我的面吹牛,可吹牛的內容都是我干的皮获。 我是一名探鬼主播焙蚓,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼洒宝!你這毒婦竟也來了购公?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤雁歌,失蹤者是張志新(化名)和其女友劉穎宏浩,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體靠瞎,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡比庄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年求妹,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佳窑。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡扒最,死狀恐怖,靈堂內的尸體忽然破棺而出华嘹,到底是詐尸還是另有隱情吧趣,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布耙厚,位于F島的核電站强挫,受9級特大地震影響,放射性物質發(fā)生泄漏薛躬。R本人自食惡果不足惜俯渤,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望型宝。 院中可真熱鬧八匠,春花似錦、人聲如沸趴酣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岖寞。三九已至抡四,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仗谆,已是汗流浹背指巡。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留隶垮,地道東北人藻雪。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像狸吞,于是被迫代替她去往敵國和親勉耀。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355