美文网首页
iOS runtime

iOS runtime

作者: _牙牙 | 来源:发表于2017-06-27 11:56 被阅读0次

一.什么是runtime?

runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数。

二.runtime能做什么?

可以在程序运行时创建,检查,修改类、对象和它们的方法。
具体应用举例:

- 获取类的方法列表/参数列表;
- 方法调用;
- 方法拦截、动态添加方法;
- 方法替换: method swizzling
- 关联对象,动态添加属性;

再具体些:

1.对象关联
2.解归档
3.字典与模型互转
4.方法的调用,交换,插入,拦截,动态添加。
。。。。。。。

三.基础知识

实践出真理,开始吧。
先从Objective-C 中的方法调用说起。其实 [receiver message] 会被编译器转化为: objc_msgSend(receiver, selector)。
例:
创建一个名为testClass的新类

#import "testClass.h"

@implementation testClass
-(instancetype)init{
    if (self = [super init]) {
        [self runTimeTestAction];
    }
    return self;
}

-(void)runTimeTestAction{
    NSLog(@"runTime");
}
@end

在终端使用 clang 重写命令,得到testClass.cpp文件:

 clang -rewrite-objc /Users/apple/Desktop/runTimeTest/runTimeTest/TestClass/testClass.m 

双击打开,command+F 搜索“static instancetype”,可以看到被编译器转化后的Init方法


static instancetype _I_testClass_init(testClass * self, SEL _cmd) {
    if (self = ((testClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("testClass"))}, sel_registerName("init"))) {
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));
    }
    return self;
}

可以看到[self runTimeTestAction];被编译后的样子:
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));

objc_msgSend 函数被定义在 objc/message.h 目录下,其函数原型为

OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )

可以看到该函数有两个参数,一个id类型,一个SEL类型,再来看看这两个类型被如何定义,他们都被定义在objc/objc.h目录下:

SEL:

typedef struct objc_selector *SEL;
//这是个映射到方法的C字符串,你可以用 Objective-C 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。

id:

typedef struct objc_object *id;
//id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。
//objc_object 结构体定义如下:
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

这就是所谓的对象,isa为其一个Class类型的成员变量,isa指定对象所属的类。
那么再来看看Class是如何定义的:

Class:

Class 也是一个结构体指针类型:

typedef struct objc_class *Class;

objc_class 结构体:

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;

·Class 也有一个 isa 指针,指向其所属的元类(meta).
·super_class:指向其超类.
·name:类名.
·version:类的版本信息.
·info:类的详情.
·instance_size:该类的实例对象的大小.
·ivars:指向该类的成员变量列表.
·methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因.
·cache:Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高.
·protocols:指向该类的协议列表.

PS

methodLists 指向该类的实例方法列表实例方法-方法,那么类方法(+方法)存储在哪儿呢?类方法被存储在元类中,Class 通过 isa 指针即可找到其所属的元类.


上图实线是 super_class 指针,虚线是 isa 指针。根元类的超类是NSObject,而 isa 指向了自己。NSObject 的超类为 nil,也就是它没有超类。

现在结合上图来看下方法调用的整体流程:

1.首先,Runtime 系统会把方法调用转化为消息发送,即 objc_msgSend,并且把方法的调用者,和方法选择器,当做参数传递过去

2.此时,方法的调用者会通过 isa 指针来找到其所属的类,然后在 cache 或者 methodLists 中查找该方法,找得到就跳到对应的方法去执行

3.如果在中没有找到该方法,则通过 super_class 往上一级超类查找(如果一直找到 NSObject 都没有找到该方法的话,这种情况,我们放到后面消息转发的时候再说)

4.如果一直找到 NSObject 都没有找到该方法的话就进入消息转发了,消息转发的大致过程如图(我们可以利用这一过程,添加动态方法)
消息转发的大致过程如图:


①进入 resolveInstanceMethod: 方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕.

②resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步,返回某个对象,则会调用该对象的方法.

③若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步.

④当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等.

⑤如果到最后,消息还是没有得到响应,程序就会crash
一般情况下程序会crash,错误信息类似下面这样:

unrecognized selector sent to instance 0x7fd0a141afd0

四.实践及应用

上面说了这么多只是根据自己的理解通过Runtime的消息发送机制初步整理了下,runtime基础原理和内部结构,插图及解释大部份摘自网络。下面开始说些具体应用。

1.对象关联

对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态给对象增加成员变量。相对的,对象的方法定义都保存在类的可变区域中。

methodList,通过修改methodList指针所指向的指针的值,我们可以实现为类动态增加方法实现。

因此,我们可以实现动态为一个类增加成员方法,但是却不能直接为类增加成员变量,这就是category的实现原理。

对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

·object 是源对象.
·value 是被关联的对象.
·key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象.
·policy 是一个枚举值,表示关联对象的行为:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示关联对象copy,不是线程安全的
};

要取出被关联的对象使用 objc_getAssociatedObject 方法。
要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可:

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象.
举个栗子,假如我们要给 UIButton 添加一个监听单击事件的 block 属性,新建 UIButton 的 Category:


#import <UIKit/UIKit.h>
typedef void(^whenClickBlock)(void);
@interface UIButton(ClickBlock)
@property (nonatomic,copy) whenClickBlock whenClickBlock;
@end

#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>
static const void *associatedKey = "associatedKey";
@implementation UIButton(ClickBlock)

-(void)setWhenClickBlock:(whenClickBlock)whenClickBlock{
    objc_setAssociatedObject(self, associatedKey, whenClickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    if (whenClickBlock) {
        [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    }
}

-(whenClickBlock)whenClickBlock{
    return objc_getAssociatedObject(self, associatedKey);
}

-(void)buttonClick{
    if (self.whenClickBlock) {
        self.whenClickBlock();
    }
}

@end

调用:

    //用对象关联在Category内加属性
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = self.view.bounds;
    [self.view addSubview:button];
    __weak UIButton * weakbutton=button;
    button.whenClickBlock = ^{
        //随机改变颜色
        weakbutton.backgroundColor=randomColor;
    };
    
    

2.交换方法,动态加载方法

方法交换

关于这部分操作,都被定义在#import <objc/runtime.h>目录下,下面介绍几个常用的,其他的感兴趣的话可以自行查看:

    class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)// 替换类方法的定义
    method_exchangeImplementations(Method m1, Method m2) //交换两个方法的实现
    method_setImplementation(Method m, IMP imp)  //设置一个方法的实现

PS:class_replaceMethod 试图替换一个不存在的方法时候,会调用class_addMethod为该类增加一个新方法
例:

#import "ViewController+login.h"
#import <objc/runtime.h>

@implementation UIViewController (login)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class=[self class];
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzledViewDidAppear:);
        swizzleMethod(class, originalSelector, swizzledSelector);
       
    });
    
    
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
     // Method : 包含了一个方法的  方法名 + 实现 + 参数个数及类型 + 返回值个数及类型 等信息
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //IMP:方法的地址
    IMP swizzledImp = method_getImplementation(swizzledMethod);
    char *swizzledTypes = (char *)method_getTypeEncoding(swizzledMethod);
    IMP originalImp = method_getImplementation(originalMethod);
    char *originalTypes = (char *)method_getTypeEncoding(originalMethod);
    BOOL success = class_addMethod(class, originalSelector, swizzledImp, swizzledTypes);
    //避免UIViewController父类实现swizzledViewDidAppear影响
    if (success) {
       //替换
        class_replaceMethod(class, swizzledSelector, originalImp, originalTypes);
    }else {
        // 失败,表明已经有这个方法,直接交换
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}



- (void)swizzledViewDidAppear:(BOOL)animation {
    [self swizzledViewDidAppear:animation];
    NSLog(@"YLZ:%@ viewDidAppear", NSStringFromClass([self class]));
}
@end

调用:

    ViewController * VC=[ViewController new];

动态加载方法

首先看下消息转发,上文也提到过,具体过程:

//  第一步,消息接收者没有找到对应的方法时候,会先调用此方法,可在此方法实现中动态添加新的方法
//  返回YES表示相应selector的实现已经被找到,或者添加新方法到了类中,否则返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

//  第二步, 如果第一步的返回NO或者直接返回了YES而没有添加方法,该方法被调用
//  在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象, 注意如果返回self就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;
}


//  第三步, 如果forwardingTargetForSelector:返回了nil,则该方法会被调用,系统会询问我们要一个合法的『类型编码(Type Encoding)』
//  若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 在这里可以改变方法选择器
    [anInvocation setSelector:@selector(unknown)];
    // 改变方法选择器后,需要指定消息的接收者
    [anInvocation invokeWithTarget:self];
}

- (void)unknown {
    NSLog(@"unkown method.......");
}

// 如果没有实现消息转发 forwardInvocation  则调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"unresolved method :%@", NSStringFromSelector(aSelector));
}

根据消息转发过程,一般我们在resolveInstanceMethod:(SEL)sel动态添加方法:
构建一个Cat ,动态添加miaow:方法,我们可以这样做,例:

#import "cat.h"
#import <objc/runtime.h>
@implementation cat

- (void)eat{
    NSLog(@"cat eat....");
}

- (void)jump{
    NSLog(@"cat jump....");
}
//prama:传人参数,可自定义
void  miaow(id self ,SEL _cmd,NSString * param){
    NSLog(@"%@",param);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel==NSSelectorFromString(@"miaow:")) {
        /**
         *  动态添加方法
         *
         *  @param self cls:为哪个类添加方法
         *  @param sel  SEL:添加方法的方法编号(方法名)是什么
         *  @param IMP  IMP:方法实现
         *  @param const char * types方法类型
         *
         */
        BOOL isSuccess = class_addMethod(self, sel, (IMP)miaow, "v@:@");
        return isSuccess;
    }
        return [super resolveInstanceMethod:sel];
        }

@end


调用:

    [[cat new]performSelector:@selector(miaow:) withObject:@"喵~"];

自动解归档

在归档的过程中,当属性较多时,用普通的解归档操作,比较麻烦,🔎很多人深有体会,通过runtime可以简化这一操作,具体思路为:
-使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.
-使用 ivar_getName 方法获取成员变量的名称.
-通过 KVC 来归档,解档.
代码:



#import <Foundation/Foundation.h>
@interface myModel : NSObject<NSCoding>
@property (nonatomic, copy) NSNumber *sex;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, copy) NSNumber *height;
@property (nonatomic, copy) NSNumber *weight;
@end

///////////////////////////////////
#import "myModel.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation myModel
- (id) initWithCoder: (NSCoder *)aDecoder{
    if (self=[super init]) {
        unsigned int outcount=0;
        Ivar * vars=class_copyIvarList([self class], &outcount);
        for (int i=0; i<outcount; i++) {
            
            Ivar var=vars[i];
            const char * name=ivar_getName(var);
            NSString * key=[NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        //释放vars
           free(vars);
    }
    return self;
}
- (void) encodeWithCoder: (NSCoder *)aCoder{
    unsigned int outcount=0;
    Ivar * vars=class_copyIvarList([self class],&outcount );
    for (int i=0 ; i<outcount; i++) {
        Ivar var=vars[i];
        const char * name=ivar_getName(var);
        NSString * key=[NSString stringWithUTF8String:name];
        id value=[self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(vars);
    
}
@end

3.字典模型互转

跟归档遇到的问题一样,当有很多属性的时候,常规方法会比较麻烦
说下利用runtime做字典模型互转的思路:

字典转模型:

1.根据字典的 key 生成 setter 方法.
2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC).

模型转字典:

1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性.
2.调用 property_getName 获取属性名称.
3.根据属性名称生成 getter 方法.
4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC).

代码:

#import <Foundation/Foundation.h>

@interface NSObject (KeyValues)

+(id)objectWithKeyValues:(NSDictionary *)aDictionary;

-(NSDictionary *)keyValuesWithObject;

@end
//////////////////////////////////////////////////////////////

#import "NSObject+KeyValues.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation NSObject (KeyValues)
//字典转模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
    id objc = [[self alloc] init];
    for (NSString *key in aDictionary.allKeys) {
        id value = aDictionary[key];
     
        /*判断当前属性是不是Model*/
        objc_property_t property = class_getProperty(self, key.UTF8String);
        unsigned int outCount = 0;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        objc_property_attribute_t attribute = attributeList[0];
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];
        if ([typeString isEqualToString:@"@\"TestModel\""]) {
            value = [self objectWithKeyValues:value];
        }
        /**********************/
        
        //生成setter方法,并用objc_msgSend调用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
        SEL setter = sel_registerName(methodName.UTF8String);
        if ([objc respondsToSelector:setter]) {
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
        free(attributeList);
    }
    return objc;
}

//模型转字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];
        
        //生成getter方法,并用objc_msgSend调用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
            
            /*判断当前属性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }
            /**********************/
            
            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }
        
    }
    free(propertyList);
    return dict;
}
@end

*-_

写在最后:

到这里,日常常用的基本都有了。一直想写一篇简单的介绍runtime的短文,供自己和大家借鉴学习,工作繁忙,前后断断续续写了几个星期,也总算完成了,严格来说这篇文章算所有人共同完成的,借鉴了大量网络资源,作者也算是在自己理解的基础上对网络上现有资源的初步整理,非常感谢各位大神的分享🙏。后面如果有机会再补充一些别的用例,供大家参考。
参考:
Runtime全方位装逼指南🙏
代码:
runTimeTest

相关文章

网友评论

      本文标题:iOS runtime

      本文链接:https://www.haomeiwen.com/subject/wssjfxtx.html