美文网首页iOS_Runtime
iOS Runtime理解与运用

iOS Runtime理解与运用

作者: 大大盆子 | 来源:发表于2017-05-12 16:39 被阅读90次

    ping怎么这么高?


    哈哈,进入正题!

    什么是Runtime?

    这还要说?run( 运行)、time(时),runtime(运行时),没毛病!好了,我们都知道Objective-C是基于C衍生出来的动态语言,加入了面向对象特征和消息机制,这都归功于Runtime,它将静态语言在编译和链接时期做的事放到了运行时来处理。在我们Objective-C中,runtime是一个运行时库,是一套纯C的API。

    • 面向对象,在OC中一切都被设计成对象,它们的基础数据结构在Runtime库中用C语言的结构体表示。

      当一个类被初始化成一个实例,这个实例就是一个对象,在runtime中用objc_object结构体表示,可以到objc/objc.h查看定义
    typedef struct objc_class *Class;
    /// Represents an instance of a class.
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY; //结构体指针,指向类对象,这样,当我们向对象发送消息时,runtime库会根据这个isa指针找到对象所属的类,然后从类的方法列表及父类方法列表中查找消息对应的selector指向的函数实现,然后执行。
    };
    /// A pointer to an instance of a class.
    typedef struct objc_object *id; //该类型对象可以转换成任意对象
    

    当然类也是对象,由Class类型表示,它实际上是一个指向 objc_class结构体的指针,可以到objc/runtime.h中查看定义

    typedef struct objc_class *Class;
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;  //结构体的指针,每个对象都有一个isa指针,实例的isa指向类对象,类对象的isa指向元类。
    
        #if !__OBJC2__
        Class super_class                                        //父类
        const char *name                                         //类名
        long version                                             //类的版本信息,默认为0
        long info                                                //类信息,提供一些标识
        long instance_size                                       //实例变量大小
        struct objc_ivar_list *ivars                             //成员变量列表
        struct objc_method_list **methodLists                    //方法列表
        struct objc_cache *cache                                 //方法缓存
        struct objc_protocol_list *protocols                     //协议列表
        #endif
    }
    

    上面引出一个元类(Meta Class):类对象的类,它储存着一个类所有的类方法,每个类都有一个单独的meta-class,因为每个类的类方法基本不可能完全相同,那么细想,元类也是有isa指针的,它指向谁呢?为了不让这种结构无限延伸下去,isa指向基类的meta-class,而基类的meta-class的isa指针指向它自己。

    • 消息机制,在OC中任何的方法调用,其本质都是消息发送,id objc_msgSend(id self, SEL op, ...),属于动态调用的过程,比如[receiver doSomething],在运行时就会转成objc_msgSend(receiver,@selector(doSomething)),receiver作为一个消息接收对象,@selector(doSomething)是一个消息体,函数内部执行顺序:
    1. 检查消息对象是否为nil,如果是,则什么都不做。
    2. 通过receiver 的isa指针找到receiver对应的类,从类的方法缓存中通过SEL查找IMP,有,调用;没有,往下。(类的方法很多,如果每次都去方法列表中查找就会影响到效率,所以每一个类都会有一个方法缓存)。
    3. 从方法列表中查找,有,调用;没有,往下。
    4. 查找父类的方法缓存,有,直接调用;没有,往下。
    5. 查找父类的方法列表,有,直接调用;没有,往下,一直找到基类,以上就是一个正常的消息发送过程。
    6. 如果在基类也没有找到,则会调用NSObject的决议方法 + (BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel,返回YES则重启一次消息的发送过程,返回NO则会进入消息转发
    7. 调用- (id)forwardingTargetForSelector:(SEL)aSelector,如果实现了这个方法,并返回一个非nil的对象,则这个对象会作为消息的新接收者,且消息会被分发到这个对象,当然这个对象不能是self自身,否则就是出现无限循环;如果返回的是nil,往下继续。
    8. 调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector ,生成一个方法签名,接着会创建一个NSInvocation(消息调用对象,包含target,selector,以及方法签名),并传给- (void)forwardInvocation:(NSInvocation *)anInvocation,进行转发调用。

    消息异常处理

    当消息异常的时候,会执行方法决议以及消息转发,在上面的消息发送过程中也具体介绍了,这里借用一张图片来更好的理解


    • 在这个过程中,我们可以在方法决议中添加方法实现并返回YES,来阻止crash

      @implementation NSObject (ZMSafe)
      
      +(void)load{
      
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
        [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                            srcSel:@selector(forwardInvocation:)
                                       swizzledSel:@selector(zm_forwardInvocation:)];
        
        [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                            srcSel:@selector(methodSignatureForSelector:)
                                       swizzledSel:@selector(zm_methodSignatureForSelector:)];
        
        [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                            srcSel:@selector(forwardingTargetForSelector:)
                                       swizzledSel:@selector(zm_forwardingTargetForSelector:)];
        
        [self zm_swizzleClassMethodWithSrcClass:[self class]
                                         srcSel:@selector(resolveInstanceMethod:)
                                    swizzledSel:@selector(zm_resolveInstanceMethod:)];
        
        });
      }
      +(BOOL)zm_resolveInstanceMethod:(SEL)sel{
        if(sel == NSSelectorFromString(@"push")){
           NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass(self),NSStringFromSelector(sel),__FUNCTION__);
           /* 
              //这是method的数据结构,在method其实就相当于在SEL跟IMP之间作了一个映射,有了SEL,我们便可以找到对应的IMP
              struct objc_method {
                  SEL method_name                 //方法名                          
                  char *method_types              //方法类型                           
                  IMP method_imp                  //实现地址                            
              }   
           */
           Method method = class_getClassMethod([self class], @selector(empty));
           //  获取函数类型,有没有返回参数,传入参数
           const char *type = method_getTypeEncoding(method);
           // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联
           class_addMethod([self class], sel, method_getImplementation(method), type);
           // 返回YES,重启一次消息的发送过程,现在已经添加了方法实现empty,所以会直接调用它
           return YES;
        
        } 
        return [[self class]zm_resolveInstanceMethod:sel];
      }
      - (void)empty{
         NSLog(@"empty");
      }
      

      看调用结果,执行了决议方法和自定义的方法实现empty,并没有crash。

      其实在这里还可以做很多的事情,比如版本的适配,在低版本中调用了高版本的方法,在这里就可以把方法名提取出来,再指向我们自定义的方法实现,等等。

    • 也可以在- (id)forwardingTargetForSelector:(SEL)aSelector替换消息接收对象

      - (id)zm_forwardingTargetForSelector:(SEL)aSelector{
          if(aSelector == NSSelectorFromString(@"push")){
        
            NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass([self class]),NSStringFromSelector(aSelector),__FUNCTION__);
            // 我这里就直接动态创建一个类
            Class ZMClass = objc_allocateClassPair([NSObject class], "ZMClass", 0);
            // 注册类
            objc_registerClassPair(ZMClass);
            // 获取自定义empty方法
            Method method = class_getClassMethod([self class], @selector(empty));
            // 获取函数类型,有没有返回参数,传入参数
            const char *type = method_getTypeEncoding(method);
            // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联
            class_addMethod(ZMClass, aSelector, method_getImplementation(method), type);
            // 返回该对象来接收消息
            return [[ZMClass alloc]init];
        
         }
         return [self zm_forwardingTargetForSelector:aSelector];
      
      }
      

    再看调用结果,效果是一样的,只是不同的处理方式而已,从打印上可以看出,这是在- (id)zm_forwardingTargetForSelector:(SEL)aSelector中进行处理的,也是替换的- (id)forwardingTargetForSelector:(SEL)aSelector方法,找到返回的备用对象去执行调用的方法。

    • 或者在最后一步也就是消息真正转发的方法中做处理,重写- (void)forwardInvocation:(NSInvocation *)anInvocation,同时一定要重写- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,因为anInvocation对象是通过返回方法签名来创建的。

      /**
       消息转发方法
      
       @param anInvocation 消息转发对象
      */
      - (void)zm_forwardInvocation:(NSInvocation *)anInvocation{
      
          NSLog(@"unrecognized selector -[%@ %@]\n%s",anInvocation.target,NSStringFromSelector([anInvocation selector]),__FUNCTION__);
      
          //如果自定义实现方法中什么都没做,只是为了能在运行时找到该实现方法,不至于crash,那么这里可以不进行消息发送,可以注释掉
          if (![self respondsToSelector:anInvocation.selector]) {
             //  拿到方法对象
             Method method = class_getClassMethod([self class], @selector(empty));
             //  获取函数类型,有没有返回参数,传入参数
             const char *type = method_getTypeEncoding(method);
             // 添加方法
             class_addMethod([self class], anInvocation.selector, method_getImplementation(method), type);
             // 转发给自己,没毛病
             [anInvocation invokeWithTarget:self];
        
          }
      
      }
      /**
       构造一个方法签名,提供给- (void)forwardInvocation:(NSInvocation *)anInvocation方法,如果aSelector没有对应的IMP,则会生成一个空的方法签名,最终导致程序报错崩溃,所以必须重写。
      
       @param aSelector 方法编号
       @return 方法签名
      */
      - (NSMethodSignature *)zm_methodSignatureForSelector:(SEL)aSelector {
      
          if ([self respondsToSelector:aSelector]) {
             // 如果能够响应则返回原始方法签名
             return [self zm_methodSignatureForSelector:aSelector];
        
          }else{
            // 构造自定义方法的签名,不实现则返回nil,导致crash
            return [[self class] instanceMethodSignatureForSelector: @selector(empty)];
        
          }
      
      }
      

    调用结果也是一样的,这也是异常消息处理最后的机会,错过了就没机会了。


    小结

    这里主要是通过一个异常的消息来演示消息发送以及转发的过程,并在消息转发过程中对异常消息的捕捉及处理,我把这些写到NSObject的类目中主要为了防止开发中调用了不存在的方法导致的crash,当然如果在子类中重写了这些方法,可以调用super,也是一样的。


    基础用法

    • 对象关联objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy),这也是我在实际开发中使用<objc/runtime.h>的第一个API,方法的意思就是将两个不相关的对象通过一个特定的key关联起来,这样我们拿到对象object可以通过key找到对象Value,最具有代表性的运用就是给类目添加属性了。

      @interface NSObject (Property)
      
      @property (nonatomic,copy)NSString *text;
      
      @end
      
      @implementation NSObject (Property)
      // 手动构造Set方法,让text对象通过SEL指针跟self关联起来
      - (void)setText:(NSString *)text{
      
           objc_setAssociatedObject(self, @selector(setText:), text, OBJC_ASSOCIATION_COPY_NONATOMIC);
      }
      // 手动构造Get方法,通过SEL指针获取text对象
      - (NSString *)text{
      
          return objc_getAssociatedObject(self, @selector(setText:));
      }
      // 移除该对象下所有关联的对象
      - (void)removeProperty{
      
          objc_removeAssociatedObjects(self);
      }
      
    • 获取属性列表objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount),cls表示获取该类的属性列表,outCount表示属性的总个数。
      举栗:模型转字典
      - (NSDictionary *)dictionary{

          NSMutableDictionary *dic = [NSMutableDictionary dictionary];
          unsigned int count;
          objc_property_t *propertyList = class_copyPropertyList([self class], &count);
          for (int i = 0; i < count; i ++) {
        
              //生成key
              NSString *key = [NSString stringWithUTF8String:property_getName(propertyList[i])];
              //获取value
              id value = [self valueForKey:key];
        
              if (!value) break;
        
              [dic setObject:value forKey:key];
          }
         free(propertyList);
         return dic;
      }
      
    • 获取成员变量列表Ivar *class_copyIvarList(Class cls, unsigned int *outCount),跟获取属性列表一个意思,不同的是这里会获取该类所有的成员变量,当然其中也包括所有的属性。
      举栗:NSCoding协议,我们想要把模型直接写成本地文件,是要实现编解码协议的,而且要一个一个的写,这里通过拿到属性列表来对所有属性来编解码,一劳永逸。
      - (id)initWithCoder:(NSCoder *)aDecoder {
      self = [super init];
      if (self) {
      unsigned int count;
      Ivar *ivarList = class_copyIvarList([self class], &count);

              for (int i = 0; i < count; i ++) {
                  //拿到成员变量
                  Ivar ivar = ivarList[i];
                  //生成key
                  NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                  //获取value
                  id value = [aDecoder decodeObjectForKey:key];
                  [self setValue:value forKey:key];
              }
      
              free(ivarList);
          }
      
          return self;
      }
      
      - (void)encodeWithCoder:(NSCoder *)aCoder{
      
          unsigned int count;
          Ivar *ivarList = class_copyIvarList([self class], &count);
          for (int i = 0; i < count; i ++) {
        
              //拿到成员变量
              Ivar ivar = ivarList[i];
              //获取key
              NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
              //获取value
              id value = [self valueForKey:key];
              [aCoder encodeObject:value forKey:key];
        
          }
      
          free(ivarList);
      }
      
    • 获取方法列表Method *class_copyMethodList(Class cls, unsigned int *outCount),可以获取cls类的方法列表,包括私有方法,这样我们就可以调用对象的私有方法。
      @interface Student : NSObject
      @end
      @implementation Student
      - (void)study{

          NSLog(@"学习");
      }
      - (void)goHome{
      
          NSLog(@"回家");
      }
      @end
      
      @interface ViewController ()
      
      @end
      
      @implementation ViewController
      
      - (void)findStudentMethods{
      
          Student *student = [[Student alloc]init];
          unsigned int count;
          Method *methodList = class_copyMethodList([Student class], &count);
          for (int i = 0; i < count; i ++) {
              //获取方法对象
              Method method = methodList[i];
              //获取方法名
              SEL sel = method_getName(method);
              NSLog(@"方法名:%@",NSStringFromSelector(sel));
              if (sel == NSSelectorFromString(@"study")) {
                 //通过NSInvocation来转发消息
                  NSMethodSignature *methodSign = [[Student class] instanceMethodSignatureForSelector:sel];
                  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSign];
                  invocation.selector = sel;
                  [invocation invokeWithTarget:student];
              }
          }
      }
      

    打印结果如我们所料,能够拿到所有的方法,也能调用私有方法。


    • 动态添加方法动态创建类,细心的会发现,我在上面第一二段代码就已经描述过了,这里也不在啰嗦了。
    • Method Swizzling,这个我也不在这里多说了,之前写过一篇关于Method Swizzling的介绍,iOS Method Swizzling理解与运用

    总结

    这里主要是写了自己对Runtime的理解,以及在平时开发中的运用。Runtime里面的API有很多,目前对它的理解以及运用程度有限,所以借此来抛砖引玉,同时有什么错误的地方,希望朋友们能够指出改正,谢谢。

    相关文章

      网友评论

      本文标题:iOS Runtime理解与运用

      本文链接:https://www.haomeiwen.com/subject/owmwtxtx.html