详解 Objective-C 中的 Runtime(上)

公司项目用到一个三方开源库,里面有个bug,不能改动源码,我想来想去,只能通过runtime这个万能的手段来解决。但是runtime 并不怎么会用,怎么办,马上学习呗。说到runtime,它是Objective-C里面最核心的技术,被人们传呼的神乎其神,但是感觉有一层神秘的面纱笼罩其上,毕竟使用场景不多,相信大多数开发者都不会熟练的运用。而网络上也有无数的文章来讲解runtime,但是真的非常的乱,非常的碎片化,很少有讲解的比较全面的。


最初是在onevcat的博客上看到runtime的runtime的博客,说句实话,看完后我还是蒙的,这里面主要讲了一下runtime 比较核心的功能-Method Swizzling,不过看完后还是有些不知如何下手的感觉。下面是我自己对runtime的整理,从零开始,由浅入深,并且带了几个runtime实际的应用场景。看完之后,你可以再回过头来看喵神的这篇文章,应该就能看的懂了。


一:基本概念


Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。


  • RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。

  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。

  • OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。

  • 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。


二:runtime的具体实现


我们写的oc代码,它在运行的时候也是转换成了runtime方式运行的,更好的理解runtime,也能帮我们更深的掌握oc语言。

每一个oc的方法,底层必然有一个与之对应的runtime方法。



  • 当我们用OC写下这样一段代码

    [tableView cellForRowAtIndexPath:indexPath];

  • 在编译时RunTime会将上述代码转化成[发送消息]

    objc_msgSend(tableView, @selector(cellForRowAtIndexPath:),indexPath);


三:常见方法


unsigned int count;


  • 获取属性列表


objc_property_t *propertyList = class_copyPropertyList([self class], &count);

for (unsigned int i=0; i%@", [NSString stringWithUTF8String:propertyName]);

}


  • 获取方法列表


Method *methodList = class_copyMethodList([self class], &count);

for (unsigned int i; i%@", NSStringFromSelector(method_getName(method)));

}


  • 获取成员变量列表


Ivar *ivarList = class_copyIvarList([self class], &count);

  for (unsigned int i; i%@", [NSString stringWithUTF8String:ivarName]);

  }


  • 获取协议列表


__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

  for (unsigned int i; i%@", [NSString stringWithUTF8String:protocolName]);

  }


现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法


  • 获得类方法


Class PersonClass = object_getClass([Person class]);

SEL oriSEL = @selector(test1);

Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);


  • 获得实例方法


Class PersonClass = object_getClass([xiaoming class]);

SEL oriSEL = @selector(test2);

Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);


  • 添加方法


BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));


  • 替换原方法实现


class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));


  • 交换两个方法


method_exchangeImplementations(oriMethod, cusMethod);


四:常见作用


  • 动态的添加对象的成员变量和方法

  • 动态交换两个方法的实现

  • 拦截并替换方法

  • 在方法上增加额外功能

  • 实现NSCoding的自动归档和解档

  • 实现字典转模型的自动转换


五:代码实现


要使用runtime,要先引入头文件#import 

这些代码的实例有浅入深逐步讲解,最后附上一个我在公司项目中遇到的一个实际问题。


1. 动态变量控制


在程序中,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


2.动态添加方法


在程序当中,假设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 performSelector:@selector(guess)];


3.编写guessAnswer的实现:


void guessAnswer(id self,SEL _cmd){

NSLog(@”i am from beijing”);

}


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


  • void的前面没有+、-号,因为只是C的代码。

  • 必须有两个指定参数(id self,SEL _cmd)


4.代码参考


-(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");

 

}


3:动态交换两个方法的实现


在程序当中,假设XiaoMing的中有test1 和 test2这两个方法,后来被Runtime交换方法后,每次调动test1 的时候就会去执行test2,调动test2 的时候就会去执行test1, 。那么,Runtime是如何做到的呢?


  1. 获取这个类中的两个方法并交换


Method m1 = class_getInstanceMethod([self.xiaoMing class], @selector(test1));

    Method m2 = class_getInstanceMethod([self.xiaoMing class], @selector(test2));

    method_exchangeImplementations(m1, m2);


交换方法之后,以后每次调用这两个方法都会交换方法的实现


4:拦截并替换方法


在程序当中,假设XiaoMing的中有test1这个方法,但是由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(正如一些开源库出现问题的时候),这个时候runtime就派上用场了。


我们先增加一个tool类,然后写一个我们自己实现的方法-change,

通过runtime把test1替换成change。


Class PersionClass = object_getClass([Person class]);

Class toolClass = object_getClass([tool class]);

 

    源方法的SEL和Method

 

    SEL oriSEL = @selector(test1);

    Method oriMethod = class_getInstanceMethod(PersionClass, oriSEL);

 

    交换方法的SEL和Method

 

    SEL cusSEL = @selector(change);

    Method cusMethod = class_getInstanceMethod(toolClass, cusSEL);

 

    先尝试給源方法添加实现,这里是为了避免源方法没有实现的情况

 

    BOOL addSucc = class_addMethod(PersionClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));

    if (addSucc) {

          // 添加成功:将源方法的实现替换到交换方法的实现    

        class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

 

    }else {

    //添加失败:说明源方法已经有实现,直接将两个方法的实现交换即

method_exchangeImplementations(oriMethod, cusMethod);  

  }


5:在方法上增加额外功能


有这样一个场景,出于某些需求,我们需要跟踪记录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方法会在类第一次加载的时候被调用,调用的时间比较靠前,适合在这个方法里做方法交换,方法交换应该被保证,在程序中只会执行一次。


6.实现NSCoding的自动归档和解档


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

假设现在有一个Movie类,有3个属性,它的h文件这这样的


#import

 

//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding

@interface Movie : NSObject

 

@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

@implementation Movie

 

- (void)encodeWithCoder:(NSCoder *)encoder

 

{

    unsigned int count = 0;

    Ivar *ivars = class_copyIvarList([Movie class], &count);

 

    for (int i = 0; i


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

我们把encodeWithCoder 和 initWithCoder这两个方法抽成宏


#import "Movie.h"

#import

 

#define encodeRuntime(A)

 

unsigned int count = 0;

Ivar *ivars = class_copyIvarList([A class], &count);

for (int i = 0; i


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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值