开篇
马上就要年底了再码一波,自己总结一下Runtime,打算总结一下Runtime的各种用法,结合一些常见的源码来分析一下,有错误在所难免,希望尽量少一点。。。
NullSafe与消息转发
在处理后台返回的数据时会碰到返回的空的情况,大家有自己的处理方式来增加代码的稳健性,这里就借常见的NullSafe的源码来举例。
NullSafe源码内容并不算很多,主要的实现代码如下
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
//look up method signature
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if (!signature)
{
//check implementation cache first
NSString *selectorString = NSStringFromSelector(selector);
signature = signatureCache[selectorString];
if (!signature)
{
@synchronized([NSNull class])
{
//check again, in case it was resolved while we were waitimg
signature = signatureCache[selectorString];
if (!signature)
{
//创建一个缓存 获取到所有的类名
//not supported by NSNull, search other classes
if (signatureCache == nil)
{
if ([NSThread isMainThread])
{
cacheSignatures();
}
else
{
dispatch_sync(dispatch_get_main_queue(), ^{
cacheSignatures();
});
}
}
//遍历缓存,寻找是否已经有可以执行此方法的类
//find implementation
for (Class someClass in classList)
{
if ([someClass instancesRespondToSelector:selector])
{
//其次如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;
signature = [someClass instanceMethodSignatureForSelector:selector];
break;
}
}
//cache for next time
signatureCache[selectorString] = signature ?: [NSNull null];
}
else if ([signature isKindOfClass:[NSNull class]])
{
signature = nil;
}
}
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
invocation.target = nil;
[invocation invoke];
}
主要方法
int objc_getClassList(Class *buffer, int bufferLen) //获取class列表
获取到,项目中类的所有类名。
static void cacheSignatures()
{
classList = [[NSMutableSet alloc] init];
signatureCache = [[NSMutableDictionary alloc] init];
//get class list
int numClasses = objc_getClassList(NULL, 0);
Class *classes = (Class *)malloc(sizeof(Class) * (unsigned long)numClasses);
numClasses = objc_getClassList(classes, numClasses);
//add to list for checking
for (int i = 0; i < numClasses; i++)
{
//determine if class has a superclass
Class someClass = classes[i];
Class superclass = class_getSuperclass(someClass);
while (superclass)
{
if (superclass == [NSObject class])
{
[classList addObject:someClass];
[classList removeObject:[someClass superclass]];
break;
}
superclass = class_getSuperclass(superclass);
}
}
//free class list
free(classes);
}
NullSafe的实现原理
把发送给NSNull的而NSNull又无法处理的消息经过如下几步处理:
- 创建一个方法缓存,这个缓存会缓存项目中类的所有类名( cacheSignatures()方法),并且对缓存查找,看是否有可以执行的类方法。
- 其次如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数。(想要走 forwardInvocation方法, 必须 先实现 methodSignatureForSelector 而且 返回 NSMethodSignature 必须不能为空。)
- 如果没有的话,返回nil,接下来会走forwardInvocation:方法。
- [invocation invokeWithTarget:nil];将消息转发给nil。
下面我们看一下Runtime的消息转发原理以及实现。
Runtime中的消息转发
原理我就不一个个敲了。如下:
消息的转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。第二阶段涉及“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接受者看看有没有其他对象处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一起如常。若没有“备援的接收者”,则启动完整的消息转发机制,运行期系统会把于消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。(深入理解Objective-C消息转发机制)
我们新建一个项目,定义一个goHome方法,但是不去实现,直接调用,可以见到如下错误
常见的错误
unrecognized selector sent to instance
因为我们的方法并没有实现,去调用,在消息的发送过程中并没有找到接受的对象去处理这个消息,导致了项目抛错。在Runtime中我们可以通过其他的方式去进行项目Crash的补救。
消息转发的三种处理方式
- 动态方法解析 接受者1
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)name
在调用方法时,对象在收到无法解读的消息后,首先将调用其所属类的上述类方法,然后在方法中判断方法名,利用class_addMethod动态添加新的方法名,IMP的功能是实现的新方法 必须有两个参数。这时我们再运行项目,调用 [self goHome];虽然没有实现,但是会走我们动态添加的方法
newGoHOme并打印数据。
- (void)viewDidLoad {
[super viewDidLoad];
[self goHome];
}
//1 防止crash
void newGoHOme(id self, SEL _cmd){
// implementation
NSLog(@"这里执行goHOme的打印");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
if (name == @selector(goHome)) {
class_addMethod([self class], name, (IMP)newGoHOme, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
return [super resolveClassMethod:name];
}
/****
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
IMP的功能是实现的新方法 必须有两个参数
****/
- 备援接收者2
- (id)forwardingTargetForSelector:(SEL)aSelector
如果接受者1并未执行,则消息传递到被接受者2处理。我们新建一个CompanyVC,设置并实现goHome方法,
- (void)viewDidLoad {
[super viewDidLoad];
[self goHome];
}
//执行其他已实现的方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
return [[CompanyVC alloc]init];
}
我们在ViewController中引入CompanyVC头文件,然后在forwardingTargetForSelector返回CompanyVC,然后运行项目,我们可以看到虽然我们仍然没有实现ViewController中的goHome方法,但是没有crash并且打印出了CompanyVC中方法的数据。
其实他实现的原理就是接收到转发信息时看当前是否能找到援助对象,如果有则将其返回,若找不到就返回nil。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回。这里找到了CompanyVC,所以不会崩溃。
- 备援接收者3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
如果消息转发到这一步基本就是一个完整的消息转发了。
完整的消息转发过程的图片,如下(手绘,可能有点丑😆)
如上图由1开始事件的调取,然后由2转发至3再转发至4进入如下代码
- (void)viewDidLoad {
[super viewDidLoad];
[self goHome];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 为方法生成方法签名,这个签名就是给下面的anInvocation调用的 invocation中有一个方法就是通过sig生成的
NSString *sel = NSStringFromSelector(aSelector);
if ([sel isEqualToString:@"goHome"]) {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // 方法签名
return sig;
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
CompanyVC *newHome = [CompanyVC new];
if ([newHome respondsToSelector:selector]) {
[anInvocation invokeWithTarget:newHome];
}
}
查找goHome方法,然后生成签名,给anInvocation去调用,一样执行了CompanyVC中的方法,同时避免了程序的Crash。
消息转发机制总结
如转发机制图,在由1到5的过程中,不管是2,还是3对接受到的消息进行了处理,就结束了一次消息的转发不会再往下进行消息的转发处理,只有2和3都没有找到相关的对象可以处理这个方法,转发消息给4 5处理,进行一次完整的消息转发过程。
如果所有的都无法找到处理这个方法的对象执行下一个方法
- (void)doesNotRecognizeSelector:(SEL)aSelector
之后调用
- (void)forwardInvocation:(NSInvocation *)anInvocation
到这里一样结束一次消息的转发过程。
到这里我们再去看之前的NullSafe就会清楚这个分类的实现,在接收到Null无法处理时利用转发机制对消息进行了处理,防止了项目Crash。
参考资料及拓展阅读
http://www.cocoachina.com/ios/20160830/17424.html
http://blog.csdn.net/mangosnow/article/details/36183535
http://blog.csdn.net/hello_hwc/article/details/49687543
http://blog.csdn.net/app_ios/article/details/52411076
http://www.jianshu.com/p/151edae1d6ee