iOS 深入理解isa指针和superclass指针

一、通过源码理解instance(实例对象) 、class object (类对象)、metaclass(元类对象)

苹果开源代码(https://opensource.apple.com/tarballs/)

1.instance 实例对象

1.1定义

实例对象是通过类alloc出来的对象,每次调用alloc都会产生新的实例对象

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];

以上object1、object2都是实例对象,占用两块儿不同的内存

1.2、底层实现

首先,终端定位到需要转化文件的文件夹下,用以下命令行将OC代码转成C++/C代码

xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的CPP文件

以NSObject对象为例,我们用以上方法看看它的底层实现

int main(int argc, const char * argv[]) {
@autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

转成C++代码后,我们找到了这个对象的实现

struct NSObject_IMPL {
Class isa;
};
//Class是指针类型,指向objc_class类型的结构体
typedef struct objc_class *Class;

obj对象内部实际上只有一个isa指针,指向objc_class类型的结构体。 那isa指针到底指向谁,它又有什么用呢? 在下文中我们会讲到

1.3、更复杂的继承结构

我们举一反三,设计一个父类Father,继承于NSObject,再设计一个子类son继承于父类,看看他们的底层实现

@interface Father : NSObject {
    int _age;
}
@end

@interface Son : Father {
   double _height;
}
@end

把代码转成C++,看看内部实现。直接查找类名_IMPL

struct Father_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
};

struct Son_IMPL {
    struct Father_IMPL Father_IVARS; 
    double _height;
};

上述代码相当于

struct Father_IMPL {
    Class isa;
    int _age;
};

struct Son_IMPL {
    Class isa;
    int _age;
    double _height;
};

所以实例对象的本质是结构体,(在c++文件中查找类名_IMPL就能找到这个结构体),里面有一个isa指针和其他成员变量。所以实例对象在内存中存储的内容是 isa指针 + 其他成员变量。

如下图


22813661-930c9636126bc071.png

我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址

2、class-类对象

从第一部分我们了解到,实例对象只存储isa指针和成员变量,那类里面的方法啊,属性啊等等一些类信息存放在哪里?为什么这些信息没有存放在实例对象里?

因为实例对象可能有很多个,不可能每创建一个实例对象都存一份方法、属性.....的。 这些只需要存一份就可以了。一个类有且只有一个类对象。把属性啊方法啊等信息存在类对象中也再合适不过了。

2.1 创建类对象
NSObject *obj = [[NSObject alloc] init];
Class objClass1 = [NSObject class];
Class objClass2 = [obj class];
Class objClass3 = object_getClass(obj);

我们可以打印一下,objClass1,objClass12,objClass13他们的内存地址都是一样的。也验证了一个类有且一有一个类对象

2.2 类对象的本质
7819764-55a4e660ba37ea51.png

如上我们可以看出,类对象中存放了
1、isa指针
2、super指针
3、类的属性信息(@property)、类的对象方法信息(instance method)
4、类的协议信息(protocol)、类的成员变量信息(ivar)
........

类对象里面也有一个isa指针,还有一个super指针,那他们分别指向哪里,又有什么作用呢? 我们稍后就会讲到。 当然这里还有一个疑问,既然类对象里面存放的是对象方法信息,那类方法信息存放在哪里呢?

22813661-8d5f2056e0a8a28b.png

3、meta-class-元类对象

构建

 Class objectMetaClass = object_getClass([NSObject class]);

如上,objectMetaClass 就是 NSObject的元类对象,并且 每个类只有一个元类对象
元类对象和类对象的结构是一样的,都是objc_class类型的结构体,元类对象存放类方法信息
1、isa指针
2、super指针
3、类的类方法信息(class method)
.........
meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。

22813661-771d3d0ee1491460.png

4、isa指针和super指针

实例对象,类对象,元类对象中都有isa指针,类对象和元类对象中有super指针。他们分别指向哪里?

4.1、 实例对象的isa指针指向

eg1:子类Son中有一个实例方法- (void)sonTest ,创建一个实例对象son,然后用这个对象调用方法[son sonTest];。

类对象中存储着实例方法信息。当实例对象调用实例方法时
实例方法存储在类对象中。实例对象调用实例方法时,实例对象对象通过isa指针找到类对象,进而找到类对象中相应的实例方法进行调用。
实例对象的isa指针指向它的类对象。

4.2、类对象的isa指针指向

元类对象中存储着类方法信息。当类对象调用类方法时,类对象通过isa指针找到元类对象,进而找到元类对象中相应的类方法进行调用,类对象的isa指针指向它的元类对象。

4.3、元类对象的isa指针指向

元类对象的isa指针指向基类的元类对象(eg:Son的元类对象和Father的元类对象都指向NSObject的元类对象)

4.4、类对象的super指针指向

eg4:父类Father中有一个实例方法- (void)fatherTest ,创建一个子类实例对象son,然后用这个对象调用方法[son fatherTest];。

从4.1我们知道,son对象的isa指针会找到它的类对象,但是类对象中没有fatherTest这个对象方法,所以类对象会通过它的super指针找到父类的类对象,而fatherTest这个方法是存放在Father的类对象中的,进而调用。类对象的super指针是指向父类的类对象的。
特例:当这个类没有父类时(基类),则指向nil

4.5、元类对象的super指针指向

元类对象的super指针指向父类的元类对象
特例:基类的元类对象super指针指向基类的类对象

1853063-ab35405f00cf1bfc.png

总结如下

  • instance的isa指向class
  • class的isa指向meta-class
  • meta-class的isa指向基类的meta-class
  • class的superclass指向父类的class
    如果没有父类,superclass指针为nil
  • meta-class的superclass指向父类的meta-class
    基类的meta-class的superclass指向基类的class
  • instance调用对象方法的轨迹
    isa找到class,方法不存在,就通过superclass找父类
  • class调用类方法的轨迹
    isa找meta-class,方法不存在,就通过superclass找父类

二、通过object_getClass探究isa指针指向问题

介绍几个函数

objc_getMetaClass(const char * _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
object_getClass(id _Nullable obj) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
class_isMetaClass(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- (Class)superclass
+ (Class)class
  • objc_getMetaClass用于获取元类对象
  • class_isMetaClass用于判断Class对象是否为元类
  • object_getClass用于获取对象的isa指针指向的对象
  • -(Class)superclass 用于获取对象的superclass指针所指向的对象
  • +(Class)class 获取类对象

1.有代码如下:

    // EDStudent 继承于 EDPerson ,EDPerson 继承于 NSObject
    NSObject *object1 = [[NSObject alloc] init]; // 基类 实例对象
    Class object2 = object_getClass(object1); // object1 的isa指针 所指向的对象
    Class object3 = object_getClass(object2); // object2 的isa指针 所指向的对象
    Class object4 = [NSObject class]; // 基类 类对象
    Class object5 = objc_getMetaClass("NSObject"); //基类 元类对象
    Class object6 = object_getClass(object5); // 元类对象的isa指针 所指向的对象
    NSLog(@"runtimeTest1 %p %p %p %p %p %p %d",object1,object2,object3,object4,object5,object6,class_isMetaClass(object3));

运行结果如下:

runtimeTest1 0x60000311c410 0x7fff86b6c660 0x7fff86b6c638 0x7fff86b6c660 0x7fff86b6c638 0x7fff86b6c638 1

上文我们不难发现,object2 和object4 的内存地址是一样的,object3、object5、object6的内存地址是一样的,object4 是类对象,object5是元类对象~

也就是说基类的实例对象object1的isa指针是指向基类的类对象object2(object4),基类的类对象object2的isa指针是指向基类的元类对象object3(object5),基类的元类对象object5的isa指针指向基类的元类对象本身object6(object5)

2.有代码如下

    // EDStudent 继承于 EDPerson ,EDPerson 继承于 NSObject
    EDPerson *p1 = [[EDPerson alloc] init]; // 实例对象
    Class p2 = object_getClass(p1); // p1 的isa指针 所指向的对象
    Class p3 = object_getClass(p2); // p2 的isa指针 所指向的对象
    Class p4 = [EDPerson class]; // 类对象
    Class p5 = objc_getMetaClass("EDPerson"); // 元类对象
    Class p6 = object_getClass(p5); // 元类对象p5的isa指针 所指向的对象
    Class objc = objc_getMetaClass("NSObject"); // 基类 元类对象
    NSLog(@"runtimeTest2 %p %p %p %p %p %p %p %d",p1,p2,p3,p4,p5,p6,objc,class_isMetaClass(p3));

运行结果如下:

runtimeTest2 0x600003b339a0 0x1066168b0 0x106616888 0x1066168b0 0x106616888 0x7fff86b6c638 0x7fff86b6c638 1

上文我们不难发现,p2和p4的内存地址一样,p3和p5的内存地址一样,p6和objc内存地址一样,p4是类对象,p5是元类对象,objc是基类的元类对象

也就是说 实例对象的isa指针是指向类对象的,类对象的isa指针是指向元类对象的,元类对象的isa指针是指向基类的元类对象

综上:
实例对象的isa指针是指向类对象,类对象的isa指针是指向元类对象,元类对象的isa指针都是指向基类的元类对象

3.有代码如下

    // EDStudent 继承于 EDPerson ,EDPerson 继承于 NSObject
    Class objc1 = [EDStudent class];  // EDStudent 类对象 objc1
    Class objc2 = [objc1 superclass]; // objc1 的 superclass指针 所指向的对象 (EDPerson 类对象) objc2
    Class objc3 = [objc2 superclass]; // objc2 的 superclass指针 所指向的对象 (NSObject 类对象) objc3
    Class objc4 = [objc3 superclass]; // objc3 的 superclass指针 所指向的对象 (nil) objc4
    
    Class objc5 = [EDPerson class]; // EDPerson 类对象 objc5
    Class objc6 = [NSObject class]; // NSObject 类对象 objc6
    
    NSLog(@"runtimeTest3 %p %p %p %p %p %p",objc1,objc2,objc3,objc4,objc5,objc6);

运行结果如下:

runtimeTest3 0x102965950 0x102965900 0x7fff86b6c660 0x0 0x102965900 0x7fff86b6c660

上文我们不难发现,objc2、objc5内存地址一样,objc3、objc6内存地址一样,objc4是nil。objc5 是EDPerson类对象,objc6 是NSObject类对象。

也就是说子类EDStudent的类对象的superclass指针是指向父类EDPerson的类对象,父类EDPerson的类对象的superclass指针是指向基类NSObject的类对象,基类NSObject的类对象的superclass指针是指向nil

4.有代码如下

    // EDStudent 继承于 EDPerson ,EDPerson 继承于 NSObject
    Class objc1 = objc_getMetaClass("EDStudent"); // EDStudent 元类对象 objc1
    Class objc2 = [objc1 superclass]; // objc1 的 superclass指针 所指向的对象 (EDPerson 元类对象)
    Class objc3 = [objc2 superclass]; // objc2 的 superclass指针 所指向的对象 (NSObject 元类对象)
    Class objc4 = [objc3 superclass]; // objc3 的 superclass指针 所指向的对象 (NSObject 类对象) 基类的元类对象的superclass指针是指向基类的类对象的
    
    Class objc5 = objc_getMetaClass("EDPerson"); // EDPerson 元类对象 objc5
    Class objc6 = objc_getMetaClass("NSObject"); // NSObject 元类对象 objc6
    Class objc7 = [NSObject class]; // NSObject 类对象 objc7
    
    NSLog(@"runtimeTest4 %p %p %p %p %p %p %p",objc1,objc2,objc3,objc4,objc5,objc6,objc7);

运行结果如下:

runtimeTest4 0x102965928 0x1029658d8 0x7fff86b6c638 0x7fff86b6c660 0x1029658d8 0x7fff86b6c638 0x7fff86b6c660

上文不难发现,objc2、objc5内存地址一样,objc3、objc6内存地址一样,objc4、objc7内存地址一样。objc5是EDPerson元类对象,objc6是NSObject元类对象,objc7是NSObject类对象

也就是说,子类EDStudent的元类对象的superclass指针是指向父类EDPerson的元类对象,父类EDPerson的元类对象的superclass指针指向基类NSObject的元类对象,基类的元类对象的superclass指针指向基类NSObject的类对象

综上:
子类的类对象的superclass指针是指向父类的类对象,父类的类对象的superclass指针指向基类的类对象,基类的类对象的superclass指针是指向nil
子类的元类对象的superclass指针是指向父类的元类对象,父类的元类对象的superclass指针是指向基类的元类对象,基类的元类对象的superclass指针是指向基类的类对象

三、实例方法和类方法的调用

有代码如下:

@interface NSObject (runtime)
- (void)test;
+ (void)test;
@end

@implementation NSObject (runtime)
- (void)test{
    NSLog(@"NSObject 实例方法 Test 执行");
}
+ (void)test{
    NSLog(@"NSObject 类方法 Test 执行");
}
@end

@interface EDPerson : NSObject

@end

@implementation EDPerson
- (void)test {
    NSLog(@"EDPerson 实例方法 Test 执行");
}
+ (void)test{
    NSLog(@"EDPerson 类方法 Test 执行");
}
@end

@interface EDStudent : EDPerson

@end

@implementation EDStudent
- (void)test {
    NSLog(@"EDStudent 实例方法 Test 执行");
}
+ (void)test{
    NSLog(@"EDStudent 类方法 Test 执行");
}
@end

- (void)runtimeTest5 {
    [[EDStudent new] test];
    [EDStudent test];
}

当前控制台会打印“ EDStudent 实例方法 Test 执行 ” 和 “EDStudent 类方法 Test 执行”
如果注释掉EDStudent类的- (void)test 和+ (void)test,控制台会打印 “EDPerson 实例方法 Test 执行”和“EDPerson 类方法 Test 执行”
如果再注释掉EDPerson类的- (void)test 和+ (void)test,控制台会打印“NSObject 实例方法 Test 执行” 和 “NSObject 类方法 Test 执行”
如果此时再注释掉NSObject类的+ (void)test,此时[EDStudent test] 并不会报错,控制台会打印“NSObject 实例方法 Test 执行” 和 “NSObject 实例方法 Test 执行”

综上:
实例方法的调用轨迹:实例对象通过isa指针找到类对象,在类对象的方法列表中查找该方法,如果找不到,就通过superclass指针继续向上查找。
类方法的调用轨迹:类对象通过isa指针找到元类对象,在元类对象的方法列表中查找该方法,如果找不到,就通过superclass指针继续向上查找。

四、相关知识点

在苹果开源代码(https://opensource.apple.com/tarballs/)中下载objc4

1. class

代码如下:

    Class objc1 = [EDStudent class];
    Class objc2 = [[EDStudent new] class];
    NSLog(@"runtimeTest6 %p %p",objc1,objc2);

运行结果:

runtimeTest6 0x10ae388c0 0x10ae388c0

我们不难发现,objc1 和 objc2的内存地址是一样的
为什么呢,我们看下源码

  • object_getClass
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

object_getClass 返回的是当前对象的isa指针所指向的对象

  • class
+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

类方法class,是返回当前类的类对象
实例方法class,也是返回当前类的类对象。object_getClass(self),是获取self的isa指针所指向的对象,而self是当前类的实例对象,实例对象的isa指针是指向类对象,因而此时object_getClass(self)是获取当前类的对象的。

综上
class方法是返回当前类的类对象

2.superclass

代码如下:

    Class objc1 = [EDStudent superclass];
    Class objc2 = [[EDStudent new] superclass];
    NSLog(@"runtimeTest7 %p %p",objc1,objc2);

运行结果:

runtimeTest7 0x10ae38870 0x10ae38870

我们不难发现,objc1 和 objc2的内存地址是一样的。
来我们继续看源码

  • superclass
+ (Class)superclass {
    return self->getSuperclass();
}

- (Class)superclass {
    return [self class]->getSuperclass();
}

类方法superclass的实质是self->getSuperclass(),此时self是类对象,也就是说类方法superclass 返回的是当前类的类对象的superclass指针所指向的对象。
实例方法superclass的实质是[self class]->getSuperclass(),此时self是实例对象,[self class]的实质是object_getClass(self),因而此时的[self class] 返回的是实例对象self的isa指针所指向的对象,即当前类的类对象,也就是说,实例方法superclass 返回的也是当前类的类对象的superclass指针所指向的对象。

综上
superclass 方法是返回当前类的类对象的superclass指针所指向的对象

3.isKindOfClass 和 isMemberOfClass

代码如下:

    BOOL res1 = [[EDStudent new] isKindOfClass:[EDStudent class]];
    BOOL res2 = [[EDStudent class] isKindOfClass:[EDStudent class]];
    BOOL res3 = [[NSObject class] isKindOfClass:[NSObject class]];
    NSLog(@"runtimeTest8 %d %d %d",res1,res2,res3);

运行结果:

runtimeTest8 1 0 1

代码如下:

    BOOL res1 = [[EDStudent new] isMemberOfClass:[EDStudent class]];
    BOOL res2 = [[EDStudent class] isMemberOfClass:[EDStudent class]];
    BOOL res3 = [[NSObject class] isMemberOfClass:[NSObject class]];
    NSLog(@"runtimeTest9 %d %d %d",res1,res2,res3);

运行结果:

runtimeTest9 1 0 0

是不是和我们平时的认知有些不一样,别急,想要理解为什么是这样,我们来看下源码

  • isKindOfClass
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

在源码上,我们不难发现,isKindOfClass的类方法和实例方法的区别在于for循环的初始值tcls,类方法isKindOfClass的for循环初始值是tcls = self->ISA(),初始值tcls是当前对象的isa指针所指向的对象,实例方法isKindOfClass的for循环初始值是tcls = [self class],由于是实例方法,因而[self class]可以改写为object_getClass(self),而object_getClass表示当前对象的isa指针所指向的对象,因而isKindOfClass的类方法和实例方法for循环的初始值tcls都是当前对象的isa指针所指向的对象

综上
isKindOfClass方法是一个for循环查找,由当前对象的isa指针所指向的对象(tcls = self->ISA() 或 tcls = [self class])开始,通过superclass指针(tcls = tcls->getSuperclass())向上查找,如果找到的对象和cls相同就返回YES,否则返回NO

  • isMemberOfClass
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

和 isKindOfClass 类似,实例方法isMemberOfClass 中的[self class] 可以改写为object_getClass(self),而object_getClass(self)表示当前对象的isa指针所指向的对象,因而self->ISA() 和 [self class] 都是表示当前对象的isa指针所指向的对象

综上
isMemberOfClass方法是判断当前对象的isa指针所指向的对象和cls是否相同

看到这,不知道大家有没有一个小疑问,当前类的类对象,不管是通过何种方式获取的,它们的内存地址都是一样的,当前类的元类对象,也是如此~

类对象保存的是创建实例对象所需要的信息(实例方法 协议等),因而它只需要一份,不一定是单例
元类对象保存的是创建类对象所需的信息(类方法),因而它也只需一份,不一定是单例

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

推荐阅读更多精彩内容