Runtime总结
一, runtime 关联属性
- 1,设置关联值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
object:与谁关联,通常是传self
key:唯一键,在获取值时通过该键获取,通常是使用static const void *来声明
value:关联所设置的值
policy:内存管理策略,比如使用copy
- 2,获取关联值
id objc_getAssociatedObject(id object, const void *key)
object:与谁关联,通常是传self,在设置关联时所指定的与哪个对象关联的那个对象
key:唯一键,在设置关联时所指定的键
- 3, 移除对象上所有关联
objc_removeAssociatedObjects
二, runtime在模型与字典互转当中的应用
- 1,字典转模型
for 循环字典,根据key 生成set 方法,类似 setName: , 然后调用 ((void (*)(id, SEL, id))objc_msgSend)(self, setter, value) 方法,调用set方式,就可以给模型的属性赋值了。
//开头大写,其余小写
NSString *propertySetter = key.capitalizedString;
//拼接set方法
propertySetter = [NSString stringWithFormat:@"set%@:", propertySetter];
// 生成setter方法
SEL setter = NSSelectorFromString(propertySetter);
if ([self respondsToSelector:setter]) {
if (setter != nil) {
//利用运行时调用set 方法
((void (*)(id, SEL, id))objc_msgSend)(self, setter, value);
}
}
还有种方式,通过KVC 方式也可以实现,具体步骤 :根据 class_copyIvarList方法获取该类的成员变量列表, 然后for 循环 根据 Ivar 作为key 去字典中取出value ,最后根据setValue:forKey 去设置。
unsigned int count;
// 获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员属性
Ivar ivar = ivarList[i];
// 获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key
// 从第一个角标开始截取,去掉下划线
NSString *key = [name substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
if (value) { // 有值,才需要给模型的属性赋值
// 利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}
}
- 2, 模型转字典
第一种思路:先获取属性列表,然后根据单个属性生成getter 方法,然后运用 objc_msgSend q去调用getter 方法,获取到属性的值,添加到字典中去。
unsigned int outCount = 0;
//获取属性列表
objc_property_t* properties = class_copyPropertyList([self class], &outCount);
if (outCount != 0) {
NSMutableDictionary * dict = [[NSMutableDictionary alloc] initWithCapacity:outCount];
for (unsigned int i = 0; i < outCount; ++i) {
//获取单个属性
objc_property_t property = properties[i];
//获取属性名称
const void * propertyName = property_getName(property);
//获取getter 方法
SEL getter = sel_registerName(propertyName);
if ([self respondsToSelector:getter]) {
//调用getter 方法,获取value
id value = ((id (*) (id,SEL)) objc_msgSend)(self,getter);
if (value) {
NSString * key = [NSString stringWithUTF8String:propertyName];
[dict setObject:value forKey:key];
}
}
}
return dict;
}
第二种思路:生成getter 方法的方式差不多,调用getter 方法不再是通过objc_msgSend,而是通过 NSInvocation ,NSInvocation 可以指定某个 类去掉用某个方法,具体代码如下。
unsigned int outCount = 0;
//获取属性列表
objc_property_t* properties = class_copyPropertyList([self class], &outCount);
if (outCount != 0) {
NSMutableDictionary * dict = [[NSMutableDictionary alloc] initWithCapacity:outCount];
for (unsigned int i = 0; i < outCount; ++i) {
//获取单个属性
objc_property_t property = properties[i];
//获取属性名称
const void * propertyName = property_getName(property);
NSString * key = [NSString stringWithUTF8String:propertyName];
//获取getter 方法
SEL getter = NSSelectorFromString(key);
if ([self respondsToSelector:getter]) {
//获取方法签名
NSMethodSignature * signature= [NSMethodSignature methodSignatureForSelector:getter];
// 根据方法签名获取NSInvocation对象
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:signature];
// 设置target
[invocation setTarget:self];
// 设置selector
[invocation setSelector:getter];
//调用
[invocation invoke];
__unsafe_unretained NSObject * propertyValue = nil;
//返回值
[invocation getReturnValue:&propertyValue];
if (propertyValue != nil) {
[dict setObject:propertyValue forKey:key];
}
}
}
return dict;
}
三,消息转发机制
#import <Foundation/Foundation.h>
int main(){
@autoreleasepool {
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
}
return 0;
}
上面的main.m 的文件,我们可以用Clang 编译的知识,通过命令 clang -rewrite-objc main.m ,就能得到main.cpp 文件,打开后就会发现转换后的C 代码,其中一个重要的函数就是 objc_msgSend。
int main(){
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSString* site = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), (const char *)"starming");
}
return 0;
}
我们先来了解下objc_msgSend 的调用的检测过程:
- 1, 检测这个selector 是否要忽略。
- 2, 检测这个target 是不是nil 对象,nil 对象执行任何一个方法不会crash 是因为会被忽略掉。
- 3, 查找这个类的IMP,也就是方法实现。先从方法缓存列表cache中查找,若找到则跳到对应的函数去执行;若找不到,则查找方法分发表。如果分发表找不到就到父类的分发表去找,直到找到或者查找到NSObject根类为止。
- 4, 前三步都找不到,则开始进入动态方法解析了
其流程是这样的:
第一步:+ (BOOL)resolveInstanceMethod:(SEL)sel
实现方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过class_addMethod
函数动态地添加方法,消息得到处理,此流程完毕。
第二步:在第一步返回的是NO时,就会进入
- (id)forwardingTargetForSelector:(SEL)aSelector
方法,这是运行时给我们的第二次机会,用于指定哪个对象响应这个selector。不能指定为self。若返回nil,表示没有响应者,则会进入第三步。若返回某个对象,则会调用该对象的方法。
第三步:若第二步返回的是nil,则我们首先要通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
指定方法签名,若返回nil,则表示不处理。若返回方法签名,则会进入下一步。
第四步:当第三步返回方法方法签名后,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation
方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等
第五步:若没有实现- (void)forwardInvocation:(NSInvocation *)anInvocation
方法,那么会进入- (void)doesNotRecognizeSelector:(SEL)aSelector
方法。若我们没有实现这个方法,那么就会crash,然后提示找不到响应的方法。到此,动态解析的流程就结束了。
- 1, 第一种思路:在
+ (BOOL)resolveInstanceMethod:(SEL)sel
拦截然后动态添加方法。
//第一步:在调用某个对象的某个方法找不到的时候,会先调用这个方法,允许我动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
class_addMethod(self.class, sel, (IMP)(eat), "v:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void eat(id self, SEL cmd){
NSLog(@"%@ is eat",self);
}
- 2,第二种思路,重定向接受者
@interface ZDPig ()
{
ZDDog * _dog;
}
@end
@implementation ZDPig
- (instancetype)init
{
self = [super init];
if (self) {
_dog = [ZDDog new];
}
return self;
}
//第一步,我们不动态添加方法,返回NO
+(BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
//第二步,重定向接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(eat)) {
//要确定 _dog 要有这个方法,不然也会crash 的
return _dog;
}
return [super forwardingTargetForSelector:aSelector];
}
- 3, 第三种思路:在消息转发的的最后一个阶段,将消息转给其他对象
// 第一步,我们不动态添加方法,返回NO
+(BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二步,备选提供响应aSelector的对象,我们不备选,因此设置为nil,就会进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
//必须重写这个方法,消息转发使用这个方法获得的信息创建NSInvocation对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([ZDDog instancesRespondToSelector:aSelector]) {
return [ZDDog instanceMethodSignatureForSelector:aSelector];
}
return nil;
}
//这一步是最后机会将消息转发给其它对象,对象会将未处理的消息相关的selector,target和参数都封装在anInvocation中。forwardInvocation:像未知消息分发中心,将未知消息转发给其它对象。注意的是forwardInvocation:方法只有在消息接收对象无法正常响应消息时才被调用。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget:[ZDDog new]];
}
四,Method Swizzling
Method 的相关函数
// 调用指定方法的实现,返回的是方法实现时的返回,参数receiver不能为空,这个比method_getImplementation和method_getName快
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名,希望获得方法明的C字符串,使用sel_getName(method_getName(method))
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
关于 Method Swizzling 已经有很多文章了,这里就不多说了,推荐一个第三库 RSSwizzle,介绍RSSwizzle 的资料