转载

TPreventUnrecognizedSEL实现思路以及原理

本文是来自風淩鶴雪的投稿

TPreventUnrecognizedSEL实现思路以及原理

TPreventUnrecognizedSEL 项目的 GitHub 地址: https://github.com/tobedefined/TPreventUnrecognizedSEL
TPreventUnrecognizedSEL 的使用方法: LINK

objc_msgSend(id, SEL, ...)

在谈论这个之前,先写一段测试代码,如下:

// main.m
#import #pragma mark - OBJ
@interface TestOBJ: NSObject
- (void)instanceMethod;
+ (void)classMethod;
@end

@implementation TestOBJ
- (void)instanceMethod {}
+ (void)classMethod {}
@end

#pragma mark - Main
int main(int argc, const char * argv[]) {
    TestOBJ *obj = [[TestOBJ alloc] init];
    [obj instanceMethod];
    [TestOBJ classMethod];
    return 0;
}

在命令行中运行clang -rewrite-objc main.m,在文件夹下生成main.cpp的C++文件,打开main.cpp找到C++的main函数(我的在第96634行),可以看到如下代码

int main(int argc, const char * argv[]) {
    TestOBJ *obj = ((TestOBJ *(*)(id, SEL))(void *)objc_msgSend)((id)((TestOBJ *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestOBJ"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("instanceMethod"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestOBJ"), sel_registerName("classMethod"));
    return 0;
}

这段代码中包含了很多的类型转换,现在将类型转换去掉,转变后的代码更容易阅读,如下:

int main(int argc, const char * argv[]) {
    // 1
    TestOBJ *obj = objc_msgSend(objc_msgSend(objc_getClass("TestOBJ"), sel_registerName("alloc")), sel_registerName("init"));
    // 2
    objc_msgSend(obj, sel_registerName("instanceMethod"));
    // 3
    objc_msgSend(objc_getClass("TestOBJ"), sel_registerName("classMethod"));
    return 0;
}

首先看第二行代码objc_msgSend(obj, sel_registerName("instanceMethod"));,这段代码将[obj instanceMethod]; rewrite 成了这种方式,也就是说ObjC中中括号调用方法实际上是调用了OBJC_EXPORT id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)的方法,第一个参数传的是调用方法的对象,第二个参数传的是方法选择器。

然后看第三行代码objc_msgSend(objc_getClass("TestOBJ"), sel_registerName("classMethod"));,与上面类似,只是第一个含参数传的是调用方法的Class(他也就是调用类方法的对象)。

那么回头看第一行代码,就很容易理解了,TestOBJ *obj = objc_msgSend(objc_msgSend(objc_getClass("TestOBJ"), sel_registerName("alloc")), sel_registerName("init"));可以拆分成两个部分,如下,就不在讨论了

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
......
}

类是什么?

网上很多的博文都写到了,类也是一个对象,关于这块有很多资料。可以从runtime源码中看到类就是一个对象。

runtime可运行修改版GitHub地址 git@github.com:RetVal/objc-runtime.git, 下面代码摘自这个版本的runtime源码。

打开/Project Headers/objc-private.h文件可以看到objc_object的实现,我们用的oc对象其实就是一个objc_object

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
......
}

打开/Project Headers/objc-runtime-new.h文件可以看到objc_class的实现,oc中的类就是objc_class

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();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
......
}

可以看到objc_class是继承自objc_object的,所以objc_class也是一个对象是没错的。

这里我们关注下面这部分:(下面的代码进行了改写,比较容易理解混合了OBJC和OBJC2的代码,实际情况远比这复杂的多,详细了解参考文末的资料)

// 上述的runtime源码
// File: "/Project Headers/objc-private.h"
// Line: 48
struct objc_class;
struct objc_object;
typedef struct objc_class *Class;
typedef struct objc_object *id;

// 上述的runtime源码
// File: "/Project Headers/objc-private.h"
// Line: 168
struct objc_object {
    isa_t isa;
};

// macOS 的runtime.h头文件和上述的runtime源码
// File: "/Project Headers/objc-runtime-new.h"
// Line: 1064
struct objc_class : objc_object {
    Class superclass;
    struct objc_method_list **methodLists;
};


// 上述的runtime源码
// File: "/Project Headers/objc-private.h"
// Line: 68
union isa_t  {
    Class cls;
}

可以看到以下重要的几点内容:

  • id 是一个指向 objc_object 的指针

  • Class 是一个指向 objc_class 的指针

  • objc_object 中包含 isa_t isa;

  • isa 中包含 Class cls;,cls 指向对应的Class

  • objc_class 继承自 objc_object,所以也属于 objc_object,同样也包含 isa,也指向一个 Class ,这个 Class 我们称之为元类,即 meta class

  • meta class 也是 Class,同样也包含 isa,它指向 NSObject 的 meta class,另外,NSObject meta class 的 isa 指向自身

这里有一张经典的图:

TPreventUnrecognizedSEL实现思路以及原理

方法的调用过程

关于 objc_class 中的 methodLists ,很明显, methodLists 就是函数列表,他存储着这个类的所有对象方法,没错, objc_class 中 methodLists 存储的是对象方法;而类方法则是存储在 meta class 的 methodLists 中。通俗的说,就是我们写的 - 号开头的方法(对象方法),则加入 Class 的 methodLists 之中;+ 写的方法(类方法),加入 meta class 的 methodLists之中。

对象方法和类方法的调用运行走的是同一套流程。

对象方法

  • 转换成 objc_msgSend(obj, sel, ...) 形式

  • 对象调用方法时,首先通过 isa 找到所属的类,然后获取 methodLists 中对应的 sel 的函数的地址

  • 运行函数

类方法

  • 转换成 objc_msgSend(cls, sel, ...) 形式

  • 类调用类方法时,首先通过该类的 isa 找到所属的元类,然后获取 methodLists 中对应的 sel 的函数的地址

  • 运行函数

Unrecognized Selector 错误

对象方法:*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TestClass losted:instance:method:]: unrecognized selector sent to instance 0x102c....'

类方法:*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[TestClass losted:class:method:]: unrecognized selector sent to class 0x10000....'

众所周知,产生 Unrecognized Selector 错误之前,系统会给出多次机会让用户进行添加方法或转发,一般分为两个步骤(Method Resolution、 Forwarding)或三个步骤(Method Resolution、 Fast Forwarding 和 Normal Forwarding

  • Method Resolution
    ObjC运行时会调用 + (BOOL)resolveInstanceMethod:(SEL)sel 或者 + (BOOL)resolveClassMethod:(SEL)sel,让你有机会提供一个函数实现添加为对象方法或类方法。添加了函数之后,返回YES,系统就会重新启动一次消息发送的过程,如果返回NO,系统就会开始进行消息转发的过程。

  • Fast Forwarding
    快速转发过程,- (id)forwardingTargetForSelector:(SEL)aSelector,系统会调用这个方法对对象方法的调用进行转发,如果返回一个newObject,则相当于转发后运行 [newObject performSelector:aSelector]。如果返回的是nil,则进入默认转发的过程。

  • Normal Forwarding
    这一步是runtime的最后一次转发过程。在这一个过程中,首先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector获取SEL的函数签名,这个时候你应该返回一个存在的已经函数签名,如果返回的是nil,此时runtime就会发出- (void)doesNotRecognizeSelector:(SEL)aSelector消息,这个时候,程序就会crash崩溃掉了。如果返回了一个函数签名,就会根据这个函数签名创建一个NSInvocation的对象并发送- (void)forwardInvocation:(NSInvocation *)anInvocation消息给目标对象。

我们主要通过Forwarding的两步操作进行防止产生 Unrecognized Selector 错误(Method Resolution过程在runtime中经常会进行主动去掉用,如果使用Resolution,可能产生死循环)。但是可以发现,-forwardingTargetForSelector:、 -methodSignatureForSelector:、 -forwardInvocation: 都只存在对象方法,而不存在类方法。这时候改如何解决?

回到前面,类其实也是一个对象,是元类(meta class)的对象,而元类(meta class)的结构是和Class的结构是相同的,都是objc_class,而类方法的调用和对象方法的调用走的是同一个流程,则我们添加这几个方法到元类(meta class)中,就可以解决类方法不存在的流程;即将-forwardingTargetForSelector:、 -methodSignatureForSelector:、 -forwardInvocation: 写作 +forwardingTargetForSelector:、 +methodSignatureForSelector:、 +forwardInvocation: 既可以添加到元类(meta class)的 methodLists之中

关于一些runtime的函数以及作用

class_getName

OBJC_EXPORT const char * _Nonnull
class_getName(Class _Nullable cls);

class_getName方法是获取Class的 C String,相当于C语言版本的 NSString *NSStringFromClass(Class aClass)

objc_getClass

OBJC_EXPORT Class _Nullable
objc_getClass(const char * _Nonnull name);

objc_getClass方法是获取根据 C String 获取一个 Class,相当于C语言版本的 Class _Nullable NSClassFromString(NSString *aClassName)

objc_getMetaClass

OBJC_EXPORT Class _Nullable
objc_getMetaClass(const char * _Nonnull name);

objc_getMetaClass方法是获取根据 C string 获取这个 String 所表示的 Class 的 MetaClass
比如以下代码:

char myClassName[] = "MyClass";
Class myClass = objc_getClass(myClassName);
Class myMetaClass = objc_getMetaClass(myClassName);


其中 myClass 是 MyClass 这个类,myMetaClass 是 MyClass 这个类的元类
所以获取当前类的元类可以使用objc_getMetaClass(class_getName(self)) 或者 objc_getMetaClass(class_getName([self class]))

class_getInstanceMethod

OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

返回Class的对象方法SEL的函数指针

class_getClassMethod

OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);

返回Class的类方法SEL的函数指针,等同于class_getInstanceMethod(objc_getMetaClass(class_getName(self)), name)

method_exchangeImplementations

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

用于替换两个函数的实现。

比如以下代码中

@implementation MyOBJ
+ (void)load {
    method_exchangeImplementations(class_getInstanceMethod(self, @selector(instanceMethodOne)),
                                   class_getInstanceMethod(self, @selector(instanceMethodTwo)));
}

- (void)instanceMethodOne {
    // 函数体1
}

- (void)instanceMethodTwo {
    // 函数体2
}
@end

-instanceMethodOne将与-instanceMethodTwo将会指向对方的函数体,也就是说[obj instanceMethodOne]其实是调用的函数体2,[obj instanceMethodTwo]其实是调用的函数体1。

class_addMethod

OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
                const char * _Nullable types);

对 Class cls 添加名为 name 的 selector,指向的函数实现为 imp, 函数的 Type Encoding 为 types

Type Encoding 可以参考:Type Encoding

关于+load

load方法的调用规则:

  • 调用顺序 父类 > 子类 > 分类

  • 调用+load之前先调用父类的+load,这个过程自动实现,不需要手动添加[super load];

  • 如果一个类没有实现+load方法,不会沿用父类的实现

  • 一个+load函数只会执行一次

  • 类和分类都执行中的+load都会执行

  • 调用时机:类被添加到 runtime 时(对于动态库和静态库中的class和category都有效)

实现

HandleUnrecognizedSELErrorBlock

// NSObject+TPUSELFastForwarding.h 或 NSObject+TPUSELNormalForwarding.h
typedef NS_ENUM(NSUInteger, UnrecognizedMethodType) {
    UnrecognizedMethodTypeClassMethod       = 1,
    UnrecognizedMethodTypeInstanceMethod    = 2,
};

typedef void (^ __nullable HandleUnrecognizedSELErrorBlock)(Class cls, SEL selector, UnrecognizedMethodType methodType);


// NSObject+TPUSELFastForwarding.m 或 NSObject+TPUSELNormalForwarding.m

@implementation NSObject (ADD)

+ (void)setHandleUnrecognizedSELErrorBlock:(HandleUnrecognizedSELErrorBlock)handleBlock {
    objc_setAssociatedObject(self, @selector(handleUnrecognizedSELErrorBlock), handleBlock, OBJC_ASSOCIATION_RETAIN);
}

+ (HandleUnrecognizedSELErrorBlock)handleUnrecognizedSELErrorBlock {
    return objc_getAssociatedObject(self, @selector(handleUnrecognizedSELErrorBlock));
}
@end

用于向外部传出所丢失函数的具体错误信息,向 NSObject 的 Class对象添加关联block,并在在适当的时候调用,具体参数的信息如下

  • clsClass类型;为缺失方法的类或对象的Class,可使用NSStringFromClass(cls)返回类名字符串

  • selectorSEL类型;为所缺失的方法名,可使用NSStringFromSelector(selector)返回方法名的字符串

  • methodTypeUnrecognizedMethodType类型;为所缺失的方法类型(类方法or对象方法)

TPUSELFastForwarding

核心代码如下:

// NSObject+TPUSELFastForwarding.m

void __c_t_resolveLostedMethod(id self, SEL _cmd, ...) {}

@implementation NSObject (TPUSELFastForwarding)

#pragma mark - FastForwarding

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        // exchange instance function in class
        method_exchangeImplementations(class_getInstanceMethod(self, @selector(forwardingTargetForSelector:)),
                                       class_getInstanceMethod(self, @selector(__t_forwardingTargetForSelector:)));

        // exchange class function in meta class
        method_exchangeImplementations(class_getClassMethod(self, @selector(forwardingTargetForSelector:)),
                                       class_getClassMethod(self, @selector(__t_forwardingTargetForSelector:)));

        // 后一部分等同于下面这种方式
        // Class metaClass = objc_getMetaClass(class_getName(self));
        // method_exchangeImplementations(class_getInstanceMethod(metaClass, @selector(forwardingTargetForSelector:)),
        //                                class_getInstanceMethod(metaClass, @selector(__t_forwardingTargetForSelector:)));
    });
}

- (id)__t_forwardingTargetForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {
        return self;
    }
    
    if (![[TUndertakeObject sharedInstance] respondsToSelector:aSelector]) {
        class_addMethod([TUndertakeObject class],
                        aSelector,
                        (IMP)__c_t_resolveLostedMethod,
                        "v@:");
    }
    
    HandleUnrecognizedSELErrorBlock handleBlock = [NSObject handleUnrecognizedSELErrorBlock];
    if (handleBlock != nil) {
        handleBlock([self class], aSelector, UnrecognizedMethodTypeInstanceMethod);
    }
    
    return [TUndertakeObject sharedInstance];
}

+ (id)__t_forwardingTargetForSelector:(SEL)aSelector {
    if ([self respondsToSelector:aSelector]) {
        return self;
    }
    
    if (![TUndertakeObject respondsToSelector:aSelector]) {
        class_addMethod(objc_getMetaClass(class_getName([TUndertakeObject class])),
                        aSelector,
                        (IMP)__c_t_resolveLostedMethod,
                        "v@:");
    }
    
    HandleUnrecognizedSELErrorBlock handleBlock = [NSObject handleUnrecognizedSELErrorBlock];
    if (handleBlock != nil) {
        handleBlock([self class], aSelector, UnrecognizedMethodTypeClassMethod);
    }
    
    return [TUndertakeObject class];
}

@end

思路:

  • +load中替换forwardingTargetForSelector:方法(类中和元类中都进行替换)

  • TUndertakeObject为一个承载添加方法的类,使用单例,只用于添加缺失的方法

  • __t_forwardingTargetForSelector:中:

    • 判断本类是否包含aSelector,如果包含,返回自身;若不包含走下一步。

    • 使用 class_addMethod 对 TUndertakeObject 添加方法的实现(解决对象方法缺失问题)或对 TUndertakeObject 的元类添加方法的实现(解决类方法缺失问题);

    • 获取 handleBlock,调用block将具体信息传出去。

    • 返回 TUndertakeObject 的单例对象(解决对象方法缺失问题);或者 TUndertakeObject 的 Class对象(解决类方法缺失问题)

TPUSELNormalForwarding

// NSObject+TPUSELNormalForwarding.m

void __c_t_resolveLostedMethod(id self, SEL _cmd, ...) {}

@implementation NSObject (TPUSELNormalForwarding)

#pragma mark - ForwardInvocation

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        // exchange instance function in class
        method_exchangeImplementations(class_getInstanceMethod(self, @selector(methodSignatureForSelector:)),
                                       class_getInstanceMethod(self, @selector(__t_methodSignatureForSelector:)));
        method_exchangeImplementations(class_getInstanceMethod(self, @selector(forwardInvocation:)),
                                       class_getInstanceMethod(self, @selector(__t_forwardInvocation:)));

        // exchange class function in meta class
        method_exchangeImplementations(class_getClassMethod(self, @selector(methodSignatureForSelector:)),
                                       class_getClassMethod(self, @selector(__t_methodSignatureForSelector:)));
        method_exchangeImplementations(class_getClassMethod(self, @selector(forwardInvocation:)),
                                       class_getClassMethod(self, @selector(__t_forwardInvocation:)));

        // 后一部分等同于下面这种方式
        // Class metaClass = objc_getMetaClass(class_getName(self));
        // method_exchangeImplementations(class_getInstanceMethod(metaClass, @selector(methodSignatureForSelector:)),
        //                                class_getInstanceMethod(metaClass, @selector(__t_methodSignatureForSelector:)));
        // method_exchangeImplementations(class_getInstanceMethod(metaClass, @selector(forwardInvocation:)),
        //                                class_getInstanceMethod(metaClass, @selector(__t_forwardInvocation:)));
    });
}

- (NSMethodSignature *)__t_methodSignatureForSelector:(SEL)aSelector {
    if (![self respondsToSelector:aSelector]) {
        class_addMethod([self class],
                        aSelector,
                        (IMP)__c_t_resolveLostedMethod,
                        "v@:");
        HandleUnrecognizedSELErrorBlock handleBlock = [NSObject handleUnrecognizedSELErrorBlock];
        if (handleBlock != nil) {
            handleBlock([self class], aSelector, UnrecognizedMethodTypeInstanceMethod);
        }
    }
    return [self __t_methodSignatureForSelector:aSelector];
}

- (void)__t_forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    if ([self respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:self];
    } else {
        [self __t_forwardInvocation:anInvocation];
    }
}

+ (NSMethodSignature *)__t_methodSignatureForSelector:(SEL)aSelector {
    if (![self respondsToSelector:aSelector]) {
        class_addMethod(objc_getMetaClass(class_getName(self)),
                        aSelector,
                        (IMP)__c_t_resolveLostedMethod,
                        "v@:");
        HandleUnrecognizedSELErrorBlock handleBlock = [NSObject handleUnrecognizedSELErrorBlock];
        if (handleBlock != nil) {
            handleBlock([self class], aSelector, UnrecognizedMethodTypeClassMethod);
        }
    }
    return [self __t_methodSignatureForSelector:aSelector];
}

+ (void)__t_forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    if ([self respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:self];
    } else {
        [self __t_forwardInvocation:anInvocation];
    }
}

思路:

  • +load中替换methodSignatureForSelector:forwardInvocation:方法(类中和元类中都进行替换)

  • __t_methodSignatureForSelector:中:

    • 判断本类是否包含aSelector,如果包含,不进行处理,调用__t_methodSignatureForSelector去执行原函数;如果不包含,走下一步

    • 使用 class_addMethod 对自己的Class添加方法的实现(解决对象方法缺失问题)或对自己的的元类添加方法的实现(解决类方法缺失问题);

    • 获取 handleBlock,调用block将具体信息传出去。

    • 调用 __t_methodSignatureForSelector 去执行原函数获得添加后的aSelectorNSMethodSignature

  • __t_forwardInvocation:中,如果现在自己包含了对应的selector,则执行[anInvocation invokeWithTarget:self];;如果还不包含(应该不会有这一步),则调用__t_forwardInvocation去执行原函数


原文地址:LINK

项目地址:GitHub

参考资料:

runtime可运行修改版GitHub地址
从 NSObject 的初始化了解 isa
深入解析 ObjC 中方法的结构
从源代码看 ObjC 中消息的发送
Type Encoding
Apple Doc: load
Objective-C +load vs +initialize
Objective-C Runtime

正文到此结束
Loading...