詳解iOS中的關聯對象

首發於個人我的博客html

從給分類添加屬性提及

詳解iOS中分類Cateogry 一文中,咱們提出一個問題,c++

Category可否添加成員變量?若是能夠,如何給Category添加成員變量?

  • 不能直接給Category添加成員變量,可是能夠間接實現Category有成員變量的效果,用關聯對象技術

那這裏就詳細說明git

添加屬性,實際上都作了什麼

首先咱們要回憶一下,添加屬性,實際上作了三件事github

  • 生成成員變量
  • 生成set方法和get方法的聲明
  • 生成set方法和get方法的實現

eg: 定義一個 YZPerson 類,並定義age屬性數組

#import <Foundation/Foundation.h>

@interface YZPerson : NSObject

@property (assign, nonatomic) int age;


@end

複製代碼

就至關於幹了三件事安全

  • 生成成員變量_age
  • 生成set方法和get方法的聲明
  • 生成set方法和get方法的實現 以下
#import <Foundation/Foundation.h>


@interface YZPerson : NSObject

{
    int _age;
}
- (void)setAge:(int)age;
- (int)age;

@end



#import "YZPerson.h"

@implementation YZPerson
- (void)setAge:(int)age{
    _age = age;
}

- (int)age{
    return _age;
}
@end

複製代碼

那在分類中添加屬性怎麼就不行?

先說結論

  • 生成成員變量_age
  • 不會生成set方法和get方法的聲明
  • 不會生成set方法和get方法的實現

不會生成set方法和get方法的實現

定義一個分類 YZPerson+Ext.h,而後添加屬性weightbash

#import "YZPerson.h"
@interface YZPerson (Ext)
@property (nonatomic ,assign)  int weight;
@end
複製代碼

使用app

YZPerson *person = [[YZPerson alloc] init];
person.weight = 10;
複製代碼

會直接報錯,ide

iOS-關聯對象[1009:10944] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff3550d063 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x00007fff6ac8e06b objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff355961bd -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff354b34b4 ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff354b2e98 _CF_forwarding_prep_0 + 120
	
	6   libdyld.dylib                       0x00007fff6c0183f9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Program ended with exit code: 9
複製代碼

reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10' 可知,分類中添加屬性,沒有生成set方法和get方法的實現函數

會生成set方法和get方法的聲明

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)
- (void)setWeight:(int)weight{
    
}
- (int)weight{
    return 100;
}
@end

複製代碼

而後再調用

YZPerson *person = [[YZPerson alloc] init];
person.age = 25;
person.weight = 10;
NSLog(@"person.age = %d",person.age);
NSLog(@"person.weight = %d",person.weight);
複製代碼

輸出

2019-07-10 08:28:04.406972+0800 iOS-關聯對象[1620:18520] person.age = 25
2019-07-10 08:28:04.407291+0800 iOS-關聯對象[1620:18520] person.weight = 100
複製代碼

進一步證實了,不會生成set方法和get方法的實現,可是會生成set方法和get方法的聲明,由於若是沒有生成set方法和get方法的聲明,這個方法就不能調用。

咱們還能夠這樣:在YZPerson+Ext.h文件中聲明瞭weight,而後再YZPerson+Ext.m中寫實現的時候,會有提示的

更加說明了是有聲明的。

分類中不能直接定義成員變量

#import "YZPerson.h"


@interface YZPerson (Ext)
{
    int _weight; // 報錯 Instance variables may not be placed in categories
}
@property (nonatomic ,assign)  int weight;
@end

複製代碼

會直接報錯Instance variables may not be placed in categories,成員變量不能定義在分類中

源碼角度證實

前面的文章詳解iOS中分類Cateogry 中分析過源碼,objc-runtime-new.h中分類結構體是這樣的

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *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);
};
複製代碼

可知,這個結構體中,沒有數組存放成員變量,只有屬性,協議等。

怎麼來完善屬性

有什麼辦法能夠實如今分類中添加屬性和在類中添加屬性同樣的效果麼?答案是有的

方案一 用全局變量

分類YZPerson+Ext.m中定義全局變量 _weight

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

int _weight;

- (void)setWeight:(int)weight{
    _weight = weight;
}
- (int)weight{
    return _weight;
}
@end

複製代碼

使用時候

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);
複製代碼

輸出爲

iOS-關聯對象[1983:23793] person.weight = 103
複製代碼

看起來確實能夠,而後實際上咱們不能這麼用,由於,全局變量是共享的,假設有兩個 Person,第二個Person修改了weight屬性,而後打印第一個Person.weight

YZPerson *person = [[YZPerson alloc] init];
person.weight = 103;
NSLog(@"person.weight = %d",person.weight);

YZPerson *person2 = [[YZPerson alloc] init];
person2.weight = 10;
NSLog(@"person.weight = %d",person.weight);
複製代碼

輸出爲

iOS-關聯對象[1983:23793] person.weight = 103
iOS-關聯對象[1983:23793] person.weight = 10
複製代碼

可知,修改了Person2.weight 會改變Person.weight的值,由於是全局變量的緣故。因此這種方法不行

方案二 用字典

既然前面方案不能用的緣由是全局變量,共享一份,那咱們是否是隻要保證,一對一的關係,是否是就能夠了呢?

定義 字典weights_ 以對象的地址值做爲key來,weight的值做爲value來存儲和使用

#import "YZPerson+Ext.h"

@implementation YZPerson (Ext)

NSMutableDictionary *weights_;

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 寫在這裏,保證s只初始化一次
        weights_ = [NSMutableDictionary dictionary];
    });
}

- (void)setWeight:(int)weight{
    NSString *key = [NSString stringWithFormat:@"%p",self];//self 地址值做爲key
    weights_[key] = @(weight);//字典中的value不能直接放int,須要包裝成對象
}
- (int)weight{
     NSString *key = [NSString stringWithFormat:@"%p",self];
    return  [weights_[key] intValue];
}

@end
複製代碼

這樣的話,使用起來,就不會由於不一樣對象而干擾了 結果以下

存在的問題

  • 由於是全局的,存在內存泄露問題
  • 線程安全問題,多個線程同時訪問的話,有線程安全問題
  • 代碼太多,若是每次增長一個屬性,都要寫好多代碼。不利於維護

關聯對象方案

關聯對象的使用

下面先簡單說明關聯對象的使用

動態添加

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
複製代碼
  • 參數一:id object : 給哪一個對象添加屬性,這裏要給本身添加屬性,用self
  • 參數二:void * == id key : key值,根據key獲取關聯對象的屬性的值,在objc_getAssociatedObject中經過次key得到屬性的值並返回。
  • 參數三:id value : 關聯的值,也就是set方法傳入的值給屬性去保存。
  • 參數四:objc_AssociationPolicy policy: 策略,屬性以什麼形式保存。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一個弱引用相關聯的對象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相關的對象被複制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相關對象的強引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相關的對象被複制,原子性   
};
複製代碼

整理成表格以下

objc_AssociationPolicy 對應的修飾符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

eg: 咱們在代碼中使用了 OBJC_ASSOCIATION_RETAIN_NONATOMIC 就至關於使用了 nonatomicstrong 修飾符。

注意點 上面列表中,沒有對應weak修飾的策略, 緣由是 object通過DISGUISE函數被轉化爲了disguised_ptr_t類型的disguised_object

disguised_ptr_t disguised_object = DISGUISE(object);
複製代碼

weak修飾的屬性,當沒有擁有對象以後就會被銷燬,而且指針置爲nil,那麼在對象銷燬以後,雖然在map中仍然存在值object對應的AssociationsHashMap,可是由於object地址已經被置爲nil,會形成壞地址訪問而沒法根據object對象的地址轉化爲disguised_object了,這段話能夠再看徹底文以後,再回來體會下。

取值

objc_getAssociatedObject(id object, const void *key);
複製代碼
  • 參數一:id object : 獲取哪一個對象裏面的關聯的屬性。
  • 參數二:void * == id key : 什麼屬性,與objc_setAssociatedObject中的key相對應,即經過key值取出value

移除關聯對象

- (void)removeAssociatedObjects
{
    // 移除關聯對象
    objc_removeAssociatedObjects(self);
}
複製代碼

具體應用

#import "YZPerson.h"

@interface YZPerson (Ext)
@property (nonatomic,strong) NSString *name;
@end


#import "YZPerson+Ext.h"
#import <objc/runtime.h>
@implementation YZPerson (Ext)

const void *YZNameKey = &YZNameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, YZNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
   return objc_getAssociatedObject(self, YZNameKey);
}

- (void)dealloc
{
    objc_removeAssociatedObjects(self);
}

@end
複製代碼

使用的時候,正常使用,就能夠了

YZPerson *person = [[YZPerson alloc] init];
person.name = @"jack";

YZPerson *person2 = [[YZPerson alloc] init];
person2.name = @"rose";
        
NSLog(@"person.name = %@",person.name);
NSLog(@"person2.name = %@",person2.name);
複製代碼

輸出

iOS-關聯對象[4266:52285] person.name = jack
iOS-關聯對象[4266:52285] person2.name = rose
複製代碼

使用起來就是這麼簡單

關聯對象原理

四個核心對象

實現關聯對象技術的核心對象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

源碼解讀

關聯對象的源碼在 Runtime源碼

objc_setAssociatedObject

查看objc-runtime.mm類,首先找到objc_setAssociatedObject函數,看一下其實現

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
複製代碼

_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.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
    	
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            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()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

複製代碼

如圖所示

_object_set_associative_reference函數內部咱們能夠找到咱們上面說過的實現關聯對象技術的四個核心對象。接下來咱們來一個一個看其內部實現原理探尋他們之間的關係。

AssociationsManager

查看 AssociationsManager 咱們知道AssociationsManager 內部有static AssociationsHashMap *_map;

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};
複製代碼

AssociationsHashMap

接下來看 AssociationsHashMap

上圖中 AssociationsHashMap的源碼咱們發現AssociationsHashMap繼承自unordered_map首先來看一下unordered_map內的源碼

unordered_map源碼中咱們能夠看出 參數 _Key_Tp 對應着map中的KeyValue,那麼對照上面AssociationsHashMap的源碼,能夠發現_Key中傳入的是unordered_map<disguised_ptr_t_Tp中傳入的值則爲ObjectAssociationMap *

而後 咱們查看ObjectAssociationMap的源碼,上圖中ObjectAssociationMap已經標記出,咱們能夠知道ObjectAssociationMap中一樣以keyValue的方式存儲着ObjcAssociation

ObjcAssociation

接着咱們來到ObjcAssociation中,能夠看到

class ObjcAssociation {
        uintptr_t _policy; // 策略
        id _value; // 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存儲着_policy_value,而這兩個值咱們能夠發現正是咱們調用objc_setAssociatedObject函數傳入的值,換句話說咱們在調用objc_setAssociatedObject函數中傳入valuepolicy這兩個值最終是存儲在ObjcAssociation中的。

如今咱們已經對四個核心對象AssociationsManagerAssociationsHashMapObjectAssociationMapObjcAssociation之間的關係有了初步的瞭解,那麼接下繼續仔細閱讀源碼,看一下objc_setAssociatedObject函數中傳入的四個參數分別放在哪一個對象中充當什麼做用

細讀 _object_set_associative_reference

_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.
    ObjcAssociation old_association(0, nil);
    // 根據value的值經過acquireValue函數獲取獲得new_value
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        // 獲取 manager 內的 AssociationsHashMap 也就是 associations
        AssociationsHashMap &associations(manager.associations());
        // object 通過 DISGUISE 函數被轉化爲了disguised_ptr_t類型的disguised_object
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // policy和new_value 做爲鍵值對存入了ObjcAssociation
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // policy和new_value 做爲鍵值對存入了ObjcAssociation
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // 來到這裏說明,value爲空
            // setting the association to nil breaks the association.
            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()) {
                    old_association = j->second;
                    //移除關聯對象
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製代碼

**acquireValue**內部實現 經過對策略的判斷返回不一樣的值

static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}
複製代碼
  • 首先根據咱們傳入的value通過acquireValue函數處理返回了new_valueacquireValue函數內部實際上是經過對策略的判斷返回不一樣的值
typedef uintptr_t disguised_ptr_t;
    inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
    inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
複製代碼
  • 以後建立AssociationsManager manager,獲得manager內部的AssociationsHashMapassociations。 以後咱們看到了咱們傳入的第一個參數object通過DISGUISE函數被轉化爲了disguised_ptr_t類型的disguised_object
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
複製代碼
  • 以後被處理成new_valuevalue,和policy一塊兒被存入了ObjcAssociation中。 而ObjcAssociation對應咱們傳入的key被存入了ObjectAssociationMap中。 disguised_objectObjectAssociationMap則以key-value的形式對應存儲在associations中也就是AssociationsHashMap中。

value爲空

若是傳入的value爲空,那麼就刪除這個關聯對象

// 來到這裏說明,value爲空
            // setting the association to nil breaks the association.
            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()) {
                    old_association = j->second;
                    //移除關聯對象
                    refs->erase(j);
                }
            }
複製代碼

本文參考資料:

表格總結

用表格總結來展現這幾個核心類的關係以下

小結

  • 關聯對象並不存儲在被關聯對象自己內存中,而是有一個全局統一的 AssociationsManager
  • 一個實例對象就對應一個ObjectAssociationMap
  • ObjectAssociationMap中存儲着多個此實例對象的關聯對象的key以及ObjcAssociation
  • ObjcAssociation中存儲着關聯對象的valuepolicy策略

objc_getAssociatedObject

objc_getAssociatedObject內部調用的是_object_get_associative_reference

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
複製代碼

_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);
        // 查找 disguised_object
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            //查看key 和value
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // 存在key 和value 就取出對應的值
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        // 不存在key value 就把這個關聯對象擦除
        objc_autorelease(value);
    }
    return value;
}
複製代碼

關鍵代碼已經在上文中給了註釋

objc_removeAssociatedObjects函數

objc_removeAssociatedObjects函數用來刪除全部關聯對象,內部調用了_object_remove_assocations

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
複製代碼

_object_remove_assocations

再來看看_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()) { // 遍歷AssociationsHashMap 取出值
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            // 刪除
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}
複製代碼

代碼中能夠看出,接受一個object對象,而後遍歷刪除該對象全部的關聯對象

總結

用表格總結來展現這幾個核心類的關係以下

  • 關聯對象並不存儲在被關聯對象自己內存中,而是有一個全局統一的 AssociationsManager
  • 一個實例對象就對應一個ObjectAssociationMap
  • ObjectAssociationMap中存儲着多個此實例對象的關聯對象的key以及ObjcAssociation
  • ObjcAssociation中存儲着關聯對象的valuepolicy策略
  • 刪除的時候接收一個object對象,而後遍歷刪除該對象全部的關聯對象
  • 設置關聯對象_object_set_associative_reference的是時候,若是傳入的value爲空就刪除這個關聯對象

本文參考資料:

本文相關代碼github地址 github

Runtime源碼

iOS底層原理

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索