iOS底层知识 -- runtime(运行时)详解
1.什么是运行时?
1>运行时是一套 纯C(C和汇编写的) 的API。而 OC 就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
2>编译器最终都会讲OC代码转换为运行时代码
我们先来看看官方函数objc_msgSend的声明:
/* Basic Messaging Primitives
*
* On some architectures, use objc_msgSend_stret for some struct return types.
* On some architectures, use objc_msgSend_fpret for some float return types.
* On some architectures, use objc_msgSend_fp2ret for some float return types.
*
* These functions must be cast to an appropriate function pointer type
* before being called.
*/
void objc_msgSend(void /* id self, SEL op, ... */ )
从这个函数的注释可以看出来了,这是个最基本的用于发送消息的函数。另外,这个函数并不能发送所有类型的消息,只能发送基本的消息。比如,在一些处理器上,我们必须使用objc_msgSend_stret来发送返回值类型为结构体的消息,使用objc_msgSend_fpret来发送返回值类型为浮点类型的消息,而又在一些处理器上,还得使用objc_msgSend_fp2ret来发送返回值类型为浮点类型的消息。
最关键的一点:无论何时,要调用objc_msgSend函数,必须要将函数强制转换成合适的函数指针类型才能调用。
从objc_msgSend函数的声明来看,它应该是不带返回值的,但是我们在使用中却可以强制转换类型,以便接收返回值。另外,它的参数列表是可以任意多个的,前提也是要强制函数指针类型。
例如:
[[NSObject alloc] init];
等于是给 NSObject 送一个消息 objc_msgSend() 如alloc方法 objc_msgSend(NSObject, @selector(alloc));
objc_msgSend();
每一句OC代码到最底层都是转换成 runtime 运行时代码
objc_msgSend的原型是:
id objc_msgSend(id theReceiver, SELtheSelector, ...)
参数分别是消息接收对象,消息对应的方法的标识SEL,以及参数。
在执行objc_msgSend方法时,主要完成了以下几个工作:
The messaging function does everything necessary for dynamic binding:
(1),It first finds the procedure (method implementation) that the selector refers to. Since the same method can be implemented differently by separate classes, the precise procedure that it finds depends on the class of the receiver.
它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的方法实现依赖于消息接收者的类型。
(2),It then calls the procedure, passing it the receiving object (a pointer to its data), along with any arguments that were specified for the method.
然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
(3),Finally, it passes on the return value of the procedure as its own return value.
最后,将方法实现的返回值作为该函数的返回值返回。
OC代码转换为C语言代码验证:
clang -rewrite-objc 的作用是把oc代码转写成c/c++代码,我们常用它来窥探OC的底层实现。
打开终端,来到main.m所在目录,执行如下命令:
执行之后,目录下多出一个main.cpp文件,然后就可以打开看看看具体实现
可以看到main函数的具体实现为
image.png
可以知道这句代码 [[NSObject alloc] init] 底层C语言实现为
((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
3>运行时可以做很多底层操作比如:
(1)动态添加对象的成员变量和成员方法
- runtime动态添加属性
应用场景
在分类中,所写的@property (nonatomic, strong) NSString *name;都仅仅是生成了get和set方法,并没有生成对应的_name属性,但是有时候我们会有一种需求,想要让分类中保存一下新的属性值,因为set和get方法只能是对已经有的东西做操作,比如说最常用的UIView的分类我们对frame中的x,y,width,height做操作。
解决
用runtime动态的给分类添加属性,并且另他产生关联,使用如下
.h文件实现
@property (nonatomic, strong) NSString *name;
.m文件实现
#import <objc/runtime.h>
// 动态添加属性
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, @"name");
}
-
runtime实现动态添加方法
由于OC是动态语言,所以只要声明了一个方法,那么这个对象就是可以调用这个方法的,无论这个方法是否实现。当执行这个方式时,发现没有被实现那么就会报错,通过可以使用runtime动态添加方法,来解决这个问题。其次,使用performSelector方法也可以给对象方法消息。
一般情况,只要是声明的方法一定要实现,但是这样做有定义的弊端就是无论这些方法是否要用,都会被实现,那么就会添加到相应的“方法编号区”、“方法列表区”、“方法区”这样就会消耗内存,其实可以使用runtime的动态添加方法来解决这一状况。
具体实现为:
.m文件
#import <objc/runtime.h>
// 动态添加方法
void addtest(id self, SEL sel) {
NSLog(@"动态添加方法%@", self);
}
//有未实现的 ‘对象方法’的时候就会调用这个方法,在这个方法中进行动态添加方法的处理
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"addtest")) {
//class: 给那个类添加方法
//SEL:添加那个方法
//IMP:方法实现 函数 函数入口 函数名
// type: 包含方法的参数
class_addMethod(self, sel, (IMP)addtest, "v@:");
return YES;
} return [super resolveInstanceMethod:sel];
}
(2)获得某个类的所有成员方法、所有成员变量
unsigned int count;
- 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
}
- 获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method---->%@",
NSStringFromSelector(method_getName(method)));
}
- 获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"Ivar---->%@",
[NSString stringWithUTF8String:ivarName]);
}
(3)动态交换两个方法的实现(特别是系统自带方法)
适用场景:iOS6(拟物化)-->iOS7(扁平化),从iOS7开始使用扁平化的图标风格,这代表着要把之前所有设置图片的地方重新设置一遍,工作比较繁琐,还适用于重大的促销节日,不同的节日显示不同的图标,等等
- 为UIImage添加拓展UIImage+Extension
具体实现方法:
#import "UIImage+Extension.h"
#import <objc/runtime.h>
@implementation UIImage (Extension)
// 当某个类或分类加载进内存时,会调用一次
+ (void)load{
// 得到类方法
// class_getClassMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
// 得到对象方法
// class_getInstanceMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(runtime_imageNamed:));
method_exchangeImplementations(m1, m2);
}
// 自定义新方法覆盖系统方法
+ (UIImage *)runtime_imageNamed:(NSString *)name{
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) { // 如果系统版本大于7.0 就加载另一套图片, 这里也可以加日期判断,如果是节日促销就显示不同的图标
name = [name stringByAppendingString:@"_os7"];
}
return [UIImage imageNamed:name];
}
2.如何利用运行时
1>将某些OC代码转为运行时代码,探究底层,比如block实现原理
2>拦截系统自带的方法调用,比如拦截imageNamed: viewDidLoad alloc(ObjC黑科技 - Method Swizzle)
3>实现字典和模型的自动转换
4>实现NSCoding属性的自动归档和自动解档
归档接档可以避免以下这样的代码
image.png
具体做法为
.h文件
#import <Foundation/Foundation.h>
@interface Model : NSObject<NSCoding>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;
@property (copy, nonatomic) NSString *name;
@end
.m文件
#import "Model.h"
#import <objc/runtime.h>
@implementation Model
/*
从文件中读取对象时会调用这个方法(开发者需要在此方法中说明那些属性需要提取出来)
*/
- (instancetype)initWithCoder:(NSCoder *)decoder{
if (self = [super init]) {
// 用来存储成员变量的数量
unsigned int outCount = 0;
// 获得当前类下所有的成员变量
Ivar *ivars =class_copyIvarList([self class], &outCount);
// 遍历所有的成员变量
for (int i = 0; i < outCount; i ++) {
// 取出i位置所有对应的成员变量
Ivar ivar = ivars[i];
// C语言代码转变为OC代码
// + (nullable instancetype)stringWithUTF8String:(const char *)nullTerminatedCString;
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获得key对应的值
id value = [decoder decodeObjectForKey:key];
// 设置到成员变量上
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
/*
将对象写入文件的时候会调用这个方法(开发者需要在此方法中说明要存储那些属性)
*/
- (void)encodeWithCoder:(NSCoder *)encoder{
// 用来存储成员变量的数量
unsigned int outCount = 0;
// 获得当前类下所有的成员变量
Ivar *ivars =class_copyIvarList([self class], &outCount);
// 遍历所有的成员变量
for (int i = 0; i < outCount; i ++) {
// 取出i位置所有对应的成员变量
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获得key对应的值
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
}
调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
Model *model = [[Model alloc] init];
model.age = 25;
model.name = @"Bob";
model.weight = 60.3;
[NSKeyedArchiver archiveRootObject:model toFile:@"/Users/bobzhou/Desktop/model.data"];
Model *model1 = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/bobzhou/Desktop/model.data"];
NSLog(@"%zd---%@---%lf", model1.age, model1.name, model1.weight);
}
5>实现分类增加属性(实现分类属性互不干扰)
在分类中声明一个@property,只会生成get和set方法的声明,并不会实现
场景:为所有的对象都增加一个name属性,任何对象都可以赋值调用该属性
为所有的对象增加属性,我们可以考虑为NSObject写一个拓展
.h文件实现
#import <Foundation/Foundation.h>
@interface NSObject (Extension)
/*
相当于是是有声明,没有方法的实现
- (void)setName:(NSString *)name
- (NSString *)name
*/
@property (nonatomic, copy) NSString *name;
@end
.m文件实现
/** 为所有OC 对象都添加一个属性 NSString *name*/
#import "NSObject+Extension.h"
#import <objc/runtime.h>
@implementation NSObject (Extension)
char NameKey;
- (void)setName:(NSString *)name{
// 将某个值 跟 某个对象关联起来 (将某个值 存储到 某个对象中)
/*
id _Nonnull object:表示关联者,是一个对象,变量名理所当然也是object
const void * _Nonnull key:获取被关联者的索引key
id _Nullable value:被关联者,这里是一个block
objc_AssociationPolicy policy: 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/
objc_setAssociatedObject(self, &NameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
// 利用参数key可以将对象object中存储的对应值取出来
return objc_getAssociatedObject(self, &NameKey);
}
调用如下:
/*
为所有的对象增加一个属相name
*/
NSObject *obj = [[NSObject alloc] init];
obj.name = @"rumtime-obj";
UITableView *tableview = [[UITableView alloc] init];
tableview.name = @"runtime-tableview";
NSArray *array = [NSArray array];
array.name = @"runtime-array";
NSLog(@"%@--%@--%@", obj.name, tableview.name, array.name);
打印结果如下: rumtime-obj+++++++runtime-tableview+++++++runtime-array
3.运行时常用的函数
- 得到类方法<objc/runtime.h>
class_getClassMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
- 得到对象方法<objc/runtime.h>
class_getInstanceMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>)
- 交换两个方法的实现<objc/runtime.h>
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
- 给某个对象发消息<objc/message.h>
objc_msgSend()
- 将某个值 跟 某个对象关联起来 (将某个值 存储到 某个对象中)
/*
id _Nonnull object:表示关联者,是一个对象,变量名理所当然也是object
const void * _Nonnull key:获取被关联者的索引key
id _Nullable value:被关联者,这里是一个block
objc_AssociationPolicy policy: 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
*/
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
- 利用参数key可以将对象object中存储的对应值取出来
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
- 获得当前类下所有的成员变量(outCount返回成员变量总数)
Ivar *ivars = class_copyIvarList([self class], &outCount);
- 获得成员变量名称
ivar_getName(Ivar _Nonnull v)
- 获得成员变量类型
ivar_getTypeEncoding(<#Ivar _Nonnull v#>)
- 释放内存(当C语言中包含copy,create,retain,new等词语,那需要在最后面释放资源)
free(ivars);