Runtime 总结

参考文章

1. `文/滕先洪(简书作者)原文链接:http://www.jianshu.com/p/ab966e8a82e2著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”[下载地址]        (https://github.com/XHTeng/XHRuntimeDemo)`

2.`http://www.code4app.com/forum.php?mod=viewthread&tid=8241&highlight=runtime`

什么是runtime

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h> ),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。

  • RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。
  • OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
  • 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
  • 我们写的oc代码,它在运行的时候也是转换成了runtime方式运行的,更好的理解runtime,也能帮我们更深的掌握oc语言。
  • 每一个oc的方法,底层必然有一个与之对应的runtime方法。
  • 当我们用OC写下这样一段代码[tableView cellForRowAtIndexPath:indexPath];
  • 在编译时RunTime会将上述代码转化成[发送消息]objc_msgSend(tableView, @selector(cellForRowAtIndexPath,indexPath);

runtime的作用

  1. 动态交换两个方法的实现(特别是交换系统自带的方法);
  2. 动态添加对象的成员变量和成员方法;
  3. 获得某个类的所有成员方法、所有成员变量;

具体一点就是

  1. 获得某个类的所有成员方法、所有成员变量;
  2. 拦截系统自带的方法调用(Swizzle 黑魔法),比如拦截imageNamed:、viewDidLoad、alloc;
  3. 实现分类也可以增加属性;
  4. 实现NSCoding的自动归档和自动解档;
  5. 实现字典和模型的自动转换。

案例汇总

案例一:方法简单的交换

需要用到的方法 <objc/runtime.h>

  • 获得某个类的类方法
    Method class_getClassMethod(Class cls , SEL name)
  • 获得某个类的实例对象方法
    Method class_getInstanceMethod(Class cls , SEL name)
  • 交换两个方法的实现
    void method_exchangeImplementations(Method m1 , Method m2)

创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

+ (void)run {
NSLog(@"跑");
}

+ (void)study {
NSLog(@"学习");
}

调用方法,并通过runtime实现方法交换

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

person *aperson = [[person alloc]init];

[person run];
[person study];

// 获取两个类的类方法
Method m1 = class_getClassMethod([person class], @selector(run));
Method m2 = class_getClassMethod([person class], @selector(study));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
// 交换后,先打印学习,再打印跑!
[person run];
[person study];

}

打印结果为:

2016-07-11 14:10:28.033 runtime demo[37610:2393684] 跑
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 学习
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 学习
2016-07-11 14:10:28.034 runtime demo[37610:2393684] 跑

案例二:拦截并替换方法

需求:比如iOS6 升级 iOS7 后需要版本适配,根据不同系统使用不同样式图片(拟物化和扁平化),如何通过不去手动一个个修改每个UIImage的imageNamed:方法就可 以实现为该方法中加入版本判断语句?

步骤:
1、为UIImage建一个分类(UIImage+Category)
2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,比如版本判断

+ (UIImage *)xh_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {
    // 如果系统版本是7.0以上,使用另外一套文件名结尾是‘_os7’的扁平化图片
    name = [name stringByAppendingString:@"_os7"];
}
return [UIImage xh_imageNamed:name];
}

3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

+ (void)load {
// 获取两个类的类方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
}

注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(有点绕,就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!

利用以上思路,我们还可以给 NSObject 添加分类,统计创建了多少个对象,给控制器添加分类,统计有创建了多少个控制器,特别是公司需求总变的时候,在一些原有控件或模块上添加一个功能,建议使用该方法!

案例三:在分类中设置属性,给任何一个对象设置属性

众所周知,分类中是无法设置属性的,如果在分类的声明中写@property 只能为其生成get 和 set 方法的声明,但无法生成成员变量,就是虽然点语法能调用出来,但程序执行后会crash,有人会想到使用全局变量呢?比如这样:

int _age;

- (int )age {
    return _age;
}

- (void)setAge:(int)age {
    _age = age;
}

但是全局变量程序整个执行过程中内存中只有一份,我们创建多个对象修改其属性值都会修改同一个变量,这样就无法保证像属性一样每个对象都拥有其自己的属性值。这时我们就需要借助runtime为分类增加属性的功能了。

需要用到的方法 <objc/runtime.h>

  • set方法,将值value 跟对象object 关联起来(将值value 存储到对象object 中)
    参数 object:给哪个对象设置属性
    参数 key:一个属性对应一个Key,将来可以通过key取出这个存储的值,key 可以是任何类型:double、int 等,建议用char 可以节省字节
    参数 value:给属性设置的值
    参数policy:存储策略 (assign 、copy 、 retain就是strong)
    void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)
  • 利用参数key 将对象object中存储的对应值取出来
    id objc_getAssociatedObject(id object , const void *key)

步骤:
1、创建一个分类,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)

2、先在.h 中@property 声明出get 和 set 方法,方便点语法调用

@property(nonatomic,copy)NSString *name;

3、在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值

char nameKey;

- (void)setName:(NSString *)name {
    // 将某个值跟某个对象关联起来,将某个值存储到某个对象中
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}

案例四:获得一个类的所有成员变量

最典型的用法就是一个对象在归档和解档的 encodeWithCoder和initWithCoder:方法中需要该对象所有的属性进行decodeObjectForKey: 和 encodeObject:,通过runtime我们声明中无论写多少个属性,都不需要再修改实现中的代码了。

需要用到的方法 <objc/runtime.h>

  • 获得某个类的所有成员变量(outCount 会返回成员变量的总数)
    参数:
    1、哪个类
    2、放一个接收值的地址,用来存放属性的个数
    3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
  • 获得成员变量的名字
const char *ivar_getName(Ivar v)
  • 获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)

获取Person类中所有成员变量的名字和类型

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([Person class], &outCount);

// 遍历所有成员变量
for (int i = 0; i < outCount; i++) {
    // 取出i位置对应的成员变量
    Ivar ivar = ivars[i];
    const char *name = ivar_getName(ivar);
    const char *type = ivar_getTypeEncoding(ivar);
    NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
}
// 注意释放内存!
free(ivars);

利用runtime 获取所有属性来重写归档解档方法

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个Movie类,有3个属性,它的h文件这这样的

#import <Foundation/Foundation.h>
 
//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding
@interface Movie : NSObject<NSCoding>
 
@property (nonatomic, copy) NSString *movieId;
@property (nonatomic, copy) NSString *movieName;
@property (nonatomic, copy) NSString *pic_url;
 
@end

如果是正常写法, m文件应该是这样的:

#import "Movie.h"
@implementation Movie
 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_movieId forKey:@"id"];
    [aCoder encodeObject:_movieName forKey:@"name"];
    [aCoder encodeObject:_pic_url forKey:@"url"];
 
}
 
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        self.movieId = [aDecoder decodeObjectForKey:@"id"];
        self.movieName = [aDecoder decodeObjectForKey:@"name"];
        self.pic_url = [aDecoder decodeObjectForKey:@"url"];
    }
    return self;
}
@end

如果这里有100个属性,那么我们也只能把100个属性都给写一遍。
不过你会使用runtime后,这里就有更简便的方法。
下面看看runtime的实现方式:

#import "Movie.h"
#import <objc/runtime.h>
@implementation Movie
 
- (void)encodeWithCoder:(NSCoder *)encoder
 
{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([Movie class], &count);
 
    for (int i = 0; i<count; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars;
        // 查看成员变量
        const char *name = ivar_getName(ivar);
        // 归档
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}
 
- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([Movie class], &count);
        for (int i = 0; i<count; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars;
        // 查看成员变量
        const char *name = ivar_getName(ivar);
       // 归档
       NSString *key = [NSString stringWithUTF8String:name];
      id value = [decoder decodeObjectForKey:key];
       // 设置到成员变量身上
        [self setValue:value forKey:key];
 
        }
        free(ivars);
    } 
    return self;
}
@end

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,还嫌麻烦,下面看看更加简便的方法:两句代码搞定。
我们把encodeWithCoder 和 initWithCoder这两个方法抽成宏

#import "Movie.h"
#import <objc/runtime.h>
 
#define encodeRuntime(A) 

unsigned int count = 0;
Ivar *ivars = class_copyIvarList([A class], &count);    
for (int i = 0; i<count; i++) {
Ivar ivar = ivars;
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);

 
#define initCoderRuntime(A) 

if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([A class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars;
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [decoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;

 
@implementation Movie
 
- (void)encodeWithCoder:(NSCoder *)encoder
 
{
    encodeRuntime(Movie)
}
 
- (id)initWithCoder:(NSCoder *)decoder
{
    initCoderRuntime(Movie)
}
@end

我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。

案例五:利用runtime 获取所有属性来进行字典转模型

以往我们都是利用KVC进行字典转模型,但是它还是有一定的局限性,例如:模型属性和键值对对应不上会crash(虽然可以重写setValue:forUndefinedKey:方法防止报错),模型属性是一个对象或者数组时不好处理等问题,所以无论是效率还是功能上,利用runtime进行字典转模型都是比较好的选择。

字典转模型我们需要考虑三种特殊情况:
1.当字典的key和模型的属性匹配不上
2.模型中嵌套模型(模型属性是另外一个模型对象)
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)

字典转模型的应用可以说是每个app必然会使用的场景,虽然实现的方式略有不同,但是原理都是一致的:遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
像几个出名的开源库:JSONModel,MJExtension等都是通过这种方式实现的。

  • 先实现最外层的属性转换

    // 创建对应模型对象
    id objc = [[self alloc] init];

    unsigned int count = 0;
    
    // 1.获取成员属性数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
    for (int i = 0; i < count; i++) {
    
      // 2.1 获取成员属性
      Ivar ivar = ivarList;
    
      // 2.2 获取成员属性名 C -> OC 字符串
     NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
    
      // 2.3 _成员属性名 => 字典key
      NSString *key = [ivarName substringFromIndex:1];
    
      // 2.4 去字典中取出对应value给模型属性赋值
      id value = dict[key];
    
      // 获取成员属性类型
      NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
      }
    

如果模型比较简单,只有NSString,NSNumber等,这样就可以搞定了。但是如果模型含有NSArray,或者NSDictionary等,那么我们还需要进行第二步转换。

  • 内层数组,字典的转换

    if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { 
    
           //  是字典对象,并且属性名对应类型是自定义类型
          // 处理类型字符串 @\"User\" -> User
          ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
          ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
          // 自定义对象,并且值是字典
          // value:user字典 -> User模型
          // 获取模型(user)类对象
          Class modalClass = NSClassFromString(ivarType);
    
          // 字典转模型
          if (modalClass) {
              // 字典转模型 user
              value = [modalClass objectWithDict:value];
          }
    
      }
    
      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 objectWithDict:dict];
                  [arrM addObject:model];
              }
    
              // 把模型数组赋值给value
              value = arrM;
    
          }
      }
    

我自己觉得系统自带的KVC模式字典转模型就挺好的,假设movie是一个模型对象,dict 是一个需要转化的 [movie setValuesForKeysWithDictionary:dict]; 这个是系统自带的字典转模型方法,个人感觉也还是挺好用的,不过使用这个方法的时候需要在模型里面再实现一个方法才行,
- (void)setValue: (id)value forUndefinedKey: (NSString *)key

重写这个方法为了实现两个目的:

  1. 模型中的属性和字典中的key不一致的情况,比如字典中有个id,我们需要把它赋值给uid属性;
  2. 字典中属性比模型的属性还多的情况。如果出现以上两种情况而没有实现这个方法的话,程序就会崩溃。

这个方法的实现:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"id"]) {
    self.uid = value;
}
}

案例六:动态变量控制

在程序中,xiaoming的age是10,后来被runtime变成了20,来看看runtime是怎么做到的。

  • 1.动态获取XiaoMing类中的所有属性[当然包括私有]

      `Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);`  
    
  • 2.遍历属性找到对应name字段

      `const char *varName = ivar_getName(var);`
    
  • 3.修改对应的字段值成20

      `object_setIvar(self.xiaoMing, var, @"20");`  
    
  • 4.代码参考

    -(void)answer{
      unsigned int count = 0;
      Ivar ivar = class_copyIvarList([self.xiaoMing class], &count);
      for (int i = 0; i<count; i++) {
    Ivar var = ivar[i];
    const char varName = ivar_getName(var);
    NSString *name = [NSString stringWithUTF8String:varName];
    if ([name isEqualToString:@”_age”]) {
    object_setIvar(self.xiaoMing, var, @”20”);
    break;
    }
    }
    NSLog(@”XiaoMing’s age is %@”,self.xiaoMing.age);
    }
    

案例七:动态添加方法

在程序当中,假设XiaoMing的中没有guess这个方法,后来被Runtime添加一个名字叫guess的方法,最终再调用guess方法做出相应。那么,Runtime是如何做到的呢?

1.动态给XiaoMing类中添加guess方法:

class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@: ");

这里参数地方说明一下:

(IMP)guessAnswer 意思是guessAnswer的地址指针;
"v@:" 意思是,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
“v@😡@” 意思是,两个参数的没有返回值。

2.调用guess方法响应事件:

[self.xiaoMing performSelectorselector(guess)];

3.编写guessAnswer的实现:

void guessAnswer(id self,SEL _cmd){
 NSLog(@"i am from beijing");
  } 

这个有两个地方留意一下:

  • void的前面没有+、-号,因为只是C的代码。
  • 必须有两个指定参数(id self,SEL _cmd)。

代码参考:

-(void)answer{
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");
 if ([self.xiaoMing respondsToSelector:@selector(guess)]) {

     [self.xiaoMing performSelector:@selector(guess)];

  } else{
     NSLog(@"Sorry,I don't know");
 }
 }
  void guessAnswer(id self,SEL _cmd){

  NSLog(@"i am from beijing");

  }

案例八:在方法上增加额外功能

有这样一个场景,出于某些需求,我们需要跟踪记录APP中按钮的点击次数和频率等数据,怎么解决?当然通过继承按钮类或者通过类别实现是一个办法,但是带来其他问题比如别人不一定会去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:

@implementation UIButton (Hook)

+ (void)load {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

    Class selfClass = [self class];

    SEL oriSEL = @selector(sendAction:to:forEvent:);
    Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

    SEL cusSEL = @selector(mySendAction:to:forEvent:);
    Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

    BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
    if (addSucc) {
        class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else {
        method_exchangeImplementations(oriMethod, cusMethod);
    }

  });
 }
 
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[CountTool addClickCount];
[self mySendAction:action to:target forEvent:event];
}

@end

load方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换,方法交换应该被保证,在程序中只会执行一次。

posted @ 2016-07-11 14:53  微博和csdn还有你  阅读(247)  评论(0编辑  收藏  举报