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扩大响应范围"的那篇文章.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,703评论 0 9
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 750评论 0 3
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 730评论 0 2
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 757评论 0 1
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,551评论 33 466