长路漫漫,唯剑作伴--Runtime

一、为什么写这篇文章

  • 正是有了Runtime ,OC才有了面向对象的能力,也正是有了Runtime,OC语言的动态性才能体现的淋淋尽致,作为一个iOS开发者,Runtime是不可避免的重点话题之一。

二、Runtime之于OC

  • OC是在C语言的基础之上增加了一层面向对象的语言,同时又是一种动态语言。它将很多静态语言在编译和链接时期所做的事放到了运行时来处理,OC的这种特性决定了它不仅需要一个编译器,而且需要一个运行时系统执行编译的代码。这个运行时系统可以让所有的工作可以正常执行,这个运行时系统就是Runtime。

  • Runtime其实是一个库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。Runtime使OC的编程更具有灵活性,例如可以把消息转发给我们想要的对象,随意交换两个方法的实现等等。

三、总结一下Runtime做了那些事情

  • 封装:

    • 在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。

  • 找到最终执行的代码:

四、OC的方法调用

  • OC中方法调用的本质是让对象发送消息:

    // 创建person对象
    Person *p = [[Person alloc] init];
    
    // 调用对象方法
    [p eat];
    
    // 本质:让对象发送消息
    objc_msgSend(p, @selector(eat));
  • 函数调用过程

    • 编译器执行上述转换。在objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。每个对象内部都默认有一个isa指针指向这个对象所使用的类。isa是对象中的隐藏指针,指向创建这个对象的类。

    • 在Class中先去cache中通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)。

    • 若cache中未找到,再去methodList中查找,若methodlist中未找到,则取superClass中查找。

    • 若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

  • 消息转发:实现方式有三种

    • 动态方法解:

      // 默认方法都有两个隐式参数,
      void eat(id self,SEL sel) {
          NSLog(@"%@ %@",self,NSStringFromSelector(sel));
      }
      // 第一步:动态方法解析
      // 对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
      // resolve : [rɪˈzɑ:lv] 决定
      // instance : [ˈɪnstəns] 实例
      + (BOOL)resolveInstanceMethod:(SEL)sel {
          if (sel  == @selector(eat)) {
              // 第一个参数 : 方法接收者
              // 第二个参数 : 方法名
              // 第三个参数 : 方法指针
              // 第四个参数 : 方法签名,V代表方法返回值为void,@表示方法接受者,:表示_cmd,即方法名
              class_addMethod(self, @selector(eat), (IMP)eat, "v@:");
          }
          return [super resolveInstanceMethod:sel];
      }
      
      + (BOOL)resolveClassMethod:(SEL)sel {
          return [super resolveClassMethod:sel];
      }
      
    • 备援接收者

      // 第二步:备援接收者
      // 如果动态方法解析无法处理消息,实现此方法可以把这条消息转给其他接收者来处理
      // forwarding : ['fɔ:wədɪŋ] 转发
      // Target 目标
      - (id)forwardingTargetForSelector:(SEL)aSelector {
          return [[Drink alloc] init];
      }
    • 完整的消息转发:

      // 第三步:完整的消息转发
      
      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
          NSString *sel = NSStringFromSelector(aSelector);
          if ([sel isEqualToString:@"drink"]) {
              return [NSMethodSignature signatureWithObjCTypes:"v@:"];
          }
          return [super methodSignatureForSelector:aSelector];
      }
      
      - (void)forwardInvocation:(NSInvocation *)anInvocation {
          SEL sel = anInvocation.selector;
          Drink *drink = [[Drink alloc] init];
          if ([drink respondsToSelector:sel]) {
              [anInvocation invokeWithTarget:drink];
          }
      }

五、 动态性三方面:动态类型、动态绑定、动态加载

  1. 动态类型

    • 即id类型。动态类型和静态类型是相对的,如NSString、int等就属于静态类型。

    • 在静态类型中,如果数据类型不对应,就会提示警告。而动态类型则不会,动态类型会在运行时决定其属于哪一种数据类型。

  2. 动态绑定

    • 动态语言和静态语言一个区别就是,静态语言在编译时就已经确定了其逻辑,而动态语言则会在运行时决定。这是静态语言运行效率搞的原因之一。

    • 其实在OC中并没有函数调用这一概念,函数调用被称为消息发送,所谓消息发送就是给对象发送一条消息。

  3. 动态加载

六、Runtime使用场景

  1. 遍历

    #pragma mark--实例变量
    - (void)queryProperty{
        unsigned int count;
        objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
        for (unsigned int i = 0; i < count; i++) {
            objc_property_t pro = propertyList[i];
            const char *key = property_getName(pro);
            NSString *keyString = [NSString stringWithUTF8String:key];
            NSLog(@"%@",keyString);
        }
    }
    #pragma mark--成员变量
    - (void)qyeryVar{
        unsigned int count;
        Ivar *ivar = class_copyIvarList([Person class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar var = ivar[i];
            const char *key = ivar_getName(var);
            NSString *keyString = [NSString stringWithUTF8String:key];
            NSLog(@"%@",keyString);
        }
    }
    #pragma mark--方法遍历
    - (void)querymethod{
        unsigned int count;
        Method *methodList = class_copyMethodList([Person class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Method method = methodList[i];
            SEL select = method_getName(method);
            NSString *selectString = NSStringFromSelector(select);
            NSLog(@"%@",selectString);
        }
    }
  2. 动态添加方法

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    
        Person *p = [[Person alloc] init];
        // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
        // 动态添加方法就不会报错
        [p performSelector:@selector(eat)];
    }
    @end
    
    
    @implementation Person
    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
    
        if (sel == @selector(eat)) {
            // 动态添加eat方法
    
            // 第一个参数:给哪个类添加方法
            // 第二个参数:添加方法的方法编号
            // 第三个参数:添加方法的函数实现(函数地址)
            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
            class_addMethod(self, @selector(eat), eat, "v@:");
    
        }
    
        return [super resolveInstanceMethod:sel];
    }
    @end
  3. 动态添加属性

    // 定义关联的key
    static const char *key = "name";
    
    @implementation NSObject (Property)
    
    - (NSString *)name
    {
        // 根据关联的key,获取关联的值。
        return objc_getAssociatedObject(self, key);
    }
    
    - (void)setName:(NSString *)name
    {
        // 第一个参数:给哪个对象添加关联
        // 第二个参数:关联的key,通过这个key获取
        // 第三个参数:关联的value
        // 第四个参数:关联的策略
        objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
  4. Method Swizzling

    //load方法会在类第一次加载的时候被调用
    //调用的时间比较靠前,适合在这个方法里做方法交换
    + (void)load{
        //方法交换应该被保证,在程序中只会执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            //获得viewController的生命周期方法的selector
            SEL systemSel = @selector(viewWillAppear:);
            //自己实现的将要被交换的方法的selector
            SEL swizzSel = @selector(swiz_viewWillAppear:);
            //两个方法的Method
            Method systemMethod = class_getInstanceMethod([self class], systemSel);
            Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
            
            //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
    //        class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
            BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
            if (isAdd) {
                //如果成功,说明类中不存在这个方法的实现
                //将被交换方法的实现替换到这个并不存在的实现
                class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
            }else{
                //否则,交换两个方法的实现
                method_exchangeImplementations(systemMethod, swizzMethod);
            }
            
        });
    }
    
    - (void)swiz_viewWillAppear:(BOOL)animated{
        //这时候调用自己,看起来像是死循环
        //但是其实自己的实现已经被替换了
        [self swiz_viewWillAppear:animated];
        NSLog(@"swizzle");
    }
  5. 字典转模型

    #import "NSObject+Model.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (Model)
    
    + (NSArray *)propertList
    {
        unsigned int count = 0;
        //获取模型属性, 返回值是所有属性的数组 objc_property_t
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        NSMutableArray *arr = [NSMutableArray array];
        //便利数组
        for (int i = 0; i< count; i++) {
            //获取属性
            objc_property_t property = propertyList[i];
            //获取属性名称
            const char *cName = property_getName(property);
            NSString *name = [[NSString alloc]initWithUTF8String:cName];
            //添加到数组中
            [arr addObject:name];
        }
        //释放属性组
        free(propertyList);
        return arr.copy;
    }
    
    + (instancetype)modelWithDict:(NSDictionary *)dict
    {
        id obj = [self new];
        // 遍历属性数组
        for (NSString *property in [self propertList]) {
            // 判断字典中是否包含这个key
            if (dict[property]) {
                // 使用 KVC 赋值
                [obj setValue:dict[property] forKey:property];
            }
        }
        return obj;
    }
    
    @end
  6. 打印Model属性是否正确  

    • 方式一:直接打印

      • 此方式往往只打印出对象地址
    • 方式二:重写description方法

      • 如果属性较少,此方式比较适用
    • 方式三:结合Runtime重写description方法

      • 如果属性较多,此方式比较实用。在控制台则会打印出很多属性. 看着就不舒服~~而且还有一个问题就是, 有时候我们其实并不需要打印model的属性.. 那这样重写description方法反而适得其反了。
    • 方式四:结合Runtime重写debugDescription方法

      • debugDescription是在Xcode控制台里使用po命令的时候调用的~!而debugDescription的实现其实也就是调用了description方法而已。
      • 当需要打印model的属性的时候, 在控制台里使用po命令即可.

七、总结

八、补充

 

posted @ 2017-04-11 19:02  来事啊  阅读(199)  评论(0编辑  收藏  举报