runtime的常见使用

RunTime简介

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。

RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

RunTime中主要使用的函数定义在message.h和runtime.h这两个文件中。 在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。

使用时,需要导入文件,导入如:

#import <objc/message.h>
#import <objc/runtime.h>

函数的定义

  • 对象进行操作的方法一般以object_开头
  • 进行操作的方法一般以class_开头
  • 类或对象的方法进行操作的方法一般以method_开头
  • 成员变量进行操作的方法一般以ivar_开头
  • 属性进行操作的方法一般以property_开头开头
  • 协议进行操作的方法一般以protocol_开头

根据以上的函数的前缀 可以大致了解到层级关系。

对于以objc_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。

RunTime使用
一、runtime本质,消息发送

方法调用的本质,就是让对象发送消息。
objc_msgSend,只有对象才能发送消息,因此以objc开头.
使用消息机制前提,必须导入#import <objc/message.h>
代码例子:

    // 创建person对象
    Person *p = [[Person alloc] init];
    
    // 调用对象方法
    [p eat];
    
    // 底层实现:让对象发送消息
    objc_msgSend(p, @selector(eat)); // Xcode5之后这里需要把 targets -> bulid Settings  搜索 msg 把objc_msgSend Calls YES 修改为 NO
    
    // 调用类方法的方式:三种
    // 第一种通过类名调用
    [Person eat];
    // 第二种通过类对象调用
    [[Person class] eat];
    // 第三种调用方法
    [Person performSelector:@selector(eat)];
    
    // 用类名调用类方法,底层会自动把类名转换成类对象调用
    // 底层实现 让类对象发送消息
    objc_msgSend([Person class], @selector(eat));
    /**
     还有,我是怎么知道上面的方法的本质demo的呢,
     我们可以通过clang 命令来查看代码生成的CPP代码。
     最终代码,需要把当前代码重新编译,用xcode编译器,clang
     clang -rewrite-objc main.m 查看最终生成代码
     **/

二、runtime方法交换

交换方法实现的需求场景:
1、自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。
2、可以做一些异常处理,防止APP崩溃处理,(数组越界等等)

可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。

交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。
下面demo是从这里看到的

@implementation NSMutableArray (Safe)
+ (void)load
{
    [NSClassFromString(@"__NSArrayM") swapMethod:@selector(objectAtIndex:)
                                   currentMethod:@selector(ls_objectAtIndex:)];
 
}
// 实例方法的交换
+ (void)swapMethod:(SEL)originMethod currentMethod:(SEL)currentMethod;
{
    Method firstMethod = class_getInstanceMethod(self, originMethod);
    Method secondMethod = class_getInstanceMethod(self, currentMethod);
    method_exchangeImplementations(firstMethod, secondMethod);
}

- (id)ls_objectAtIndex:(NSUInteger)index
{
    if (index >= self.count)
    {
        NSLog(@"数组越界;;;;;;;;;;;;");
        return nil;
    }
    return [self ls_objectAtIndex:index];
}
@end

三、获取相关参数

1、获取成员变量,包括属性生成的成员变量

+ (NSArray *)fetchIvarList
{
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++)
    {
        NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:2];
        const char *ivarName = ivar_getName(ivarList[i]);
        const char *ivarType = ivar_getTypeEncoding(ivarList[i]);
        dic[@"type"] = [NSString stringWithUTF8String:ivarType];
        dic[@"ivarName"] = [NSString stringWithUTF8String:ivarName];
        
        [mutableList addObject:dic];
    }
    free(ivarList);
    return [NSArray arrayWithArray:mutableList];
}

2、获取类的属性列表,包括私有和公有属性,也包括分类中的属性

+ (NSArray *)fetchPropertyList
{
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(self, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++)
    {
        const char *propertyName = property_getAttributes(propertyList[i]);
        [mutableList addObject:[NSString stringWithUTF8String:propertyName]];
    }
    free(propertyList);
    return [NSArray arrayWithArray:mutableList];
}

3、 获取对象方法列表:包括getter, setter, 分类中的方法等

+ (NSArray *)fetchInstanceMethodList
{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(self, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++)
    {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        [mutableList addObject:NSStringFromSelector(methodName)];
    }
    free(methodList);
    return [NSArray arrayWithArray:mutableList];
}

4、获取类方法列表 包括分类里面的

+ (NSArray *)fetchClassMethodList
{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(object_getClass(self), &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++)
    {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        [mutableList addObject:NSStringFromSelector(methodName)];
    }
    free(methodList);
    return [NSArray arrayWithArray:mutableList];
}

5、获取协议列表,包括.h .m 和分类里的

+ (NSArray *)fetchProtocolList
{
    unsigned int count = 0;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList(self, &count);
    
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i < count; i++ )
    {
        Protocol *protocol = protocolList[i];
        const char *protocolName = protocol_getName(protocol);
        [mutableList addObject:[NSString stringWithUTF8String:protocolName]];
    }
    
    return [NSArray arrayWithArray:mutableList];
}
四、关联对象

关联对象不是为类\对象添加属性或者成员变量(因为在设置关联后也无法通过ivarList或者propertyList取得) ,而是为类添加一个相关的对象,通常用于存储类信息,例如存储类的属性列表数组,为将来字典转模型的方便。
1、对象关联属性
列如,oc里面,类没有name 这个参数,我们可以给他添加一个name

// 定义关联的key
static const char *key = "name";
@implementation NSObject (RunTime)

- (NSString *)name
{
    // 根据关联的key,获取关联的值。
    return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
    // 第一个参数:给哪个对象添加关联
    // 第二个参数:关联的key,通过这个key获取
    // 第三个参数:关联的value
    // 第四个参数:关联的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

// 调用
    // NSObject对象添加属性
    NSObject * object = [[NSObject alloc] init];
    object.name = @"gameover";
    NSLog(@"object  name =====%@",object.name);
// 打印 如下 
//2018-06-28 14:52:54.302050+0800 Test_runTime[23578:9984095] object  name ===== gameover

2、对象关联对象
动态添加方法会用到这两个api
objc_setAssociatedObject
第一个参数 id object, 当前对象
第二个参数 const void *key, 关联的key,是c字符串
第三个参数 id value, 被关联的对象的值
第四个参数 objc_AssociationPolicy policy关联引用的规则
objc_getAssociatedObject
第一个参数 id object, 当前对象
第二个参数 const void *key, 关联的key,是c字符串

- (void)viewDidLoad {
    //  对象添加关联对象
    //  列如btn 对象添加多个参数,
    UIButton * btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 40)];
    btn.backgroundColor = [UIColor redColor];
    
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
    // 传递多参数
    objc_setAssociatedObject(btn, "yt_text", @"你好", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(btn, "yt_size", @"15", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// 点击按钮触发
- (void)btnClick:(UIButton*)btn
{
    NSString *yt_text = objc_getAssociatedObject(btn, "yt_text");
    NSString *yt_size = objc_getAssociatedObject(btn, "yt_size");
    NSLog(@"yt_text ==%@  yt_size ==%@",yt_text,yt_size);
}
// 打印 如下 
//2018-06-28 14:50:32.083111+0800 Test_runTime[23575:9981192] yt_text ==你好  yt_size ==15
五、动态添加方法

在开发中:如果一个类的方法非常多的时候,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决

动态添加方法会用到这个api
class_addMethod
第一个参数:给哪个类添加方法
第二个参数:添加方法的方法编号
第三个参数:添加方法的函数实现(函数地址)
第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

在使用[p performSelector:@selector(yt_sleep) withObject:nil]; 或者
的时候,由于 yt_sleep 这个方法在Person.m里面是没有实现
所以,会崩溃。

所以只要在下面两个方法里面分别动态添加方法即可避免该问题
在调用没有实现的实例方法会触发这个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;
在调用没有实现的类方法会触发这个方法
+(BOOL)resolveClassMethod:(SEL)sel;

#import <Foundation/Foundation.h>
@interface Person : NSObject

+ (void)eat;
- (void)eat;
+ (void)yt_sleep;
@end

#import "Person.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Person

- (void)eat
{
    NSLog(@"eat 方法调用");
}

+ (void)eat
{
    NSLog(@"eat 类方法调用");
}

//+ (void)yt_sleep
//{
//    NSLog(@"sleep 类方法调用");
//}

//当类调用一个没有实现的类方法就会到这里!!
+(BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"类方法 %@",NSStringFromSelector(sel));
    if (sel == @selector(yt_sleep)) {
        /*
         第一个参数:给哪个类添加方法
         第二个参数:添加方法的方法编号
         第三个参数:添加方法的函数实现(函数地址)
         第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
         */
        //class_addMethod(self, @selector(yt_sleep), (IMP)yt_sleep, "v@:"); // 这样会崩溃
        class_addMethod(objc_getMetaClass("Person"), @selector(yt_sleep), (IMP)yt_sleep, "v@:");
    }
    return [super resolveClassMethod:sel];
}

//当类调用一个没有实现的对象方法就会到这里!!
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"实例方法 %@",NSStringFromSelector(sel));
    if (sel == @selector(yt_sleep)) {
        // 动态添加eat方法
        /*
         第一个参数:给哪个类添加方法
         第二个参数:添加方法的方法编号
         第三个参数:添加方法的函数实现(函数地址)
         第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
         */
        class_addMethod(self, sel, (IMP)yt_sleep, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}

// 默认方法都有两个隐式参数,
void yt_sleep(id self,SEL sel)
{
    NSLog(@"%@ %@  睡觉了",self,NSStringFromSelector(sel));
}

@end


    //其他地方调用
    // 动态添加实例方法
    [p performSelector:@selector(yt_sleep) withObject:nil];
    // 动态添加类方法
    [Person performSelector:@selector(yt_sleep) withObject:nil];
六、字典转模型

MJExtension 里面就使用runTime来完成这些相应的操作的
下面是我的伪代码(没有mj那么精细)

#import <Foundation/Foundation.h>

@interface User : NSObject
@property (nonatomic ,strong) NSString * id;
@property (nonatomic ,strong) NSString * name;
@property (nonatomic ,strong) NSString * sex;
@property (nonatomic ,strong) NSString * age;
@property (nonatomic ,strong) NSString * height;
@property (nonatomic ,strong) NSString * weight;
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end


#import "User.h"
#import <objc/runtime.h>
@implementation User
// Ivar:成员变量 以下划线开头
// Property:属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    id objc = [[self alloc] init];
    
    // runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
    // 1.获取模型中所有成员变量 key
    // 获取哪个类的成员变量
    // count:成员变量个数
    unsigned int count = 0;
    // 获取成员变量数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 获取成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 获取key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 去字典中查找对应value
        // key:user  value:NSDictionary
        
        id value = dict[key];
        
        // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
        // 并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            // 字典转换成模型 userDict => User模型
            // 转换成哪个模型
            
            // 获取类
            Class modelClass = NSClassFromString(ivarType);
            
            value = [modelClass modelWithDict:value];
        }
        
        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}
@end
// 其他地方调用
    // 字典转模型
    NSDictionary * dict = @{ @"id" : @"1235",
                            @"name" : @"帅哥",
                            @"sex" : @"男",
                            @"age" : @"18",
                            @"height" : @"180cm",
                            @"weight" : @"70kg",
                            @"good" : @"eat",
                            @"money" : @"10000",
                            @"sport" : @"basketball"};
    
    
    User * user = [User modelWithDict:dict];
    NSLog(@"id =%@  name =%@ sex =%@ age =%@ height =%@ weight =%@",user.id,user.name,user.sex,user.age,user.height,user.weight);

打印如下
2018-06-28 17:00:07.724910+0800 Test_runTime[23654:10029472] id =1235 name =帅哥 sex =男 age =18 height =180cm weight =70kg

QQ20180628-170248.png


以上是本人的浅见,如有错误,望各位多多指正😘

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容