TPreventUnrecognizedSEL
项目的GitHub
地址: https://github.com/tobedefined/TPreventUnrecognizedSELTPreventUnrecognizedSEL
的使用方法: 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
指向自身
这里有一张经典的图:
关于 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
的函数的地址
运行函数
对象方法:
*** 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
之中
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都有效)
// 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,并在在适当的时候调用,具体参数的信息如下
cls
: Class
类型;为缺失方法的类或对象的Class,可使用NSStringFromClass(cls)
返回类名字符串
selector
: SEL
类型;为所缺失的方法名,可使用NSStringFromSelector(selector)
返回方法名的字符串
methodType
: UnrecognizedMethodType
类型;为所缺失的方法类型(类方法or对象方法)
核心代码如下:
// 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对象
(解决类方法缺失问题)
// 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
去执行原函数获得添加后的aSelector
的NSMethodSignature
在__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