Runtime

使用运行时方法需要引入runtime.h文件。

一、基础知识

runtime简称运行时,OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用是在编译的时候会决定调用哪个函数。
对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
事实证明:
-- 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
-- 在编译阶段,C语言调用未实现的函数就会报错。
Method :成员方法
Ivar : 成员变量

二、常用方法

class_copyPropertyList : 获取属性列表
  class_copyMethodList : 获取成员方法列表
  class_copyIvarList:获取成员变量列表
  ivar_getName:获取变量名
  property_getName:获取属性名

使用示例

2.1.获取成员变量list

        unsigned int ivarCount = 0; //成员变量数
        Ivar *ivarList = class_copyIvarList([self class], &ivarCount);//ivar数组
        
        for (int i = 0; i < ivarCount; i++) {//遍历
            Ivar ivar = ivarList[i]; //获取ivar
            const char *name = ivar_getName(ivar);//获取变量名
            NSString *key = [NSString stringWithUTF8String:name];
            NSLog(@"%@", key);

        }

      free(ivarList);

2.2.获取属性列表

unsigned int count = 0;
    objc_property_t *propertList = class_copyPropertyList([self class], &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = propertList[i];
        const char *name = property_getName(property);
        const char *attrs = property_getAttributes(property);
//        property_copyAttributeValue(,) 第一个参数为objc_property_t,第二个参数"V"获取变量名,"T"获取类型
        const char *value = property_copyAttributeValue(property, "V");
        NSLog(@"name = %s, attrs = %s, value = %s", name, attrs, value);
  /*
         各种符号对应类型,部分类型在新版SDK中有所变化,如long 和long long
         c char         C unsigned char
         i int          I unsigned int
         l long         L unsigned long
         s short        S unsigned short
         d double       D unsigned double
         f float        F unsigned float
         q long long    Q unsigned long long
         B BOOL
         @ 对象类型 //指针 对象类型 如NSString 是@“NSString”
         propertyType,你可以打印出来,看看它是什么。
         要判断某个属性的类型,只需要[propertyType hasPrefix:@"T@\"NSString\""]
          这代表它是NSString 类型。
         */
    }
    free(propertList);

2.3.获取方法列表

unsigned int count = 0;
    Method *methodList = class_copyMethodList([self class], &count);
    for (int i = 0 ; i < count; i++) {
        Method method = methodList[i];
        SEL selector = method_getName(method);//方法入口
        const char *sel_name = sel_getName(selector);
        NSLog(@"方法名 %s", sel_name);
    }
    free(methodList);

2.4.Runtime-动态创建类添加属性和方法

- (void)createClass
{
    Class MyClass = objc_allocateClassPair([NSObject class], "myclass", 0);
    //添加一个NSString的变量,第四个参数是对其方式,第五个参数是参数类型
    if (class_addIvar(MyClass, "itest", sizeof(NSString *), 0, "@")) {
        NSLog(@"add ivar success");
    }
    //myclasstest是已经实现的函数,"v@:"这种写法见参数类型连接
    class_addMethod(MyClass, @selector(myclasstest:), (IMP)myclasstest, "v@:");
    //注册这个类到runtime系统中就可以使用他了
    objc_registerClassPair(MyClass);
    //生成了一个实例化对象
    id myobj = [[MyClass alloc] init];
    NSString *str = @"asdb";
    //给刚刚添加的变量赋值
    //  object_setInstanceVariable(myobj, "itest", (void *)&str);在ARC下不允许使用
    [myobj setValue:str forKey:@"itest"];
    //调用myclasstest方法,也就是给myobj这个接受者发送myclasstest这个消息
    [myobj myclasstest:10];

}
//这个方法实际上没有被调用,但是必须实现否则不会调用下面的方法
- (void)myclasstest:(int)a
{
    
}
//调用的是这个方法
static void myclasstest(id self, SEL _cmd, int a) //self和_cmd是必须的,在之后可以随意添加其他参数
{
    
    Ivar v = class_getInstanceVariable([self class], "itest");
    //返回名为itest的ivar的变量的值
    id o = object_getIvar(self, v);
    //成功打印出结果
    NSLog(@"%@", o);
    NSLog(@"int a is %d", a);
}

详解Objective-C的meta-class:http://www.jianshu.com/p/a5ab8d998b9e

三、使用方向:归档、字典<---->模型、框架封装等

3.1. 实现归档

encode:编码  decode:解码
#define WKCodingImplementing 
- (void)encodeWithCoder:(NSCoder *)aCoder 
{ 
    unsigned int ivarCount = 0; 
    Ivar *ivarList = class_copyIvarList([self class], &ivarCount); 
    for (int i = 0; i < ivarCount; i++) { 
        Ivar ivar = ivarList[i]; 
        const char *name = ivar_getName(ivar); 
        const char *type = ivar_getTypeEncoding(ivar); 
        NSLog(@"%s-----%s", name, type); 
        NSString *key = [NSString stringWithUTF8String:name]; 
        id value = [self valueForKey:key]; 
        [aCoder encodeObject:value forKey:key]; 
    } 
    free(ivarList); 
} 
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder 
{ 
    if (self = [super init]) { 
        unsigned int ivarCount = 0; 
        Ivar *ivarList = class_copyIvarList([self class], &ivarCount); 
        for (int i = 0; i < ivarCount; i++) { 
            Ivar ivar = ivarList[i]; 
            const char *name = ivar_getName(ivar); 
            NSString *key = [NSString stringWithUTF8String:name]; 
            id value = [aDecoder decodeObjectForKey:key]; \
            NSLog(@"%@ %@", key, value); 
            [self setValue:value forKey:key]; 
        } 
    } 
    return self; 
}

3.2.如何快速生成Plist文件属性名

实现原理:通过遍历字典,判断类型,拼接字符串

  // 拼接属性字符串代码
  NSMutableString *strM = [NSMutableString string];

  // 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
  [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

  NSString *type;

      if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
          type = @"NSString";
      }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
          type = @"NSArray";
      }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
          type = @"int";
      }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
          type = @"NSDictionary";
      }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
          type = @"BooL";
      }
      // 属性字符串
      NSString *str;
      if ([type containsString:@"NS"]) {
          str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
      }else{
          str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
      }
      // 每生成属性字符串,就自动换行。
      [strM appendFormat:@"\n%@\n",str];  
  }];
  // 把拼接好的字符串打印出来,就好了。
  NSLog(@"%@",strM);

打印结果

Runtime_第1张图片
874828-7eb4ad3d25184d7a.png

3.3.KVC实现字典转模型

KVC弊端
模型中属性必须和字典的key一致,否则就报错
如果不一致,系统会调用setValue: forUndefinedKey:
解决办法,只需要重写setValue: forUndefinedKey:即可

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{

}

3.4. RunTime实现字典转模型

实现思路:遍历模型中所有属性,根据模型的属性名去字典中查找key,取出对应的的值,给模型的属性赋值

3.4.1.一级转换

class_copyIvarList(self, &count)该方法第一个参数是要获取哪个类中的成员属性,第二个参数是这个类中有多少成员属性,需要传入地址,返回值Ivar是个数组,会将所有成员属性放入这个数组中

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 思路:遍历模型中所有属性-》使用运行时

    // 0.创建对应的对象
    id objc = [[self alloc] init];

    // 1.利用runtime给对象中的成员属性赋值

    // class_copyIvarList:获取类中的所有成员属性
    // Ivar:成员属性的意思
    // 第一个参数:表示获取哪个类中的成员属性
    // 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
    // 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
    /* 类似下面这种写法

     Ivar ivar;
     Ivar ivar1;
     Ivar ivar2;
     // 定义一个ivar的数组a
     Ivar a[] = {ivar,ivar1,ivar2};

     // 用一个Ivar *指针指向数组第一个元素
     Ivar *ivarList = a;

     // 根据指针访问数组第一个元素
     ivarList[0];

     */
    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];

3.4.2.二级转换

判断字典中是否存在字典,如果存在,转为模型
字典属性生成的是@"@"xxxx""类型,需要裁减为@"xxxx"

    if ([value isKindOfClass:[NSDictionary class]]) {
            // 获取成员属性类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            // 生成的是这种@"@\"User\"" 类型 -》 @"User"  在OC字符串中 \" -> ",\是转义的意思,不占用字符
           type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
    NSLog(@"type-----------%@",type);
    
            type = [type stringByReplacingOccurrencesOfString:@"@"  withString:@""];
    NSLog(@"type-----------%@",type);
            // 根据字符串类名生成类对象
            Class modelClass = NSClassFromString(type); 
            if (modelClass) {
                value  =  [modelClass modelWithDict:value];
            }
        }

3.4.3.三级转换

通过给分类添加一个协议,来实现将数组中的字典转为模型

       // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;

                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
               Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                  id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }

                // 把模型数组赋值给value
                value = arrM;

            }
        }


        if (value) { // 有值,才需要给模型的属性赋值
            // 利用KVC给模型中的属性赋值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

@end

3.5. 使用runtime实现方法的交换

3.5.1 前言

在开发过程中,我们常常会遇到一种问题: 即当我们使用系统自带的方法时,发现系统方法的功能不能满足我们的需求,这时候需要我们给系统的方法添加额外的功能.
下面以一个例子来实现给系统自带的方法添加额外的功能.

项目背景 : 公司有个开发了很久的项目,之前使用添加图片的方法用的是:imageName:方法, 现在的项目需求是给系统自带的imageName:添加一个功能:如果下载的图片不存在,那么就提示我们当前下载的图片为空.

思路 : 给系统自带的方法添加功能的几种方法

  1. 自定义类, 重写系统自带的imageName:方法,这种方法虽然可以实现,但是它的弊端就是必须要使用自己的类,依赖性强.
  2. 给UIImage添加一个分类, 改变系统类的实现,给系统的类添加方法的时候调用(强烈不推荐)
  3. 使用runtime的交互方法,给系统的方法添加功能. 具体实现 : 添加一个分类 --> 在分类中提供一个需要添加的功能的方法 --> 将这个方法的实现和系统自带的方法的实现交互.

3.5.2.步骤

3.5.2.1. 新建一个继承自UIImage的分类,定义一个方法实现给系统的自带的方法添加功能.

#import 
@interface UIImage (WGImage)
// 声明方法
// 如果跟系统方法差不多功能,可以采取添加前缀,与系统方法区分
+ (UIImage *)wg_imageWithName:(NSString *)imageName;
@end

3.5.2.2. 实现方法的交互

定义完毕新方法后,需要弄清楚什么时候实现与系统的方法交互?
答 : 既然是给系统的方法添加额外的功能,换句话说,我们以后在开发中都是使用自己定义的方法,取代系统的方法,所以,当程序一启动,就要求能使用自己定义的功能方法.说道这里:我们必须要弄明白一下两个方法 :
+(void)initialize(当类第一次被调用的时候就会调用该方法,整个程序运行中只会调用一次)

  • (void)load(当程序启动的时候就会调用该方法,换句话说,只要程序一启动就会调用load方法,整个程序运行中只会调用一次)
+ (void)load {
/*
     self:UIImage
     谁的事情,谁开头 1.发送消息(对象:objc) 2.注册方法(方法编号:sel) 3.交互方法(方法:method) 4.获取方法(类:class)
     Method:方法名
     获取方法,方法保存到类
     Class:获取哪个类方法
     SEL:获取哪个方法
     imageName
*/
    // 获取imageName:方法的地址
    Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 获取wg_imageWithName:方法的地址
    Method wg_imageWithNameMethod = class_getClassMethod(self, @selector(wg_imageWithName:));
    // 交换方法地址,相当于交换实现方式
    method_exchangeImplementations(imageNameMethod, wg_imageWithNameMethod);
}

3.5.2.3. 加载图片, 判断当前图片是否为空

// 加载图片, 判断是否为空
+ (UIImage *)wg_imageWithName:(NSString *)imageName
{
    // 这里调用imageWithName,相当于调用imageName
    UIImage *image = [UIImage wg_imageWithName:imageName];
    if (!image) {
        NSLog(@"Alex : 图片不存在");
    }
    return image;
}

3.5.2.4. 使用交互后的方法

#import "ViewController.h"
#import "WGStudent.h"
#import "UIImage+WGImage.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

  //  wg_imageWithName ---> imageNamed(底层的操作 : 1, 下载图片. 2, 判断图片是否存在 )
    [UIImage imageNamed:@"WilliamAlex.png"];
}

@end

3.5.2.5. 打印结果

2016-03-08 17:29:50.372 fdsfsdf[1545:96854] Alex : 图片不存在
知识拓展
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.

runtime的那点事儿(一)消息机制
http://blog.csdn.net/jq2530469200/article/details/51880836
runtime的那点事儿(二)消息机制
http://blog.csdn.net/jq2530469200/article/details/51886532
runtime的那点事儿(三)消息机制
http://blog.csdn.net/jq2530469200/article/details/51886578
runtime简单使用之方法的交换:
http://www.jianshu.com/p/d28abb706add
使用RunTime实现字典转模型:
http://www.jianshu.com/p/cecfe78e9cd8

你可能感兴趣的:(Runtime)