美文网首页
神经病院Objective-C Runtime入院第一天—isa

神经病院Objective-C Runtime入院第一天—isa

作者: shen888 | 来源:发表于2019-10-24 09:15 被阅读0次

    转自神经病院Objective-C Runtime入院第一天—isa和Class

    为了进一步理解 OC 的 Runtime 机制,我走上了不归路,看来是时候到霜神的神经病院走一遭了!

    正文

    前言

    我第一次开始重视Objective-C Runtime是从2014年11月1日,@唐巧老师在微博上发的一条微博开始。

    image.png

    这是sunnyxx在线下的一次分享会。会上还给了4道题目。

    image.png

    这4道题以我当时的知识,很多就不确定,拿不准。从这次入院考试开始,就成功入院了。后来这两年对 Runtime 的理解慢慢增加了,打算今天自己总结总结平时一直躺在我印象笔记里面的笔记。有些人可能有疑惑,学习 Runtime 到底有啥用,平时好像并不会用到。希望看完我这次的总结,心中能解开一些疑惑。

    目录

    • 1.Runtime简介
    • 2.NSObject起源
      。(1) isa_t结构体的具体实现
      。(2) cache_t的具体实现
      。(3) class_data_bits_t的具体实现
    • 3.入院考试

    一、Runtime 简介

    Runtime 又叫运行时,是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接收者将要如何响应和处理这条消息,那就要看运行时来决定了。

    C 语言中,在编译期,函数的调用就会决定调用哪个函数。而 OC 的函数,属于动态调用过程,在编译期并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数来调用。

    Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态的创建类和对象、进行消息的传递和转发。

    Objc 在三种层面上与 Runtime 系统进行交互:

    Runtime System.jpg

    1. 通过 Objective-C 源代码

    一般情况下开发者只需要编写 OC 代码即可,Runtime 系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。

    2. 通过 Foundation 框架的 NSObject 类定义的方法

    在 OC 世界中,除了 NSProxy 类以外,所有的类都是 NSObject 的子类。在 Foundation 框架下,NSObject 和 NSProxy 两个基类,定义了类层次结构中该类下方所有类的公共接口和行为。NSProxy 是专门用来实现代理对象的类,这个类暂时本篇文章不提。这两个类都遵循 NSObject 协议。声明了所有OC对象的公共方法。

    在NSObject协议中,有以下5个方法,是可以从Runtime中获取信息,让对象进行自我检查。

    - (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
    - (BOOL)isKindOfClass:(Class)aClass;
    - (BOOL)isMemberOfClass:(Class)aClass;
    - (BOOL)conformsToProtocol:(Protocol *)aProtocol;
    - (BOOL)respondsToSelector:(SEL)aSelector;
    
    • class 方法返回对象的类;- isKindOfClass: 和 - isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);- responseToSelector: 检查对象能否响应指定的消息;- conformsToProtocol: 检查对象是否实现了指定协议类的方法;

    在 NSObject 的类中还定义了一个方法

    - (IMP)methodForSelector:(SEL)aSelector;
    

    这个方法会返回指定方法实现的地址IMP。

    以上这些方法会在本篇文章中详细分析具体实现。

    3. 通过对 Runtime 库函数的直接调用

    关于库函数可以在Objective-C Runtime Reference中查看 Runtime 函数的详细文档。

    关于这一点,其实还有一个小插曲。当我们导入了 objc/Runtime.h 和 objc/message.h 两个头文件之后,我们查找到了Runtime的函数之后,代码打完,发现没有代码提示了,那些函数里面的参数和描述都没有了。对于熟悉Runtime的开发者来说,这并没有什么难的,因为参数早已铭记于胸。但是对于新手来说,这是相当不友好的。而且,如果是从iOS6开始开发的同学,依稀可能能感受到,关于Runtime的具体实现的官方文档越来越少了?可能还怀疑是不是错觉。其实从Xcode5开始,苹果就不建议我们手动调用Runtime的API,也同样希望我们不要知道具体底层实现。所以IDE上面默认代了一个参数,禁止了Runtime的代码提示,源码和文档方面也删除了一些解释。
    具体设置如下:

    image.png

    如果发现导入了两个库文件之后,仍然没有代码提示,就需要把这里的设置改成NO,即可。

    二. NSObject起源

    由上面一章节,我们知道了与Runtime交互有3种方式,前两种方式都与NSObject有关,那我们就从NSObject基类开始说起。

    image.png

    以下源码分析均来自objc4-680

    NSObject的定义如下

    typedef struct objc_class *Class;
    @interface NSObject<NSObject> {
      Class isa OBJC_ISA_AVAILABAILITY;
    }
    

    在Objc2.0之前,objc_class源码如下:

    struct objc_class {
      Class isa OBJC_ISA_AVAILABAILITY;
    #if !__OBJC2__
      Class super_class                                   OBJC2_UNAVAILABLE;
      const char *name                                    OBJC2_UNAVAILABLE;
      long version                                        OBJC2_UNAVAILABLE;
      long info                                           OBJC2_UNAVAILABLE;
      long instance_size                                  OBJC2_UNAVAILABLE;
      struct objc_ivar_list *ivars                        OBJC2_UNAVAILABLE;
      struct objc_method_list **methodLists               OBJC2_UNAVAILABLE;
      struct objc_protocol_list *protocols                OBJC2_UNAVAILABLE;
    #endif
    }
    

    在这里可以看到,在一个类中,有超类的指针,类名,版本信息。ivars 是 objc_ivar_list 成员变量列表的指针;*methodLists 是指向方法列表的指针。这里如果动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。

    关于Category,这里推荐2篇文章可以仔细研读一下。 深入理解Objective-C:Category 结合 Category 工作原理分析 OC2.0 中的 runtime

    然后在2006年苹果发布Objc 2.0之后,objc_class的定义就变成下面这个样子了。

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    @interface Object {
      Class *isa;
    }
    @interface NSObject<NSObject> {
      Class *isa OBJC_ISA_AVAILABILITY;
    }
    struct objc_object {
    private:
      isa_t isa;
    }
    struct objc_class: objc_object {
      // Class ISA;
      Class superclass;
      cache_t cache; // formerly cache pointer and vtable
      class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
    }
    union isa_t {
      isa_t() {}
      isa_t(uintptr_t value): bits(value) { }
      Class cls;
      uintptr_t bits;
    }
    

    runtimeCD.jpg

    把源码的定义转化成类图,就是上图的样子。

    从上述源码中,我们可以看到,Objective-C 对象都是 C 语言结构体实现的,在 objc_2.0 中,所有的对象都会包含一个 isa_t 类型的结构体。这个结构体在下面会详细分析。

    objc_class 继承于 objc_object。所有在 objc_class 中也会包含 isa_t 类型的结构体 isa。至此,可以得出结论:Objective-C 中类也是一个对象。在 objc_class 中,除了 isa 之外,还有 3 个成员变量,一个是父类的指针,一个是方法缓存,最后一个是这个类的实例方法链表。

    object类和NSObject类里面分别都包含一个objc_class类型的isa。

    上图的左半边类的关系描述完了,接着先从isa来说起。

    当一个对象的实例方法被调用的时候,会通过 isa 找到相应的类,然后在该类的 class_data_bits_t 中去查找方法。class_data_bits_t 是指向类对象的数据区域。在该数据区域内查找相应方法的对应实现。

    但是在我们调用类方法的时候,类对象的 isa 里面是什么呢?这里为了和对象方法查找方法的机制一致,遂引入了元类(meta-class)的概念。

    关于元类,更多具体可以研究这篇文章What is a meta-class in Objective-C?

    在引入元类之后,类对象和对象查找方法的机制就完全统一了。

    对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

    meta-class 之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的 meta-class,因为每个类的类方法基本不可能完全相同。

    对应关系的图如下图,下图很好的描述了对象,类,元类之间的关系:

    runtimeCD2.jpg

    图中实线是 super_class指针,虚线是isa指针。

    1. Root class (class)其实就是 NSObject,NSObject 是没有超类的,所以 Root class(class)的 superclass 指向 nil。
    2. 每个 Class 都有一个 isa 指针指向唯一的 Meta class。
    3. Root class(meta)的 superclass 指向 Root class(class)也就是 NSObject,形成一个回路。
    4. 每个 Meta class 的 isa 指针都指向 Root class(meta)。

    我们其实应该明白,类对象和元类对象是唯一的,对象是可以在运行时创建无数个的。而在main方法执行之前,从 dyld 到 runtime 这期间,类对象和元类对象在这期间被创建。具体可看 sunnyxx 这篇iOS 程序 main 函数之前发生了什么

    1 isa_t 结构体的具体实现

    接下来我们就该研究研究isa的具体实现了。objc_object里面的isa是isa_t类型。通过查看源码,我们可以知道isa_t是一个union联合体。

    struct objc_object {
    private:
      isa_t isa;
    public:
      // initIsa() should be used to init the isa of new objects only.
      // If this object already has an isa, use changeIsa() for correctness.
      // initInstanceIsa(): objects with no custom RR/AWZ
      void initIsa(Class cls /*indexed=false*/);
      void initInstanceIsa(Class cls, bool hasCxxDtor);
     private:
      void initIsa(Class newCls, bool indexed, bool hasCxxDtor);
    }
    

    那就从initIsa方法开始研究。下面以arm64为例。

    inline void
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) {
      initIsa(cls, true, hasCxxDtor);
    }
    inline void
    objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) {
      if (!indexed) {
        isa.cls = cls;
      } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
      }
    }
    

    initIsa第二个参数传入了一个true,所以initIsa就会执行else里面的语句。

    #if __arm64__
    #  define ISA_MASK        0x0000000ffffffff8ULL
    #  define ISA_MAGIC_MASK  0x000003f000000001ULL
    #  define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
          uintptr_t indexed            :1;
          uintptr_t has_assoc          :1;
          uintptr_t has_cxx_dtor       :1;
          uintptr_t shiftcls           :33; // MACH_VM_MAX_ADDRESS 0x1000000000
          uintptr_t magic              :6;
          uintptr_t weakly_referenced  :1;
          uintptr_t deallocating       :1;
          uintptr_t has_sidetable_rc   :1;
          uintptr_t extra_rc           :19;
    #    define RC_ONE    (1UUL<<45)
    #    define RC_HALF   (1ULL<<18)
        };
    #elif __x86_64__
    #  define ISA_MASK        0x00007ffffffffff8ULL
    #  define ISA_MAGIC_MASK  0x001f800000000001ULL
    #  define ISA_MAGIC_VALUE 0x001d800000000001ULL
        struct {
          uintptr_t indexed            :1;
          uintptr_t has_assoc          :1;
          uintptr_t has_cxx_dtor       :1;
          uintptr_t shiftcls           :44;
          uintptr_t magic              :6;
          uintptr_t weakly_referenced  :1;
          uintptr_t deallocating       :1;
          uintptr_t has_sidetable_rc   :1;
          uintptr_t extra_rc           :8;
    #    define RC_ONE    (1UUL<<56)
    #    define RC_HALF   (1ULL<<7)
        };
    

    runtimeStore.jpg

    关于参数说明:

    第一位 index,代表是否开启 isa 指针优化。index = 1,代表开启 isa 指针优化。

    在2013年9月,苹果推出了iPhone5s,与此同时,iPhone5s配备了首个采用64位架构的A7双核处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,相关逻辑能减少一半的内存占用,以及3倍的访问速度提升,100倍的创建、销毁速度提升。

    在WWDC2013的《Session 404 Advanced in Objective-C》视频中,苹果介绍了 Tagged Pointer。 Tagged Pointer 的存在主要是为了节省内存。我们知道,对象的指针大小一般是与机器字长有关,在 32 位系统中,一个指针的大小是 32 位(4字节),而在 64 位系统中,一个指针的大小将是 64(8字节)。

    假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下占 8 个字节。而指针类型的大小通常也是与 CPU 位数相关,CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:

    image.png

    苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:

    image.png

    关于Tagged Pointer技术详细的,可以看上面链接那个文章。

    has_assoc 对象含有或者曾经含有关联引用,没有关联引用的可以更快的释放内存

    has_cxx_dtor 表示该对象是否有 C++ 或者 Objc 的析构器

    shiftcls 类的指针。arm64 架构中有 33 位可以存储类指针。

    源码中 isa.shiftcls = (uintptr_t)cls >> 3; 将当前地址向右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。具体可以看从 NSObject 的初始化了解 isa这篇文章里面的shiftcls分析。

    magic 判断对象是否初始化完成,在 arm64 中 0x16 是调试器判断当前对象是真的对象还是没有初始化的空间。

    weakly_referenced 对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放

    deallocating 对象是否正在释放内存

    has_sidetable_rc 判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

    extra_rc 存放对象的引用计数值减一后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数 为 10,extrac_rc 的值就为 9。

    ISA_MAGIG_MASK 和 ISA_MASK 分别通过掩码的方式获取 MAGIC 值和 isa 类指针。

    inline Class
    objc_object::ISA() {
      assert(!isTaggedPointer);
      return (Class)(isa.bit & ISA_MASK);
    }
    

    关于x86_64的架构,具体可以看从 NSObject 的初始化了解 isa文章里面的详细分析。

    cache_t 的具体实现

    还是继续看源码

    struct cache_t {
      struct bucket_t *_buckets;
      mask_t _mask;
      mask_t _occupied;
    }
    typedef unsigned int uint_32_t;
    typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bit
    typedef unsigned long uintptr_t;
    typedef uintptr_t cache_key_t;
    struct bucket_t {
    private:
      cache_key_t _key;
      IMP _imp;
    }
    

    image.png

    根据源码,我们可以知道 cache_t 中存储了一个 bucket_t 的结构体,和两个 unsigned int 的变量。

    mask:分配用来缓存 bucket 的总数。occupied:表明目前实际占用的缓存 bucket 的个数。

    bucket_t 的结构体中存储了一个 unsigned long 和一个 IMP。IMP 只一个函数指针,指向了一个方法的具体实现。

    cache_t 中的 bucket_t *_buckets 其实就是一个散列表,用来存储 Method 的链表。

    Cache 的作用主要是为了优化调用的性能。当对象 receiver 调用方法 message 时,首先根据对象 receiver 的 isa 指针查找到它对应的类,然后在类的 methodLists 中搜索方法,如果没有找到,就使用 super_class 指针到父类中的 methodLists 查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也有可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有 20% 的方法经常被调用,占总调用次数的80%。所以使用 Cache 来缓存经常调用的方法,当调用方法时,优先在 Cache 查找,如果没有找到,再到 methodLists 查找。

    class_data_bits_t 的具体实现

    源码实现如下:

    struct class_data_bits_t {
      // Values are the FAST_ flags above.
      uintptr_t bits;
    }
    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;
    }
    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;
      method_list *baseMethods() const {
        return baseMethodList;
      }
    }
    

    runtimeCD3.jpg

    在 objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。

    class_data_bits_t bits; // class_rw_t *plus custom rr/alloc flags
    

    它为我们提供了便捷方法用于返回其中的 class_rw_t *指针:

    class_rw_t *data() {
        return bits.data();
    }
    

    Objc的类的属性、方法、以及遵循的协议在 objc 2.0 的版本之后都放在 class_rw_t 中。class_ro_t 是一个指向常量的指针,存储编译器决定了的属性、方法和遵循的协议。rw-readwrite,ro-readonly

    在编译期类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t *指针:

    image.png

    在运行时调用 realizeClass 方法,会做一下 3 件事情:
    1、从 class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
    2、初始化一个 class_rw_t 结构体
    3、设置结构体 ro 的值以及 flag
    最后调用 methodizeClass 方法,把类里面的属性,协议,方法都加载进来。

    struct method_t {
      SEL name;
      const char *types;
      IMP imp;
      struct SortBySELAddress: public std::binary_function<const method_t&, const method_t&, bool> {
        bool operator() (const method_t& lhs, const method_t& rhs){
          return lhs.name << rhs.name;
        }
      }
    }
    

    方法 method 的定义如上。里面包含 3 个成员变量。SEL 是方法的名字 name。types 是 Type Encoding 类型编码,类型可参考Type Encoding,在此不细说。

    IMP 是一个函数指针,指向的是函数的具体实现。在 runtime 中消息传递和转发的目的就是为了找到 IMP,并执行函数。

    整个运行时过程可以描述如下:

    image.png

    更加详细的分析,请看@Draveness 的这篇文章深入解析 ObjC 中方法的结构

    到此,总结一下objc_class 1.0和2.0的差别。

    image.png

    image.png

    三. 入院考试

    image.png

    (一)[self class] 与 [super class]

    下面代码输出什么?

     @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self)
        {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
    return self;
    }
    @end
    

    self和super的区别:

    self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。

    super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。

    在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend

    
    OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    
    
    /// Specifies the superclass of an instance. 
    struct objc_super {
        /// Specifies an instance of a class.
        __unsafe_unretained id receiver;
    
        /// Specifies the particular superclass of the instance to message. 
    #if !defined(__cplusplus)  &&  !__OBJC2__
        /* For compatibility with old objc-runtime.h header */
        __unsafe_unretained Class class;
    #else
        __unsafe_unretained Class super_class;
    #endif
        /* super_class is the first class to search */
    };
    

    在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是
    当前类的父类super_class。

    入院考试第一题错误的原因就在这里,误认为[super class]是调用的[super_class class]。

    objc_msgSendSuper的工作原理应该是这样的:
    从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用这个selector。注意,最后的调用者是objc->receiver,而不是super_class!

    那么objc_msgSendSuper最后就转变成

    objc_msgSend(objc_super->receiver, @selector(class))
    
    (二)isKindOfClass 与 isMemberOfClass

    下面代码输出什么?

     @interface Sark : NSObject
     @end
    
     @implementation Sark
     @end
    
     int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
    
       NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
    }
    

    先来分析一下源码这两个函数的对象实现

    
    
    + (Class)class {
        return self;
    }
    
    - (Class)class {
        return object_getClass(self);
    }
    
    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    
    inline Class 
    objc_object::getIsa() 
    {
        if (isTaggedPointer()) {
            uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
            return objc_tag_classes[slot];
        }
        return ISA();
    }
    
    inline Class 
    objc_object::ISA() 
    {
        assert(!isTaggedPointer()); 
        return (Class)(isa.bits & ISA_MASK);
    }
    
    + (BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    - (BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }
        return NO;
    }
    
    + (BOOL)isMemberOfClass:(Class)cls {
        return object_getClass((id)self) == cls;
    }
    
    - (BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }
    

    首先题目中NSObject 和 Sark分别调用了class方法。

    +(BOOL)isKindOfClass:(Class)cls方法内部,会先去获得object_getClass的类,而object_getClass的源码实现是去调用当前类的obj->getIsa(),最后在ISA()方法中获得meta class的指针。

    接着在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不等再继续取super class,如此循环下去。

    [NSObject class]执行完之后调用isKindOfClass,第一次判断先判断NSObject 和 NSObject的meta class是否相当,之前讲到meta class的时候放了一张很详细的图,从图上我们也可以看出,NSObject的meta class与本身不等。接着第二次循环判断NSObject与meta class的superclass是否相当。还是从那张图上面我们可以看到:Root class(meta) 的superclass 就是 Root class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES。

    同理,[Sark class]执行完之后调用isKindOfClass,第一次for循环,Sark的Meta Class与[Sark class]不等,第二次for循环,Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等。第三次for循环,NSObject Meta Class的super class指向的是NSObject Class,和 Sark Class 不相等。第四次循环,NSObject Class 的super class 指向 nil, 和 Sark Class不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO。

    如果把这里的Sark改成它的实例对象,[sark isKindOfClass:[Sark class],那么此时就应该输出YES了。因为在isKindOfClass函数中,判断sark的meta class是自己的元类Sark,第一次for循环就能输出YES了。

    isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,是否相等。第二行isa 指向 NSObject 的 Meta Class,所以和 NSObject Class不相等。第四行,isa指向Sark的Meta Class,和Sark Class也不等,所以第二行res2和第四行res4都输出NO。

    (三)Class与内存地址

    下面的代码会?Compile Error / Runtime Crash / NSLog…?

    @interface Sark : NSObject
    @property (nonatomic, copy) NSString *name;
    - (void)speak;
    @end
    @implementation Sark
    - (void)speak {
        NSLog(@"my name's %@", self.name);
    }
    @end
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        id cls = [Sark class];
        void *obj = &cls;
        [(__bridge id)obj speak];
    }
    @end
    

    这道题有两个难点。难点一,obj调用speak方法,到底会不会崩溃。难点二,如果speak方法不崩溃,应该输出什么?

    首先需要谈谈隐藏参数self和_cmd的问题。
    当[receiver message]调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self和_cmd,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self在上面已经讲解明白了,接下来就来说说_cmd。_cmd表示当前调用方法,其实它就是一个方法选择器SEL。

    难点一,能不能调用speak方法?

    id cls = [Sark class]; 
    void *obj = &cls;
    

    答案是可以的。obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。obj现在已经是一个Sark类型的实例对象了。当然接下来可以调用speak的方法。

    难点二,如果能调用speak,会输出什么呢?

    很多人可能会认为会输出sark相关的信息。这样答案就错误了。

    正确的答案会输出

    my name is <ViewController: 0x7ff6d9f31c50>
    

    内存地址每次运行都不同,但是前面一定是ViewController。why?

    我们把代码改变一下,打印更多的信息出来。

    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
    
        id cls = [Sark class];
        NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);
    
        void *obj = &cls;
        NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);
    
        [(__bridge id)obj speak];
    
        Sark *sark = [[Sark alloc]init];
        NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);
    
        [sark speak];
    
    }
    

    我们把对象的指针地址都打印出来。输出结果:

    
    ViewController = <ViewController: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8
    Sark class = Sark 地址 = 0x7fff543f5a88
    Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80
    
    my name is <ViewController: 0x7fb570e2ad00>
    
    Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78
    my name is (null)
    

    按viewDidLoad执行时各个变量入栈顺序从高到底为self, _cmd, self.class, self, obj。

    第一个self和第二个_cmd是隐藏参数。第三个self.class和第四个self是[super viewDidLoad]方法执行时候的参数。

    在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。在32位下面,一个指针是4字节=4*8bit=32bit。

    从打印结果我们可以看到,obj就是cls的地址。在obj向上偏移32bit就到了0x7fff543f5aa8,这正好是ViewController的地址。
    所以输出为my name is 。

    入院考试由于还有一题没有解答出来,所以医院决定让我住院一天观察。

    未完待续,请大家多多指教。

    相关文章

      网友评论

          本文标题:神经病院Objective-C Runtime入院第一天—isa

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