#iOS Runtime 从初识到实践
** 本文适用于没有揭开runtime神秘面纱的读者参考, 大神看到这里的话还希望向小弟甩点墨水, 增长学识! **
Runtime简介:
它是个运行时的库,基本是C和汇编写的。可以把一些工作从编译推迟到运行时处理, 也就是说是运行时系统来执行编译后的代码. 因为其动态性, 所以我们可以在程序运行的时候为类添加,修改,匹配方法等.
初识Runtime:
我们所创建的对象或者说类在runtime中是结构体的形式存在的, 进入runtime的头文件即可获悉
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method; // 描述一个方法
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar; // 实例变量
/// An opaque type that represents a category.
typedef struct objc_category *Category; // 分类
/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t; // 类中属性
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指针, 实例的指针指向类对象象,类对象的指针指向元类
#if !__OBJC2__
Class super_class //指向父类 OBJC2_UNAVAILABLE;
const char *name //类的名称 OBJC2_UNAVAILABLE;
long version //类的版本信息 OBJC2_UNAVAILABLE;
long info // 类的信息 OBJC2_UNAVAILABLE;
long instance_size //该类实例变量的大小 OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars //成员变量列表 OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists //方法列表 OBJC2_UNAVAILABLE;
struct objc_cache *cache //缓存列表(对于已经调用的方法会存入其中,下次调用优先从缓存找) OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols //协议列表 OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
相信看到这如果我问你category为什么不能添加属性?(不能在.h中写成员变量,却可以写属性,但调用的时候会crash) 你应该可以知道了吧! 但可以通过其它方式间接实现,详情一会会写到**
Runtime作用
1. 发送消息
方法的调用即是让对象发送消息, 运行时会转换成objc_msgSend, 使用消息机制须#import <objc/message.h>
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现
Cat *cat = [Cat new];
// 直接调用
[self eatFish];
// 运行时会转成
objc_msgSend(cat, @selector(eatFish));
2. 动态添加方法
当我们调用[cat performSelector:@selector(play)];的时候程序会carsh, 因为没有实现play方法, 怎么办呢? 可以通过下面的方式解决
@implementation Cat
void c_play(id self,SEL sel) // 如果不写括号里的self和_cmd会隐式添加 self为方法调用者 _cmd为方法编号
{
NSLog(@"自己玩");
}
//消息接收者没有找到对应的方法时候,会先调用此方法,我们拦截没实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(play)) {
//这里我们添加play方法
// 参数1:哪个类添加方法 2:方法编号 3:方法实现的函数地址 4:函数类型
//函数类型解释:v代表没有返回值void, @:代表对象, :代表SEL
class_addMethod(self, @selector(play), c_play, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
这样调用[cat performSelector:@selector(play)]程序跑起来的时候就不会crash了, 因为我们已经动态添加了;若添加类方法为resolveClassMethod, 同理的.
3. 方法的交换
**若有这样一个需求, 觉得系统的功能不太受用, 在无法改变系统方法的前提下, 为这个方法添加一些功能, 比如即使数组中插入nil时也不让其crash, 用imageNamed:这个方法时候知道图片到底加载成功了没 **
你有可能想到的是继承或者重写该方法(但分类中无法调用super,重写会覆盖之前功能), 下面我们用runtime试试看
@implementation UIImage (Image)
// 分类在内存中的时候很早就被调用的一个方法,一般都在它中实现方法的交换
+ (void)load
{
// 获取类方法imageNamed:
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
// // 获取类方法rt_imageNamed:
Method m2 = class_getClassMethod([UIImage class], @selector(rt_imageNamed:));
// 交换彼此方法的实现
method_exchangeImplementations(m1, m2);
}
// 需要交换的方法实现
+ (UIImage *)rt_imageNamed:(NSString *)imageName
{
// 可能有读者会问,这不是死循环了吗, no, 我们在load里已经交换了方法实现了哦, 所以看似调用自己而已
UIImage *image = [UIImage rt_imageNamed:imageName];
//功能
if (image == nil) {
// do something
}
return image;
}
@end
4. 添加属性
我们之前说过如何变相的给category添加属性, 答案是objc_getAssociatedObject, 也是大家常用的一个函数, 即关联.
值得注意的是关联对象不是为类\对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得, 至于后面这两个函数我们稍后实践)
使用场景: 例如给NSArray添加一个name属性,我们这里不用继承实现
@interface NSArray (TestAssociated)
@property (nonatomic, copy)NSString *name;
@end
#import "NSArray+TestAssociated.h"
@implementation NSArray (TestAssociated)
static char associatedKey;
- (void)setName:(NSString *)name{
//参数1: 给谁添加关联 2: 关联的key(通过key获取关联的对象) 3: 被关联的对象 4: 关联策略(点进入看一下就知道了)
objc_setAssociatedObject(self, &associatedKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
// 同上
return objc_getAssociatedObject(self, &associatedKey);
}
@end
这样我们就不仅能用tag值了, 使用就这样
NSArray *arr = @[];
arr.name = @"111";
NSLog(@"%@", arr.name);//打印的值为111
5. 打印对象的属性等
unsigned int i = 0;
// 打印当前对象的属性
objc_property_t *pros = class_copyPropertyList([self class], &i);
for (int j = 0; j < i; j++) {
NSString *name = @(property_getName(pros[j]));
NSLog(@":%@", name);
}
// 同理我们可以打印更多的东西从头文件可知
objc_ivar_list //成员变量列表
objc_method_list //方法列表
...
如果你看到了这里, 你现在一定恍然大悟, 原来那些大神们写的json转model就是这么实现的啊!
使用总结: 对对象进行操作的方法一般以object_开头
对类进行操作的方法一般以class_开头
对类或对象的方法进行操作的方法一般以method_开头
对成员变量进行操作的方法一般以ivar_开头
对属性进行操作的方法一般以property_开头开头
对协议进行操作的方法一般以protocol_开头
总结:关于runtime的使用还有更多更多, 我只介绍了冰山一角,或者说助你看清了轮廓. 实际做项目中我们很少时候用到它, 但实现强大的功能时,往往却离不开它, 例如: json转model功能, 调用私有函数并且绕过苹果审核等!