Files
knowledge-kit/Chapter1 - iOS/1.7.md
2024-07-15 20:03:01 +08:00

36 KiB
Raw Blame History

对象在内存中的存储底层原理

一、 栈、堆、BSS、数据段、代码段是什么

stack又称作堆栈用来存储程序的局部变量但不包括static声明的变量static修饰的数据存放于数据段中。除此之外在函数被调用时栈用来传递参数和返回值。

heap用于存储程序运行中被动态分配的内存段它的大小并不固定可动态的扩张和缩减。操作函数(mallocfree)

BSS段bss segment通常用来存储程序中未被初始化的全局变量和静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段输入静态内存分配

数据段data segment通常用来存储程序中已被初始化的全局变量和静态变量和字符串的一块内存区域

代码段code segment通常是指用来存储程序可执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读某些架构也允许代码段为可写即允许修改程序。在代码段中也有可能包含一些只读的常数变量例如字符串常量。

内存

二、 类的本质

Demo1

#import <Foundation/Foundation.h>

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

因为 OC 本质就是 c/c++,所以转成 c/c++ 来窥探下,采用指令 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp(不要采用 clang -rewrite-objc main.m -o main.cpp 因为这样生成的 c++ 文件由于没有指明设备和对应的架构指令集,不够准确)。

产看生成的 main-arm64.cpp 其中有段代码是定义结构体。

struct NSObject_IMPL {
	Class isa;
};

然后点击 NSObject 跳转官方的声明可以看到

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
// 剔除一些无效信息后
@interface NSObject {
    Class isa;
}

因此可以知道OC 的类底层是由 c/c++ 的继承实现的。

由于 obj 对象没有任何属性和方法,只有一个 isa 指针且类的本质就是结构体所以当结构体只有1个成员时该成员的地址值就是该结构体的地址。

即 isa 指针值为 0x100200110结构体地址为 0x100200110obj 指针值为 0x100200110。

Demo2

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

@implementation Student
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *st = [[Student alloc] init];
      	st->_no = 1;
      	st->_age = 29;
    }
    return 0;
}

采用指令 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 转换为 c++ 代码

struct NSObject_IMPL {
	Class isa;
};

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;	// 父类的所有 ivars。由于 Student 父类是 NSObject所以其实只有 isa
	int _no;
	int _age;
};

struct NSObject_IMPL NSObject_IVARS; 代表父类的所有 ivars。由于 Student 父类是 NSObject所以其实只有 isa

由于 NSObject_IMPL 结构体只有1个 isa 成员,所以上面代码等价于

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

类的本质是结构体,结构体成员内存紧挨着。内存布局如图所示:

如果上述结论正确,那是不是可以声明一个 Student_IMPL 类型的结构体指针,指向 st 指针指向的对象。然后通过结构体指针访问成员变量,看看取值是不是正确的

发现是可以正确访问的。

为什么 class_getInstanceSize 打印出16因为本质是计算所有 ivars 的内存大小1个 isa2个 int 就是16.

malloc_size 是系统真正分配的由于最小分配16所以刚好就是16.

Demo3

@interface Person : NSObject
{
    @public
    int _age;
}
@end

@implementation Person
@end

@interface Student : Person
{
    @public
    int _no;
}
@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"%zd", class_getInstanceSize([Person class]));   // 16
        NSLog(@"%zd", malloc_size((__bridge const void *)p));   // 16
        Student *st = [[Student alloc] init];
        NSLog(@"%zd", class_getInstanceSize([Student class]));  // 16
        NSLog(@"%zd", malloc_size((__bridge const void *)st));  // 16
    }
    return 0;
}

采用指令 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 转换为 c++ 代码

struct NSObject_IMPL {
	Class isa;
};

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

struct Student_IMPL {
	struct Person_IMPL Person_IVARS;
	int _no;
};

为什么 class_getInstanceSize([Person class]) 也是16不是8+4吗因为存在内存对齐结构体的大小必须是最大成员大小的倍数Person 中也就是8的倍数

Demo4

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

@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"%zd", class_getInstanceSize([Person class])); // 24
        NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32
    }
    return 0;
}

采用指令 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 转换为 c++ 代码

struct NSObject_IMPL {
	Class isa;
};
struct Person_IMPL { 
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	int _no;
	int _height;
};
// ->
struct Person_IMPL {
	Class isa;
	int _age;
	int _no;
	int _height;
};  

2个问题

  • class_getInstanceSize 不是一个 isa 8 + 3个 Int 4 = 8 + 3 * 4 = 20吗查看源码知道 class_getInstanceSize 返回的是内存对齐后的成员变量内存大小
  • malloc_size 为什么是32只需要24字节为什么分配32

三、研究下对象在内存中如何存储?

Person *p1 = [Person new]

看这行代码,先来看几个注意点:

new底层做的事情

  • 在堆内存中申请1块合适大小的空间

  • 在这块内存上根据类模版创建对象。类模版中定义了什么属性就依次把这些属性声明在对象中;对象中还存在一个属性叫做 isa,是一个指针,指向对象所属的类在代码段中地址

  • 初始化对象的属性。这里初始化有几个原则:

    • 如果属性的数据类型是基本数据类型则赋值为 0
    • 如果属性的数据类型是 C 语言的指针类型则赋值为 NULL
    • 如果属性的数据类型为 OC 的指针类型则赋值为 nil
  • 返回堆空间上对象的地址

注意:

  • 对象只有属性没有方法。包括类本身的属性和一个指向代码段中的类isa指针
  • 如何访问对象的属性?指针名->属性名;本质:根据指针名找到指针指向的对象,再根据属性名查找来访问对象的属性值
  • 如何调用方法?[指针名 方法];本质根据指针名找到指针指向的对象再发现对象需要调用方法再通过对象的isa指针找到代码段中的类再调用类里面方法

为什么不把方法存储在对象中?

  • 因为以类为模版创建的对象只有属性可能不相同,而方法相同,如果堆区的对象里面也保存方法的话就会很重复,浪费了堆空间,因此将方法存储与代码段

  • 所以一个类创建的n个对象的isa指针的地址值都相同都指向代码段中的类地址

做个小实验

#import <Foundation/Foundation.h>
@interface Person : NSObject{
    @public
    int _age;
    NSString *_name;
    int *p;
}

-(void)sayHi;
@end

@implementation Person

-(void)sayHi{
    NSLog(@"Hi, %@",_name);
}

@end

int main(int argc, const char * argv[]) {
    Person *p1 = [Person new];
    Person *p2 = [Person new];
    Person *p3 = [Person new];
    p1->_age = 20;
    p2->_age = 20;

    [p1 sayHi];
    [p2 sayHi];
    [p3 sayHi];

    return 0;
}

Person *p1 = [Person new]; 这句代码在内存分配原理如下图所示

解析图

结论

p1 p2

可以 看到Person类的3个对象p1、p2、p3的isa的值相同。

四、一个对象占用多少内存空间?

Demo

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
				// 获取类的实例对象的成员变量所占用内存大小
        NSLog(@"%zd", class_getInstanceSize([NSObject class])); 	// 8
        // 获取 obj 指针,所指向内存大小
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));		// 16
    }
    return 0;
}

为什么一个是8一个是16查看 objc 源代码可以看到:

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() {
    return word_align(unalignedInstanceSize());
}
id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

alloc 本质上调用的就是 _objc_rootAllocWithZone ,继续查看源码

// NSObject.mm
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if (!zone) {
        obj = class_createInstance(cls, 0);
    }
    else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
}

继续调用的是 class_createInstance

// objc-runtime-new.mm
id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

可以看到调用的是 c 的 obj = (id)calloc(1, size);,其中 size 是前面计算好的,继续看看这个 size 的计算 size_t size = cls->instanceSize(extraBytes);

// objc-runtime-new.h
size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

可以看到计算好 size 之后有个判断如果小于16则赋值为16也就是最小为16CF requires all objects be at least 16 bytes)。

结论我们用2种方式获取内存大小其中

  • class_getInstanceSize([NSObject class]) 8返回实例对象内存对齐后的的成员变量所占用的内存大小即代码注释的 Class's ivar size rounded up to a pointer-size boundary. ),一个空对象,只有 isa 指针所以只有8字节。可以理解为 创建一个对象,至少需要多少内存

  • malloc_size((__bridge const void *)obj) 16Apple 规定对象至少16个字节。但是只有一个 isa所以只占用8个字节。 内存对齐:结构体的最终大小必须是最大成员的倍数。可以理解为创建一个对象,实际上分配了多少内存

  • 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)

  • 但 NSObject 对象内部只使用了8个字节的空间64位环境下通过 class_getInstanceSize 函数获得)

五、属性和方法

@interface Person : NSObject
{
    @public
    int _age;
}
@property (nonatomic, assign) NSInteger height;
@end

采用指令 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

struct NSObject_IMPL {
	Class isa;
};

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	NSInteger _height;
};

我们知道 @property 的本质是生成一个带下划线的 ivar_height,还有 height 的 getter、setter 方法。为什么在结构体里没有看到方法?因为对象可以存在多个,这些方法的实现需要公用,没必要每个对象里都保存一份。

五、类继承的本质

QA 结构体计算大小为什么需要内存对齐?

iOS 分配内存为什么需要内存对齐libmalloc 可以看到至少是16的倍数。

写一个最基础的 Person 类

@interface Person : NSObject
@end

@implementation Person
@end

clang 转为 c 代码看看,因为同样的代码经过 clang 后转成 c/c++ 后,不同平台具有不同的实现,所以为了精确研究 iOS最好指明 arm64 架构后再研究,具体指令为:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

struct NSObject_IMPL {
	Class isa;
}

struct Person_IMPL {
 struct NSObject_IMPL NSObject_IVARS;
};

如果给 Person 增加属性

@interface Person : NSObject
@property (nonatomic, assign) double height;
@property (nonatomic, assign) double weight;
@property (nonatomic, assign) int salary;
- (void)test;
@end

@implementation Person
@end

创建一个继承自 Person 的 Student 类

@interface Student : Person
@property (nonatomic, assign) NSInteger score;
- (void)test;
@end
  
@implementation Student
@end 

利用 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp 转换为 c++,将主要的摘出来

struct NSObject_IMPL {
	Class isa;
};

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _salary;
	double _height;
	double _weight;
};

struct Student_IMPL {
	struct Person_IMPL Person_IVARS;
	NSInteger _score;
};

结构体 Person_IMPL 等价于

struct Person_IMPL {
	Class isa;
	int _salary;
	double _height;
	double _weight;
};

结构体 Student_IMPL 等价于

struct Student_IMPL {
	Class isa;
	int _salary;
	double _height;
	double _weight;
	NSInteger _score;
};

首先我们可以知道一个 OC 类的属性可以写多种数据类型,那么大概率是 C 结构体实现。用 clang 转为 c 可以得到印证。另外存在类的继承关系的时候:子类结构体中第一个信息是父类结构体对象;其次是当前子类自己的信息;根节点一定是 NSObject_IMPL 结构体;且其中只有 Class isa。也就是说,一个实例对象,内部的第一个成员就是 isa 指针,其次是父类属性,最后的自己的属性。且 isa 指针地址就是当前实例对象的地址

观察 clang 转换后的 c 代码,发现 property 没有看到 setter、getter 方法。为什么这么设计? 方法不会存储到实例对象中去的。因为方法在各个对象中是通用的,方法存储在类对象的方法列表中。

@interface Person:NSObject
{
 int _height;
 int _age;
 int _weight;
}
@end

@implementation Person
@end

struct NSObject_IMPL {
 Class isa;
};

struct PersonIMPL {
 struct NSObject_IMPL ivars;
 int _height;
 int _age;
 int _weight;
};

struct PersonIMPL person = {};
Person *p = [[Person alloc] init];
NSLog(@"%zd", class_getInstanceSize([Person class])); // 24这个数值代表我们这个类这个结构体创建出来至少只需要24字节.
NSLog(@"%zd", sizeof(person)); // 24这个数值代表我们这个类这个结构体只需要24字节就够
NSLog(@"%zd", malloc_size((__bridge const void *)p)); // 32。iOS 系统会做优化比如为了加速访问速度会按照16的倍数进行分配。

class_getInstanceSize这个数值代表我们这个类这个结构体创建出来至少只需要24字节malloc_size iOS 系统会做优化比如为了加速访问速度会按照16的倍数进行分配。

iOS 中系统分配内存都是16的倍数。pageSize系统在分配内存的时候也存在内存对齐。 GUN 都存在内存对齐这个概念。 sizeof 本质是运算符。在 Xcode 编译后就替换为真正的值。通过指令 xcrun --sdk iphoneos clang -arch arm64 -S -emit-llvm ViewController.m -o ViewController.ll 查看 IR。

实例对象: 类对象isa、superclass、属性信息、对象方法信息、协议信息、成员变量信息... 元类对象:存储 isa、superclass、类方法信息... 一个实例对象只有一个类对象,一个实例对象只有一个元类对象。 class_isMetaClass()判断一个类是否为元类对象

objc_getClass() 如果传递 instance 实例对象,返回 class 类对象;传递 Class 类对象,返回 meta-class 元类对象;传递 meta-class 元对象,则返回 NSObject(基类)的 meta-class 对象

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

当 Student 实例对象调用 Person 的对象方法时,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,在类对象的对象方法列表中找到方法实现并调用。 当 Stduent 实例对象调用 init 方法时候,首先根据 Student 对象的 isa 找到 Stduent 的 Class 类对象,然后根据 Stduent Class 类对象中的 superClass 找到 Person 的 Class 类对象,找到 Person Claas 的 superClass 到 NSObject 类对象,在 NSObject 类对象的方法列表中找到 init 方法并调用。

当 Stduent 对象调用类方法的时候,先根据 isa 找到 Student 的元类对象,然后在元类对象的 superclass 找到 Person 的元类对象,再根据 Person 元类对象的 superClass 找到 NSObject 的元类对象。最后找到元类对象的方法列表,调用到对象方法。

@interface Student : NSObject
@end

@implementation Person
@end

@interface NSObject(TestMessage)
@end

@implementation NSObject(TestMessage)

- (void)test
{
  NSLog(@"%s", __func__);
}
@end

奇怪的是,我们给 Student 类对象调用 test 方法,[Student test] 则调用成功。是不是很奇怪站在面向对象的角度出发Student 类对象根据 isa 找到元对象,此时元对象方法列表中没有类对象,所以根据 superclass 找到 NSObject 元类对象,发现元类对象自身也没有类方法。但是为什么调用了 -(void)test 对象方法?

因为NSObject 元类对象的 superClass 继承自 NSObject 的类对象,类对象是存储对象方法的,所以定义在 NSObject 分类中的 -(void)test 最终会被调用。

从64位开始iOS 的 isa 需要与 ISA_MASK 进行与位运算。& ISA_MASK 才可以得到真正的类对象地址。 为了打印和研究类对象中的 superclass、isa

// 实例对象
Person *p = [[Person alloc] init];
Student *s = [[Student alloc] init];
// 类对象
class pclass = object_getClass(p);
class sclass = object_getClass(s);
// Mock 系统结构体 object_class
struct mock_object_class {
  class isa;
  class superclass;
};
// 转换如下
struct mock_object_class *person = (__bridge mock_object_class *)[[Person alloc] init];
struct mock_object_class *student = (__bridge mock_object_class *)[[Student alloc] init];

如何查看类真正的结构?在 Xcode 中打印出来 思路:查看 Class 内部的数据,发现是 struct所以我们自己定义一个 struct去承接类对象的元类对象信息

#import <Foundation/Foundation.h>

#ifndef MockClassInfo_h
#define MockClassInfo_h

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

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

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;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC对象 */
struct mock_objc_object {
    void *isa;
};

/* 类对象 */
struct mock_objc_class : mock_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }

    mock_objc_class* metaClass() {
        return (mock_objc_class *)((long long)isa & ISA_MASK);
    }
};

#endif /* MockClassInfo_h */

Student *stu = [[Student alloc] init];
stu->_weight = 10;

mock_objc_class *studentClass = (__bridge mock_objc_class *)([Student class]);
mock_objc_class *personClass = (__bridge mock_objc_class *)([Person 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();

六、 内存对齐

内存对齐是指数据在内存中存储时按照一定规则对齐到特定的地址上。在 iOS 开发中,内存对齐是为了提高内存访问的效率和性能。内存对齐的原因主要包括以下几点:

  1. 提高访问速度内存对齐可以使数据在内存中的存储更加高效因为大部分计算机体系结构都要求数据按照特定的边界对齐这样可以减少内存访问的次数提高访问速度。CPU访问非对齐的内存时需要进行多次拼接。 如下图,比如需要读取从[2, 5]的内存,需要分别读取两次,然后还需要做位移的运算,最后才能得到需要的数据。这中间的损耗就会影响访问速度。

  2. 为了方便移植。CPU是一块块的进行进行内存访问。有一些硬件平台不允许随机访问只能访问对齐后的内存地址否则会报异常。 很多 CPU如基于 AlphaIA-64MIPS和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。举个例子,在 ARMMIPS和 SH 硬件平台上,当操作系统被要求存取一个未对齐数据时会默认给应用程序抛出硬件异常。

  3. 硬件要求某些硬件平台对于数据的访问有特定的要求例如ARM架构的处理器对于某些数据类型的访问需要按照特定的对齐方式进行

  4. 数据结构优化:内存对齐也有助于优化数据结构的布局,使得数据在内存中的存储更加紧凑和高效。

Demo1

@interface Person : NSObject
{
    int _age;
    int _height;
}
@end

struct Person_IMPL {
    Class isa;
    int _age;
    int _height;
};

Person *person = [[Person alloc] init];
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 16
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 16

isa 指针 8字节 + int _age 4字节 + _hright 字节 = 16 字节

Demo2

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

struct Person_IMPL {
    Class isa;
    int _age;
    int _height;
    int _no;
};

Person *person = [[Person alloc] init];
NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
NSLog(@"%zd", malloc_size((__bridge const void *)person)); // 32

isa 指针8字节 + int _age 4字节 + _height 4字节 + _no 4 字节 = 20 字节因为存在内存对齐因为结构体本身对齐内存对齐必须为8的倍数所以占据24个字节的内存。结构体成员变量内存对齐时对齐基数必须是各个成员变量中最大字节数的一个。

结构体占据24字节为什么运行起来后通过 malloc_size 得到32个字节这个涉及到运行时内存对齐。规定 iOS 中内存对齐以 16 的倍数为准

Demo

void *temp = malloc(4);
NSLog(@"%zd", malloc_size(temp));
// 16

可以看到 malloc 申请了4个字节但是打印却看到16个字节。

查看 libmalloc 源码也可以出来分配内存最小是以16的倍数为基准进行分配的。

#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

为什么系统是由16字节对齐的

成员变量占用8字节对齐每个对象的第一个都是 isa 指针必须要占用8字节。举例一个极端 case假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐会加快访问速度参考链表和数组的设计

上述是 Apple 官方的角度出发探究的,其他系统,比如 Linux 也是存在内存对齐的。由于 Linux 也是采用 GNU 的东西,所以探索下 GNU 下 glibc malloc 的实现。从这里下载 glibc 源码。然后拖到 Xcode 中查看

可以看到 GNU 源码里面,内存对齐 MALLOC_ALIGNMENT 在 i386 里面是16在非 i386 里面有个判断

#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
			  ? __alignof__ (long double) : 2 * SIZE_SZ)

三目运算符的后2个结果分别是 __alignof__ (long double)2 * SIZE_SZ。其中 SIZE_SZ 又是一个宏定义,等价于 (sizeof (INTERNAL_SIZE_T)),即 2*sizeof(size_t)

#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
# define INTERNAL_SIZE_T size_t

在 Xcode 打印输出, __alignof__ (long double) 为16sizeof(size_t) 为82 * SIZE_SZ = 16所以不管怎么看在 GUN 里面内存对齐一定都是16. Todo: 研究探索 libmalloc 源码

七、class 对象

// instance 对象,实例对象
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
// class 对象,类对象
Class cls1 = [obj1 class];
Class cls2 = [obj2 class];
Class cls3 = object_getClass(obj1);
Class cls4 = object_getClass(obj2);
Class cls5 = [NSObject class];
NSLog(@"%p %p", obj1, obj2); // 0x600000004040 0x600000004050
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5); // 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270 0x7ff85ca74270
  • cls1...cls5 都是 NSObject 的 class 对象,也就是类对象。
  • 它都是同一个对象,每个类在内存中有且只有一个 class 对象

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

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

八、元类对象

Objective-C 中对象分维三类:

  • instance 对象,实例对象。例如 NSObject *obj1 = [[NSObject alloc] init];
  • class 对象,类对象。例如 Class cls1 = [obj1 class];
  • 元类对象meta-class。例如 Class metaClass = object_getClass(cls1);

如何获取元类对象?

利用 runtime object_getClassAPI传入类对象获取。例如 Class objectMetaClass = object_getClass([NSObject class])

不可以通过2次调用 class 方法获取 meta-class 对象。调用 class 方法只可以获取到 class 对象。

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

  • meta-class 对象和 Class 对象的内存结构是一样的(都是 Class但是用途不一样在内存中存储的信息主要包括

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

如何判断是否是元类对象 class_isMetaClass()

Demo:

// instance 对象,实例对象
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
// class 对象,类对象
// class 方法返回的就是类对象
Class cls1 = [obj1 class];
Class cls2 = [obj2 class];
Class cls3 = object_getClass(obj1);
Class cls4 = object_getClass(obj2);
Class cls5 = [NSObject class];
NSLog(@"%p %p", obj1, obj2);
NSLog(@"%p %p %p %p %p", cls1, cls2, cls3, cls4, cls5);

// 将类对象当作参数传入获取元类对象meta-class
Class metaClass = object_getClass(cls1);
Class metaClass2 = [cls2 class];
NSLog(@"%p %p", metaClass, metaClass2);

BOOL isMetaClass = class_isMetaClass(metaClass);
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");

isMetaClass = class_isMetaClass(metaClass2);
NSLog(@"%@", isMetaClass ? @"是元类对象" : @"不是元类对象");

// console
0x60000000c030 0x60000000c040
0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270 0x7ff85ec7c270
0x7ff85ec7c220 0x7ff85ec7c270
是元类对象
不是元类对象

objc 源代码中:

Class object_getClass(id obj)
{
		// 传入的如果是实例对象,则返回 class 类对象
  	// 传入的如果是 class 类对象,则返回 meta-class 元类对象
  	// 传入的如果是 meta-class 元类对象,返回 NSObject基类的 meta-class 元类对象
    if (obj) return obj->getIsa();
    else return Nil;
}

Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;

    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}

object_getClass

  • 传入的如果是实例对象,则返回 class 类对象
  • 传入的如果是 class 类对象,则返回 meta-class 元类对象
  • 传入的如果是 meta-class 元类对象,返回 NSObject基类的 meta-class 元类对象

-(Class)class+(Class)class:返回的是类对象

QA

对象的 isa 指向什么?

  • Instance 对象的 isa 指向类对象Class
  • Class 对象的 isa 指向元类对象Meta-Class
  • Meta-Class 对象的 isa 指向基类的元类对象Meta-Class

但注意:

  • 实例对象的 isa 需要与 ISA_MASK 按位与之后才可以得到类对象的地址值。
  • 类对象的 isa 需要与 ISA_MASK 按位与之后才可以得元类对象的地址值。

OC 的类信息存放在哪?

  • Instance 对象:成员变量具体的值,存放在实例对象中
    struct NSObject_IMPL {
        class isa;
    }
    
    struct Person_IMPL {
        struct NSObject_IMPL NSObject_IAVRS;
        int _age;
        int _height;
    }
    
    // 
    struct Person_IMPL {
        class isa;
        int _age;
        int _height;
    }
    
  • Class 对象属性信息、对象方法信息、成员变量信息、协议信息、superclass、isa 存放在类对象中。
  • Meta-Class 对象:类方法信息,存放在元类对象中。