iOS开发小记-Runtime篇

2,163 阅读7分钟

现在看起来Runtime篇整理的少了,有时间再完善下,将就着看吧

什么是Runtime?


Objective-C将很多静态语言在编译和链接时期做的工作放在了Runtime运行时处理,可以说Runtime就是Objective-C的幕后工作者。

  1. Runtime(简称运行时),是一套由纯C写的API。
  2. 对于C语言,函数的调用会在编译的时候决定调用哪个函数。
  3. OC中的函数调用成为 消息发送 ,属于动态调用过程。在编译的时候并不能真正决定调用那个函数,只有真正运行的时候才会根据函数名称找到对应的函数来调用。
  4. 事实证明:在编译阶段,OC可以调用任意函数,即使这个函数并未实现,只有声明过就不会报错,只有运行时才会报错,这是因为OC是动态调用的。而C语言调用未实现的函数就会报错。

消息机制


任何方法调用的本质,就是发送了一个消息(用Runtime发送消息,OC底层实现通过Runtime实现)。

  • 原理

对象根据方法编号SEL去隐射表查找对应的方法实现。

  • 方法调用流程
  1. OC在向一个对象发送消息时,Runtime会根据该对象的isa指针找到该对象对应的类或者父类。
  2. 根据编号SEL在Method_List中查找对应方法。
  3. 如果找到最终函数实现地址,根据地址去方法区调用对应函数。如果没找到,会有三次拯救机会,否则抛出异常。
  4. Method resolution:objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。
  5. Fast forwarding:如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
  6. Normal forwarding:这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。 PS:对象方法保存在类对象的方法列表中,类方法保存在元类的方法列表中。

常用场景


  • 交换方法

有时候我们需要对类的方法进行修改,但是又无法拿到源码,我们便可以通过Runtime来交换方法实现。

+ (void)load {
    //获取实例方法实现
    Method method1 = class_getInstanceMethod(self, @selector(show));
    Method method2 = class_getInstanceMethod(self, @selector(ln_show));
    //获取类方法实现
//    Method method3 = class_getClassMethod(self, @selector(show));
//    Method method4 = class_getClassMethod(self, @selector(ln_show));

    //交换两个方法的实现
    method_exchangeImplementations(method1, method2);
    //将method1的实现换成method2
//    method_setImplementation(method1, method_getImplementation(method2));
}

- (void)show {
    NSLog(@"show person");
}

- (void)ln_show {
    NSLog(@"show person exchange");
}
  • 添加属性

实际上并没有产生真正的成员变量,通过关联对象来实现,具体参考分类。

  • 字典转模型

除了可以使用KVC实现外,还可以通过Runtime实现,就是取出所有ivars遍历赋值。但实际情况一般比较复杂:

  1. 当字典的key和模型的属性匹配不上。
  2. 模型中嵌套模型(模型属性是另外一个模型对象)。
  3. 数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。 我们这里仅考虑最简单的情况
+ (instancetype)modelWithDic:(NSDictionary *)dic {
    /*
     1.初始化实例对象
     */
    id object = [[self alloc] init];
    
    /**
     2.获取ivars
     class_copyIvarList: 获取类中的所有成员变量
     Ivar:成员变量
     第一个参数:表示获取哪个类中的成员变量
     第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
     返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    /*
     3.遍历赋值
     */
    for(int i = 0; i < count; i++) {
        //获取ivar属性
        Ivar ivar = ivarList[i];
        //获取属性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //去掉成员变量的下划线
        NSString *key = [ivarName substringFromIndex:1];
        //获取dic中对应值
        id value = dic[ivarName];
        //如果值存在,则赋值
        if(value) {
            [object setValue:value forKey:ivarName];
        }
    }
    
    return object;
}
  • 动态添加方法

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

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end
  • NSCoding的自动归档和解档

在实现encodeObjectdecodeObjectForKey方法中,我们一般需要把每个属性都要写一遍,这样很麻烦,我们可以通过Runtime来自动化。


- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    for(int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id value = [self valueForKey:ivarName];
        [aCoder encodeObject:value forKey:ivarName];
    }
    free(ivarList);
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    if(self == [super init]) {
        unsigned int count = 0;
        Ivar *ivarList = class_copyIvarList([self class], &count);
        
        for(int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [aDecoder decodeObjectForKey:ivarName];
            [self setValue:value forKey:ivarName];
        }
        free(ivarList);
    }
    return self;
}

还有更简便的方法,抽象成宏,参考网上资料。

  • 常用API
    unsigned int count = 0;
    //获取属性列表
    Ivar *propertyList = class_copyPropertyList([self class], &count);
    
    //获取方法列表
    Method *methodList = class_copyMethodList([self class], &count);
    
    //获取成员变量列表
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
    //获取协议列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

    //获取类方法
    Method method1 = class_getClassMethod([self class], @selector(run));
    
    //获取实例方法
    Method method2 = class_getInstanceMethod([self class], @selector(tempRun));
    
    //添加方法
    class_addMethod([self class], @selector(run), method_getImplementation(method2), method_getTypeEncoding(method2));
    
    //替换方法
    class_replaceMethod;
    
    //交换方法
    method_exchangeImplementations;

load与initialize


  • load

当类被引进项目的时候会执行load函数(在main函数开始之前),与这个类是会被用到无关,每个类的load函数只会被调用一次。由于load函数是自动加载的,不需要调用父类的load函数。

  1. 当父类和子类都实现了load函数时,先调用父类再调用子类。
  2. 当子类未实现load方法时,不会调用父类的load方法。
  3. 类中的load执行顺序要优于分类。
  4. 多个类别都有load方法时,其执行顺序与分类中其他相同方法一样,根据编译顺序决定。
  • initialize

这个方法会在类接收到第一次消息时调用。由于是系统调用,也不需要调用父类方法。

  1. 父类的initialize方法比子类优先。
  2. 当子类未实现initialize方法,会调用父类initialize方法;子类实现initialize方法时,会重载initialize方法。
  3. 当多个分类都实现了initialize方法,会执行最后一个编译的分类中的initialize方法。