Category的本質<三>關聯對象

Category的本質<一>
Category的本質<二>load,initialize方法
面試題:Category能否添加成員變量?如果可以,如何給Category添加成員變量?
我們首先創建一個類Person類繼承自NSObject,給這個類聲明一個屬性name:

@property (nonatomic, strong)NSString *name;

我們聲明了這句話之后,實際是做了三件事:

  • 1.聲明了一個成員變量_name。
NSString *_name;
  • 2.聲明了set和get方法:
- (void)setName:(NSString *)name;
- (NSString *)name;
  • 3.在.m文件中實現set和get方法:
- (void)setName:(NSString *)name{
    
    _name = name;
}

- (NSString *)name{
    
    return _name;
}

以上是給一個類添加屬性。下面給一個分類添加屬性:
我們創建一個Person類的分類Test1,然后給這個分類添加一個height屬性:

@property (nonatomic, assign)int height;

這樣只會申明set和get方法,而不會申明成員變量和實現set,get方法:

- (void)setHeight:(int)height;
- (int)height;

既然系統沒有幫我們聲明成員變量和實現set和get方法,那么我們能不能自己去聲明一下呢?我們嘗試一下:

F971DF04-F0BD-4B7C-8108-376F0DECDF3C.png

出現了報錯Instance variables may not be placed in categories,意思就是成員變量不能聲明在分類中。所以我們得出結論,分類中不能添加成員變量。

我們從分類的結構的角度來考慮一下分類中為什么不能添加成員變量:
image.png

通過分類的底層結構我們可以看到,分類中可以存放實例方法,類方法,協議,屬性,但是沒有存放成員變量的地方。
既然分類中不能添加成員變量,那么我們給分類添加屬性時,它的功能不是完整的,比如說我們分別給Person類的name和height這兩個屬性賦值,然后打印讀出這兩個屬性:

Person *person = [[Person alloc] init];
person.name = @"dongdong";
person.height = 180;
    
NSLog(@"name: %@, height : %d", person.name, person.height);

程序崩潰了,崩潰原因是:-[Person setHeight:]: unrecognized selector sent to instance 0x60400020a0d0,意思就是給這個person對象發送了沒有實現的消息:setHeight:,這應該是在我們的預料之中,為什么呢?因為我們在分類中聲明height這個屬性的時候,不像在類中聲明屬性一樣,系統只會聲明set和get方法,而不會在.m中去實現set和get方法,因此導致了程序崩潰。因此我們在分類的.m文件中去實現set和get方法:

//Person+Test1.m文件
- (void)setHeight:(int)height{
    
}

- (int)height{
    
    return 0;
}

再次運行代碼,這次程序不崩潰了,打印結果是:

Category[9030:308848] name: dongdong, height : 0

我們看到name屬性賦值成功了,而height屬性顯然沒有賦值成功。

person.height = 180;

這句代碼顯然是調用了set方法,但是在分類中的set方法什么也沒有實現,沒有存儲下這個設置的值180。

person.height

實則是調用了get方法,由于不能保存傳遞過來的height值,所以上面的代碼中我們返回固定值0。
而name屬性能夠賦值和讀取成功,是因為在其set方法中用_name這個成員變量保存的賦的值:

- (void)setName:(NSString *)name{
    
    _name = name;
}

在其get方法中利用_name成員變量返回存儲的值:

- (NSString *)name{
    
    return _name;
}

所以如果我們在分類的.m文件中保存傳遞過來的值,然后在取值的時候返回存儲的值,那么應該也能實現屬性的完整功能。

方法一 全局變量

第一種方法是使用全局變量來存儲傳遞進來的值:

int height_;

- (void)setHeight:(int)height{
    height_ = height;
}

- (int)height{
    
    return height_;
}

然后我們運行一下程序:

Category[9497:328996] name: dongdong, height : 180

這次好像是賦值成功了,返回也對。我們再把height改成190試試:

Category[9533:330381] name: dongdong, height : 190

這次打印的也是對的,那么這樣是不是就真的可以完全實現屬性的功能呢?
問題在于,height_是全局變量,所有的對象共用這一個全局變量,如果有個對象的height值變了,其他的對象的height值也會跟著改變,也是不符合我們的需求的,我們可以測試一下:

Person *person1 = [[Person alloc] init];
person1.height = 180;
Person *person2 = [[Person alloc] init];
person2.height = 190;
    
 NSLog(@"person1: %d, person2 : %d", person1.height, person2.height);

打印結果:

Category[9648:335004] person1: 190, person2 : 190

所以這種方法就被pass掉了。

方法二 字典

第一種方法全局變量失敗的原因就是不能做到每個對象和自己的height值一一對應。這就讓我們想到了一個數據結構-字典。假如我們通過鍵值對的形式存放height值,這樣是否可以呢?我們使用person對象指向的地址作為鍵,將height值作為值存儲在字典中:

NSMutableDictionary *heights_;
//由于load方法只初始化一次,所以我們可以在這個方法里做一些初始化操作
+ (void)load{
    
    heights_ = [NSMutableDictionary dictionary];
}

- (void)setHeight:(int)height{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    heights_[key] = @(height);
}

- (int)height{
    
    NSString *key = [NSString stringWithFormat:@"%p", self];
    return [heights_[key] intValue];
}
Person *person1 = [[Person alloc] init];
person1.height = 180;
Person *person2 = [[Person alloc] init];
person2.height = 190;
    
NSLog(@"person1: %d, person2 : %d", person1.height, person2.height);

打印結果:

Category[10166:350395] person1: 180, person2 : 190

所以采用字典這種方式是完全可行的。

使用字典存在的問題:
  • 1.非線程安全
    由于這個字典是全局的,所有的對象的height屬性值都是存儲在這個全局字典里面,當不同的對象在不同的線程同時訪問這個全局字典時,這個時候就容易產生線程安全問題,需要去加線程鎖,有些復雜。
  • 2.需要創建多個全局字典
    剛才已經看到了,我們需要為分類中的每一個屬性值創建一個全局字典,這是非常麻煩又復雜的事。

方法三 關聯對象

關聯對象使用的是runtime的API:

/****
//這個方法是在set方法中使用,目的是把傳遞進來的value值和object這個對象關聯起來
@object:這個參數是要關聯的對象
@key:在這里設置了key值,那么在get方法里面就可以根據這個key取得值
@value:傳遞進來的值
@policy:它是個一個枚舉值,用來修飾value
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_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
/***
//這個方法是在get方法中使用,獲得關聯對象的值。
@object:關聯的對象
@key:set方法中設置的key值
***/
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

我們再給Person類的分類聲明一個屬性:

@property (nonatomic, copy)NSString *sex;

然后我們使用關聯對象的方法給sex這個屬性賦值和取值:

//由于key的類型是`void *`類型,也就是一個指針類型,所以這里聲明了一個指針類型的sexKey
const void *sexKey;

- (void)setSex:(NSString *)sex{
    
    objc_setAssociatedObject(self, sexKey, sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)sex{
    
    return objc_getAssociatedObject(self, sexKey);
}
Person *person1 = [[Person alloc] init];
person1.sex = @"man";
Person *person2 = [[Person alloc] init];
person2.sex = @"women";
    
NSLog(@"person1: %@, person2 : %@", person1.sex, person2.sex);

打印結果:

Category[11243:396207] person1: man, person2 : women

我們發現打印結果是正確的。
但是這里存在一個問題就是我們設置的key沒有賦值,也即是sexKey相當于NULL,假如我們再給height屬性設置一個key為heightKey,那么這個heightKey也是NULL,那么在get方法中通過key值來取得值時,由于屬性的key都是一樣的,所以就很容易出錯。

  • 方法一
    因此我們需要給這個sexKey賦值一個獨一無二的值:
const void *sexKey = &sexKey;

這句話就是直接將sexKey這個指針的地址值賦給自己。對于height:

const void *heightKey = &heightKey;

由于這兩個指針分類在不同的內存地址中,所以heightKey和sexKey可以保證是不相同的,這樣就能在get方法中取出正確的值。

  • 方法二
    上面這種方式實在是非常啰嗦又累贅,我們要聲明指針,初始化指針,下面介紹一種更簡單的方法:
- (void)setSex:(NSString *)sex{
    
    objc_setAssociatedObject(self, @"sex", sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)sex{
    
    return objc_getAssociatedObject(self, @"sex");
}

我們直接把@"sex"這個字符串傳進去作為key,這樣就不用聲明指針又初始化了。有人就有疑問了,這里的key明明要求是指針類型的,我們傳進一個字符串可以嗎?我們分析一下下面這句代碼:

NSString *name = @"dongdong";

這里name變量是一個指針變量。那么我們為什么能用一個字符串去初始化一個指針變量呢?原因就是這里傳進去的是@"dongdong"這個字符串的地址。這樣我們就能明白,上面@"sex"其實傳進去的也是這個字符串的地址。
為了防止誤寫,我們還可以把字符串抽成宏:

#define SEX @"sex"

- (void)setSex:(NSString *)sex{
    
    objc_setAssociatedObject(self, SEX, sex, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)sex{
    
    return objc_getAssociatedObject(self, SEX);
}

方法三
第二種方法已經非常簡便了,但是為了方便準確我們還要把字符串抽成宏。有沒有更加簡便的方法呢?我們可以嘗試傳進一個方法的地址作為key,比如說set或get方法:

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

- (NSString *)sex{
    
    return objc_getAssociatedObject(self, @selector(sex));
}

這里傳進去的key是@selector(sex),也就是sex這個get方法的地址。當然我們也可以傳進set方法的地址作為key。最后我們還可以更進一步的簡化:

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

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

這里在get方法里把@selector(sex)換成了_cmd,這是因為我們使用的key是sex這個方法的地址,在這個方法內部,我們可以直接使用_cmd獲取本方法。那這樣就非常方便簡潔了。

關聯對象的原理

set方法

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)方法
我們直接去runtime的源碼中去查看關聯對象的具體實現,直接搜索objc_setA,

24E2331B-598F-4F7B-A012-B03564C99C88.png

  • 1.選擇objc-runtime.mm這個文件中的實現:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
  • 2.點進_object_set_associative_reference(object, (void *)key, value, policy);這個真實的實現函數:
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);
}

這個函數的實現看起來非常復雜,都是C++的語法,對于不了解C++的人來說非常困難,不過沒關系,即便我們看不懂上面的代碼,通過下面的分析,我們也能明白關聯對象的原理:
實現關聯對象技術的核心對象有:

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjectAssociation
    這里面經常出現Map這個東西,這其實和我們Objective-c中的字典是一樣的,我們可以把它當字典來看待。在第二種方法里面我們是用字典去實現的,這里又出現了和字典相似的結構,那它們的實現會不會相似呢?
    在上面的一大段源碼中,我們在開頭的位置找到這一句:
AssociationsManager manager;

我們點進AssociationsManager查看其結構:

759A21C4-A407-490A-9F90-AE740E138B4D.png

前面講了Map類型是字典,那么什么是key,什么是value呢?然后我們繼續點進AssociationsHashMap
D72D1231-FFD8-40ED-8CE0-E9D2146A4ECD.png

我們前面也講了,ObjectAssociationMap這個結構也是字典,那么這個字典里面裝的是什么呢?我們點進去看看:
893E11B0-3813-441F-9833-5839CC45FD7C.png

那這個ObjcAssociation又是什么東西呢?我們進去看看:
35314FE8-4390-4BEA-9619-6FF40D3FB9F2.png

總結一下上面四個核心對象的結構:
9C8C4B75-E1B0-425C-8E91-C7B833181741.png

下面這張圖總結的是這四個核心對象之間的聯系:
D8080A40-5585-474E-A224-B5BBE573DB22.png

那么問題來了,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)中的四個參數分別對應上面結構中的哪個結構呢?
下圖就展示了它們的對應關系:
0423B811-3976-46A5-95CC-FCDFDB9FE236.png

拿我們之前寫的作為例子:

objc_setAssociatedObject(self, @selector(sex), sex, OBJC_ASSOCIATION_COPY_NONATOMIC);

這句代碼中,self也就是person對象被賦給了AssociationHashMap的key,而@selector(sex)的地址被賦給了AssociationMap的key,策略OBJC_ASSOCIATION_COPY_NONATOMIC被賦值給了ObjectAssociation的policy,傳遞進來的值sex被賦值給了ObjectAssociation的value。
這種設計的巧妙之處就在于:
當一個person對象不光有一個屬性值要關聯時,比如我們要關聯height和sex這兩個屬性時,我們以person對象作為key,然后值是AssociationMap這個字典類型,在這個字典類型中,分別使用@selector(sex)@selector(height)作為key,然后分別利用sex屬性的policy和傳遞進來的value和height屬性的policy和傳遞進來的value生成ObjectAssociation作為value。而如果有多個person對象需要關聯時,我們只需要在AssociationHashMap中創造更多的鍵值對就可以解決這個問題。
通過這個過程我們也能明白:
關聯對象的值它不是存儲在自己的實例對象的結構中,而是維護了一個全局的結構AssociationManager

get方法

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)方法
經過了上面的分析,基本上就對set方法的原理比較清楚了,下面我們直接看一下get方法的源碼:

  • 1.在runtime的源碼中找到這個函數:
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
  • 2.點進_object_get_associative_reference(object, (void *)key);
    3EFE0EEF-7D66-44FF-AC39-55386E6AE3BB.png

回答面試題

Category能否添加成員變量?如果可以,如何給Category添加成員變量?
答:不能直接給Category添加成員變量,但是可以間接實現Category有成員變量的效果。我們可以使用runtime的API,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)這兩個來實現。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容