iOS底层-Runtime系列二

Class的结构

获取源码

  1. 查看源码

     typedef struct objc_class *Class;
    
  2. Class是objc_class结构体指针

     struct objc_class : objc_object {
         ....
     }
    
  3. objc_class继承自objc_object

     struct objc_object {
     private:
         isa_t isa;
     ...    
     }
    
  4. 将父类也整合到objc_class类中如下:(注意这里只考虑成员变量)

     struct objc_class  {
     // Class ISA;
     private:
         isa_t isa;
     public:
         Class superclass;
         cache_t cache;             // formerly cache pointer and vtable
         class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
            
         //这个成员函数是特殊。使用bits成员的data()函数可以获取class_rw_t*
         class_rw_t *data() { 
             return bits.data();
         }
         ....成员函数
     }
    
  5. class_rw_t 如下:

     struct class_rw_t {
         uint32_t flags;
         uint32_t version;
         const class_ro_t *ro; //其他信息
         method_array_t methods;//方法列表
         property_array_t properties; //属性列表
         protocol_array_t protocols; //协议列表
         Class firstSubclass;
         Class nextSiblingClass;
         char *demangledName;
         ...成员函数
     }
    
  6. class_ro_t如下

     struct class_ro_t {
         uint32_t flags;
         uint32_t instanceStart;
         uint32_t instanceSize;
     #ifdef __LP64__
         uint32_t reserved;
     #endif
         const uint8_t * ivarLayout;
         const char * name;//类名
         method_list_t * baseMethodList;
         protocol_list_t * baseProtocols;
         const ivar_list_t * ivars;//成员变量列表
        
         const uint8_t * weakIvarLayout;
         property_list_t *baseProperties;
     };
    
  7. 分析:

    1. class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
    2. class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容

method_t

  1. method_t是对方法\函数的封装

     //对方法函数的封装
     struct method_t {
         SEL name;//函数名
         const char *types;//编码(返回值类型、参数类型)
         MethodListIMP imp;//指向函数的指针(函数地址)
     };
    
  2. IMP代表函数的具体实现:

     /// A pointer to the function of a method implementation. 
     #if !OBJC_OLD_DISPATCH_PROTOTYPES
     typedef void (*IMP)(void /* id, SEL, ... */ ); 
     #else
     typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
     #endif
    
  3. SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似(即相当于C的字符串)
    1. 可以通过@selector()sel_registerName()获得
    2. 可以通过sel_getName()NSStringFromSelector()转成字符串
    3. 不同类中相同名字的方法,所对应的方法选择器是相同的
     /// An opaque type that represents a method selector.
     typedef struct objc_selector *SEL;
    
  4. types包含了函数返回值、参数编码的字符串
    1. 这个C字符串组成拼接:返回值、参数1、参数2…
  5. Type Encoding
    1. iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码
    2. 比如:@encode(id);,这返回值是:@ 图1
    3. 举例使用

        Method mthod =  class_getInstanceMethod (Person.class, @selector(test1));
       Method mthod2 =  class_getInstanceMethod (Person.class, @selector(test2:andCan2:));
       const char *type =  method_getTypeEncoding(mthod);
       const char *type2 =  method_getTypeEncoding(mthod2);
              
       NSLog(@"===%s",type);//v16@0:8
       NSLog(@"===%s",type2);//v28@0:8i16d20
      
      1. oc中的每个方法都有2个默认参数:self、SEL,self为id类型
      2. v28@0:8i16d20分析
        1. 去除数字:v@:id 代表:void id SEL int double
        2. 28代表参数占用的总字节数:
          1. id:指针类型,8个字节
          2. SEL:指针类型,8个字节
          3. int:4个字节
          4. double:8个字节
        3. 0代表:@是从第0个字节开始的
        4. 8代表::是从第8个字节开始的
        5. 16代表:i是从第16个字节开始的
        6. 20代表:d是从第20个字节开始的

方法缓存

  1. Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度

     struct cache_t {
         struct bucket_t *_buckets; //散列表 (数组指针)
         mask_t _mask;//散列表的长度 -1
         mask_t _occupied;// 已经缓存的方法数量
     }
    
    1. 散列表_buckets是一个数组指针,数组元素为:bucket_t类型

       struct bucket_t {
       private:
           MethodCacheIMP _imp;//函数的内存地址
           cache_key_t _key; //SEL作为key
       }
      
  2. 缓存方法查找:

    1. 通过cache_t的成员函数find查找

       struct bucket_t * find(cache_key_t key, id receiver);
              
       /**
        获取缓存方法
        @param k 传入的@selector(方法名)
        @param receiver 方法的接收这
        @return 返回方法bucket_t
        */
       bucket_t * cache_t::find(cache_key_t k, id receiver)
       {
           assert(k != 0);
           //拿到该类所有的缓存方法列表数组--散列表
           bucket_t *b = buckets();
           //拿到该类散列表长度-1值,就是数组长度-1
           mask_t m = mask();
           //cache_hash是个内联函数,本质是:k&m
           mask_t begin = cache_hash(k, m);
           //将begin作为遍历的第一个值
           mask_t i = begin;
           do {
               //拿到数组元素bucket_t中的key值,如果为0,或者等于k,那么就返回这个方法存储对象bucket_t指针
               if (b[i].key() == 0  ||  b[i].key() == k) {
                   return &b[i];
               }
               //如果不成立,i-1,继续遍历,直到找到k==对应元素的key
           } while ((i = cache_next(i, m)) != begin);
              
           // hack
           Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
           cache_t::bad_cache(receiver, (SEL)k, cls);
       }
      
    2. 内部原理:
      1. 当对象第一次调用一个方法时,它会将 @selector(方法名) & mask作为数组的索引值存入到buckets数组中
      2. 由于不同的方法名&mask时可能生成同一个索引,因为@selector(方法名)地址是不同的而且很大(比如:0x 0000 0000 0000 0FFE),&一个很小的mask(比如:64),肯定会出现同一个值的可能,那么&之后去存储到数组buckets时,发现该索引已经有值,那么它就会将索引自动-1,然后在存储,直到没有值为止
      3. 因此上面的find方法也是如此,现根据 @selector(方法名) & mask作为索引查找buckets数组中相应的元素,然后拿到元素的key值比较,看看是否相等,不相等则-1遍历,继续查找,直到直到为止。
    3. 散列表存储的特点
      1. 也是一个数组
      2. 存储方式不是一个一个按顺序存入
      3. 通过某种方式指定index指定存入
        1. 比如&数组的长度-1
        2. 注意:x&y 结果一定小于y
        3. 所以可以通过&数组长度-1来获取索引值存储

objc_msgSend 分析

  1. objc_msgSend执行流程
    1. 示例:

       //OC方法
       [person p_test];
       //转化为C++编译
       objc_msgSend(person, sel_registerName("p_test"));
      
    2. OC中的方法调用,其实都是转换为objc_msgSend函数的调用
    3. objc_msgSend的执行流程可以分为3大阶段
      1. 消息发送
      2. 动态方法解析
      3. 消息转发
    4. objc_msgSend源码分析,在objc源码中的objc-msg-arm64.s文件,发现objc_msgSend是用汇编实现的.OC中有些调用频率很高的方法直接用汇编实现

       objc-msg-arm64.s
       ENTRY _objc_msgSend
       b.le    LNilOrTagged
       CacheLookup NORMAL
       .macro CacheLookup
       .macro CheckMiss
       STATIC_ENTRY __objc_msgSend_uncached
       .macro MethodTableLookup
       __class_lookupMethodAndLoadCache3
      
      1. __class_lookupMethodAndLoadCache3汇编中没有,直接找对应的C函数_class_lookupMethodAndLoadCache3

         objc-runtime-new.mm
         _class_lookupMethodAndLoadCache3
         lookUpImpOrForward
         getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
         cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
         _class_resolveInstanceMethod
         _objc_msgForward_impcache
        
      2. 查找_objc_msgForward_impcache方法实现

         objc-msg-arm64.s
         STATIC_ENTRY __objc_msgForward_impcache
         ENTRY __objc_msgForward
                    
         Core Foundation
         __forwarding__(不开源)
        
  2. 从上面的分析,消息查找的本质都在:lookUpImpOrForward这个方法中
    1. 这个方法中可以看出,把消息查找分为3个阶段
      1. 消息发送
      2. 动态方法解析
      3. 消息转发
    2. 而且这个方法的名字:lookUpImpOrForward,查找IMP或者Forward
  3. 消息发送阶段流程图如下:

    图1

    1. 如果是从class_rw_t中查找方法
      1. 已经排序的,二分查找
      2. 没有排序的,遍历查找
    2. receiver通过isa指针找到receiverClass
    3. receiverClass通过superclass指针找到superClass
  4. 动态方法解析阶段流程图如下:

    图1

    1. 开发者可以实现以下方法,来动态添加方法实现
      1. 动态添加实例方法:+resolveInstanceMethod:
      2. 动态添加类方法: +resolveClassMethod:
    2. 动态解析过后,会重新走“消息发送”的流程
      1. “从receiverClass的cache中查找方法”这一步开始执行
    3. 代码示例:

       #import "Person.h"
       #import <objc/runtime.h>
       @implementation Person
       //-(void)p_test{
       //    NSLog(@"===========");
       //}
              
       //也可以直接用C语言来动态添加
       //但是必须有2个默认参数id,SEL
       void test2(id self,SEL _cmd){
           NSLog(@"=====C语言函数======");
       }
              
       //调用p_test,会动态调用到这
       -(void)test{
           NSLog(@"===========");
       }
              
       //对象方法动态解析
       /**
        消息发送阶段没找到对应实例方法,会来到这--动态方法解析
        作用:动态的添加一个方法
        本质:将正在寻找的sel指向动态添加的方法的IMP
        @param sel 正在寻找的sel
        @return 是否有动态添加方法
        */
       +(BOOL)resolveInstanceMethod:(SEL)sel{
       //    if (sel == @selector(p_test)) {
       //        //获取一个方法的实现
       //        Method method =  class_getInstanceMethod(self, @selector(test));
       //        const char *typeEncode = method_getTypeEncoding(method);
       //        IMP imptest =  method_getImplementation(method);
       //        //动态添加方法:添加实例方法,实例方法添加到类对象
       //        class_addMethod(self, @selector(p_test), imptest, typeEncode);
       //        return YES;
       //    }
           //或者添加C函数
           if (sel == @selector(p_test)) {
               //设置type编码
               const char *typeEncode = "v16@0:8";
               //C语言函数名就是函数地址
               IMP imptest = (IMP)test2;
               //动态添加方法:添加实例方法,实例方法添加到类对象
               class_addMethod(self, @selector(p_test), imptest, typeEncode);
               return YES;
           }
       //    NSLog(@"===========");
           return [super resolveInstanceMethod:sel];
       }
       //类方法解析
              
       /**
        消息发送阶段没找到对应类方法,会来到这--动态方法解析
        作用:动态的添加一个方法
        本质:将正在寻找的sel指向动态添加的方法的IMP
        @param sel 正在寻找的sel
        @return 是否有动态添加方法
        */
       +(BOOL)resolveClassMethod:(SEL)sel{
           NSLog(@"===========");
           return [super resolveClassMethod:sel];
       }
       @end
              
              
       //使用
       Person *person = [[Person alloc] init];
       [person p_test];
      
  5. 消息转发阶段
    1. 源码如下:

       _objc_msgForward_impcache
       //实现在objc-msg-arm64.s中
       STATIC_ENTRY __objc_msgForward_impcache
       ENTRY __objc_msgForward
              
       Core Foundation
       __forwarding__(不开源)
      
    2. 分析流程如下:

      图1

      1. 开发者可以在forwardInvocation:方法中自定义任何逻辑
      2. 以上方法都有对象方法、类方法2个版本(前面可以是加号+,也可以是减号-)
    3. 代码举例:

       #import "Person.h"
       #import "Person2.h"
       @implementation Person
       /**
        调用条件:当前类消息发送阶段、动态解析阶段都没有找到时
               
        作用: 返回一个能够处理aSelector的对象id
              
        @param aSelector 要处理的方法
        @return 要转发的对象
        */
       //也可以是类方法:+(id)forwardingTargetForSelector:(SEL)aSelector
       //但是返回值应该是类对象:[Person2 class];而不是对象;
       //-(id)forwardingTargetForSelector:(SEL)aSelector{
       //    if (aSelector == @selector(p_test)) {
       //
       //        return [[Person2 alloc] init];
       //    }
       //    return [super forwardingTargetForSelector:aSelector];
       //}
              
       /**
        调用条件:没有实现forwardingTargetForSelector,或者forwardingTargetForSelector返回值为nil
        作用:返回一个方法的签名
        @param aSelector 要处理的方法
        @return 方法的签名: 返回值类型、参数类型
        */
       //也可以是类方法:+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
       -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
           if (aSelector == @selector(p_test)) {
                      
               return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
           }
           return  [super methodSignatureForSelector:aSelector];
       }
              
       /**
        调用条件:methodSignatureForSelector返回了一个方法签名
        作用: 指定调用对象,调用方法
        注意: 走到这里就不会报错了,这里可以写任何东西,甚至可以什么都不写
               
        anInvocation.target; //调用者
        anInvocation.selector; //方法名
        [anInvocation getArgument:NULL atIndex:2]; 获取参数
              
        @param anInvocation 封装了一个函数调用,包括方法调用者、方法名、参数,根据上面的方法签名包装的
        */
       //也可以是类方法:+(void)forwardInvocation:(NSInvocation *)anInvocation
       -(void)forwardInvocation:(NSInvocation *)anInvocation{
                  
           //这里可以什么都不写
                  
           //也可以指定相应的对象调用
       //    anInvocation.target = [[Person2 alloc] init];
       //    [anInvocation invoke];
           //等价
           [anInvocation invokeWithTarget:[[Person2 alloc] init]];
       }
       @end
      

消息转发的应用

  1. 情景:避免应用因方法找不到而崩溃,同时又想收集哪些方法找不到
  2. 上面分析知道,只要实现了消息转发的最后一个阶段的方法,即使方法找不到,也一定不会崩溃
  3. 那么就可以创建一个基类或者直接给NSObject写一个分类,实现下面方法即可

     #import "BeseObject.h"
    
     @implementation BeseObject
        
     -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
         //正常的方法
         if ([self respondsToSelector:aSelector]) {
             [super methodSignatureForSelector:aSelector];
         }
            
         //找不到的方法
         return  [NSMethodSignature signatureWithObjCTypes:"v@:"];
     }
        
     -(void)forwardInvocation:(NSInvocation *)anInvocation{
         //这里可以同步到服务器。。。
         NSLog(@"==找不到的方法====%@",NSStringFromSelector(anInvocation.selector));  
     }
     @end
    
Table of Contents