OC对象的本质

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个字节

更多文章

  • ReactNative开源项目OneM(1200+star):https://github.com/guangqiang-liu/OneM:欢迎小伙伴们 star
  • 简书主页:包含多篇iOS和RN开发相关的技术文章http://www.jianshu.com/u/023338566ca5 欢迎小伙伴们:多多关注,点赞
  • ReactNative QQ技术交流群(2000人):620792950 欢迎小伙伴进群交流学习
  • iOS QQ技术交流群:678441305 欢迎小伙伴进群交流学习
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270