Objective-C对象的本质

0. 基础准备

0.1 大小端模式的内存存储和读取规则

arm64采用的是小端模式

存储:数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中

读取:从高位地址到地位地址开始读取

大端模式与之相反

字节转化

1byte(字节) = 8bit(位)二进制

1位十六进制 = 4位二进制

2位十六进制 = 8位二进制 = 1byte(字节)

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

0.2 将OC的文件转化为C++文件

这种方式没有指定架构

clang -rewrite-objc main.m -o main.cpp 

我们可以指定架构模式的命令行,使用Xcode工具 xcrun

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

0.3 lldb操作内存

读取内存:

memory read 0x10074c450
// 简写形式:x 0x10074c450
    
// 增加读取条件:
x/4xw 0x10074c450 
// 或者memory read/4xw 0x10074c450
    
/** 后面的读取条件参数 “4xw” 解读:
    1. 4则表示读取4次内存
    2. x表示以16进制的方式读取数据
       x是16进制,f是浮点,d是10进制
    3. w表示每次读取4字节
       b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节
*/

往内存写入的数据:

memory write 0x100400c68 6

通过打断点查看内存数据:

Debug Workflow -> viewMemory address -> 输入内存地址

1. 系统对象的底层实现

我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。

graph LR
Objective-C --> C/C++
C/C++ --> 汇编语言
汇编语言 --> 机器语言

OC的代码:

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

我们通过xcrun命令行将OC的main.m文件转化成C++的文件main-arm64.cpp

然后在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPLIMPL代表 implementation 实现)

// NSObject的实现
struct NSObject_IMPL {
    Class isa;
};

/** 查看Class的本质
我们发现Class其实就是一个结构体指针,对象底层实现其实就是这个样子。
*/
typedef struct objc_class *Class;

通过查看底层的C++代码,我们发现NSObject对象在底层就是一个结构体指针

那么这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员,那就是isa指针,而指针在64位架构中占8个字节。也就是说一个NSObjec对象所占用的内存是8个字节。

假设isa的地址为0x100400110,那么上述代码分配存储空间给NSObject对象,然后将存储空间的地址赋值给obj指针。obj存储的就是isa的地址。obj指向内存中NSObject对象地址,即指向内存中的结构体的地址,也就是isa的位置。

graph LR
objc --> isa

1.1 NSObject对象的内存布局

通过函数获取NSObject对象内存大小:

NSObject *obj = [[NSObject alloc]init];
// 获得NSObject类的实例对象的成员变量占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void*)obj));

我们发现两种方式获取的大小是不一致的,我们通过底层源码来查看为什么会不一样。

class_getInstanceSize

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// 返回成员变量占用大小
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
 }

alloc方法其实是调用了allocWithZone方法

- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
}

↓↓↓

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

↓↓↓


// 调用 [cls alloc]或者 [cls allocWithZone:nil]
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

↓↓↓

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

allocWithZone

id 
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    void *bytes;
    size_t size;

    // Can't create something for nothing
    if (!cls) return nil;

     // 分配内存,此处获得的是实例对象占用的大小,为8
    // Allocate and initialize
    size = cls->alignedInstanceSize() + extraBytes;

    // 在这里做判断,CoreFoundation要求所有对象至少为16字节
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;

    if (zone) {
        bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 对应上面的size
        bytes = calloc(1, size);
    }

    return objc_constructInstance(cls, bytes);
}

通过lldb查看内存分配:

(lldb) x 0x600003ba00a0
0x600003ba00a0: 00 1d be 89 ff 7f 00 00 00 00 00 00 00 00 00 00  ................
0x600003ba00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

我们发现只用了前8字节00 1d be 89 ff 7f 00 00,后8字节都是000 00 00 00 00 00 00 00

一个NSObject对象占用多少内存?

系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
,但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

2. 自定义对象的底层实现

#import <Foundation/Foundation.h>

// Student继承自NSObject
@interface Student: NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation Student

@end

生成C++文件,并且查找Student_IMPL

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

我们发现结构体的第一个成员是其父类NSObject的底层实现 NSObject_IMPL结构体。而通过上面的验证我们知道NSObject_IMPL内部其实就是Class isa,所以struct NSObject_IMPL NSObject_IVARS等价于Class isa
上面的实现可以转化为:

struct Student_IMPL {
    Class *isa;
    int _no;
    int _age;
};

因此此Student_IMPL结构体占用多少存储空间,对象就占用多少存储空间。

结构体占用的存储空间为:isa指针(8字节) + int类型_no(4字节) + int类型_age(4字节),共16字节。

上述代码实际上在内存中的体现为:创建Student对象首先会分配16字节,存储3个东西:

  1. 8字节的isa指针
  2. 4字节的_no 成员变量
  3. 4字节的_age成员变量

2.1 Student对象的内存布局

#import <Foundation/Foundation.h>

struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

@interface Student: NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation Student


int main(int argc, char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu -> _no = 4;
        stu -> _age = 5;
        NSLog(@"%@", stu); // 0x600002994000
        
        // 将oc的对象指针stu强制转化为一个结构体指针,成员变量会一一对应
        struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)(stu);
        NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age);
        // 打印结果:_no = 4, _age = 5
    }
    return 0;
}

@end

上述代码将OC对象强转成Student_IMPL的结构体。也就是说把一个指向OC对象的指针,指向这种结构体,如果可以转化成功,说明我们的猜想正确。由此说明stu这个对象指向的内存确实是一个结构体指针,准确的说是指向结构体的第一个成员Class isa的地址

通过函数来获取内存大小:

NSLog(@"%zd", class_getInstanceSize([Student class])); // 16
NSLog(@"%zd", malloc_size((__bridge const void *)stu)); // 16

实时查看内存数据:

从上图中,我们可以发现stu对象的内存读取数据从高位数据开始读,查看前16字节,每四个字节读出的数据为

16进制地址:0x0000004(4字节,_no),0x0000005(4字节, _age),00D1081000001119(8字节, isa)

通过lldb

2020-01-08 11:37:23.581510+0800 OC对象的本质[7208:132335] <Student: 0x6000033701a0>
2020-01-08 11:37:23.582071+0800 OC对象的本质[7208:132335] _no = 4, _age = 5
(lldb) memory read 0x6000033701a0
0x6000033701a0: 78 ae d4 06 01 00 00 00 04 00 00 00 05 00 00 00  x...............
0x6000033701b0: b0 01 82 92 60 9c 00 00 fb 07 6c 6f 67 64 00 00  ....`.....logd..
(lldb) x/4xw 0x6000033701a0 // 16进制,每4字节(8位16进制地址)读取一次,读取16字节的内容
0x6000033701a0: 0x06d4ae78 0x00000001 0x00000004 0x00000005
(lldb) x/4dw 0x6000033701a0 // 10进制读取
0x6000033701a0: 114601592
0x6000033701a4: 1
0x6000033701a8: 4
0x6000033701ac: 5
(lldb) memory write 0x6000033701a8 6 // 修改_no的值
(lldb) po stu
<Student: 0x6000033701a0>

(lldb) po stu -> _no
6

(lldb) 

3. 继承关系底层实现

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

// Son
@interface Son : Father {
    int _no;
}
@end
@implementation Son
@end


Father *father = [[Father alloc] init];
Son *son = [[Son alloc] init];
            
NSLog(@"%zd",class_getInstanceSize([Father class])); // 16
NSLog(@"%zd",malloc_size((__bridge const void *)father));// 16
            
NSLog(@"%zd",class_getInstanceSize([Son class])); // 16
NSLog(@"%zd",malloc_size((__bridge const void *)son)); // 16

依据上面的分析与发现,对象实质上是以结构体的形式存储在内存中

struct NSObject_IMPL  {
    Class isa;
};
struct Father_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
};
// father的最终实现
struct Father_IMPL {
    Class isa; // 8字节
    int _age; // 4字节
};// 16字节,内存对齐


struct Son_IMPL {
    struct Father_IMPL Father_IVARS;// 12字节
    int _no; // 4字节
};
// son的最终实现
struct Son_IMPL {
    Class isa; // 8字节
    int _age; // 4字节
    int _no; // 4字节
};// 16字节,内存对齐

我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。那么他们所占的内存空间是多少呢?单纯的将指针和成员变量所占的内存相加即可吗?

上述代码实际打印的内容都是16,也就是说son对象和father对象所占用的内存空间都为16个字节。

而且我们发现通函数class_getInstanceSize打印出是16,但是father对象的结构体只占12字节,这是因为内存对齐的原因,class_getInstanceSize函数打印出的是内存对齐之后的大小。

3.1 内存对齐

编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。

为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

我们可以总结内存对齐为两个原则:

  1. 前面成员的地址必须是后面成员的地址整数倍,不是就补齐。
  2. 整个Struct结构体的地址必须是占用最大字节成员的整数倍。

通过上述内存对齐的原则我们来看,father对象的第一个地址要存放isa指针需要8个字节,第二个地址要存放_age成员变量需要4个字节,根据原则1,8是4的整数倍,符合原则一,不需要补齐。然后检查原则2,目前father对象共占据12个字节的内存,不是最大字节数8个字节的整数倍,所以需要补齐4个字节,因此father对象就占用16个字节空间。

而对于son对象,我们知道son对象中,包含father对象的结构体实现,和一个int类型的_no成员变量,同样isa指针8个字节,_age成员变量4个字节,_no成员变量4个字节,刚好满足原则1和原则2,所以student对象占据的内存空间也是16个字节。

3.2 OC对象的属性

@interface Father : NSObject {
    @public
    int _age;
}
@property (nonatomic, assign) int height; // 属性会自动生成成员变量_height
/**
属性还会生成set方法和get方法
但是方法不可能放在实例对象里面
不同的实例对象拥有自己的成员变量,拥有不同的内存但是方法是公共的,每个对象调用的方法都是一样的
每必要每次都存储实例对象里面
*/

// 底层实现
struct Father_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height // 属性生成成员变量
};

3.3 allocsize分析

@interface HJRPerson : NSObject
{
    int _age;
    int _height;
    int _no;
}
@end
@implementation HJRPerson
@end

// 底层实现
struct Student_IMPL {
    Class isa; // 父类NSObject只有一个isa,8字节
    int _age; // 4字节
    int _height; // 4字节
    int _no; // 4字节
}; // 内存对齐之后:24字节

使用函数打印内存占用的大小

HJRPerson *person = [[HJRPerson alloc]init];
NSLog(@"%zd", class_getInstanceSize([HJRPerson class])); // 24
NSLog(@"%zd", malloc_size((__bridge const void*)person)); // 32

我发现实例对象的成员变量占用的内存确实是24字节,但是person对象是32字节,为什么会多分配内存?我查看源码:

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

id 
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    void *bytes;
    size_t size;

    // Can't create something for nothing
    if (!cls) return nil;

    // 调用类的alignedInstanceSize
    size = cls->alignedInstanceSize() + extraBytes;

    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;

    if (zone) {
        bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        // 调用calloc分类内存,size是内存对齐字后的字节,在上面的代码中就是24
        // calloc函数分配了32字节,所以需要查看calloc函数的实现
        bytes = calloc(1, size);
    }

    return objc_constructInstance(cls, bytes);
}

3.4 libmalloc源码

源码:https://opensource.apple.com/tarballs/libmalloc/

我们发现源码里面有好多的内存实现C文件:

frozen_malloc.c
legacy_malloc.c
magazine_malloc.c
malloc.c
nano_malloc.c
nanov2_malloc.c
purgeable_malloc.c

Apple会采用不同的方式分配内存。

我们在malloc.c发现了alloc(size_t num_items, size_t size)函数:

void *
calloc(size_t num_items, size_t size)```
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
}

底层的操作系统在分配内存的时候也需要内存对齐,

#define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */

主要是为了提高CPU的访问速度,在此为OC对象分配内存都是16的倍数,即使calloc(size_t num_items, size_t size)函数参数传的是24字节,但是操作系统为了提高CPU的访问速度,所以需要内存对齐,在这儿分配了32字节。

所以我们可以认为class_getInstanceSize函数返回的是实例对象至少需要多少内存,是返回成员变量的内存字节,而malloc_size返回的是系统实际返回多少内存,

sizeof()返回的是类型占用的字节数,sizeof()不是个函数,而是个运算符,在编译期间值已经就是确定的,即使参数传的是对象和变量,在编译期间也会转成计算类型的大小

int a = 10;
sizeof(a); // 4

NSObject *objc = [[NSObject alloc]init];
sizeof(objc); // 8

4. OC对象的种类

OC中的对象,主要可以分为3种:instance对象(实例对象)、class对象(类对象)、meta-class对象(元类对象)。

4.1 instance对象(实例对象)

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

instance对象在内存中存储的信息包括:

  • isa指针
  • 其他成员变量
Father *father1 = [[Father alloc] init];
father1->_age = 10;

father1对象指向isa指针,isa指针在底层的结构体当中总是最前面的。

struct Father_IMPL {
    Class isa
    _age;
}

即使是继承关系也遵循这条原则。

在实例对象中根本没有看到方法,那么实例对象的方法的代码放在什么地方呢?那么类的方法的信息,协议的信息,属性的信息都存放在什么地方呢?

4.2 class对象(类对象)

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
// 调用的对象的class方法获取类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
// 调用类的class方法获取类对象
Class objectClass3 = [NSObject class];
// 调用runtime方法object_getClass获取类对象
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);

// 调用class方法获取到的一直都是class对象

它们是同一个对象。每个类在内存中有且只有一个class对象。所以它里面存储一些固定的相关信息(类型、名称等)

class对象在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)、类的对象方法信息(instance method
  • 类的协议信息(protocol)、类的成员变量信息(ivar

这里的类的成员变量信息(ivar)不是指成员变量的值,而是一些描述信息(类型、名字等),不要和实例对象存储成员变量的值搞混。

成员变量的值是存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。

那么类方法放在哪里?

4.3 meta-class对象(元类对象)

// runtime方法中传入类对象获得的就是元类对象
Class objectMetaClass = object_getClass([NSObject class]);

// 而调用类对象的class方法时得到还是类对象,无论调用多少次都是类对象
Class cls = [[NSObject class] class];

// 判断该对象是否为元类对象
class_isMetaClass(objectMetaClass); 

每个类在内存中有且只有一个meta-class对象。

meta-class对象和class对象的内存结构是一样的(都是class对象),但是用途不一样,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的,在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的类方法信息(class method

4.4 objc_getClassobject_getClass区别

源码:

Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;
    
    // 根据类的名字,返回类对象
    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}


Class object_getClass(id obj)
{
    // obj如果是instance对象,返回class对象
    // obj如果是class对象,返回meta-class对象
    // obj如果是meta-class对象,返回NSObject的meta-class对象
    if (obj) return obj->getIsa();
    else return Nil;
}

// class方法返回的就是类对象

5. isasuper_class

问题

  • 对象的isa指针指向哪里?
  • OC的类信息存放在哪里?

5.1 instance实例对象的isa

// 实例对象调用实例方法
[father1 fatherInstanceFunc];

当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了。

instance实力对象的isa指向class类对象,当调用对象方法时,通过instanceisa找到class类对象,最后找到对象方法的实现进行调用。

5.2 class类对象的isa

// 类对象调用类方法
[Father fatherClassFunc];

当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象。

class类对象的isa指向meta-class元类对象
,当调用类方法时,通过classisa找到meta-class类对象,最后找到类方法的实现进行调用。

实例对象、类对象、元类对象通过各自的isa指针关联了起来。

当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象的superclass指针。

5.3 super_class指针

类对象的super_class

[son fatherInstanceFunc];

当子类Son的实例对象要调用父类Father的实例方法时,会先通过自己Son的实例对象的isa找到自己Son的类对象,然后通过自己Son的类对象的superclass找到父类Fatherclass类对象,最后找到实例方法的实现进行调用,同样如果父类Father发现自己没有响应的实例方法,又会通过Father类对象的superclass指针找到其父类NSObjectclass类对象,去寻找响应的方法。

继承关系的方法调用通过各自的superclass指针关联起来。

元类对象的superclass

当类对象调用父类的类方法时,就需要先通过自己的isa指针找到自己的meta-class元类对象,然后通过superclass去寻找响应的方法

[Son load];
[Son fatherClassFunc];

当子类Sonclass类对象要调用父类Father的类方法时,会先通过自己的类对象的isa找到自己Sonmeta-class元类对象,然后通过superclass找到Fathermeta-class元类对象,最后在元类对象找到类方法的实现进行调用。

官方isasuperclass指向图:

基类的元类对象的superclass指向类对象

HJRPerson:

interface HJRPerson : NSObject
+ (void)test;
@end
@implementation HJRPerson
//+ (void)test {
//    NSLog(@"+[HJRPerson test], %p", self);
//}
@end

NSObject+Test:

@interface NSObject (Test)

//- (void)test;
+ (void)test;

@end

@implementation NSObject (Test)

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

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

@end

调用:

HJRPerson *person = [[HJRPerson alloc] init];
NSObject *objc = [[NSObject alloc] init];
NSLog(@"HJRPerson: %p, NSObject: %p", [HJRPerson class], [NSObject class]);
            
[HJRPerson test];
[NSObject test];

输出:

2020-04-06 21:30:49.124546+0800 OC对象的本质[79409:6378199] HJRPerson: 0x10709b328, NSObject: 0x7fff89be1d00
2020-04-06 21:30:49.125176+0800 OC对象的本质[79409:6378199] -[NSObject test], 0x10709b328
2020-04-06 21:30:49.125258+0800 OC对象的本质[79409:6378199] -[NSObject test], 0x7fff89be1d00

我们发现一个类对象可以成功的调用一个实例方法,具体分析一下:

HJRPerson通过实例对象的isa找到自己的类对象,再通过类对象的isa找到元类对象,发现元类对象里面没有实现test方法,所以通过元类对象的superclass找到NSObject的的元类,还没有找到test方法,所以再通过NSObject的元类的superclass找到了NSObject的类对象,在类对象中找到了test方法,所以最后调用成功了test实例方法

NSObject类对象为什么也会调用成功实例方法test,上面已经说明了。

5.4 总结

  1. instance实例对象的isa指向class类对象
  2. class类对象的isa指向meta-class元类对象
  3. meta-class元类对象的isa指向基类的meta-class元类对象,基类的isa指向自己
  4. class类对象的superclass指向父类的````class类对象,如果没有父类,superclass```指针为nil
  5. meta-class元类对象的superclass指向父类的meta-class元类对象,基类的meta-classsuperclass指向基类的class类对象
  6. 实例对象调用对象方法的轨迹,通过isa找到自己的类对象,如果方法不存在,就通过superclass找父类
  7. 类对象调用类方法的轨迹,通过isa找到元类对象,如果方法不存在,就通过superclass找父类

6. 验证isasuper_class指针指向

6.1 isa指向

我们通过如下代码证明:

// 实例对象
NSObject *instanceObjc = [[HJRPerson alloc] init];
// 类对象
Class classObjc = [HJRPerson class];
// 元类对象
Class metaClassObjc = object_getClass([HJRPerson class]);

打断点并通过控制台打印相应对象的isa指针

(lldb) p/x (long)instanceObjc->isa
(long) $0 = 0x000000010d3b5320
(lldb) p/x classObjc
(Class) $1 = 0x000000010d3b5320 HJRPerson
(lldb) 

发现实例对象的isa确实是指向类对象。

再进行验证类对象的isa指向:

(lldb) p/x classObjc->isa
error: <user expression 3>:1:10: member reference base type 'Class' is not a structure or union
classObjc->isa
~~~~~~~~~^ ~~~
(lldb) 

发现类对象classObjc中并没有isa指针,我们来到Class内部看一下数据结构:

typedef struct objc_class *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 *` */

objc_class结构体内确实是有一个isasuper_class指针,为了拿到isa指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa指针:

// isa指针结构
struct hjr_objc_class{
    Class isa;
};
// 类对象的isa指向自己的元类对象
struct hjr_objc_class *classObjc_isa = (__bridge struct hjr_objc_class *)(classObjc);

此时我们重新验证一下:

(lldb) p/x classObjc_isa->isa
(Class) $2 = 0x000000010d3b52f8
(lldb) p/x metaClassObjc
(Class) $3 = 0x000000010d3b52f8
(lldb)

类对象的isa指针的地址是元类对象的地址。

6.2 super_class指向

再来看看super_class指向:

// 拿到super_class指针
struct hjr_objc_class{
    Class isa;
    Class super_class;
    const char *name;
};

struct hjr_objc_class *superClsObjc = (__bridge struct hjr_objc_class *)[Father class];
struct hjr_objc_class *sonClsObjc = (__bridge struct hjr_objc_class *)[Son class];
(lldb) p/x sonClsObjc->super_class
(Class) $0 = 0x000000010a9db388 Father
(lldb) p/x superClsObjc
(hjr_objc_class *) $1 = 0x000000010a9db388

子类类对象的super_class指针确实是指向父类的类对象。

Tips

有的版本的Xcode 我们发现isa与对象的地址不同
isa需要进行一次位&运算,才能计算出真实地址,而位&运算的值我们可以通过objc源代码找到:

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

推荐阅读更多精彩内容