iOS开发小记-Runtime篇

如今看起来Runtime篇整理的少了,有时间再完善下,将就着看吧数组

什么是Runtime?


Objective-C将不少静态语言在编译和连接时期作的工做放在了Runtime运行时处理,能够说Runtime就是Objective-C的幕后工做者。bash

  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来交换方法实现。ui

+ (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");
}
复制代码
  • 添加属性

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

  • 字典转模型

除了可使用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;
}
复制代码
  • 动态添加方法

若是一个类方法很是多,加载类到内存的时候也比较耗费资源,须要给每一个方法生成映射表,可使用动态给某个类,添加方法解决。code

- (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来自动化。orm

- (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方法。
相关文章
相关标签/搜索