OC isa結構、訊息傳遞、Method Swizzling

韋家冰發表於2017-12-13

參考 Objective-C Runtime 1小時入門教程 Objective-C特性:Runtime

Objc 物件的今生今世 神經病院Objective-C Runtime入院第一天——isa和Class 深入解析 ObjC 中方法的結構

iOS黑魔法-Method Swizzling 玉令天下:Objective-C Method Swizzling

####例項物件結構 id就是一個指向類例項的指標

typedef struct objc_object *id;
struct objc_object {
    isa_t _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
複製程式碼

arm64 架構中的 isa_t 結構體

#define ISA_MASK        0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK  0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
#define RC_ONE   (1ULL<<45)
#define RC_HALF  (1ULL<<18)
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

    struct {
       uintptr_t indexed           : 1;  // 0 表示普通的 isa 指標,1 表示使用優化,儲存引用計數
       uintptr_t has_assoc         : 1;  // 表示該物件是否包含 associated object,如果沒有,則析構時會更快
       uintptr_t has_cxx_dtor      : 1;  // 表示該物件是否有 C++ 或 ARC 的解構函式,如果沒有,則析構時更快
       uintptr_t shiftcls          : 33; // 類的指標
       uintptr_t magic             : 6;  // 固定值為 0xd2,用於在除錯時分辨物件是否未完成初始化。
       uintptr_t weakly_referenced : 1;  // 表示該物件是否有過 weak 物件,如果沒有,則析構時更快
       uintptr_t deallocating      : 1;  // 表示該物件是否正在析構
       uintptr_t has_sidetable_rc  : 1;  // 表示該物件的引用計數值是否過大無法儲存在 isa 指標
       uintptr_t extra_rc          : 19; // 儲存引用計數值減一後的結果
    };
};
複製程式碼

####類結構 類其實也是物件,叫做類物件

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    ........
}
複製程式碼

NSObject 結構圖.png

#####class_rw_t 執行期拷貝class_ro_t中的部分資訊存入此結構體中,並存放執行期新增的資訊

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
};
複製程式碼

#####class_ro_t 記錄編譯期就已經確定的資訊

struct class_ro_t {
    const char * name; // 類名
    uint32_t reserved; // 預留欄位

    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;

    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    property_list_t *baseProperties;
    const ivar_list_t * ivars;

    const uint8_t * ivarLayout;
    const uint8_t * weakIvarLayout;
    
};
複製程式碼
baseMethodList、baseProtocols、baseProperties,ivars編譯器確定好的方法、協議、屬性、成員變數

######flags:各種資訊合集

OC  isa結構、訊息傳遞、Method Swizzling

######instanceStart、instanceSize 1、instanceStart之所以等於8,是因為每個物件的isa佔用了前8個位元組。 2、instanceSize = isa + 3個ivar,$6的size只有1,但是為了對齊,也佔用了8 繼承體系就在父類上面加

// ZNObjectFather,三個成員變數
instanceStart = 8
instanceSize = 32 (instanceStart + 3個ivar)

// ZNObjectSon,也有三個成員變數
instanceStart = 32
instanceSize = 56 (instanceStart + 3個ivar)
複製程式碼
    size_t objSize = class_getInstanceSize([ZNObjectFather class]);// 32
    size_t objSize2 = class_getInstanceSize([ZNObjectSon class]);// 56
複製程式碼

######ivarLayout和 weakIvarLayout:成員變數的strong與weak資訊 1、ivarLayout = "\x01",表示在先有0個弱屬性,接著有1個連續的強屬性。若之後沒有強屬性了,則忽略後面的弱屬性 2、weakIvarLayout = "\x11",表示先有1個強屬性,然後才有1個連續的弱屬性,若之後沒有弱屬性了,則忽略後面的強屬性

const uint8_t *
class_getIvarLayout(Class cls)
{
    if (cls) return cls->data()->ro->ivarLayout;
    else return nil;
}

const uint8_t *
class_getWeakIvarLayout(Class cls)
{
    if (cls) return cls->data()->ro->weakIvarLayout;
    else return nil;
}

複製程式碼
@interface BBObject : NSObject
{
    
    NSString *name1;
    __weak NSString *name2;
    NSString *name3;
    NSString *name4;
    NSString *name5;
    NSString *name6;
    __weak NSString *name7;
    NSString *name8;
    __weak NSString *name9;
}

const uint8_t *ivarLayout = class_getIvarLayout([BBObject class]);
const uint8_t *weakIvarLayout = class_getWeakIvarLayout([BBObject class]);

// ivarLayout=\x01\x14\x11
// weakIvarLayout =\x11\x31\x11
複製程式碼

ivarLayout=\x01\x14\x11

OC  isa結構、訊息傳遞、Method Swizzling
weakIvarLayout =\x11\x31\x11
OC  isa結構、訊息傳遞、Method Swizzling

######例項物件的isa的isa...是什麼?各層isa是什麼? 舉例子: BBObj *obj = [BBObj new];

1、obj->isa是一個objc_class結構物件,存放在普通成員變數、動態方法(“-”開頭的方法)、isa(metaclass)、super_class

2、obj->isa->isa也是一個objc_class結構物件,叫做元類metaclass,存放著static型別的成員變數與static型別的方法(“+”開頭的方法)

3、obj->isa->super_class也是一個objc_class結構物件:父類例項物件

4、obj->isa->isa->isa(metaclass->isa)是NSObject類物件

5、 obj->isa->isa->super_class(metaclass->super_class)是父類metaclass物件

        Student  *stu = [[Student alloc]init];

        NSLog(@"Student's class is %@", [stu class]);
        NSLog(@"Student's meta class is %@", object_getClass([stu class]));
        NSLog(@"Student's meta class's superclass is %@", object_getClass(object_getClass([stu class])));

        Class currentClass = [Student class];
        for (int i = 1; i < 5; i++)
        {
            NSLog(@"Following the isa pointer %d times gives %p %@", i, currentClass,currentClass);
            currentClass = object_getClass(currentClass);
        }

        NSLog(@"NSObject's class is %p", [NSObject class]);
        NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));

複製程式碼

輸出如下:

Student's class is Student
Student's meta class is Student
Student's meta class's superclass is NSObject
Following the isa pointer 1 times gives 0x100004d90 Student
Following the isa pointer 2 times gives 0x100004d68 Student
Following the isa pointer 3 times gives 0x7fffba0b20f0 NSObject
Following the isa pointer 4 times gives 0x7fffba0b20f0 NSObject
NSObject's class is 0x7fffba0b2140
NSObject's meta class is 0x7fffba0b20f0
複製程式碼

class與meta class關係.jpg

#####objc_ivar_list *ivars是什麼? objc_ivar_list其實就是一個連結串列,儲存多個objc_ivar

struct objc_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1];
}
複製程式碼
objc_ivar是什麼?

objc_ivar結構體儲存類的單個成員變數資訊

typedef struct objc_ivar *Ivar;
struct objc_ivar {
    char *ivar_name; // 變數名
    char *ivar_type; // 變數型別
    int ivar_offset; // 基地址偏移位元組
#ifdef __LP64__
    int space;       // 佔用空間
#endif
}
複製程式碼

######使用物件成員變數流程 呼叫 +alloc 方法來初始化一個物件時,也僅僅在記憶體中生成了一個objc_object結構體,並根據其instanceSize來分配空間,將其isa指標指向所屬的類。 類的成員變數ivar_t儲存在class_ro_t中的ivar_list_t * ivars中, 其中offset 是成員變數相對於物件記憶體地址的偏移量,正是通過它來完成變數定址。 當我們使用物件的成員變數時,如 myObject.var ,編譯器會將其轉化為object_getInstanceVariable(myObject, 'var', **value) 找到其ivar_t結構體ivar,然後呼叫object_getIvar(myObject, ivar)來獲取成員變數的記憶體地址。其計算公式如下:

id *location = (id *)((char *)obj + ivar_offset); 基於此,雖然多個物件的isa指標指向同一個objc_class,但由於物件的記憶體地址不一樣,所以它們的例項變數儲存位置也不一樣,從而實現物件與類之間的多對一關係。

#####objc_method_list是什麼? objc_method_list是一個連結串列,儲存多個objc_method,而objc_method結構體儲存類的某個方法的資訊

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;
}

// 表示類中的某個方法
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name;    // 方法名
    char *method_types; // 方法型別
    IMP method_imp;     // 方法實現
}
複製程式碼

######呼叫物件方法: 當obj_object接收到訊息後,通過其isa指標找到對應的objc_class,objc_class又通過其data() 方法,查詢class_rw_t的methods列表。

SEL是什麼?

SEL是selector在Objective-C中的表示型別

typedef struct objc_selector *SEL;
struct objc_selector {
    char *name;     // 名稱
    char *types;    // 型別
};
複製程式碼

#####IMP是什麼? IMP本質上就是一個函式指標,指向方法的實現(方法的程式碼)

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
複製程式碼

#####Cache是什麼? Cache其實就是一個儲存Method的連結串列,主要是為了優化方法呼叫的效能。

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

/*
 當物件receiver呼叫方法message時,
 1、先在Cache查詢IMP,找到返回IMP
 2、如果沒有找到,再在類的methodLists中查詢,
 3、如果沒有找到,就在super_class父類重複(1、2),
 4、如果找到把方法加入receiver類中的Cache、返回IMP
複製程式碼

類的相關操作函式有如下: 1、iOS Class結構分析 2、Objective-C Runtime 執行時之一:類與物件 ####三、Objective-C的訊息傳遞 #####1、基本訊息傳遞 物件呼叫方法叫做傳送訊息。在編譯時,程式的原始碼就會從物件傳送訊息轉換成Runtime的objc_msgSend函式呼叫。

例如某例項變數receiver實現某一個方法oneMethod
[receiver oneMethod];
Runtime會將其轉成類似這樣的程式碼
objc_msgSend(receiver, selector);
複製程式碼

具體會轉換成什麼程式碼呢?Runtime會根據型別自動轉換成下列某一個函式:

1、objc_msgSend:普通的訊息都會通過該函式傳送
2、objc_msgSend_stret:訊息中有資料結構作為返回值(不是簡單值)時,通過此函式傳送和接收返回值
3、objc_msgSendSuper:和objc_msgSend類似,這裡把訊息傳送給父類的例項
4、objc_msgSendSuper_stret:和objc_msgSend_stret類似,這裡把訊息傳送給父類的例項並接收返回值
複製程式碼

#####2、objc_msgSend函式的呼叫過程: 第一步:檢測這個selector是不是要忽略的。

第二步:檢測這個target是不是nil物件。nil物件傳送任何一個訊息都會被忽略掉。

第三步: 1、先在Cache查詢IMP,找到返回IMP 2、如果沒有找到,再在類的methodLists中查詢, 3、如果沒有找到,就在super_class父類重複(1、2), 4、如果找到把方法加入Cache、返回IMP

第四步:前三步都找不到就會進入動態方法解析(看下文)。

#####3、訊息轉發及動態解析方法 當一個物件能接收一個訊息時,會走正常的方法呼叫流程。但如果一個物件無法接收一個訊息時,就會走訊息轉發機制。

訊息轉發機制基本上分為三個步驟:

訊息轉發流程

######(1)動態增加方法階段:方法目的是為了給類利用 class_addMethod 新增方法的機會,如下:

//類方法
+(BOOL)resolveClassMethod:(SEL)sel {
    // 寫法與下面resolveInstanceMethod類似
}
//例項方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(XXXX)) {
        //const char *types:
        //"v@:"  返回值void型別的方法,沒有引數傳入。
        //"i@:"  返回值int型別的方法,沒有引數傳入。
        //"i@:@" 返回值int型別的方法,又一個引數傳入。
        //"s@:@" 返回值string型別的方法,又一個引數傳入。
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(AAA)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
複製程式碼

表示方法的引數和返回值,詳情請參考Type Encodings

######(2)物件轉發階段:詢問是否把訊息給其他接收者處理(單一),返回id是執行者(非self非nil) 如下:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    MessageForwarding *obj=[MessageForwarding new];// 訊息執行者
    if ([obj respondsToSelector:aSelector]) {
        return obj;
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製程式碼

######(3)NSInvocation執行階段:

//首先呼叫methodSignatureForSelector:方法來獲取函式的引數和返回值,如果返回為nil,程式會Crash掉,並丟擲unrecognized selector sent to instance異常資訊
// methodSignatureForSelector例項方法;instanceMethodSignatureForSelector類方法
//如果返回一個函式簽名,系統就會建立一個NSInvocation物件並呼叫-forwardInvocation:方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    
    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    MessageForwarding *messageForwarding1 = [MessageForwarding new];
    if ([messageForwarding1 respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding1];
    }
    //可以多個物件,區別於第二個,步驟越往後,處理訊息的代價越大,到最後一個階段時,都建立了 NSInvocation 物件了
    MessageForwarding *messageForwarding2 = [MessageForwarding new];
    if ([messageForwarding2 respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding2];
    }
}
複製程式碼

######(4)以上三點都不處理,就doesNotRecognizeSelector 只是程式主動丟擲一個-[類 XXX方法]: unrecognized selector sent to instance不能識別方法的異常

訊息傳送與轉發路徑流程圖.jpg

玉令天下:Objective-C 訊息傳送與轉發機制原理

lookUpImpOrForward

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver) {
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();
    // 檢查是否新增快取鎖,如果沒有進行快取查詢。
    // 查到便返回IMP指標
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
	// 通過呼叫realizeClass方法,分配可讀寫`class_rw_t`的空間
    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }
	// 倘若未進行初始化,則初始化
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }
	// 保證方法查詢,並進行快取填充(cache-fill)
retry:
    runtimeLock.read();
    // 是否忽略GC垃圾回收機制(僅用在macOS中)
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }
    // 當前類的快取列表中進行查詢
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    // 從類的方法列表中進行查詢
    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
    // 從父類中迴圈遍歷
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // 父類的快取列表中查詢
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // 如果在父類中發現方法,則填充到該類快取列表
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                break;
            }
        }
        // 從父類的方法列表中查詢
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
    // 進入method resolve過程
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        // 呼叫_class_resolveMethod,解析沒有實現的方法
        _class_resolveMethod(cls, sel, inst);
        // 進行二次嘗試
        triedResolver = YES;
        goto retry;
    }
    // 沒有找到方法,啟動訊息轉發
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlockRead();
    return imp;
}
複製程式碼

從類的方法列表中進行查詢

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel) {
    runtimeLock.assertLocked();
    // 遍歷所在類的methods,這裡的methods是List鏈式型別,裡面存放的都是指標
    for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
複製程式碼

呼叫_class_resolveMethod,嘗試類是否現實了resolveInstanceMethod或resolveClassMethod

void _class_resolveMethod(Class cls, SEL sel, id inst) {
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        // 針對於物件方法的操作
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        // 針對於類方法的操作
        _class_resolveClassMethod(cls, sel, inst);
        // 再次啟動查詢,並且判斷是否擁有快取中訊息標記_objc_msgForward_impcache
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) {
            // 說明可能不是 metaclass 的方法實現,當做物件方法嘗試
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製程式碼

####四、self與super

  • self是類的一個隱藏引數,每個方法的實現的第一個引數即為self。
  • super並不是隱藏引數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當呼叫方法時,去呼叫父類的方法,而不是本類中的方法。

下面的程式碼分別輸出什麼?

@implementation Son : Father
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end
複製程式碼

結果:輸出兩個Son

解析 當呼叫[self class]方法時,會轉化為objc_msgSend函式。 當呼叫[super class]方法時,會轉化為objc_msgSendSuper。

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"));
複製程式碼

簡化

__rw_objc_super objc_super = (__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}
class_getSuperclass(objc_super,sel_registerName("class"))
複製程式碼

objc_super *super是什麼?

struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;
};
複製程式碼

在objc_msgSendSuper方法中,第一個引數是一個objc_super的結構體,這個結構體裡面有兩個變數,一個是接收訊息的receiver,一個是當前類的父類super_class。

objc_msgSendSuper的工作原理應該是這樣的: 從objc_super結構體指向的superClass父類的方法列表開始查詢selector,找到後以objc_super的receiver(就是self)去呼叫父類的這個selector。注意,最後的呼叫者是self,而不是super_class!

####五、Method Swizzling 比較簡單、常用的方式,方案A

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)  {
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // originalMethod 已經存在  class_addMethod 方法就會失敗
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    // 方法存在就替換掉,如果不存在就直接新增
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
複製程式碼

1、RSSwizzle 被很多人推薦,它用很複雜的方式解決了 What are the Dangers of Method Swizzling in Objective C? 中提到的一系列問題。不過引入它還是有一些成本的,建議在本文列舉的那些極端特殊情況下才使用它,畢竟方案 A 已經能 Cover 到大部分情況了。

2、JRSwizzle 嘗試解決在不同平臺和系統版本上的 Method Swizzling 與類繼承關係的衝突。對各平臺低版本系統相容性較強。

實用例子: iOS 萬能跳轉介面方法 OC最實用的runtime總結,面試、工作你看我就足夠了!

相關文章