前言

系列第五篇:将展示几个runtime的例子以及runtime的实际应用。这些例子包括:

  1. 动态添加属性以及添加用来存储属性对应的实例变量的关联对象
  2. 方法决议:父类实现resolveInstanceMethod,然后在子类中添加方法
  3. 消息转发示例:
    1. forwardingTargetForSelector
    2. forwardInvocation

本文的demo代码可以在我的github上找到

几个有趣的示例

动态添加任意属性

class_addProperty runtime添加属性API

class_addMethod runtime添加方法API

仅仅添加属性是没什么用的,因为还需要添加属性对应的实例变量。

虽然runtime提供了class_addIvar方法来给类添加实例变量,但是注意,该方法只能在创建新的类的时候才能使用;对于已经存在的类,是不允许添加实例变量的

鉴于上述原因,所以可以采用动态添加关联对象来存储属性对应的实例变量。

添加属性

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)

  1. cls : 要添加属性的类
  2. name : 添加属性的名字
  3. attributes : 属性的特性
  4. attributeCount : 属性特性数量
1
2
3
4
5
6
// runtime add property
objc_property_attribute_t attribute1 = {"T", @encode(NSString *)};
objc_property_attribute_t attribute2 = {"N", ""};
objc_property_attribute_t attribute3 = {"&", ""};
objc_property_attribute_t attribute[] = {attribute1, attribute2, attribute3};
class_addProperty([HCBase class], "sb", attribute, 3);

如上代码,添加了一个属性sb,该属性有3个特性:1. NSString类型 2. nonatomic 3. strong

添加关联对象

为了向正常属性那样访问,给该类添加sb属性的存取方法sbsetSb:

1
2
3
4
5
// add property getter and setter
//class_addMethod([HCBase class], @selector(sb), (IMP) sb, "@@:");
class_addMethod([HCBase class], NSSelectorFromString(@"sb"), (IMP) customGetter, "@@:");
//class_addMethod([HCBase class], @selector(setSb:), (IMP) setSb, "v@:@");
class_addMethod([HCBase class], NSSelectorFromString(@"setSb:"), (IMP) customSetter, "v@:@");

注意该方法的最后一个参数是添加的方法的签名:返回类型 + self + selector + 参数 的字符串:

  1. sb方法,返回类型为id(@),self为id(@),选择子(:),参数为空 => @@:
  2. setSb:方法,返回类型为void(v),self为id(@),选择子(:),参数为id(@) => v@:@

现在来看看setter和getter的通用实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// custom getter && setter

void customSetter(id self, SEL _cmd, id value) {
NSString *propertyStr = NSStringFromSelector(_cmd);
// divide set
NSString *realProperty = [propertyStr substringFromIndex:3];
realProperty = [realProperty substringToIndex:realProperty.length - 1];
realProperty = [realProperty lowercaseString];
//const NSString *key = [realProperty copy];
objc_setAssociatedObject(self, NSSelectorFromString(realProperty), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//objc_setAssociatedObject(self, "sb", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

id customGetter(id self, SEL _cmd) {
id result = objc_getAssociatedObject(self, _cmd);
//id result = objc_getAssociatedObject(self, "sb");
return result;
}

这两个方法通过判断selector来判断要添加的是哪一个属性的关联对象,也就是说这个方法是对任何属性通用的(之后新添加其他的属性,也可以使用该实现)

注意:objc_setAssociationObjectobjc_getAssociationObject方法中的第二个参数,也就是对象的key是一个 const void * 类型:常量指针;正好@selector也是常量指针。

访问属性

1
2
3
4
5
6
// new HCBase and set && printf sb
HCBase *baseOBJ = [[HCBase alloc] init];
//[baseOBJ performSelector:@selector(setSb:) withObject:@"sb"];
[baseOBJ performSelector:NSSelectorFromString(@"setSb:") withObject:@"sb"];
//NSLog(@"vanney code log : new property is %@", [baseOBJ performSelector:@selector(sb)]);
NSLog(@"vanney code log : new property is %@", [baseOBJ performSelector:NSSelectorFromString(@"sb")]);

已经添加了属性,并添加了存取方法。现在可以使用performSelector方法来执行存取方法

方法决议

1
+ (BOOL)resolveInstanceMethod:(SEL)sel;

当没有找到方法的实现时,会执行该方法,可以在该方法里面动态添加缺失的方法的实现。

来看看两个类的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*--------- HCBase -----------*/
#import "runtime.h"
#import <Foundation/Foundation.h>

@interface HCBase : NSObject

//@property (nonatomic, strong) NSString *curry;
//@property (nonatomic, copy) NSString *kd;
//@property (nonatomic, assign) int kt;

- (void)needResolve;

- (void)needForwardTarget;

- (void)needFinalForward;

@end

/*----------- HCSon -----------*/
#import "HCBase.h"

@interface HCSon : HCBase

@end

HCSon 继承自 HCBase 类,另外HCBase中声明了一个方法needResolve,但是没有提供实现。

那么当一个HCSon对象调用needResolve方法时,无法找到实现;接着会去寻找resolveInstanceMethod:,并执行该方法。但是该resolveInstanceMethod方法定义在父类HCBase里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void resolveImp(id self, SEL _cmd) {
NSLog(@"vanney code log : resolved function");
}

@implementation HCBase
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"needResolve"]) {
class_addMethod(self, @selector(needResolve), (IMP) resolveImp, "v@:");
return YES;
} else {
return [[self superclass] resolveInstanceMethod:sel];
}
}
@end

该方法给self类添加一个needResolve方法,之后会重新执行needResolve方法。

现在来看看一个HCSon对象调用needResolve的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// resolve

// print method
int methodCount;
Method *methodList = class_copyMethodList([HCBase class], &methodCount);
for (int j = 0; j < methodCount; ++j) {
Method curMethod = methodList[j];
printf("before HCBase method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

int methodCountS;
Method *methodListS = class_copyMethodList([HCSon class], &methodCountS);
for (int j = 0; j < methodCountS; ++j) {
Method curMethod = methodListS[j];
printf("before HCSon method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

HCSon *son = [[HCSon alloc] init];
//[baseOBJ needResolve];
[son needResolve];

Method *newMethodList = class_copyMethodList([HCBase class], &methodCount);
for (int j = 0; j < methodCount; ++j) {
Method curMethod = newMethodList[j];
printf("new HCBase method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

Method *newMethodListS = class_copyMethodList([HCSon class], &methodCountS);
for (int j = 0; j < methodCountS; ++j) {
Method curMethod = newMethodListS[j];
printf("new HCSon method name is %s, and type encoding is %s\n", method_getName(curMethod), method_getTypeEncoding(curMethod));
}

在执行[son needResolve]之前,先打印父类和子类的方法,可以发现都没有needResolve方法;执行之后再次打印,发现HCSon里面增加了needResolve方法。

因为执行resolveInstanceMethod方法时,传入的selfHCSon,所以方法就添加在HCSon里面

resolve

注意图片的最后一行

消息转发

备援对象

- (id)forwardingTargetForSelector:(SEL)selector;

HCBase声明了needForwardTarget方法,但是没有实现;但是HCBeitai类有该方法。可以使用forwardingTargetForSelector方法,返回一个HCBeitai对象,让该对象来执行needForwardTarget方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*------------ HCBeiTai -----------*/
#import <Foundation/Foundation.h>

@interface HCBeiTai : NSObject

- (void)needForwardTarget;

- (void)beitaiForwarding:(int)index;

@end

@implementation HCBeiTai

- (void)needForwardTarget {
NSLog(@"bei tai 作用了");
}

- (void)beitaiForwarding:(int)index {
NSLog(@"vanney code log : bei tai 终极forwarding起作用了 : index is %d", index);
}

@end

/*-------------- HCBase ------------*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(needForwardTarget)) {
NSLog(@"vanney code log : forwarding Target start");
HCBeiTai *beitai = [[HCBeiTai alloc] init];
return beitai;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}

这么一来,就好像HCBase继承了HCBeitai一样,可以用来模拟多重继承。来看看运行的结果:

1
2
// forwardingTarget
[baseOBJ needForwardTarget];

forwarding1

完整的消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)invocation;

当所有的手段都无法处理该方法时,就开启了完整的消息转发。先调用methodSignatureForSelector方法,给该未知方法签名— 类似**@@:** 。当签名不为空时,调用forwardInvocation,将位置方法的所有信息存放在NSInvocation对象中;这时可以更改方法的信息,比如selector,signature等等,然后交由其他对象来执行更改之后的方法。

来看看一个示例:HCBase声明了needFinalForward方法,但是没有提供实现;HCBeitai实现了beitaiForwarding:方法。消息转发,将给HCBase对象的needFinalForward方法 转发给了 HCBeitai对象的beitaiForwarding:方法。注意:这两个方法的selector和signature都不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"vanney code log : selector is %@", NSStringFromSelector(aSelector));
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
NSLog(@"vanney code log : inside create method signature");
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:i"];
}

return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"vanney code log : forwarding invocation selector is %@", NSStringFromSelector(anInvocation.selector));
NSLog(@"vanney code log : invocation is %@, and method signature is %@", anInvocation, anInvocation.methodSignature);

if (anInvocation.selector == @selector(needFinalForward)) {
NSLog(@"vanney code log : inside forwarding");
anInvocation.selector = NSSelectorFromString(@"beitaiForwarding:");
int *a;
*a = 99;
[anInvocation setArgument:a atIndex:2];
HCBeiTai *beiTai = [[HCBeiTai alloc] init];
[anInvocation invokeWithTarget:beiTai];
} else {
NSLog(@"vanney code log : else forwarding");
[super forwardInvocation:anInvocation];
}

}

转发的同时,还可以设置参数。把99传递给新的方法。来看看运行结果:

1
2
// final forwarding
[baseOBJ needFinalForward];

forwarding2

runtime应用

Method Swizzling

可以在类的load方法中,通过class_addMethod class_replaceMethod来改变方法的默认实现方式。因为load方法执行时,所有类的方法都有了,但都还没有执行过一次

JSON -> Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (instancetype)initWithDict:(NSDictionary *)dict {

if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);

//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;

}

该方法参考自[iOS] runtime 的使用场景–实战篇

KVO, KVC

海带学习

JSPatch

JSPatch充分的应用了runtime的性质,详细内容可以参考我的这篇文章JSPatch源码分析

参考