iOS进阶

01-OC对象的本质

2020-06-14  本文已影响0人  光强_上海

OC对象的本质

我们平时编写的OC代码,最终转换为底层实现基本上绝大部分都是基于C\C++来实现的

下面展示OC代码最终编译转换的大致流程

OC -> C\C++ -> 汇编代码 -> 机器代码

也可以理解为OC的面向对象语法基本上都是基于C\C++的数据结构来实现的

那么OC中的类和对象,最终究竟是基于C\C++的哪种数据结构来实现的尼?

我们创建一个新工程,然后在main函数中创建一个obj对象,然后我们将这个main.m文件的代码转换为cpp文件,测试代码如下:

#import <Foundation/Foundation.h>

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

转换为cpp文件的命令如下:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

这句命令的格式如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx1 -o xxx2

其中的xxx1代表的是OC源文件名,也就是上面的main.m

其中的xxx2代表的是输出的CPP文件名,也就是上面的main.cpp

执行完上面的命令后,会生成一个main.cpp的新文件,我们将此新文件拖拽到工程中,然后取消Add to Tagerts的选中,也就是让工程不编译main.cpp文件

我们在main.cpp文件中搜索NSObject_IMPL,找到如下结构体代码:

image
struct NSObject_IMPL {
    Class isa;
};

从上面转换的cpp文件中的代码可以看到,NSObject对象的底层数据结构就是一个结构体对象

我们看下NSObject的OC定义如下:

image

OC中NSObject最终转换为底层代码如下:

image

我们发现不管是OC中NSObject定义,还是转换为底层的代码,在NSObject的内部都包含了一个Class isa 指针

我们进入Class内部定义查看,Class定义如下:

image
typedef struct objc_class *Class;

我们发现Class是一个指向objc_class结构体的指针。既然Class是一个指针,也就类似于void *NSObject底层代码也就类似于下面代码:

struct NSObject_IMPL {
    // 这里isa就是'void *'类型的指针
    void * isa;
};

在C语言中,void *:表示不确定类型指针,void *可以用来申明指针,例如:void * a,这里a就是void *类型的指针

最终main函数中创建的obj对象,结构表示如下

NSObject *obj = [[NSObject alloc] init];

如图:


image

接下来我们再来探究下创建一个NSObject对象系统会分配多少内存?

NSLog(@"---%zd",class_getInstanceSize([NSObject class])); // 8

NSLog(@"---%zd", malloc_size((__bridge const void *)obj)); // 16

class_getInstanceSize():表示NSObject的实例对象的成员变量所占用的内存大小

malloc_size():表示obj指针所指向的内存大小,也就是创建一个obj实例系统所分配的内存大小

需要注意:

虽然创建一个obj实例对象系统分配了16个字节的内存,但是obj对象内部真正用到的大小就只有前面8个字节,也就是用来存放isa指针的那8个字节大小,还剩余8个字节大小没有被使用,也就是说NSObject_IMPL结构体也就占用8个字节

对于class_getInstanceSize()函数,我们可以通过查看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());
    }

通过alignedInstanceSize()函数注释:

Class's ivar size rounded up to a pointer-size boundary.

可知class_getInstanceSize()函数返回的确实就是实例对象成员变量所占用的内存大小

我们也可以查看objc底层源码来确认创建一个obj实例对象分配内存的情况

底层源码查看路径如下:
objc4源码 -> 全局搜索allocWithZone()-> NSObject.mm -> _objc_rootAllocWithZone() -> class_createInstance() -> _class_createInstanceFromZone() -> calloc() -> instanceSize()

instanceSize()函数中,我们可以看到如下定义:

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

instanceSize函数的注释可以看出,在OC中创建一个实例对象,系统最少分配16个字节内存

// CF requires all objects be at least 16 bytes.

我们再来查看下继承自NSObject类的对象,系统分配多少内存?

我们来看如下代码:

// GQPerson的本质就是`struct GQPerson_IMPL`
@interface GQPerson : NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation GQPerson

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        GQPerson *person = [[GQPerson alloc] init];
        person -> _no = 10;
        person -> _age = 20;
        
        // 可以将person对象转为`struct GQPerson_IMPL`类型对象,因为`GQPerson`类型的本质就是`struct GQPerson_IMPL`类型
        struct GQPerson_IMPL *personImpl = (__bridge struct GQPerson_IMPL *)(person);
        NSLog(@"%d -- %d", personImpl ->_no, personImpl ->_age); // 10 20
    }
    return 0;
}

将上面代码转换为objc底层代码如下:

struct NSObject_IMPL {
    Class isa; // 8个字节
};

struct GQPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 这句等价于`Class isa` 
    int _no; // 4个字节
    int _age; // 4个字节
};

通过转换后的底层代码GQPerson_IMPL,我们知道创建一个GQPerson_IMPL对象,系统分配了16个字节

我们通过下面的这两个函数打印也可以看出来,系统分配了16个字节内存

NSLog(@"---%zd",class_getInstanceSize([GQPerson class])); // 16

NSLog(@"---%zd", malloc_size((__bridge const void *)person)); // 16

示例图如下:

image

下面我们再来看一个自定义类继承自另一个自定义类的例子,示例代码如下:

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

@implementation Student
@end


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

@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
    }
    return 0;
}

我们将示例代码转换为底层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;
};

我们将上底层代码进行等价优化如下:

struct NSObject_IMPL {
    Class isa;
};

// Person_IMPL结构体占用16个字节
// 结构体内存对齐:结构体占用内存大小,必须是其中最大成员占用内存的倍数
struct Person_IMPL {
    Class isa; // 8
    int _age; // 4
};

// Student_IMPL结构体占用16个字节,是因为_no的4个字节正好用在Person_IMPL结构体空余的4个字节上
struct Student_IMPL {
    Class isa; 8
    int _age; // 4
    int _no; // 4
};

示例分析图如下:


image

通过分析优化的底层代码,我们可以知道personstudent对象都占16个字节内存

下面我们再来看一个示例:

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

@implementation Person
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        
        NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
        NSLog(@"%zd", class_getInstanceSize([Person class])); // 24
        NSLog(@"%zd", malloc_size((__bridge struct Person_IMPL *)person)); // 32
    }
    return 0;
}

我们将上面的代码转换为底层c++代码如下:

struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
    int _height; // 4
    int _no; // 4
};

我们通过打印可以看出,Person_IMPL结构体所占用内存大小为24,这个正好符合结构体内存对齐原理,但是创建一个Person实例对象为啥会分配32个字节尼,这个是苹果操作系统内存分配原则,具体参照底层代码:

底层源码查找路径如下:

libmalloc库 -> 搜索Buckets -> Buckets sized:注释`

Buckets sized:

/* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

我们从Buckets sized的注释可以看出,苹果操作系统分配内存的原则:分配最小内存为16,最大为256,所有的分配的内存都是16的倍数。上面24个字节大小不是16的倍数,所以最终分配内存大小为32个字节

更多文章

上一篇 下一篇

猜你喜欢

热点阅读