一、runtime验证Objective-C语言的底层实现(消息发送机制)
1,首先我们创建一个person类,添加一个实例方法eat,并实现eat方法
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat;
@end
#import "Person.h"
@implementation Person
- (void)eat{
NSLog(@"吃了");
}
@end
2,然后我们在ViewController里面来调用这个方法,首先我们导入#import "Person.h",并且实例化对象 ,我这里以2种常用方式来调用的方法,后面以消息发送机制来实现
Person *p = [[Person alloc] init];
方式1:[p eat];
方式2(通过performselector方法编号方式调用):[p performSelector:@selector(eat)];
performselector拓展:@selector() ---> Implementation(IMP:函数指针) 通过方法编号可以找到这个方法实现
方式3(消息发送机制):objc_msgSend(<#id _Nullable self#>, <#SEL _Nonnull op, ...#>)
使用这个方法需要导入#import <objc/message.h>头文件,而且依然是报错了,是编译时的错误(苹果不让你使用),因为Xcode自从5.0开始,苹果就不建议大家直接使用消息发送机制了,为什么不建议大家直接使用呢,因为RunTime的出现,运行时机制的出现,我们所写的OC代码底层都用运行时帮助你包装成了运行时的C语言代码,你会发现里面走的就是消息机制。
我们想要验证首先需要关掉Xcode的编译消息机制的检查,打开Build Settings,搜索msg,Enable Strict Checking of objc_msgSend calls 设置为NO。
objc_msgSend(p, @selector(eat));(第一个参数对象,第二个参数方法编号),运行结果成功,我这里就不截屏了。接下来我们既然了解了OC实现是以消息发送机制来做的,我们模拟一下,用消息发送机制的方式来实例
Person *p = [[Person alloc] init];
第一步在堆内开辟空间
Person *p = objc_msgSend([Person class], @selector(alloc));
在这里我想补充一下栈与堆内存的相关知识,栈的平衡:函数调用前后,栈指针是只想同一个地方的,栈:是系统分配的,堆:是程序员分配的,这里就涉及到了一个内存泄漏的问题,就是因为内存一直不释放,而且内存释放并不是把这块区域删除掉了,只是告诉系统这块区域可以用了!这块我提出一个问题:当内存释放之后,这块区域还有数据吗???
第二步初始化(依然是发送消息):
p = objc_msgSend(p, @selector(init));
运行结果
综上所述,让我门来看一下OC变异成的C++代码
重新创建一个工程,选择command Line Tool(命令行工具)
创建Person类,然后在main里面导入头文件,只需要初始化person就可以了
#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
}
return 0;
}
然后show in finder这个main文件,打开终端 cd 到目录下,执行ls命令,执行命令重写main.m
clang -rewrite-objc main.m 这里生成了一个main.cpp文件,打开这个文件
这就是初始化的person代码,是不是很熟悉,嗯,就是消息发送机制
二、runTime的运行时机制
苹果为我们提供一个一整套API,是相对底层的C语言API
runtime是OC的底层实现,可以进行一些非常底层的操作,用OC无法实现的
#import <objc/runtime.h>
typedef struct objc_method *Method; 成员方法
typedef struct objc_ivar *Ivar; 成员变量
这里带入一个HOOK思想(面向切面编程),改变原有的方法,动态的去修改这个方法
利用runtime来实现方法欺骗
下面一个应用场景来解决OC不严谨的问题
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"%@",request);
当创建一个url的时候,若果url错误,那么下面创建NSURLRequest也就没有了意义,我门可以这样解决
为url创建一个cagetory,可以进行一个方法的扩展来判断路径是否正确,这就到这我每当用到路径的时候都需要先导入头文件,在替换方法
+ (instancetype)HK_urlWithString:(NSString *)str{
NSURL *url = [NSURL HK_urlWithString:str];
if (url == nil) {
NSLog(@"空了");
}
return url;
}
然而我们可以使用HOOK思想来做这件事,这里需要用到两个方法
HOOK:勾住一个方法,动态改变这个方法
class_getClassMethod 获取方法
method_exchangeImplementations 交换方法
具体实现
//当这个累加载进内存的时候
+(void)load{
//获取method
Method URLWithString = class_getClassMethod([NSURL class], @selector(URLWithString:));
Method HK_urlWithString = class_getClassMethod([NSURL class], @selector(HK_urlWithString:));
//交换方法
method_exchangeImplementations(URLWithString, HK_urlWithString);
}
+ (instancetype)HK_urlWithString:(NSString *)str{
NSURL *url = [NSURL HK_urlWithString:str];
if (url == nil) {
NSLog(@"空了");
}
return url;
}
这样我们每次使用URLWithString的时候都会偷偷的把这个方法替换掉,进行一次判断!这样做是不是很好呢!,但是这样做有一个严重的问题,慎用,有可能对你的小伙伴造成疑惑(如果对runtime不了解的话)!方法固然好用,但是一定要注意应用场景!!!
在此附上一幅Hank老师的图
三、动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@",NSStringFromSelector(sel));
/*
cls: 类类型
sel: 方法编号
imp: 方法实现(函数指针)
types:(返回值&参数 类型)
*/
class_addMethod([Person class], sel, (IMP)eat, "v@:@");
return [super resolveInstanceMethod:sel];
}
//OC方法的隐式参数
//1、方法的调用者 id self
//2、方法编号 SEL _cmd
void eat(id self, SEL _cmd,NSString *str){
NSLog(@"吃了%@---%@---%@",str,self,NSStringFromSelector(_cmd));
}