OC对象的本质(下)—— 详解isa&superclass指针

OC对象的本质(上):OC对象的底层实现原理git

OC对象的本质(中):OC对象的种类
github

isa指针

先总结一下咱们在对象的分类一文里面分析过的问题,OC对象氛围三类面试

instance对象,内部包含markdown

  • 成员变量
  • 特殊的成员变量isa指针

class对象,用来描述instance对象,内部包含app

  • isa指针
  • superclass指针
  • 属性信息
  • 对象方法信息(-方法)
  • 协议信息
  • instance对象的成员变量的描述信息

meta-class对象,用来存放类方法,内部包含ide

  • isa指针
  • superclass指针
  • 类方法信息(+方法)

oc对象的分类以及内部结构

对一个类来讲,它的instanceclassmete-class对象之间,必定是有某种联系的。假设这种联系不存在,咱们看看会碰到什么问题。好比我调用一个instance对象的方法oop

[InstanceObj InstanceObjMethod];
复制代码

它的底层是post

objc_msgSend(instanceObj, @sel_registerName("instanceObjMethod"));
复制代码

也就是给instanceObj对象发消息。ui

instance对象不跟外界关联的状况下,它内部只有一些成员变量信息,是不可能完成方法调用的,由于对象方法是存放在class对象里面的,对class对象调用+方法的时候也是同样,必须有办法跟meta-class对象关联起来,才能完成对+方法的调用。因此isa指针,就只它们之间的关联。能够经过下图来理解isa的做用。spa

isa指针的做用

大体能够概括为

  • instance对象isa指针指向class对象。当调用对象方法(-方法)时,经过instanceisa找到class,而后在class的方法列表里面找到对应的实现进行调用。
  • class对象的isa指针指向meta-class对象。当调用类方法(+)方法时,经过classisa找到meta-class,最后在meta-class的方法列表找到对应的实现进行调用。

那么经过isa的桥接做用,我梦应该能更近一步地理解OC消息发送以及方法调用的过程了。

superclass指针

显而易见,从字面意思,咱们就能知道,superclass就是父类的意思。 假定咱们有如下几个类

@interface Person : NSObject
@end

@interface Student : Person
@end
复制代码

咱们知道superclass指针存在于class对象meta-class对象里面。咱们根据接下来的图示来阐述一下:

class的superclass指针

一个类的class对象里面的superclass指针指向该类的父类的class对象 Studentinstance对象要调用Person的对象方法时,会先经过isa找到Studentclass对象,而后经过这个class对象superclass找到Person(Student的父类)class对象,最后找到相应的对象方法(-方法)的实现进行调用

meta-class的superclass指针

一个类的meta-class里面的superclass指针指向该类的父类的meta-class对象 Studentclass对象要调用Person的类方法时,会先经过isa找到Studentmeta-class对象,而后经过这个meta-class对象superclass找到Person(Student的父类)meta-class对象,最后找到相应的类方法(+方法)的实现进行调用

isa、superclass总结

isa、superclass指针做用图例 上图来自苹果官方,完整描述了isa、superclass指针的做用,为了更加便于理解,咱们在后面的图例中用Student代替subclass,Person代替superclass,NSObject代替rootclass。

  • instanceisa指向class
  • classisa指向meta-class
  • meta-classisa指向基类的meta-class
  • classsuperclass指向父类的class若是没有父类,superclass指针为nil
  • meta-classsuperclass指向父类的meta-class,基类的meta-classsuperclass指向基类的class

instance调用对象方法的轨迹 咱们以[student abc];为例,studentStudent类的实例对象,调用轨迹以下图

对于student来讲,并不知道abc方法在哪里,惟一知道的就是能够去它的class对象里面找,

  • 因而先经过isa指针进入Student类的class对象,若是在其中找到了abc就直接进行调用,调用过程结束,
  • 没找到的话,就经过class对象superclass指针进入Student类的父类,也就是Person类的class对象,重复上一步的查找逻辑
  • 以此类推,一层一层往上寻找,若是最终到了基类,也就是NSObject类的class对象里面,还没找到的话,因为它的superclassnil,最终就会碰到一个经典的报错[ERROR: unrecognized selector sent to instance],调用轨迹结束

class调用类方法的轨迹 咱们以[Student abc];为例调用轨迹图以下

对与Student类来讲,abc在哪也是不知道的,咱们知道类方法被规定放在meta-class对象里面,因此

  • 首先,经过Studentclass对象isa指针找到其meta-class对象,而后在方法列表里面寻找是否有abc,有的话就调用,调用逻辑结束。
  • 没有的话,就经过meta-class对象superclass指针找到Student的父类Personmeta-class对象,而后查找abc方法,找到就调用,结束调用轨迹
  • 没有的话,就经过Personmeta-class对象superclass指针,重复上一步的流程
  • 一次类推,经过meta-class对象superclass指针,一层层往上查找
  • 若是到了基类(NSObject)的meta-class还没可以找到abc,此时比较特殊,接下来的superclass指针会找到NSObject的class对象,你可能会奇怪,咱们调用一个类方法,怎么跑到class对象里面来了,先保留你的疑问,只需记住,苹果确实是这么设计的,此时会继续在NSObject的class对象里面,寻找abc,若是真的找到了abc,就会调用
  • 若是尚未找到,因为此时的superclassnil,最终系统将给出报错

面试题 isa指针指向哪里?

根据咱们上面的梳理和总结,咱们能够得出结论

isa(of instance) --> isa(of class) --> isa(of meta-class)

下面咱们经过代码来验证一下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface CLPerson : NSObject <NSCopying>

@end

@implementation CLPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CLPerson *person = [[CLPerson alloc] init];
        Class personClass = [CLPerson class];
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}
复制代码

咱们在代码中加入断点,经过控制台查看一下personisa信息。可是貌似系统只给出了有限信息 还有个办法,能够右击红框中的isa,下拉菜单第一个有个打印功能Print Description of "xxx",能够获得更为详细的输出 看起来结果仍然被系统包裹了一层 若是你习惯直接在代码上快捷操做,也能够这么作试试 但我仍是喜欢用LLDB来查看,便于比较,和复制数据。 经过p/x命令来打印指针,/后面是打印参数,x参数表示用16进制数输出。由于咱们知道person这个instance的结构体的包含一个isa成员变量,person自己就是指针,因此能够经过person->isa访问isa的值。 代码里面,personClassPerson类class对象,输出结果显示, person的isa = 0x001d8001000014d1 personClass = 0x00000001000014d0 它俩。。。并不相等!!! 这是什么状况?不是说好了instance对象isa指向class对象嘛?

其实在64位机器出现以前,instance对象isa确实是直接指向class对象的, 也就是
person->isa == personClass, 从64bit开始,isa须要进行一次为运算,才能计算出真实的class对象地址,系统给咱们提供了一个ISA_MASK,这个能够在objc4源码里面找到。我先直接贴出来

# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \ uintptr_t nonpointer : 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
# define ISA_BITFIELD \ uintptr_t nonpointer : 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)

# else
# error unknown architecture for packed isa
# endif
复制代码

你们请看清这里是分了arm64和x86_64的,分别对应的是移动设备开发和mac开发。个人代码是一个mac命令行工程,因此咱们用x86的这个值来试一下 能够看到,结果就显而易见了。经过和ISA_MASK进行一次&运算,咱们获得了personClass的地址。一样,咱们来试一下personClass的isa指针。 结果我试图经过personClass->isa先打印出其isa指针的时候,获得了错误提示,告诉咱们说personClass的类型Class不是一个结构体,看不太明白,那就先查看一下Class的定义,typedef struct objc_class *Class;,而后在往下看一下objc_class的细节

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

虽然这个结构体里面有isa指针,可是尾部的OBJC2_UNAVAILABLE;提示咱们,这已是过期的API了。 不过咱们在第一篇文章中,已经得出结论,知道class对象里面第一个成员变量确实是一个isa指针,咱们能够经过一个小技巧来处理这个问题

struct cl_objc_class {
    Class isa;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CLPerson *person = [[CLPerson alloc] init];
        Class personClass = [CLPerson class];
        struct cl_objc_class *personClass2 = (__bridge struct cl_objc_class *)(personClass);
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}
复制代码

咱们自定义一个struct,包含一个isa指针,而后再借助这个结构体类型来读取personClass里面的内容,如上代码,咱们用personClass2在来尝试一次 ok,结果显示,class对象isa指针通过ISA_MASK转换以后,获得了正确的mete-class对象的地址。到此,上面的面试题相信你们已经能够完整回答了。在用一个图来总结一下就是

你会许还会问,那么superclass指针呢,是否是也须要一个什么mask转换?答案是不须要的,能够用上面相同的方法进行验证,这里不做赘述。总之isa指针稍微特殊一点点,特别记住一下关于ISA_MASK的细节就行。

深度窥探class/meta-class的内部结构----struct objc_class

在OC对象的本质(一)中,咱们得知了一个事实,在class对象中,存放了一个类的方法列表、属性信息、协议信息、成员变量信息;在meta-class对象中,存放了类的类信息。可是尚未对其仔细验证过。下面咱们就来研究一下这个问题。 由于classmeta-class的类型都是struct objc_class*,因此咱们问题的答案,就都在这个objc_class里面。上面的段落咱们已经看了它的结构了,很惋惜是一个已经废弃的API,因此咱们必须去最新的源码里面,去看一下它的实现。在objc4源码里面,咱们找到以下objc_class的实现

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

    class_rw_t *data() { 
        return bits.data();
    }
//下面是一大堆的方法
...
...
...
}
复制代码

这里的objc_class是一个C++的结构体,若是对C++不太熟的话,先不用过度纠结,能够借用OC的类来理解就好了,它们的类似度很高,能够有成员变脸,也能够有方法,区别主要是一些成员变量的默认做用域不同。能够看到objc_class继承自objc_object,咱们能够在源码objc-private.h里看一下objc_object的实现

struct objc_object {
private:
    isa_t isa;
//剩下的都是方法
...
...
...
}
复制代码

看得出来,其实就是一个isa指针。因而和objc_class的内容融合一下,咱们能够理解成下面的这个结构

struct objc_class {
    isa_t isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
//下面是一大堆的方法
...
...
...
}
复制代码

很明显,objc_class的内部,头两个成员分别是isa和superclass,跑不了。可是下面的好像不是咱们期待的内容,没看到方法列表、属性协议信息啥的呀。可是我门能够看到这里的第一个方法,返回一个class_rw_t *,看字面意思,class表明类,rw一般表明读写(readwrite),t一般指的是 表信息(table),也就是类的可读写信息。那么咱们有理由怀疑这里面确定有宝贝。进去看一看 果真,原来方法、属性、协议信息都放在了这里。同时,咱们还发现了一个class_ro_t *ro,字面就是只读表,类对象里面有什么信息是只读的呢?没错,成员变量信息,因而咱们在进去验证一下 看上去推断是对的,确实找到成员变量信息。注意一下,这里的ivars是成员变量的描述信息,如名称,类型等,只须要一份的,因此存在class对象里面,成员变量的具体值是存在具体的instance对象里面的,不要理解混了。

对于meta-class来讲,结构上和class是同样的,只不过有些内容可能用不到,例如属性,协议列表。meta-class的类方法信息其实就放在咱们刚才看到的那个方法列表里面,没错。class对象的对象方法信息也正是放在这个方法列表里的

还有一点就是,怎么说呢,仍是看图明白

途中咱们看出来,objc_class 有个成员变量bits,正是经过 bits & FAST_DATA_MASK,将objc_class 和它的可读写表关联起来了。下面我引用大神的一张ppt总结一下struct objc_class 的结构

面试题解答

  • 对象的isa指针指向哪里?
  1. instance对象的isa指针指向class对象
  2. class对象的isa指针指向meta-class对象
  3. meta-class对象的isa指针指向基类(也就是NSObject)的meta-class对象
  • OC的类信息存放在哪里?
  1. 对象方法,属性信息,成员变量信息,协议信息,存放在class对象中
  2. 类方法,存放在meta-class对象中
  3. 成员变量的具体值,存放在instance对象中

OC对象的本质(上):OC对象的底层实现原理

OC对象的本质(中):OC对象的种类

特别备注

本系列文章总结自MJ老师在腾讯课堂开设的OC底层原理课程,相关图片素材均取自课程中的课件。

相关文章
相关标签/搜索