iOS Runtime实用详解(一)
2017-07-28 本文已影响65人
handsome5
基本概念
- 了解 C/C++编译
- C/C++编译就是将C/C++的代码映射到相应的机器码,编译过程包括几个部分分别是编译,汇编和链接(具体可以去看下汇编语言基础知识)。在函数的编译中,C++和C语言的编译方式是不同的,C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现)。 在C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。这个重命名的过程是通过一个特殊的算法来实现的,称为名字编码(Name Mangling)。但函数的调用都是在编译的时候会决定调用哪个函数。
- Object-C是根据C语言所衍生出来的语言,继承了C语言的特性,是扩充C的面向对象编程语言,它与C有必然的联系,但是在编译的时候又有本质的区别,对于oc方法(函数),属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数调用,这就是我们所了解的运行时系统 (runtime system),这里有几个概念必须需要掌握:
- 动态特性
Objective-C 具有相当多的动态特性,表现在三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时才会做一些事情。- 动态类型:及运行时再决定对象的类型。这类动态类型在日常应用中非常常见。简单说就是id类型。实际上静态类型因为其固定性和可预知性而使用的非常广泛,静态类型是强类型,而动态类型属于弱类型。运行时决定接受者。
- 动态绑定 :基于动态类型,在某个实例对象被确定后,其类型就被确定了。该对象的属性和响应的消息也被完全确定,这就是动态绑定。
- 动态加载 根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,让程序在运行时添加代码块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
- 消息机制
-
消息发送是 OC底层的底层操作(引入 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。删除掉一些强制转换语句,可以看到调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。简单点说函数(方法)的调用就发送消息,但是Xcode5.0开始,苹果就不建议开发者直接使用消息机制(5.0后苹果这时引用runtime,runtime对消息机制作了封装,内部就是runtime的底层实现)。但是我们一定要自己实现消息机制发送,怎么发呢?我们在编译设置Build Setting的搜索输入框,输入msg,把Yes设置成No即可发送
Snip20170728_16.png
-
- 动态特性
//Person *p = [[Person alloc] init]; 底层发送是这样
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
p = objc_msgSend(p, sel_registerName("init"));
//把p替换下,
Person *p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
// p对象发送方法
objc_msgSend(p, sel_registerName("eat"));
接下来我们去验证下oc底层发送,如何验证请看下面
Snip20170728_18.png
关掉工程 command + S,找到消息验证目录,输入clang -rewrite-objc main.m 生成main.cpp
Snip20170728_19.png
最后打开main.cpp 把main.cpp拖到底部,如图
Snip20170728_21.png
看到的Person跟我们发送的完全一样,其实从这里我们可以发现objc_msgSend的函数调用过程:
第一步:检测这个selector是不是要忽略的;
第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉
第三步:
调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,
如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,
如果仍找不到则继续通过super_class向上查找知道metaclass;
调用类方法时,首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,
如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体
第四步:如果前三步都找不到方法则进入动态方法解析
消息动态解析具体流程
第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;
第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三步;
第三步:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;
第四步:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。
-
isa指针
来自网络获取图片.png -
消息动态解析
动态解析流程图(图片来自网络).png - runtime
- runtime 也就是所谓的“运行时”,是一个机制,oc的底层实现,runtime是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数),需要引入< <objc/runtime.h>;
- runtime的运用
- 拦截系统自带的方法调用(Method Swizzling黑魔法),利用runtime 将系统的方法实现和我们自定义的方法实现进行交换
- runtime动态添加方法
- 动态创建一个类((比如KVO的底层实现))
- 实现字典的模型和自动转换
- 实现NSCoding的自动归档和接档
- ...
//获取类方法
class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
//获取实例方法
class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
案例实现:
#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
+ (void)load {
Method CustomString = class_getClassMethod([NSURL class],@selector(TestURLWithString:));
Method URLString = class_getClassMethod([NSURL class], @selector(URLWithString:));
method_exchangeImplementations(URLString, CustomString);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
}
@end
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)
#pragma mark - 这里运用了runtime
+ (instancetype)TestURLWithString:(NSString *)str
{
NSURL *url = [NSURL TestURLWithString:str];
if (!url) {
NSLog(@"url为空");
}
return url;
}
@end
- runtime动态添加方法
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
//当这个类被调用类一个没有实现的类方法!就会来这里
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//
//}
//当这个类被调用类一个没有实现的对象方法!就会来这里
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%@",NSStringFromSelector(sel));
//动态的添加方法
if (sel == sel_registerName("eat")) {
/**
class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, const char *types)
1.Class 类类型
2.SEL name 方法编号
3.IMP implementation的简称,方法的实现,就是一个函数指针,指向一个实现
4.type 返回类型 command + shift + 0 快速打开文档,搜索class_addMethod里面Parameters types 可以看到一张表,这里v表示无返回值(void)。
*/
class_addMethod([Person class], sel, (IMP)eat, "v");
}
return [super resolveInstanceMethod:sel];
}
//记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数
void eat(id self, SEL _cmd) {
NSLog(@"哥们调用了%@的%@方法",self,NSStringFromSelector(_cmd));
NSLog(@"哥们吃了");
}
////记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数,带了个参数
//void eat(id self, SEL _cmd,id objc) {
//
// NSLog(@"哥们调用了%@的%@方法%@",self,NSStringFromSelector(_cmd),objc);
//
//
//}
@end
#import "ViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p performSelector:@selector(eat)];
//带参数的测试 懒加载方法
//[p performSelector:@selector(eat) withObject:@"测试"];
}
@end
- runtime动态创建一个类((KVO的底层实现))
// KVO的底层实现原理
// KVO监听的是setter方法
#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"
@interface ViewController ()
@property (nonatomic, strong)Person *p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
//KVO的基本写法
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
_p = p;
//自定义 kvo底层
[p test_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"观察到了%@的%@属性的变化了%@",object,keyPath,change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// KVO监听的是setter方法,
//一个类的类型只能通过isa指针来看
/*如果name是成员变量的话,KVO就不能监听了*/
_p.name = @"test";//简单的说底部实现,就是利用runtime创建了个子类对象
}
@end
#import "NSObject+KVO.h"
#import <objc/message.h>
@implementation NSObject (KVO)
- (void)test_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
/**
1.自定义NSTestKVO子类
2.重写set方法,在内部恢复父类的做法,通知观察者
3.修改self的isa指针!!恢复指向自定义的NSTestKVO子类!
*/
//动态生成一个类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [@"NSTestKVO" stringByAppendingString:oldClassName];
const char *newName = [newClassName UTF8String];
/**定义一个类
1.继承那个类
2.类的名称
*/
Class MyClass = objc_allocateClassPair([self class], newName, 0);
//重写setName方法,这里写死了的
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//注册该类
objc_registerClassPair(MyClass);
//修改self的isa指针,指向自定义的子类MyClass
object_setClass(self, MyClass);
//将观察者保存到当前对象
objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void setName(id self, SEL _cmd,NSString *newName){
//保存当前类型
Class class = [self class];
//改变isa指针
object_setClass(self, class_getSuperclass(class));
//调用父类的set方法
objc_msgSend(self, @selector(setName:),newName);
//拿出观察者
id objc = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
//通知观察者, 发送消息,注意设置msg
objc_msgSend(objc,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,nil,nil);
//改回子类类型,否则没法继续监听
object_setClass(self, class);
}
@end
- runtime 实现字典的模型和自动转换
- 案例
{
"age" : "<null>",
"money" : "1000",
"cat" : {
"name" : "Persia",
"price" : "500",
"fish" : {
"name" : "鱼",
"weight" : 50
}
},
"students" : [
{
"name1" : "苦逼的码农",
"price1" : 20.8,
"publisher1" : "清华大学出版社"
}
],
"books" : [
{
"name" : "C语言程序设计",
"price" : 20.8,
"publisher" : "清华大学出版社",
"bookUsers" : [
{
"name1" : "苦逼的码农",
"price1" : 20.8,
"publisher1" : "清华大学出版社"
}
]
},
{
"name" : "乔布斯传",
"price" : 50.2,
"publisher" : "苹果出版社",
"bookUsers" : [
{
"name1" : "乔布斯传",
"price1" : 50.2,
"publisher1" : "苹果出版社"
}
]
}
],
"name" : "Tom",
"height" : "181"
}
#import <Foundation/Foundation.h>
@interface NSObject (JSONExtension)
- (void)setDict:(NSDictionary *)dict;
+ (instancetype )objectWithDict:(NSDictionary *)dict;
// 告诉数组中都是什么类型的模型对象
-(NSString *)arrayObjectClassWithKey:(NSString *)keyword;
@end
#import "NSObject+JSONExtension.h"
#import <objc/runtime.h>
@implementation NSObject (JSONExtension)
- (void)setDict:(NSDictionary *)dict {
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil || [value isEqual:[NSNull class]]) continue;
// 获得成员变量的类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 如果属性是对象类型
NSRange range = [type rangeOfString:@"@"];
if (range.location != NSNotFound) {
// 那么截取对象的名字(比如@"Dog",截取为Dog)
type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
// 排除系统的对象类型
if (![type hasPrefix:@"NS"]) {
// 将对象名转换为对象的类型,将新的对象字典转模型(递归)
Class class = NSClassFromString(type);
value = [class objectWithDict:value];
}else if ([type isEqualToString:@"NSArray"]) {
// 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
NSArray *array = (NSArray *)value;
NSLog(@"key====:%@",key);
NSMutableArray *mArray = [NSMutableArray array];
// 获取到每个模型的类型
id class ;
if ([self respondsToSelector:@selector(arrayObjectClassWithKey:)]) {
NSString *classStr = [self arrayObjectClassWithKey:key];
class = NSClassFromString(classStr);
}
else {
NSLog(@"数组内模型是未知类型");
return;
}
// 将数组中的所有模型进行字典转模型
for (int i = 0; i < array.count; i++) {
[mArray addObject:[class objectWithDict:value[i]]];
}
value = mArray;
}
}
// 将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
free(ivars);
c = [c superclass];
}
}
+ (instancetype )objectWithDict:(NSDictionary *)dict {
NSObject *obj = [[self alloc]init];
[obj setDict:dict];
return obj;
}
@end
#import "ViewController.h"
#import "Person.h"
#import "User.h"
#import "NSObject+JSONExtension.h"
#import "Book.h"
#import "BookUsers.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self json];
}
/// 字典转模型demo
- (void)json {
NSString *path = [[NSBundle mainBundle] pathForResource:@"Model2.json" ofType:nil];
NSData *jsonData = [NSData dataWithContentsOfFile:path];
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
User *user = [User objectWithDict:json];
Book *book= user.books[0];
Student *englistBook = user.students[0];
//NSLog(@"%@",book.name);
}
model
#import <Foundation/Foundation.h>
#import "Cat.h"
@interface User : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double height;
@property (nonatomic,assign) int age;
// 属性是一个对象
@property (nonatomic,strong) Cat*cat;
// 属性是一个数组
@property (nonatomic,strong) NSArray *books;
// 属性是一个数组
@property (nonatomic,strong) NSArray *students;
@end
#import "User.h"
@implementation User
//返回数组中都是什么类型的模型对象
//- (NSString *)arrayObjectClass {
// return @"Book";
//}
- (NSString *)arrayObjectClassWithKey:(NSString *)key {
if ([key isEqualToString:@"books"]) {
return @"Book";
}else
{
return @"Student";
}
}
@end
#cat model
#import <Foundation/Foundation.h>
#import "Fish.h"
@interface Cat : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double price;
// 属性是一个对象
@property (nonatomic,strong) Fish *fish;
@end
#import "Cat.h"
@implementation Cat
@end
#Book model
#import <Foundation/Foundation.h>
#import "Book.h"
@interface Book : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double price;
@property (nonatomic,copy) NSString *publisher;
@property (nonatomic,strong)NSArray *bookUsers;
@end
#import "Book.h"
@implementation Book
//返回数组中都是什么类型的模型对象
- (NSString *)arrayObjectClass {
return @"BookUsers";
}
@end
#BookUsers model
#import <Foundation/Foundation.h>
@interface BookUsers : NSObject
@property (nonatomic,copy) NSString *name1;
@property (nonatomic,assign) double price1;
@property (nonatomic,copy) NSString *publisher1;
@end
#import "BookUsers.h"
@implementation BookUsers
@end
本文参考文献
(Runtime基本原理及Demo)http://www.jianshu.com/p/e28f6b279f5f#
([iOS] runtime 的使用场景--实战篇)http://www.jianshu.com/p/07b6c4a40a90