美文网首页iOS 开发不明觉厉iOSiOS && Android
神经病院Objective-C Runtime入院第一天——is

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

作者: 一缕殇流化隐半边冰霜 | 来源:发表于2016-09-19 01:07 被阅读29990次

    前言

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

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

    这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 系统进行交互:

    1. 通过 Objective-C 源代码

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

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

    在OC的世界中,除了NSProxy类以外,所有的类都是NSObject的子类。在Foundation框架下,NSObject和NSProxy两个基类,定义了类层次结构中该类下方所有类的公共接口和行为。NSProxy是专门用于实现代理对象的类,这个类暂时本篇文章不提。这两个类都遵循了NSObject协议。在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: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
    -respondsToSelector: 检查对象能否响应指定的消息;
    -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的代码提示,源码和文档方面也删除了一些解释。

    具体设置如下:


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

    二. NSObject起源

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

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

    NSObject的定义如下

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

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

    
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
        
    #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_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
        
    } OBJC2_UNAVAILABLE;
    

    在这里可以看到,在一个类中,有超类的指针,类名,版本的信息。
    ivars是objc_ivar_list成员变量列表的指针;methodLists是指向objc_method_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;
    }
    
    

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

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

    objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个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,因为每个类的类方法基本不可能完全相同。

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


    图中实线是 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   (1ULL<<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; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
            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   (1ULL<<56)
    #       define RC_HALF  (1ULL<<7)
        };
    
    
    

    ISA_MAGIC_VALUE = 0x000001a000000001ULL转换成二进制是11010000000000000000000000000000000000001,结构如下图:

    关于参数的说明:

    第一位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位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:

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

    关于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,extra_rc的值就为 9。

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

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

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

    (2)cache_t的具体实现

    还是继续看源码

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

    根据源码,我们可以知道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查找。

    (3)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_t *baseMethods() const {
            return baseMethodList;
        }
    };
    
    

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

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

    在运行时调用 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,并执行函数。

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


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

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

    三. 入院考试

    (一)[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最后就转变成

    
    // 注意这里是从父类开始msgSend,而不是从本类开始,谢谢@Josscii 和他同事共同指点出此处描述的不妥。
    objc_msgSend(objc_super->receiver, @selector(class))
    
    /// Specifies an instance of a class.  这是类的一个实例
        __unsafe_unretained id receiver;   
    
    
    // 由于是实例调用,所以是减号方法
    - (Class)class {
        return object_getClass(self);
    }
    
    

    由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son。

    (二)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的isa指向是否是自己的类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)
    
    
    
    // objc_msgSendSuper2() takes the current search class, not its superclass.
    OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
    
    
    

    objc_msgSendSuper2方法入参是一个objc_super *super。

    
    /// 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 */
    };
    #endif
    
    
    

    所以按viewDidLoad执行时各个变量入栈顺序从高到底为self, _cmd, super_class(等同于self.class), receiver(等同于self), obj。

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

    在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。

    从打印结果我们可以看到,obj就是cls的地址。在obj向上偏移一个指针就到了0x7fff543f5a90,这正好是ViewController的地址。

    所以输出为my name is <ViewController: 0x7fb570e2ad00>。

    至此,Objc中的对象到底是什么呢?

    实质:Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N)

    加深一下对上面这句话的理解,下面这段代码会输出什么?

    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
        
        NSString *myName = @"halfrost";
        
        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: 0x7fff44404ab0> ,  地址  = 0x7fff56a48a78
    Sark class = Sark  地址  = 0x7fff56a48a50
    Void *obj = <Sark: 0x7fff56a48a50>  地址 = 0x7fff56a48a48
    
    my name is halfrost
    
    Sark instance = <Sark: 0x6080000233e0>  地址 = 0x7fff56a48a40
    my name is (null)
    
    

    由于加了一个字符串,结果输出就完全变了,[(__bridge id)obj speak];这句话会输出“my name is halfrost”

    原因还是和上面的类似。按viewDidLoad执行时各个变量入栈顺序从高到底为self,_cmd,self.class( super_class ),self ( receiver ),myName,obj。obj往上偏移一个指针,就是myName字符串,所以输出变成了输出myName了。

    这里有一点需要额外说明的是,栈里面有两个 self,可能有些人认为是指针偏移到了第一个 self 了,于是打印出了 ViewController:

    
    
    my name is <ViewController: 0x7fb570e2ad00>
    
    

    其实这种想法是不对的,从 obj 往上找 name 属性,完全是指针偏移了一个 offset 导致的,也就是说指针只往下偏移了一个。那么怎么证明指针只偏移了一个,而不是偏移了4个到最下面的 self 呢?

    obj 的地址是 0x7fff5c7b9a08,self 的地址是 0x7fff5c7b9a28。每个指针占8个字节,所以从 obj 到 self 中间确实有4个指针大小的间隔。如果从 obj 偏移一个指针,就到了 0x7fff5c7b9a10。我们需要把这个内存地址里面的内容打印出来。

    LLDB 调试中,可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:
    x/

    n、f、u是可选的参数。
    n 是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。

    f 表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是 i。

    u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

    我们用 x 命令分别打印出 0x7fff5c7b9a10 和 0x7fff5c7b9a28 内存地址里面的内容,我们会发现两个打印出来的值是一样的,都是 0x7fbf0d606aa0。

    这两个 self 的地址不同,里面存储的内容是相同的。

    所以 obj 是偏移了一个指针,而不是偏移到最下面的 self 。

    最后

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

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

    相关文章

      网友评论

      • 灰s:现在还在看大佬两年前的文章,真的是追不上阿,追不上。
      • 老骚鹅:大神你写的 "在objc2.0中,所有的对象都会包含一个isa_t类型的结构体"这句话里的对象是指类对象吗?包括类的实例对象吗? 还是说这个"所有的对象"是指:类对象+实例对象+元类对象
      • GiantAxe77:请问第三题变量入栈顺序是怎么确定的呢?
      • again_onceagain:类方法 +(Class)class {return self }????
      • MaybeLove00:看到吃力啊,然后就觉得看不下去了
      • 收纳箱:对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
        类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

        id cls = [Sark class];
        void *obj = &cls;
        [(__bridge id)obj speak];

        cls应该是一个类对象吧,虽然转成了obj,但发消息的时候 isa 应该还是对应的是元类,应该只只能访问类方法。但是结果来看并不是这样,我哪个地方没有理解对呢?
      • aefa7dfe3af9:对于第三个问题,有点疑问想请教下霜神。
        [super viewDidLoad]; 文中说到依次有 self, _cmd, self.class, receiver(self) 入栈。
        我挺好奇为什么在执行完这句调用之后,相关的参数没有出栈呢?
      • 啊哈呵:入院考试
        (一)里有说:“在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class。”
        (三)里有说:“super_class(等同于self.class)”

        一个说“当前类的父类”,一个是“等同于self.class”,这怎么理解啊?
        啊哈呵:@啊哈呵 查到的资料,有人研究过http://www.jianshu.com/p/87fe5efe104e
        objc_msgSendSuper2中的super_class是self.class然后从self.class偏移8位到superClass,然后在superClass找方法,
        objc_msgSendSuper中的super_class是self.superClass,然后直接在superClass找方法。
        objc_msgSendSuper2 也注释着takes the current search class, not its superclass.
        我目前的认知就只能这样子了,但是.cpp文件看到都是objc_msgSendSuper,
        疑问:不知道objc_msgSendSuper2是怎么触发的?
        啊哈呵:@一缕殇流化隐半边冰霜
        一个是objc_msgSendSuper
        一个是objc_msgSendSuper2
        不都是[super xxx]吗,怎么区分什么时候objc_msgSendSuper,什么时候objc_msgSendSuper2?
        一缕殇流化隐半边冰霜:@啊哈呵 你可能对第一题没有看全,objc_msgSendSuper的工作原理应该是这样的:
        从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class!所以第一题输出的是 Son。同理,在第三题中,调用者也是 self,而不是父类的class。
      • 啊哈呵:以前看到这句话时候:“在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。”
        很不明白,然后去了解对象访问属性到底是什么,原来是在类的地址上各种偏移。
        我乱玩了两个例子,有意思啊
        demo1:viewDidLoad不变,Sark弄三个属性
        - (void)speak {
        NSLog(@"my name's %@", self.name1);// “my name's <ViewController: 0x7fd31ce05d00>” // 一个对象
        NSLog(@"my name's %@", self.name2);// “my name's ViewController” // 一个类
        NSLog(@"my name's %@", self.name3);// crash
        }

        demo2:三个属性,然后viewDidLoad建三个对象
        - (void)viewDidLoad {
        [super viewDidLoad];

        NSString *string1 = [NSString stringWithFormat:@"%@",@"11111111111111111"];
        NSString *string2 = [NSString stringWithFormat:@"%@",@"22222222222222222"];
        NSString *string3 = [NSString stringWithFormat:@"%@",@"33333333333333333"];
        id cls = [BBObject class];
        void *obj = &cls;
        [(__bridge id)obj speak];
        // 输出 33333、22222、111111
        }
        这题被玩坏了。
        我是卖报滴小行家:@啊哈呵 你能不能给我推荐一下 对象访问属性的相关博客?谢谢呐!因为我没有搜到好的博客!
        一缕殇流化隐半边冰霜:@啊哈呵 从你的这个例子里面,就知道你已经明白了一切了!!理解很透彻啊!!!:+1: :+1: :+1: :clap:
        啊哈呵:demo1有误,补充:应该name1是self,name2是super_class,name3是_cmd啊
      • da27c260cc85:霜神,有个链接404了,分类的那个,能发我一下么?我想复习一下,节后看看机会
        一缕殇流化隐半边冰霜:@ArthurChi 这篇文章搜名字,可以在网上找到,http://www.jianshu.com/p/d66d65314add
        da27c260cc85:@一缕殇流化隐半边冰霜 结合 Category 工作原理分析 OC2.0 中的 runtime
        一缕殇流化隐半边冰霜:@ArthurChi 哪个链接?能告诉我一下链接前一句是什么么??
      • 2d899c5242bd:第三题,对证明过程提出疑问,根据x/g的两次取值相同证明了 ‘这两个 self 的地址不同,里面存储的内容是相同的。’但并不能说明是偏移到了哪个指针对吧?
        一缕殇流化隐半边冰霜:@亮仔123 取值相同不能证明。。但是结合上面压栈的顺序还有对象属性偏移量,可以看出来是偏移到的哪个指针
      • 2d899c5242bd:针对第一题,有一点自己的看法,首先都输出的“Son”,不是“son”,然后,[self class] --> Son,没什么好说的, [super class],这条指令的意思明确说明是要去父类找对应的方法实现,父类没有实现class方法,最终找到了Father的父类NSObject class ,看源码知道 实现代码为 ‘object_getClass(self); ’,而self 就是当前实例对象,所以输出的也为 Son. 在objc_msgSendSuper 传入的结构体 objc_super 的 super_class ,这里不清楚楼主以为是谁呢,我觉得应该就是Father吧。
        一缕殇流化隐半边冰霜:是Father,你觉得的是正确的。
      • 郑一一一一:“Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。”、“每个Meta class的isa指针都指向Root class (meta)。”这两句表述,请问为什么要这样做呢?
        一缕殇流化隐半边冰霜:@小屁番茄 不用谢哈,有问题再一起讨论
        郑一一一一:@一缕殇流化隐半边冰霜 谢
        一缕殇流化隐半边冰霜:@小屁番茄 嗯,你这个问题问的非常好!你去看我另外一个文章里面有写到这个问题的答案,《Objc 对象的今生今世》这篇文章里面搜这句话“当然在runtime的meta-class有一处很特殊,那就是NSObject的meta-class,它的superclass是它自己本身”,下面就是你要的答案了。
      • hopestar90:大神,给你点个赞~
      • DamonYJ:彻底看晕了:cry:
        一缕殇流化隐半边冰霜:@DamonYJ 哪里晕了??
      • 小包包包:楼主,想问下,上哪里看2.0的代码,我看runtime里面还是 1.0 的
        一缕殇流化隐半边冰霜:@小包包包 这块是开源的,可以直接上github搜到
      • XIAODAO:你好,"class_data_bits_t的具体实现“ ,在Runtime源码680版本里 class_data_bits_t 是C++实现的,你这个好像省略了后面一大坨源码是吧,只留了bits进行分析
        一缕殇流化隐半边冰霜:@XIAODAO :sweat::sweat:是的,我就把bits分析了一下。后面那大段就自己看看啰
      • 破弓:33位表示地址,后3位无效,右移3位成了30位,所以我想问的是isa.shiftcls为什么是33位,而不是30位呢?
        一缕殇流化隐半边冰霜:@破弓 这个对齐不影响isa里面的位数吧
      • 荼菜:请问有没有sunnyxx线下分享的《RunTime》视频??
        一缕殇流化隐半边冰霜:@啊啦哈 优酷上有的。
        郑一一一一:@一缕殇流化隐半边冰霜 搜不到啊
        一缕殇流化隐半边冰霜:@荼菜 优酷可以在线看
      • SeaHub:您好~阅读您的文章之后获益良多,但文章内似乎有一个问题

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

        对于[sark isKindOfClass:[Sark class]],我认为判断的是Sark的类,而不是元类,因为他们的类相等,所以第一次循环的时候输出为YES;而不是元类相等,才输出为YES,不知道我有没有理解错误~
      • PetitBread:好文章。

        不过 判断sark的meta class是自己的元类Sark,第一次for循环就能输出YES了.

        按道理说 sark是实例方法的时候是获取到[self class], 返回的就是自己的类Sark而不应该说是元类吧? 元类只和类对象有关吧
        PetitBread:看错了。。:grin:
      • Lion_Liu:最后一道题,有关入栈的理解还是差一点,可能回头在路上走着走着就明白了。。。
        da27c260cc85:@Lion_Liu 是入栈顺序那里么?
      • Link913:收获不少,但是由于自己水平还有限,现在也只是会用一些runtime来完成需求,你的文章还是得多读几遍
      • 橡皮泥飞机:看了那么多就你的靠谱
        其他的Class列出来都是OC2.0之前的结构体
        一缕殇流化隐半边冰霜:@橡皮泥飞机 虽然都继承自NSObject,但是每个类都有自己的方法列表。只存自己的方法。不存父类的
        橡皮泥飞机:@一缕殇流化隐半边冰霜 大神 突然想问个问题.ios基本上所有类都继承自NSObject,那么不是有同一个元类吗 那这个元类的方法列表有多大
        一缕殇流化隐半边冰霜:@橡皮泥飞机 是的!!之前的那些都是老的
      • 7d747130efea:楼主请问为什么入栈的顺序是 self.class 然后才是self,是如何查看的呢?第三题的
        da27c260cc85:@迷失的野菊花 你理解了么?
        一缕殇流化隐半边冰霜:这个是msgSuper里面的那两个结构体。receiver = self , super_class 就是我说的self.class
      • mark666:有个疑惑,结构体可以继承吗?
        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
        }
      • Raaaaamsey:lz 有机个问题想请教下,是关乎第三题的
        1. objc_msgSendSuper2的SEL 不需要申请栈空间,为什么呢?
        2. 入栈的四个参数,后面两个是一个是self.class,一个是self,为什么?
        如果根据objc_super这个结构体来看的话,这里有两个变量,一个是receiver,一个是super_class,这个receiver应该就是self,super_class为何是self.class呢?
        3.“ 在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。”这句话如何理解?
        望楼主能有空回答,多谢哦。
        Raaaaamsey:@一缕殇流化隐半边冰霜 多谢谢楼主回复 我先看看 有问题再请教哦
        一缕殇流化隐半边冰霜:@Raaaaamsey 1. objc_msgSendSuper2不是掉了super的viewDidload方法嘛,这个方法有,所以不需要额外申请了。3.这个问题你看看我另外一篇,objc的前世今生,里面就详细的谈到了这个问题。
        Raaaaamsey:lz 我在源码中找到了
        struct objc_super2 {
        id receiver;
        Class current_class;
        };

        之前一直看的是
        /// 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 */
        };
        #endif

        如果说objc2用的是struct objc_super2,那第三个参数应该是self,第四个是self.class,这样子应该是对的。这样理解对么?
        还有剩下两个问题,多谢楼主啦
      • 随风而行之青衫磊落险峰行:花点时间 慢慢消化一下
      • Bwing:感谢分享。

        第三题有些疑问和你讨论一下:(64 位环境)
        2016-12-29 04:40:09.569 TestUIDemo[33942:1297325] ViewController = <ViewController: 0x7fa620806150> , 地址 = 0x7fff55f81998
        2016-12-29 04:40:09.569 TestUIDemo[33942:1297325] myName's point: 0x7fff55f81978
        2016-12-29 04:40:09.570 TestUIDemo[33942:1297325] Sark class = TestObject 地址 = 0x7fff55f81970
        2016-12-29 04:40:09.571 TestUIDemo[33942:1297325] Void *obj = <TestObject: 0x7fff55f81970> 地址 = 0x7fff55f81968
        2016-12-29 04:40:09.571 TestUIDemo[33942:1297325] my name's halfrost
        2016-12-29 04:40:09.572 TestUIDemo[33942:1297325] Sark instance = <TestObject: 0x600000012b30> 地址 = 0x7fff55f81960
        2016-12-29 04:40:09.572 TestUIDemo[33942:1297325] my name's (null)

        我把 myName 的地址打了出来,是 0x7fff55f81978 ,和 Sark 的地址 0x7fff55f81970 差 8 ,如果按你的解释解释不通。

        我觉得地址的偏移单位不是位,而是字节
        比如文中的:
        Sark class = Sark 地址 = 0x7fff543f5a88
        Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80
        之间相差 8,只有当单位是字节的时候,8 * 8 = 64,刚好一个指针的长度

        在寻找 self.name 的时候,是在 obj 向上偏移一个指针长度,即我上述 demo 中的 0x7fff55f81970 偏移到 0x7fff55f81978 ,并不是文中所说的偏移 32 位(或 64 位)。
        之于为什么看起来像偏移了 32 位,我觉得应该是偏移了 1 个指针长度,刚好指向了 self,而 self 在栈中有两个,即栈中的 self, _cmd, self.class, self

        新手,一点迷惑,感谢不吝赐教
        一缕殇流化隐半边冰霜:@Bwing 嗯啊。。是这样打印的。。这个问题我在另外一篇详细分析过了。你看看《objc今生今世》这篇文章第三大点里面的描述,详细说了你这个问题:kissing_heart::kissing_heart:
        Bwing:@一缕殇流化隐半边冰霜 感谢回复!
        NSString *myName = @"halfrost";

        NSLog(@"myName's point: %p", &myName);

        我打印的是myName的地址哈
        一缕殇流化隐半边冰霜:@Bwing 你打印出来的myName有点奇怪。。你如果是打印这个字符串的话,它应该会在常量区,地址应该不会和下面这些变量在一起的。能让我看看你打印这些信息的代码么?
      • Link913:厉害了,我的哥
      • Delpan:第三题有一点不是太明白想请教一下,id cls本身是一个指针,为什么void *obj = &cls之后obj变成了一个Sark类型的实例对象了?obj不应该是一个双重指针吗?
        一缕殇流化隐半边冰霜:@Delpan 一个对象的实例的isa,指向的是这个类对象的地址,然后类对象的isa指向meta-class的地址。所以这里cls就是类对象,而一个对象实例就是类对象的地址。
        Delpan:@一缕殇流化隐半边冰霜 cls是一个指向Sark Class类的指针这个没有问题,但obj是存cls的地址而不是cls的值,cls的值才是Sark Class的地址,obj和Sark Class类中间隔了一层cls,怎么关联起来的,这个不太明白
        一缕殇流化隐半边冰霜:@Delpan [Sark class] 是一个指向Sark Class类的指针,然后使用id转换成了objc_object类型。obj就已经是一个Sark类型的实例对象了。你要分开来想这个问题。
      • a4e2403d69e1:大神请教下,第一题如果最后调用的receiver是自身,那我重写了父类方法后,它为什么还是调用了父类的方法呢?
        一缕殇流化隐半边冰霜:@不甜的姜饼 你只有调用super的时候,才会调用到objc_super,平时调用普通的方法,调用的是msgSend。
        a4e2403d69e1:不知道是不是我理解的不对,您最后写到objc_super->receiver=self,那这样的话所有的方法实际上都是用self调用的,为什么class方法会调用本类,而别的方法后会调用父类方法呢,谢谢解答:grin:
        一缕殇流化隐半边冰霜:@不甜的姜饼 把receiver改成父类还是???
      • Enum:翻来覆去看了好几遍都没完全看懂啊,跟你的差距实在是太大了。 :joy:
        Enum:@一缕殇流化隐半边冰霜 哦不是第一题,是第二题。
        Enum:@一缕殇流化隐半边冰霜 比如说,文章开头说“-isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);”,如果用这个概念去看第一题的话,res1 != res3的结果怎么就觉得这么诡异呢。估计我还得消化消化分析过程。 :confounded:
        一缕殇流化隐半边冰霜:@Enum 额。。。哪里没看懂呢???是不是我没有写清楚。。
      • KFAaron:霜神,这么拼,注意身体哦~ 第三道题很有意思,你这三篇我都收录了 ,慢慢来学习消化:smile:
        一缕殇流化隐半边冰霜:@KFAaron 一起学习:kissing_heart::kissing_heart::kissing_heart:如果原来没接触过底层,可以研究研究
      • DDDDeveloper:能不能不要这么牛逼?加油!
        一缕殇流化隐半边冰霜:@一个有前途的男人 一起学习:kissing_heart::kissing_heart:
      • Haer不变:厉害,想问下0x7fff543f5a80偏移32位得到0x7fff543f5aa8是怎么来的?非常谢谢。
        一缕殇流化隐半边冰霜:@Haer不变 没事。。一起交流,一起进步!!
        Haer不变:@一缕殇流化隐半边冰霜 哦 哦 ,看懂了。感谢! :+1: :+1:
        一缕殇流化隐半边冰霜:@Haer不变 是0x7fff543f5aa8到0x7fff543f5a88,这之间只相差32。
      • 水晶可乐Z:然而, 我并看不懂.....在runtime里 这算基础么
        一缕殇流化隐半边冰霜:@水晶可乐Z 算是基础吧,isa这个在runtime里面算是一个很核心的部分
      • 一缕殇流化隐半边冰霜:调用viewdidload,一开始就是调用[super viewDidLoad]这个方法呀。
        一缕殇流化隐半边冰霜:@gong2012v1987 我更新了一下文章。。。还是看那一段。。你看看。。我有没有说明白。。。一起交流
        一缕殇流化隐半边冰霜:@gong2012v1987 我去。。评论我才看到。。我一会再多给你写点解释哈。。我一会去更新一下文章。没让你们明白的说明我写的不够仔细!!:kissing_heart:
        gong2012v1987:@一缕殇流化隐半边冰霜 sorry,可能我没表达清楚,我的意思是为什么是这两个参数,而且参数压栈的顺序是先self。class然后self
      • gong2012v1987:楼主,想请问下这句话“第三个self.class和第四个self是[super viewDidLoad]方法执行时候的参数。”是如何得出来的啊??
      • YY_Lee:深入浅出
      • WellCheng:-isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);


        这个怎么理解,这两个方法还能判断成员变量?
        一缕殇流化隐半边冰霜:@WellCheng 额。。。。这句话我的意思是这样的,这两个方法是判断是否是,其子类或者父类或者当前类的成员,的变量。当前类的成员指的是类簇,子类的子类,父类的父类,就是一个家族。
      • Aeron_Xie:大神~~~膜拜中
        一缕殇流化隐半边冰霜:@Aeron_Xie 一起学习:kissing_heart::kissing_heart::kissing_heart:
      • 婉卿容若:博主你好,我想请问 (id)[NSObject class] 与[NSObject class] 有什么区别?我之前理解(id)[NSObject class] = NSObject; [NSObject class] = NSObejct 的 metaclass 但是我输出发现 两个都是 NSObject
        一缕殇流化隐半边冰霜:@婉卿容若 不懂的时候看看源码实现就知道啦:smile::smile::smile:
        婉卿容若:@一缕殇流化隐半边冰霜 懂了。之前都想岔了。十分感谢
        一缕殇流化隐半边冰霜:@婉卿容若 NSObject class] 类调用类方法的class,是返回的self,就是本身。所以两个都是返回的NSObject
      • 阿斯兰iOS:第二题,你的答案是yes,no,no,no。我的运行答案是no, no, yes, no。我的代码应该没写错的。
        一缕殇流化隐半边冰霜:@阿斯兰iOS 哦哦。。。没事没事。。。有问题交流嘛。。一起学习一起进步!!我就喜欢和提出疑问的同学一起交流!!!😘😘 :+1: 一起学习!!
        阿斯兰iOS:@一缕殇流化隐半边冰霜 醉了醉了,代码没错,答案和你的是一样的。。。是我看错了,Xcode列出的变量顺序是3、4、1、2,所以我看错了,不好意思。。。
        一缕殇流化隐半边冰霜:@阿斯兰iOS 不可能呀。。我之前都验证过的,而且你看我的分析,源码是那样写的,不会其他问题呀。。你贴一下你的代码,我看看
      • 60343a0ad510:凌晨一点发的文章,沃日。😂你是不是单身汪啊,刻苦钻研这个时候。
        60343a0ad510:@一缕殇流化隐半边冰霜 😂666.
        一缕殇流化隐半边冰霜:@晋先森 哈哈。。。不是单身啦。。趁她睡着,默默起来写文章的:kissing_heart::kissing_heart::kissing_heart:
      • 9808f2c53707:大神来个四题的答案呗
        一缕殇流化隐半边冰霜:@万里皓月25 第四题有了。。少了一个第三题。。第三题我下一篇再讲。。
      • 莫_名:看到印象笔记我就来评论了. 昨天也把印象笔记里的东西全部迁到的简书,印象笔记已卸载. 知识重在总结,乐在分享,成于提高.
        莫_名:@一缕殇流化隐半边冰霜 :+1: :clap:
        一缕殇流化隐半边冰霜:@忆语者 :stuck_out_tongue_winking_eye: 对。。总结以后对知识理解更加深了。。一起学习!!!
      • 罗火火:差点忘了我有病 看来也得入院了
        一缕殇流化隐半边冰霜:@不高不帅不体贴 额。。。哈哈。。一起学习!!
      • mojue:霜40发文我居然没及时看 罪过 罪过
        一缕殇流化隐半边冰霜:@mojue 没事。。:stuck_out_tongue_winking_eye::stuck_out_tongue_winking_eye:还需要住院几天,等写完这个系列一起看,也行呀。。
      • Tonlin:很好
        一缕殇流化隐半边冰霜:@Tonlin :kissing_heart::kissing_heart::kissing_heart:一起学习
      • Machine_C:求床位,没有床位过道也行啊:stuck_out_tongue_closed_eyes:
        一缕殇流化隐半边冰霜:@dancekiyo 哈哈。。还有还有。。还有2天才可以出院哦。。:stuck_out_tongue_winking_eye::stuck_out_tongue_winking_eye:
      • 5eedecb86bb6:霜40名不虚传
        莫_名:@一缕殇流化隐半边冰霜
        Stay Hungry, Stay Foolish
        一缕殇流化隐半边冰霜:@谜一样的zuzu 。。。。我还是菜鸟。。一起学习!!
      • 会上树的潴:涨姿势了
        一缕殇流化隐半边冰霜:@梦醒雨未停 一起学习!!!:kissing_heart::kissing_heart::kissing_heart:
      • 十一岁的加重:底层这么了解,没道理不出几个框架啊
        一缕殇流化隐半边冰霜:@十一岁的加重 额。。功底还不够:sweat::sweat::sweat:
      • scyworld:厉害~
        scyworld:@一缕殇流化隐半边冰霜 向你看齐:)
        一缕殇流化隐半边冰霜:@Sauchye 一起学习:kissing_heart::kissing_heart::kissing_heart:
      • HenryCheng:来,给我加个床位 :smile:
        一缕殇流化隐半边冰霜:@HenryCheng 我懂的。。你看我都半夜发文章了。。到家11点,洗完澡快12点,看到1点半睡觉。。哎。。确实很累的。。
        HenryCheng:@一缕殇流化隐半边冰霜 你这么爱学习,很强势啊,最近我都快忙死了,下班了也不想动,都好久没更新博客了:sob::sob::sob:
        一缕殇流化隐半边冰霜:@HenryCheng 哈哈。。加了。。未来还有2篇,基本完成了。。还需要润色润色,这周晚点时间就发:kissing_heart::kissing_heart::kissing_heart:一起学习。。程神
      • DrunkenMouse:四个答案都写对了,但最后一题的输出结果没有写- -。霜40,医院还有空床吗?我来陪你了。。
        一缕殇流化隐半边冰霜:@DrunkenMouse 哈哈。。住院还有2天。。期待我接下来的解析吧。。更加精彩:kissing_heart::kissing_heart::kissing_heart:一起学习
      • 诸葛俊伟:虽然没学过oc,直接从swift开始,但应该也很有用的吧?毕竟底层原理应该不变的,是吧?
        一缕殇流化隐半边冰霜:@诸葛俊伟 是的。。底层差不多的
      • 沈悦:ね〜
      • succes:好
        一缕殇流化隐半边冰霜:@succes :stuck_out_tongue_winking_eye: 一起学习~~
      • 酷酷的哀殿:加强版的面试题:
        如何修改,才能得到下面的输出?
        my name's Sark
        酷酷的哀殿:@gong2012v1987 赞
        gong2012v1987:@酷酷的哀殿 :smiley: [super viewDidLoad]代码之后创建一个局部的sark对象就可以了啊,函数压栈的顺序吗,先压入一个sark的对象,哈哈
      • 没了蜡笔de小新:深度好文,赞一个 :+1:
        一缕殇流化隐半边冰霜:@彩笔踮起脚丫子 一起学习~~ :stuck_out_tongue_winking_eye: :stuck_out_tongue_winking_eye:
      • 我的大名叫小爱:突然觉得好先进
        一缕殇流化隐半边冰霜:@我的大名叫小爱 :stuck_out_tongue_winking_eye: 一起学习~~
      • 98ab5bc1118d:注意休息
        荔枝lizhi_iOS程序猿:@henwan 理解透彻
      • 酷酷的哀殿:rumtime 主要是用 C++ 编写的库
      • 灰烬之心:正巧前段时间看到入院的几道题,总结的不错,赞一个~👍
        一缕殇流化隐半边冰霜:@black_heart 一起学习:kissing_heart::kissing_heart:
      • 渣渣程序猿爱次次大餐:骚年,看来公司现在不忙啊
        一缕殇流化隐半边冰霜:@海彬 睡啦。。晚安!!
        渣渣程序猿爱次次大餐:@一缕殇流化隐半边冰霜 赶紧休息吧!明天你还上班呢!
        一缕殇流化隐半边冰霜:@海彬 没看到我都几点发文章么?:cry::cry:

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

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