iOS-底层-isa指针和superclass指针+窥探Class

上一篇文章,我们介绍了OC对象的分类,它们内存中存放的信息如下图:

信息.png

但是这个isa和superclass有什么用呢?

首先我们创建两个类,如下:

// 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

其中MJStudent继承于MJPerson

一. isa指针的作用

调用方法:

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

我们都知道方法调用的本质是发送消息,所以上面的方法调用的本质就是给person对象发送personInstanceMethod消息,给MJPerson类对象发送personClassMethod消息

他们底层是这样的:(也可以通过NSObject的本质介绍的指令重写为c++文件查看)

调用对象方法本质
objc_msgSend(person, @selector(personInstanceMethod))
调用类方法的本质 (objc_msgSend内部是通过汇编实现的,半开源)
objc_msgSend([MJPerson class], @selector(personClassMethod))

但是现在有一个问题, personInstanceMethod对象方法不在person实例对象里面, personInstanceMethod类方法也不在MJPerson类对象里面,怎么办?

其实他们是通过isa指针联系起来的:

  1. instance的isa指向class
    当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
  2. class的isa指向meta-class
    当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

现在我们就明白了:
[person personInstanceMethod]方法调用的流程:
person实例对象先通过自己的isa找到MJPerson类对象,然后在MJPerson类对象里面找到personInstanceMethod方法进行调用

[MJPerson personClassMethod]方法的调用流程:
MJPerson类对象先通过自己的isa找到MJPerson的元类对象,然后在元类对象里面找到personInstanceMethod方法进行调用

如下图:

isa指针.png

二. superclass指针的作用

先看结论:

isa和superclass.png

1. 解释上图结论

关于isa指针:
  1. instance的isa指向class
  2. class的isa指向meta-class
  3. meta-class的isa指向基类(NSObject)的meta-class
  4. 基类(NSObject)的meta-class的isa指向它自己
关于superclass指针:
  1. class的superclass指向父类的class
  2. 如果没有父类,superclass指针为nil
    (最后一直找不到方法会报错:unrecognized selector sent to instance/class)
  3. meta-class的superclass指向父类的meta-class
  4. 基类的meta-class的superclass指向基类的class
instance调用对象方法的轨迹:

实例对象的isa找到class,方法不存在,就通过superclass找父类

class调用类方法的轨迹:

类对象的isa找meta-class,方法不存在,就通过superclass找父类

2. 方法调用轨迹分析

① [student personInstanceMethod]子类实例对象调用父类的对象方法

当Student的instance对象要调用Person的对象方法时,会先通过实例对象的isa找到Student的class,然后通过它的superclass找到Person的class,最后找到对象方法的实现进行调用

子类实例对象调用父类的对象方法.png

② [MJStudent personClassMethod]子类类对象调用父类的类方法

当Student的class要调用Person的类方法时,会先通过Student的class的isa找到Student的meta-class,然后通过它的superclass找到Person的meta-class,最后找到类方法的实现进行调用

子类类对象调用父类的类方法.png

补充:

如果MJStudent和MJPerson里面都有一个test对象方法,调用方法:[student test],如果按照面向对象的逻辑调用的是student的test,当明白isa和superclass就知道为什么调用的是student的test了。

小任务:

- (instancetype)init方法在NSObject的类里面,想一下调用过程
[student init];

+ (void)load在NSObject的元类里面,想一下调用过程
[MJStudent load];

三. 验证:基类的meta-class的superclass指向基类的class

下面我们验证一条最特殊的实线:基类的meta-class的superclass指向基类的class

给NSObject添加分类,只实现一个test对象方法,MJPerson不实现任何方法。

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

执行以下代码

NSLog(@"[MJPerson class] - %p", [MJPerson class]);
NSLog(@"[NSObject class] - %p", [NSObject class]);
[MJPerson test];
[NSObject test]

打印结果:

[MJPerson class] - 0x1000011e0
[NSObject class] - 0x7fff97e99140
-[NSObject test] - 0x1000011e0
-[NSObject test] - 0x7fff97e99140

可以发现,无论是MJPerson还是NSObject调用test方法后,最后都会调用NSObject的对象方法

过程:MJPerson类对象调用test方法的时候会通过MJPerson的isa去NSObject元类对象里面找test方法(元类对象里面放的都是类方法),但是没找到,然后就去NSObject元类对象的类对象里面找test方法,然后就找到了- (void)test方法,然后就调用了。

可以发现,不管是类对象还是实例对象,调用流程都是:
isa -> superclass -> suerpclass -> superclass -> .... superclass -> nil

总结:

所以,对象方法调用轨迹为:

对象方法调用轨迹.png.png

类方法调用轨迹为:

类方法调用轨迹.png.png

四. 关于ISA_MASK

上面我们说了实例对象的isa指向类对象,类对象的isa指向元类对象,其实在64位之前的确是这样的,但是64位之后,实例对象的isa & ISA_MASK = 类对象,类对象的isa & ISA_MASK = 元类对象(就是需要做个位运算)。

下面我们验证一下,首先在objc4源码中搜索ISA_MASK我们会发现以下代码:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

上面代码的意思是,如果是arm64平台用第一个,x86平台用第二个,由于我们代码是命令行的,所以用第二个,验证代码如下:

struct mj_objc_class {
    Class isa;
    Class superclass;
};

MJPerson *person = [[MJPerson alloc] init];
Class personClass = [MJPerson class];
struct mj_objc_class *personClass2 = (__bridge struct mj_objc_class *)(personClass);
Class personMetaClass = object_getClass(personClass);
NSLog(@"%p %p %p", person, personClass, personMetaClass);

为什么要定义mj_objc_class结构体?
因为Class的虽然有isa,但是没有显露出来,如果我们直接访问会报错

(lldb) p/x (long)personClass->isa
error: member reference base type 'Class' is not a structure or union

我们重新换个结构体,指向personClass就能获取内部的isa指针。

上面代码,打印结果为:
实例对象:0x100603ac0 类对象:0x1000014d0 元类对象:0x1000014a8

打断点,打印如下:

验证

其中,p/x是按照16进制打印,person->isa是直接访问指针指向的地址。
除了打印还可以直接在点击enter,即可出现地址,如上图

观察上图,验证了我们所说的,实例变量的isa和ISA_MASK做个位运算才是类对象的地址值,类对象的isa和ISA_MASK做个位运算才是元类对象的地址值。

ISA_MASK

那么superclass是不是也需要做位运算呢?
测试代码如下:

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");

打断点,结果如下:

superclass

可以发现,子类的supreclass指针里面存的直接是父类的地址,并不需要做位运算。

五. 窥探Class结构

类对象和元类对象都是Class类型的,所以他们在内存中的结构也是一样的,只不过存放的信息不一样,下面我们就窥探Class的结构。

点击Class,进去之后我们发现,Class的定义是这样的,是个结构体。

// An opaque type that represents an Objective-C 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;
/* Use `Class` instead of `struct objc_class *` */

上面有我们想要的信息,但是上面的结构体在最新的OBJC2已经过时了(OBJC2_UNAVAILABLE),所以只能我们自己查看源码了。

在objc4中,搜索struct objc_class,找到如下代码:

//class和metra-class对象的本质结构
//objc_class : objc_object  这是c++结构体的继承(c++结构体和类几乎没区别)
struct objc_class : objc_object {
    // Class ISA; // isa
    Class superclass; //superclass
    cache_t cache;  // 方法缓存
    class_data_bits_t bits; // 用于获取具体的类信息

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

再次搜索"struct objc_object {",可以找到父类

struct objc_object {
private:
    isa_t isa;
......
};

可以发现父类里面就一个isa,其他的都是方法,现在isa和superclass我们都找到了。

再次查看objc_class结构体,发现可以通过bits获取data信息,点击它的类型class_rw_t(r是read,w是write,t是table,整体是可读可写的表的意思),进去

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; //只读表

    method_array_t methods; //方法列表(包括分类里面的方法)
    property_array_t properties; //属性列表
    protocol_array_t protocols; //协议列表
......
}

这里我们可以找到属性列表,协议列表,方法列表。

再次点击class_ro_t(只读表),进去

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //instance对象占用的空间
#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;
    }
};

这里我们找到了成员变量列表

至此,isa、superclass、属性、成员变量、协议、对象方法我们都找到了。

看图总结一下结构:

窥探struct objc_class的结构.png
对象内存结构.png

总结:

现在我们明白了,无论是类对象还是元类对象,他们在内存中的结构都是objc_class这种结构体类型的,但是他们存储的信息不一样,比如对于元类对象,只有isa、superclass、类方法有值,其他的都是空。

六. 验证Class内存结构

上面我们是在源码中分析了Class和meta-Class内存结构,但是实际上是不是这样我们还需要在代码中进行验证。

如何在代码中验证?就像上面我们想查看isa一样,模仿系统自定义了一个结构体。在这里我们也是模仿系统的结构体自定义我们自己的结构体,然后把系统的结构体指向我们自己的结构体,如果他们内存结构一样,那么强制转换类型以后数据肯定都可以对的上。代码如下:

//#import "MJClassInfo.h"是c++文件
//如果文件名是main.m编译的时候只能认识OC和C代码,如果改成main.mm才认识c++文件
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJStudent *stu = [[MJStudent alloc] init];
        stu->_weight = 10; 
        
        mj_objc_class *studentClass = (__bridge mj_objc_class *)([MJStudent class]);
        mj_objc_class *personClass = (__bridge mj_objc_class *)([MJPerson class]);
        
        class_rw_t *studentClassData = studentClass->data();
        class_rw_t *personClassData = personClass->data();
        
        class_rw_t *studentMetaClassData = studentClass->metaClass()->data();
        class_rw_t *personMetaClassData = personClass->metaClass()->data();

        NSLog(@"1111");
    }
    return 0;
}

打断点,查看类对象结构如下:

类对象的内存结构.png

元类对象结构如下:

元类对象的内存结构.png

结果正如所料,下面我们就能回答面试题了。

问题二. 对象的isa指针指向哪里?

  1. instance的isa指向class
  2. class的isa指向meta-class
  3. meta-class的isa指向基类(NSObject)的meta-class
  4. 基类(NSObject)的meta-class的isa指向它自己

问题三. superclass指针指向哪里?

  1. class的superclass指向父类的class
  2. 如果没有父类,superclass指针为nil
    (最后一直找不到方法会报错:unrecognized selector sent to instance/class)
  3. meta-class的superclass指向父类的meta-class
  4. 基类的meta-class的superclass指向基类的class

问题四. OC的类信息存放在哪里?

  1. 属性、成员变量、协议、对象方法,存放在class对象中
  2. 类方法,存放在meta-class对象中
  3. 成员变量的具体值,存放在instance对象中

Demo地址: isa和superclass

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

推荐阅读更多精彩内容