RunTime之常用场景

在上篇中记录了几个常用的api的介绍,这篇主要系统的整理一下在平常项目开发中经常用到RunTime的场景,分别为"发送消息","消息转发","交换方法","动态添加方法","给分类添加属性"几种场景,通常第一种和第二种大多是共同作用的,接下来就分别介绍.
1.发送消息(void objc_msgSend)
在面向对象编程中,对象调用方法叫做"发送消息"。在编译时,程序的源代码就会从对象发送消息转换成Runtime的objc_msgSend函数调用。如我们平时写的源码[person say]其实就会转换成objc_msgSend(person, selector)其中selector可以理解成方法的唯一标示,是根据函数名以及参数生成的,这也就是为什么在同一个文件中不能有同名的函数的原因,但是在不同的文件中可以,因为每个文件中的实例方法都是分开的,就算是生成的selectoer相同但是存在两个不同的地方,找起来也不会出问题;
objc_msgSend函数的调用过程

第一步:检测这个selector是不是要忽略的。
第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉。
第三步:调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找法,如果找不到则会通过class的
super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super
_class向上一级父类结构体中查找,直至根class;当我们调用某个某个类方法时,它会首先通过自己的isa指针找到
metaclass(也就是元类),并从其中methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的
metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类
结构体中查找,直至根metaclass;
如果以上散步都找不到的话如果不进行其他的处理就会crash,报出找不到此方法的错误;但是如果我们在消息转发的过程中做处理的话就不同了,下面咱们就来看一下在消息转发的过程中怎么来做处理.

2.消息转发

第一步:
+(BOOL)resolveInstanceMethod:(SEL)sel;+(BOOL)resolveClassMethod:(SEL)sel;
当我们调用一个没有实现的方法的时候会通过上面两个方法来判断是否可以找到该方法的实现,如果返回NO,则会进入下一步(说明在解析方法的这一
步不做处理,在转发的其他过程处理,则处理进入第二个步骤),如果返回YES,说明要在此方法中处理,则可以通过class_addMethod来动态添加方法
的实现,消息得到处理,结束

第二步:
- (id)forwardingTargetForSelector:(SEL)aSelector
从字面上来理解这个方法就是转发这个方法到一个其他的target,也就是说如果在这个方法中返回一个可以执行这个方法的对象,也使这个消息得到处
理.比如你在Person类和Animal类中同时声明了-(void)eat方法,但是只实现了Person类中的方法,没实现Animal类中方法,但是你在控制器中又
调用了Animal中-(void)eat方法,如果不做处理肯定会crash,但是如果在消息转发的第二部也就是本方法中返回一个Person的对象,你会发现它执
行的是Person类中eat方法.
当然你在这一步中也可以不作处理也就是返回nil(没有指定可以实现这个方法的对象),则会进入消息转发的下一步.

第三步:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
如果在上一步返回nil则会进入此方法生成方法签名,如果此步骤方法中返回nil的话,直接调用doesNotRecognizeSelector:返回异常
如果正常生成方法签名,则进行最后一步。

第四步:
- (void)forwardInvocation:(NSInvocation *)anInvocation
我们可以通过anInvocation对象做很多处理,
比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。
如果失败,则进入`doesNotRecognizeSelector`方法,
若我们没有实现这个方法,那么就会crash

实例如下:
尝试在方法转发第一步中规避crash

首先在`Person`类中 在.h中声明两个方法,但不去实现:
 -(void)unKnowSel_obj; 
+(void)unKonwSel_class;
 在.m中实现这两个方法:
 -(void)noObjMethod{ 
NSLog(@"未实现这个实例方法");
 } 
+(void)noClassMethod{ 
NSLog(@"未实现这个类方法"); 
} 
并且重写消息转发的方法: 
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
 //注意:实例方法是存在于当前对象对应的类的方法列表中
 +(BOOL)resolveInstanceMethod:(SEL)sel{ 
SEL aSel = NSSelectorFromString(@"noObjMethod"); 
Method aMethod = class_getInstanceMethod(self, aSel);
 class_addMethod(self, sel, method_getImplementation(aMethod), "v@:"); 
return YES; 
} 
// 当一个类调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来. 
//注意:类方法是存在于类的元类的方法列表中 
+(BOOL)resolveClassMethod:(SEL)sel{ 
SEL aSel = NSSelectorFromString(@"noClassMethod"); 
Method aMethod = class_getClassMethod(self, aSel); 
class_addMethod(object_getClass(self), sel, method_getImplementation(aMethod), "v@:"); return YES; 
} 
在VC中调用未实现的两个方法:
Person* person = [[Person alloc] init]; 
[person unKnowSel_obj];
[Person unKonwSel_class];

打印结果

RuntimeTest[4503:948902] 未实现这个实例方法
RuntimeTest[4503:948902] 未实现这个类方法

接下来我们尝试在消息转发第二部中去处理(注意把消息转发中的第一步方法注释掉或返回NO)

声明一个`Boss`类,并在.m中实现方法:
 @implementation Boss 
-(void)unKnowSel_obj{ 
NSLog(@"unKnowSel_obj_Boss"); 
} 
@end 
在`Person`类中重写方法:
 -(id)forwardingTargetForSelector:(SEL)aSelector{
 return [[Boss alloc] init]; 
} 
在VC中调用未实现的两个方法: 
Person* person = [[Person alloc] init]; 
[person unKnowSel_obj];

打印结果

RuntimeTest[4540:956249] unKnowSel_obj_Boss

接下来接着测试消息转发第三四步

在`Person`类中重写方法:
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { 
if ([NSStringFromSelector(aSelector) isEqualToString:@"unKnowSel_obj"]) { 
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
 } 
return [super methodSignatureForSelector:aSelector];
 }
 -(void)forwardInvocation:(NSInvocation *)anInvocation{
 [anInvocation invokeWithTarget:[[Boss alloc] init]]; 
}
 在VC中调用未实现的两个方法: Person* person = [[Person alloc] init];
 [person unKnowSel_obj];

大家可以自行测试一下结果

3.交换方法(method_exchangeImplementations)
首先来理一理SEL,Method,IMP之间的关系
SELselector在Objective-C中的表示类型,而selector可以理解为区别方法的ID。
IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个消息后,这个函数指针决定了最终执行哪段代码.
Method是这样typedef struct objc_method *Method;声明的;是一个objc_method类型的结构体,在看objc_method是如下这样的

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;//方法名
    char *method_types                                       OBJC2_UNAVAILABLE;//方法类型,是一个char指针,存储着方法的参数类型和返回值类型
    IMP method_imp                                           OBJC2_UNAVAILABLE;//方法实现
} 

也就是说当我们调用一个函数时,编译过程中会根据函数名以及参数等信息生成一个可以表示这个函数的一个唯一标示,就是selector,并且会根据函数的实现以及函数的地址生成一个IMP(函数指针)来指向函数的真正内容,一般来说SEL和IMP是一一对应的,当运行时,就会根据唯一的标示来找到函数的实现来打到目的,因此动态交换方法其实就是在运行时,把SEL和IMP的关系进行重新整理,让原来的SEL指向另外一个IMP函数地址来打到交换的目的.实例如下

+(void)load{
    [super load];
//    交换实例方法
//    Method fromMethod = class_getInstanceMethod([self class], @selector(btAction:));
//    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingbtAction:));
//    //添加判断
//    if (!class_addMethod([self class], @selector(btAction:), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
//        method_exchangeImplementations(fromMethod, toMethod);
//    }
    //使用封装的方法进行测试
    [self swizzleSelectorOriginalSel:@selector(btAction:) withSwizzledSelector:@selector(swizzlingbtAction:)];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor lightGrayColor];
    self.bt = [UIButton buttonWithType:UIButtonTypeSystem];
    _bt.frame = CGRectMake(100, 100, 100, 100);
    [_bt setTitle:@"RunTime" forState:UIControlStateNormal];
    [self.view addSubview:_bt];
    _bt.backgroundColor = [UIColor redColor];
    [_bt addTarget:self action:@selector(btAction:) forControlEvents:UIControlEventTouchUpInside];

}
-(void)btAction:(UIButton *)bt{
    NSLog(@"原方法");
}
-(void)swizzlingbtAction:(UIButton *)bt{
    NSLog(@"替换方法");
    UIViewController *vc = [Mediar remotViewControllerWithClaStr:@"OneViewController"];
    [self presentViewController:vc animated:YES completion:nil];
}
#pragma mark - 封装替换方法
+(void)swizzleSelectorOriginalSel:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector {
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethodInit=class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethodInit) {
        class_addMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

在项目中有时会处理数组越界问题,你在NSArray的分类中添加安全取值方法,注意新添加的安全方法在分类中最后还是添加在了NSArray的methodlist中,但是由于数组是类簇的原因,因此平时调用的取值方法实际上如果数组是空的则会生成一个__NSArray0的对象,如果只有个元素则生成__NSSingleObjectArrayI,如果多与1个则是中__NSArrayI;但是对于可变数组来说都是__NSArrayM,不管是空数组还是只有一个元素的数组还是多个元素的数组;下面来看一个例子:

首先在分类中添加一个安全取值的方法
@implementation NSArray (safe)
-(id)objectAtIndexSafe:(NSUInteger)index{
    if (index>=self.count) {
        return nil;
    }
    else{
        return [self objectAtIndexSafe:index];
    }
}
@end
然后在+(void)load方法中交换方法
+(void)load{
    Method originalMethod_id = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id, swizzledMethod_id);
//当然你也可以不用exchange直接用setImplementaion来换掉IMP,但是这样的话分类中添加的方法中注意不能用自身调用自身
   // method_setImplementation(swizzledMethod_id, method_getImplementation(originalMethod_id));

    Method originalMethod_id1 = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id1 = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id1, swizzledMethod_id1);

    Method originalMethod_id2 = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"objectAtIndexSafe:"));
    Method swizzledMethod_id2 = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), NSSelectorFromString(@"objectAtIndex:"));
    method_exchangeImplementations(originalMethod_id2, swizzledMethod_id2);
}
接下来在-(void)viewdidload中测试
    NSArray *arr2 = [[NSArray alloc]init];
    NSString *str2 = [arr2 objectAtIndex:1];
    NSLog(@"%@",str2);

打印结果为:

RuntimeTest3[43662:8248532] (null)

4.给分类添加属性
可以参考之前写的"UIButton扩大响应范围"的那篇文章.

你可能感兴趣的:(RunTime之常用场景)