什么是Runtime
- 我们的代码在运行过程中都会转化为runtime的C代码执行。如
[target doSomething]
都会被转化为objc_msgSend(target, @selector(doSomething))
。 - OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
- 相关定义
// 描述类中的一个方法
typedef struct objc_method *Method;
// 实例变量
typedef struct objc_ivar *Ivar;
// 类别Category
typedef struct objc_category *Category;
// 类中声明的属性
typedef struct objc_property *objc_property_t;
- 获取列表
我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。
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]);
}
//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
objc_msgSend消息发送
//调用方法
(
(
void (*)(id, SEL)//返回值void (*)(任意类型,方法)
)
objc_msgSend
)
( //对象,方法名
[[Person alloc]init], sel_registerName("eat")
);
//调用传参
(
(
void (*)(id, SEL, NSString *) //返回值void (*)(任意类型,方法名,传入参数)
)
objc_msgSend
)
( //对象,方法名,传入参数
[[Person alloc] init], sel_registerName("eat:"), @"geyang"
);
//调用传参有返回值
NSString *name =(
(
NSString* (*)(id, SEL, NSString *) //返回值NSString* (*)(任意类型,方法名,传入参数)
)
objc_msgSend
)
( //对象,方法名,传入参数
[[Person alloc] init], sel_registerName("whoeat:"), @"geyang"
);
NSLog(@"%@", name);
Person.m
- (void)eat{
NSLog(@"吃");
}
- (void)eat:(NSString *)name{
NSLog(@"%@", name);
}
- (NSString *)whoeat:(NSString *)name{
return name;
}
动态添加方法
- 方法调用的过程
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行。
3.如果没找到,去父类指针所指向的对象中执行1,2.
4.以此类推,如果一直到根类还没找到,转向拦截调用。
5.如果没有重写拦截调用的方法,程序报错。
- 拦截调用
方法调用中说到没有找到方法会转向拦截调用。
拦截调用就是,在找不到调用的方法程序奔溃之前,你有机会通过重写NSObject
的四个方法来处理。
//调用一个不存在的 类方法触发
+(BOOL)resolveClassMethod:(SEL)sel;
//调用一个不存在的 实例方法触发
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/*
重定向
当resolveInstanceMethod方法没有处理,
即返回NO或者直接返回了YES而没有添加方法,该方法被调用
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;
/*
消息转发
当重定向方法返回了nil,则methodSignatureForSelector会被调用
系统会询问我们要一个合法的『类型编码(Type Encoding)』
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
//当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- 实例
MethodOne *method = [[MethodOne alloc]init];
//要求执行addMethod方法 但是类中没有声明该方法
[method performSelector:@selector(addMethod) withObject:nil];
MethodOne.m
/**
动态添加eat方法
第一个参数cls:给哪个类添加方法
第二个参数name:添加方法的方法编号
第三个参数imp:添加方法的函数实现(函数地址)
第四个参数types:函数的类型
v :表示函数的返回值void
@ :表示参数id self
: :表示SEL对象
*/
//没有找到addMethod方法后触发
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//判断名称是否一致(是否应没有addMethod方法而触发)
if ([NSStringFromSelector(sel) isEqualToString:@"addMethod"]) {
//动态添加一个方法dynamicAddMethod
class_addMethod(self, sel, (IMP)dynamicAddMethod, "v@");
}
return YES;
}
//定义动态添加的方法
void dynamicAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"这是动态添加的方法");
}
在没有找到`addMethod`方法时,
触发了`+ (BOOL)resolveInstanceMethod:(SEL)sel`方法
我们动态的添加一个方法。
那么就会执行 dynamicAddMethod 方法
当resolveInstanceMethod
方法没有处理, 即返回NO或者直接返回了YES而没有添加方法,我们可以做重定向
处理
//我们可以指定一个可以响应该方法的对象。返回self则会死循环。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(addMethod:)) {
//注意:Person类中定义了一个方法:-(void) addMethod;
return [[Person alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
那么就会执行 Person 类中的 addMethod 方法
当重定向
方法返回了nil
,我们可以做最后一步处理:消息转发
//方法会被调用,系统会询问我们要一个合法的类型编码
//若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}
//转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//重新设置方法的选择器
[anInvocation setSelector:@selector(jus)];
//指定消息的接受者
[anInvocation invokeWithTarget:self];
}
- (void)jus{
NSLog(@"这是转发后的执行输出");
}
那么就会执行jus方法
关联对象
现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。这种情况的一般解决办法就是继承。但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。这个时候,runtime的关联属性就发挥它的作用了。
首先创建一个UIButton
的分类 UIButton+GYButton
UIButton+GYButton.h
#import <UIKit/UIKit.h>
//在分类中添加一个按钮点击事件的block回调
typedef void(^Click)(UIButton *sender);
@interface UIButton (GYButton)
@property (nonatomic, copy) Click click;
@property (nonatomic, strong) NSString *names;//添加属性
@end
UIButton+GYButton.m
//首先定义一个全局变量,用它的地址作为关联对象的key
static id obj;
//手动添加属性names的getter与setter方法
- (NSString *)names{
return objc_getAssociatedObject(self, &name);
}
- (void)setNames:(NSString *)names{
//设置关联对象
objc_setAssociatedObject(self, &name, names, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//按钮点击事件的block回调
- (void)setClick:(Click)click{
/*
object : 源对象
key : 关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象
value : 被关联的对象(当前要被替换的)
policy : 是一个枚举值,表示关联对象的行为 copy/retain/nonatomic...
*/
//设置关联对象
objc_setAssociatedObject(self, &button, click, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (click) {
[self addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
}
}
- (Click)click{
return objc_getAssociatedObject(self, &button);
}
//按钮的点击事件
- (void)buttonClick:(UIButton *)sender{
if (self.click) {
self.click(sender);
}
}
调用
//4.给分类添加属性
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(30, 30, 50, 50)];
btn.backgroundColor = [UIColor lightGrayColor];
[btn setTitle:@"Click" forState:UIControlStateNormal];
btn.names = @"123";
NSLog(@"%@", btn.names);//输出123
//按钮的点击事件 不需要再使用addTarget方法
btn.click = ^(UIButton *sender){
NSLog(@"点击了按钮:%@", sender.titleLabel.text);
};
方法交换
方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
首先创建一个UIImage
的分类UIImage+GYImage
UIImage+GYImage.m
//load方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换
+ (void)load{
//方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//获取交换后方法
Method GYimageName = class_getClassMethod(self, @selector(GYimageName:));
//获取交换原的方法
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
//交换方法 (新方法,旧方法)
method_exchangeImplementations(GYimageName, imageName);
});
}
// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
+ (instancetype)GYimageName:(NSString *)name{
UIImage *image = [self GYimageName:name];
if (image == nil) {
NSLog(@"未加载到图片");
}
return image;
}
调用
UIImage *image = [UIImage imageNamed:@"123"];
//如果没有名为123的图片,则会打印出 “未加载到图片”。