runtime總結

在Objective-C里調用一個方法是這樣的:

[object method];

編譯器會把它翻譯成:

id objc_msgSend(object, selector,arg1,arg2,...)

id

id的定義:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

objc_object結構體里的成員變量isa指向了實例對象的類對象。

Class

Class的定義:

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

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

} OBJC2_UNAVAILABLE;

objc_class結構體里的成員變量isa指向了類的元類;成員變量super_class指向了類的父類。

類也是對象,可以看做是元類的實例,元類里的方法列表存儲的是類方法。

SEL

SEL的定義:

typedef struct objc_selector *SEL;

runtime的源碼里沒有給出objc_selector的定義。SEL類型是方法的selector,可以理解為區分方法的 ID。其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 SEL 類型的方法選擇器。

objc_cache

類有一個存放方法的方法列表和一個存放緩存方法的哈希表objc_msgSend這個函數的內部是現實是先從類的緩存哈希表里尋找方法,如果沒找到,則從類的方法列表里尋找,找到后會將方法填充到緩存哈希表里。

runtime源碼
objc_msgSend的源碼解析
類的方法的緩存解析

消息轉發

objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類對象,然后在該類對象中的方法列表以及其父類方法列表中尋找方法運行,如果,在最頂層的父類中依然找不到相應的方法時,程序在運行時會掛掉并拋出異常unrecognized selector sent to XXX 。但是在這拋出異常之前,objc的運行時會給出三次拯救程序崩潰的機會:

Dynamic Method resolution

首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知消息新增一個”處理方法””。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在運行時通過class_addMethod函數動態添加到類里面就可以了。如下代碼所示:


void dynamicMethodIMP(id self, SEL _cmd) {//至少要有兩個參數self 和 _cmd.
    // implementation ....
}

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end


不過這種方案更多的是為了實現@dynamic屬性:

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

#import "MyClass.h"
#import <objc/runtime.h>


NSString * dynamicNameIMP(id self, SEL _cmd)
{
    NSString * name = objc_getAssociatedObject(self, _cmd);
    return name;
}

void dynamicSetNameIMP(id self, SEL _cmd, NSString *name)
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}

@implementation MyClass
@dynamic name;

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(name)) {
        class_addMethod([self class], sel, (IMP)dynamicNameIMP, "v@:");
    }
    if (sel == @selector(setName:)) {
        class_addMethod([self class], sel, (IMP)dynamicSetNameIMP, "v@:@");
    }
    
    return [super resolveInstanceMethod:sel];
}
@end

如果使用class_addMethod添加類方法,則class_addMethod的第一個參數要傳入一個元類對象object_getClass(self)

Fast forwarding

如果在上一步無法處理消息,且目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啟,當然發送的對象會變成你返回的那個對象。否則,就會繼續Normal Fowarding。 這里叫Fast,只是為了區別下一步的轉發機制。因為這一步不會創建任何新的對象,但下一步轉發會創建一個NSInvocation對象,所以相對更快點。

@interface SUTRuntimeMethodHelper : NSObject
- (void)method2;
@end
@implementation SUTRuntimeMethodHelper
- (void)method2 {
    NSLog(@"%@, %p", self, _cmd);
}
@end
#pragma mark -
@interface SUTRuntimeMethod () {
    SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod
+ (instancetype)object {
    return [[self alloc] init];
}
- (instancetype)init {
    self = [super init];
    if (self != nil) {
        _helper = [[SUTRuntimeMethodHelper alloc] init];
    }
    return self;
}
- (void)test {
    [self performSelector:@selector(method2)];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 將消息轉發給_helper來處理
    if ([selectorString isEqualToString:@"method2"]) {
        return _helper;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.

Normal forwarding

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制了。
這一步是Runtime最后一次給你挽救的機會。首先它會調用-methodSignatureForSelector:方法獲得NSMethodSignature類型的函數簽名,如果返回了一個函數簽名,Runtime就會創建一個NSInvocation對象并調用-forwardInvocation:方法。如果-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。
NSObject的forwardInvocation:方法實現只是簡單調用了doesNotRecognizeSelector:方法,它不會轉發任何消息。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
            signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_helper];
    }else{
       [super forwardInvocation:anInvocation];
    }
}

Category

category筆記

Method Swizzling

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        //當類中沒有originalSelector的實現的時候才會添加成功,否則添加失敗
        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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

為什么上面不直接使用method_exchangeImplementations,而是先使用class_addMethod呢?因為直接交換 IMP 是很危險的。如果這個類中沒有實現這個方法,但是父類實現了,那么class_getInstanceMethod() 返回的是某個父類的 Method 對象,這樣 method_exchangeImplementations() 就把父類的原始實現(IMP)跟這個類的 Swizzle 實現交換了。這樣其他父類及其其他子類的方法調用就會出問題,最嚴重的就是 Crash。

有時為了避免方法命名沖突和參數 _cmd 被篡改,也會使用下面這種『靜態方法版本』的 Method Swizzle。CaptainHook 中的宏定義也是采用這種方式,比較推薦:

def IMP *IMPPointer;

static void MethodSwizzle(id self, SEL _cmd, id arg1);
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

static void MethodSwizzle(id self, SEL _cmd, id arg1) {
    // do custom work
    MethodOriginal(self, _cmd, arg1);
}

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

+ (void)load {
    [self swizzle:@selector(originalMethod:) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
}

store傳入了MethodOriginal的地址,通過*store = imp;把之前的orginal映射的IMP賦值給了MethodOriginal,之后就可以通過 MethodOriginal(self, _cmd, arg1);調用之前的方法了。

常用方法

class_getInstanceMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name):當類和其父類都沒有selector映射的方法是現時,會返回null。
class_addMethod():若果類里沒有要添加的方法(不管父類有沒有),則會添加一個新的方法(如果父類也有這個方法,則相當于重寫了父類的方法),返回值為YES;如果類里已經有了要添加的方法,則不會添加,返回值為NO。

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

推薦閱讀更多精彩內容