OC对象的本质-isa指针 superclass指针详解

特别备注

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

面试题 – 面向对象

对象的isa指针指向哪里?

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

OC的类信息存放在哪里?

  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法,存放在meta-class对象中
  • 成员变量的具体值,存放在instance对象

isa指针

image-20210408102836107

OC对象分为三类

instance对象

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

class对象,用来描述instance对象

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

meta-class对象,用来存放类方法

  • isa指针
  • superclass指针
  • 类方法信息(+方法)
oc对象的分类以及内部结构

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

MJPerson *person = [[MJPerson alloc] init];
person->_age = 10;
[person personInstanceMethod];

它的底层是

通过xcrun编译转化成cpp文件

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
image-20210408104259156
objc_msgSend([MJPerson class], @selector(personClassMethod))
objc_msgSend(instanceObj, @sel_registerName("instanceObjMethod"));

也就是给instanceObj对象发消息

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

isa指针的作用

大致可以归纳为

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

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

superclass指针

image-20210408104554176

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

// MJPerson
@interface MJPerson : NSObject <NSCopying>
{
    @public
    int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

// MJStudent
@interface MJStudent : MJPerson <NSCoding>
{
@public
    int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end

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

class的superclass指针

一个类Studentclass对象里面的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总结

image-20210408111637477
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类的实例对象,调用轨迹如下图

img

对于student来说,并不知道abc方法在哪里,唯一知道的就是可以去它的class对象里面找,

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

class调用类方法的轨迹
我们以[MJStudent abc];为例调用轨迹图如下

img

对与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,就会调用
    • 如果还没有找到,由于此时的superclass是nil,最终系统将给出报错[ERROR: unrecognized selector sent to instance],调用轨迹结束
#import "NSObject+Test.h"

@implementation NSObject (Test)

+ (void)test
{
    NSLog(@"+[NSObject test] ++++++ %p", self);
}

- (void)test
{
    NSLog(@"-[NSObject test] -------- %p", self);
}
@end
@interface MJPerson : NSObject

+ (void)test;

@end

@implementation MJPerson

+ (void)test
{
    NSLog(@"+[MJPerson test] ++++++++ %p", self);
}

@end

测试instance调用对象方法的轨迹及class调用类方法的轨迹

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"[MJPerson class] - %p", [MJPerson class]);
        NSLog(@"[NSObject class] - %p", [NSObject class]);
        
        [MJPerson test];
        
        [NSObject test];
    }
    return 0;
}

输出

2021-04-08 11:29:59.749350+0800 Interview02-isa和superclass[2891:125121] [MJPerson class] - 0x100004260
2021-04-08 11:29:59.749897+0800 Interview02-isa和superclass[2891:125121] [NSObject class] - 0x7fff8faa3118
2021-04-08 11:29:59.749951+0800 Interview02-isa和superclass[2891:125121] +[MJPerson test] ++++++++ 0x100004260
2021-04-08 11:29:59.749974+0800 Interview02-isa和superclass[2891:125121] +[NSObject test] ++++++ 0x7fff8faa3118
image-20210408113534660

当我们把NSobject的test方法去掉

@implementation NSObject (Test)

- (void)test
{
    NSLog(@"-[NSObject test] -------- %p", self);
}
@end

此时调用轨迹右会发生神马变化?

image-20210408114354345

isa指针指向哪里?

根据我们上面的梳理和总结,我们可以得出结论

isa(of instance) --> isa(of class) --> isa(of meta-class)
下面我们通过代码来验证一下

image-20210408112114296
// MJPerson
@interface MJPerson : NSObject <NSCopying>
{
@public
    int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

@implementation MJPerson

- (void)test
{
    
}

- (void)personInstanceMethod
{
    
}
+ (void)personClassMethod
{
    
}
- (id)copyWithZone:(NSZone *)zone
{
    return nil;
}
@end

// MJStudent
@interface MJStudent : MJPerson <NSCoding>
{
@public
    int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end

@implementation MJStudent
- (void)test
{
    
}
- (void)studentInstanceMethod
{
    
}
+ (void)studentClassMethod
{
    
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
    return nil;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];

        Class personClass = [MJPerson class];
        
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
}

输出

2021-04-08 11:52:08.970783+0800 Interview03-isa[3039:136373] 0x100415cf0 0x1000044c8 0x1000044a0

我们在代码中加入断点,通过控制台查看一下personisa信息。

image-20210408115845298

通过p/x命令来打印指针,/后面是打印参数,x参数表示用16进制数输出。因为我们知道person这个instance的结构体的包含一个isa成员变量,person本身就是指针,所以可以通过person->isa访问isa的值。
代码里面,personClassPerson类class对象,输出结果显示,

image-20210408120259351

person的isa = 0x001d8001000044c9
personClass = 0x00000001000044c8 它俩。。。并不相等!!! 这是什么情况?不是说好了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的这个值来试一下

image-20210408120646325

可以看到,结果就显而易见了。通过和ISA_MASK进行一次&运算,我们得到了personClass的地址。同样,我们来试一下personClass的isa指针。

image-20210408120815969

结果我试图通过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 mj_objc_class {
    Class isa;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MJPerson *person = [[MJPerson alloc] init];
        Class personClass = [MJPerson class];
        struct mj_objc_class *personClass2 = (__bridge struct cl_objc_class *)(personClass);
        Class personMetaClass = object_getClass(personClass);
        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}
image-20210408121705136

我们自定义一个struct,包含一个isa指针,然后再借助这个结构体类型来读取personClass里面的内容,如上代码,我们用personClass2在来尝试一次

image-20210408121446943

ok,结果显示,class对象isa指针直接指向了mete-class对象的地址。

到此,上面的面试题相信大家已经可以完整回答了。在用一个图来总结一下就是

image-20210408122032606

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

struct mj_objc_class {
    Class isa;
    Class superclass;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // MJPerson类对象的地址:0x00000001000014c8
        // isa & ISA_MASK:0x00000001000014c8
        // MJPerson实例对象的isa:0x001d8001000014c9
        
        struct mj_objc_class *personClass = (__bridge struct mj_objc_class *)([MJPerson class]);

        struct mj_objc_class *studentClass = (__bridge struct mj_objc_class *)([MJStudent class]);

        NSLog(@"1111");
    }
}
image-20210408122944250
特别备注

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容