iOS 底層探索 - 類拓展和關聯對象

iOS 底層探索系列html

iOS 查漏補缺系列objective-c

前面咱們探索了 iOS 中類和分類的加載,關於類這一塊的內容,咱們還有一些坑沒有填,好比類拓展和關聯對象,今天讓咱們一塊兒填下這塊的坑。bash

1、類拓展

1.1 什麼是類拓展?

關於類拓展的具體定義,你們能夠直接參考 Apple 對於類拓展的說明markdown

A class extension bears some similarity to a category, but it can only be added to a class for which you have the source code at compile time (the class is compiled at the same time as the class extension).架構

類拓展和分類很類似,可是前提是你擁有原始類的源碼,而且是在編譯時被附加到類上的。(類和類擴展同時編譯)app

類拓展的結構:ide

@interface ClassName ()
 
@end
複製代碼

Because no name is given in the parentheses, class extensions are often referred to as anonymous categories. 由於括號中沒有填寫任何內容,因此類擴展也被稱爲匿名的分類oop

咱們在 Xcode 中建立 Objective 類型的文件的時候,能夠選擇空文件、分類、協議以及類擴展。佈局

若是咱們選擇 Extension 選項,Xcode 會幫咱們生成一個 NSObject + 擴展名 的頭文件出來,也就是說類擴展的命名方式爲 類名_擴展名.hpost

而這樣的操做其實咱們不多作,咱們通常都是在 .m 文件中聲明一下當前類的拓展,基本上咱們都會在類擴展去聲明一些私有的屬性、方法。好比在 .h 文件中聲明一個只讀的屬性,而後在 .m 文件的類拓展中去重寫這個屬性爲可讀可寫。

咱們不妨使用 LLDB 打印看一下類拓展到底是不是在編譯時就被附加到了類上面了呢?

1.2 類拓展是編譯時肯定的嗎?

咱們在 objc-756 源碼中的 objc-debug 項目下新建一個類 Person,而後給這個類添加一個屬性 name,而後在 .m 文件中的類拓展中添加一個屬性 mName 和方法 extM_method,接着再建立一個 Person 的類拓展 Person+Extension.h 文件:

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

NS_ASSUME_NONNULL_END

// Person.m
#import "Person.h"
#import "Person+Extension.h"

@interface Person ()
@property (nonatomic, copy) NSString *mName;

- (void)extM_method;
@end

@implementation Person

+ (void)load{
    NSLog(@"%s",__func__);
}

- (void)extM_method{
    NSLog(@"%s",__func__);
}

- (void)extH_method{
    NSLog(@"%s",__func__);
}

@end

// Person+Extension.h
#import <AppKit/AppKit.h>
#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;

- (void)extH_method;
@end

NS_ASSUME_NONNULL_END
複製代碼

接着咱們在 main.m 中來測試一下:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        NSLog(@"%@ - %p", p, p);
    }
    return 0;
}
複製代碼

咱們在 Person 實例化對象 p 這一行打上斷點,而後運行項目。接着在控制檯進行 LLDB 打印:

由於對象的屬性以及方法都存儲在類對象上面,而因爲類結構裏面的 ro 是編譯時就肯定了其內容,因此咱們只須要打印出類對象的 ro 結構中 是否有類拓展中的 mName 屬性和 extM_method 方法 是否有類擴展中的 ext_nameext_subject 屬性以及 extH_method 方法

1.3 LLDB 驗證

  • 經過 x/4gx 命令打印出 LGPerson 類對象的內存地址,以 16 進制方式打印,打印 4 段

  • 由於類對象的內存地址起始爲 isa,緊接着是 superclass,而後是 cache_t。咱們前面已經分析過,在默認的 arm64 處理器架構下,isa 佔 8 個字節,superclass 佔 8 個字節,而 cache_t 的三個屬性加起來是 8 + 4 + 4 = 16 個字節,因此要想拿到 bits 須要進行 8 + 8 + 16 = 32 字節的內存平移,可是這裏是 16 進制,因此須要移動 0x20 個內存地址,也就是 0x100002420 + 0x20 = 0x100002440

  • 由於類對象的 data() 屬性會返回 bits.data(),因此這裏直接打印剛纔取到的 bitsdata() 屬性,而 bitsdata() 屬性其實返回的是 rw
struct objc_class : objc_object {
    class_rw_t *data() { 
        return bits.data();
    }
}
    
struct class_data_bits_t {
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}    
複製代碼

  • 接着打印 rw 的屬性 ro,而後咱們先嚐試讀取 baseMethodList 屬性,該屬性存儲的是編譯時肯定的類的全部的方法。

  • 由於 baseMethodList 屬性是一個 List 類型的容器,咱們直接使用 get(index) 來獲取其 index 處的值,結果咱們所要尋找的 extH_methodextM_method 出現了,不過還沒結束,咱們還沒驗證類拓展中聲明的兩個屬性,讓咱們打印一下 robaseProperties

  • 咱們很清楚的看到,mNameext_nameext_subject 都被找到了,那麼是否是就是說類拓展就是編譯時肯定的了呢?咱們還漏掉了這三個屬性的 gettersetter 了,讓咱們回過頭再去 baseMethodList 中查找一下

  • Bingo! 咱們類拓展定義的屬性的 gettersetter 方法也生成了,至此,咱們就徹底肯定了類拓展在編譯時就會被加載到類的 ro 中。

這裏有個注意點,就是若是咱們沒有在類的頭文件或者源文件中引入單獨的類拓展頭文件,那麼這個單獨的類拓展的頭文件裏面的屬性和方法將不會被加載到類上面來。

1.4 類拓展和分類的區別

研究對象 加載時機 操做對象 可否經過@property聲明屬性生成 getter 和 setter
分類(實現了load方法) 運行時 rw 不能,須要藉助關聯對象來實現
分類(沒有實現load方法) 編譯時 ro 不能,須要藉助關聯對象來實現
類拓展 編譯時 ro 能夠

2、關聯對象

上一節咱們探索了類拓展以及類拓展與分類的區別,咱們知道,類拓展中能夠聲明屬性,編譯器會幫助咱們生成屬性對應的 gettersetter 方法,可是分類經過 @property 的方式來聲明屬性卻不能生成 gettersetter 方法。而其實 iOS 中有一種方式能夠爲分爲增長具備 gettersetter 的屬性,那就是 - 關聯對象 Associated Objects

2.1 關聯對象定義

關聯對象的官方定義能夠在 蘋果官方文檔 上找到。

Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.

關聯引用,是從 OS X 10.6 開始啓用的,模擬了將對象實例變量添加到已經存在的類中。經過使用關聯引用,你能夠在不修改類聲明的前提下爲對象添加內容。若是你無權訪問該類的源代碼,或者因爲二進制兼容性緣由而沒法更改該對象的佈局,則這可能頗有用。

Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.

關聯引用機制基於 key。對於任何對象,你均可以根據須要添加任意數量的關聯引用,每一個關聯都使用不一樣的 key。關聯引用還能夠確保關聯的對象至少在源對象的聲明週期內保持有效。

而關於關聯對象的最佳實踐能夠參考 NSHipster - Associated Objects 一文。

從蘋果官方文檔能夠看到,關聯引用其實不是隻能在分類中使用,只不過對於咱們平常開發來講,分類中使用關聯引用仍是更經常使用的場景。相信大多數開發者都知道怎麼使用關聯引用,的確,關聯引用使用起來很簡單,不外乎兩個方法:

// 設置關聯對象
objc_setAssociatedObject()

// 獲取關聯對象
objc_getAssociatedObject()
複製代碼

咱們若是要給一個分類中的屬性設置關聯對象,須要重寫屬性的 setter 方法,而後使用 objc_setAssociatedObject

- (void)setXXX:(關聯值數據類型)關聯值
    objc_setAssociatedObject(self, 關聯的key, 關聯值, 關聯對象內存管理策略);
}
複製代碼

而後還須要重寫 getter 方法,而後使用 objc_getAssociatedObject

- (關聯值數據類型)關聯值{
    return objc_getAssociatedObject(self, 關聯的key);
}
複製代碼

這其中的關聯對象內存管理策略以下表所示:

關聯策略 等同的 @property 描述
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個關聯對象的弱引用。 指定一個關聯對象的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 指定一個關聯對象的強引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個關聯對象的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個關聯對象的強引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個關聯對象的copy引用,能被原子化使用。

2.2 關聯對象底層原理

關於關聯對象的底層原理,這裏有一篇燈塔 draveness 的博文 關聯對象 AssociatedObject 徹底解析 十分值得一讀。

固然,若是也能夠跟隨筆者一塊兒探索下關聯對象的底層原理。咱們不妨從最直觀的 objc_setAssociatedObject 方法開始切入:

2.3 objc_setAssociatedObject

// 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);
}
複製代碼

objc_setAssociatedObject 方法的實現又包裹了一層,其實現爲 _object_set_associative_reference

_object_set_associative_reference 方法的實現很是長,這裏就分段來進行探索吧。

// 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;
複製代碼

根據註釋咱們能夠知道,當傳入的 objectkey 同時爲 nil 的時候,直接返回。這樣的處理是爲了不傳入空值時而致使崩潰。

// objc-references.mm
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));

// objc-runtime-new.h 
bool forbidsAssociatedObjects() {
    return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS);
}
複製代碼

判斷要進行關聯的對象是否禁用掉了關聯引用,這裏是經過對象的 isarwflags 屬性與上一個宏 RW_FORBIDS_ASSOCIATED_OBJECTS來判斷的。

// retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
複製代碼

初始化一個 ObjcAssociation 對象,用於持有原有的關聯對象

id new_value = value ? acquireValue(value, policy) : nil;
複製代碼

判斷傳入的關聯對象值是否存在,若是存在就調用 acquireValue 方法來獲取值,咱們能夠進入 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;
}
複製代碼

能夠看到 acquireValue 會根據關聯策略來進行 retaincopy 消息的發送

AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
複製代碼

初始化一個 AssociationsManager 對象,而後獲取一個 AssociationsHashMap 哈希表,而後經過 DISGUISE 方法做爲去哈希表查找的 key。這裏的 DISGUISE 其實進行了按位取反的操做。

inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
複製代碼

若是傳入的關聯對象值存在,說明是進行賦值操做;若是傳入的關聯對象值不存在,說明是進行置空操做。這裏咱們先看一下賦值操做的流程:

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();
            }
        }
複製代碼

1.經過上一步按位取反以後的結果,在 AssociationsHashMap 哈希表中查詢,這裏是經過迭代器的方式進行查詢,查詢的結果是 ObjcAssociation 對象,這個結構也是一個哈希表,其內部存儲的是 _object_set_associative_reference 方法傳入的 key 爲鍵,ObjcAssociation 對象爲值的鍵值對 2.若是沒有查詢到,說明以前在當前類上沒有設置過關聯對象。則須要初始化一個 ObjectAssociationMap 出來,而後經過 setHasAssociatedObjects 設置當前對象的 isahas_assoc 屬性爲 true 3.若是查詢到了,說明以前在當前類上設置過關聯對象,接着須要看 key 是否存在,若是 key 存在,那麼就須要更新原有的關聯對象;若是 key 不存在,則須要新增一個關聯對象

// 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);
                }
            }
複製代碼

由於來到這裏的條件是 new_valuenil,也就表明着要刪除關聯對象,內部的邏輯和上面的流程大同小異,不過最後多了一步在 ObjectAssociationMap 擦除 key 對應的節點

// release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
複製代碼

最後會判斷 old_association 是否有值,若是有的話就釋放掉,固然前提是舊的關聯對象的策略是 OBJC_ASSOCIATION_SETTER_RETAIN

struct ReleaseValue {
複製代碼
void operator() (ObjcAssociation &association) {
    releaseValue(association.value(), association.policy());
}
複製代碼

}; static void releaseValue(id value, uintptr_t policy) { if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) { return objc_release(value); } }

複製代碼

2.4 objc_getAssociatedObject

objc_setAssociatedObject 方法分析完了,咱們接着看另一個重要的方法 objc_getAssociatedObject:

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

能夠看到,跟 objc_setAssociatedObject 同樣,objc_getAssociatedObject 這裏又包裹了一層,其實現爲 _object_get_associative_reference,而這個方法相比於上一節的 _object_set_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);
        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()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
複製代碼

1.先初始化一個空的 value,以及一個策略爲 OBJC_ASSOCIATION_ASSIGNpolicy 2.初始化一個 AssociationsManager 關聯對象管理類,接着拿到 AssociationsHashMap 對象,這個對象在 AssociationsManager 底層是靜態的 3.而後以 DISGUISE(object) 按位取反以後的結果爲鍵去查詢 AssociationsHashMap 4.若是在 AssociationsHashMap 中扎到了,接着以 key 爲鍵去 ObjectAssociationMap 中查詢 ObjcAssociation 若是在 ObjectAssociationMap 中查詢到了 ObjcAssociation,則把值和策略賦值給方法入口聲明的兩個臨時變量,而後判斷獲取到的關聯對象的策略是否爲 OBJC_ASSOCIATION_GETTER_RETAIN,若是是的話,須要對關聯值進行 retain 操做 5.最後判斷若是關聯值是否存在且策略爲 OBJC_ASSOCIATION_GETTER_AUTORELEASE,是的話就須要調用 objc_autorelease 來釋放關聯值 6.最後返回關聯值

2.5 objc_removeAssociatedObjects

objc_removeAssociatedObjects 方法咱們平時可能用的很少,從字面含義來看,這個方法應該是用來刪除關聯對象。咱們來到它的定義處:

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

這裏會判斷 object 存在且有關聯對象纔會進入真正的實現 _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()) {
            // 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());
}
複製代碼

這裏會將對象包含的全部關聯對象加入到一個 vector 中,而後對全部的 ObjcAssociation 對象調用 ReleaseValue() 方法,釋放再也不被須要的值。

3、總結

  • 類拓展是一種匿名的分類,加載時機爲編譯時
  • 類拓展能夠添加屬性和方法以及實例變量,分類只能添加方法,屬性,可是須要藉助關聯對象來生成 gettersetter,並且分類不能聲明實例變量
  • 關聯對象在底層實際上是 ObjcAssociation 對象的結構
  • 全局有一個 AssociationsManager 管理類存儲了一個靜態的哈希表 AssociationsHashMap,這個哈希表存儲的是以對象指針爲鍵,以該對象全部的關聯對象爲值,而對象全部的關聯對象又是以 ObjectAssociationMap 來存儲的
  • ObjectAssociationMap 存儲結構爲 key 爲鍵,ObjcAssociation 爲值
  • 快速判斷一個對象是否存在關聯對象,能夠直接取對象 isahas_assoc

4、參考資料

Apple - 類拓展

Apple - 關聯對象

NSHipster - Associated Objects

Draveness - 關聯對象 AssociatedObject 徹底解析

相關文章
相關標籤/搜索