oc的本质、底层结构、内存分析、isa指针和superclass指针分析

1、在开始前先说下怎么将oc代码转为c++代码

方法1
1、打开终端cd到目标的工程文件
2、终端输入:clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxx.m,其中xxx.m替换成自己需要转换的文件,然后敲回车

方法2
1、打开终端cd到目标的工程文件
2、xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m -o xxx.cpp,将xxx改为自己需要转换的文件名才回车就可以了
如果需要链接其他框架,使用-framework参数。比如-framework UIKit

在终端上执行了上面两个方法中任何一个后,回到工程文件中就可以看到多了一个cpp文件,将cpp文件拖拽到工程中就可以在xcode里看到了

注意
在将cpp文件添加到工程中后最好将cpp文件从编译器中移除,否则在编译的时候会报错。

1-1.png

oc转c\c++详细流程可以看这里iOS将oc的.m文件编译成C++的.cpp文件

2、oc的底层实现

  • oc的底层实现都是c\c++代码,oc的面向对象都是基于c\c++的数据结构实现的
  • oc的对象和类主要是基于c\c++的结构体实现的
  • 编译器会先将oc代码转化为c\c++代码,再将c\c++代码转化为汇编语言,然后再转化为机器语言


    1-2.png

下面我们创建一个NSObject对象,然后再转化成c++代码,看下在c++中是什么样的结构

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

在oc中NSObject的定义是这样的

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

在c++中是一个结构体

struct NSObject_IMPL {
    Class isa;
};

由此可以证明上面的结论oc的对象和类主要是基于c\c++的结构体实现的

class又是什么呢?clas是一个指针

typedef struct objc_class *Class;

如果创建一个Person类继承自NSObject其底层又是怎样实现的呢?

Person * person = [[Person alloc] init];

转化为c++后

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

NSObject_IMPL就是NSObject的底层

struct NSObject_IMPL {
    Class isa;
};

那么可以在Person_IMPL中将NSObject_IMPL看成是isa指针,那么就等价于下面的写法

struct Person_IMPL {
    Class isa;
};

如果Person带有成员变量呢?

@interface Person : NSObject
{
    int _age;
    NSString * _name;
}

其底层为

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    NSString *_name;
};

如果再创建一个Student类继承自Person并带有了height成员变量呢?

@interface Student : Person
{
    int _height;
}

转化为c++后

struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _height;
};
  • 由上面的Person和Student转化为c++后可以看出,子类中包含了父类的结构体
  • 每个对象都包含一个isa指针

3、oc对象内存详解

下面先用两个方法去打印NSObject对象的内存大小

NSObject * object = [[NSObject alloc] init];
        
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
NSLog(@"%zd",malloc_size((__bridge const void *)object));

这两个方法分别打印的是8和16,为什么这两个方法打印出来的内存大小不一样呢。
class_getInstanceSize是获取一个实例对象创建至少需要多少内存
malloc_size是获取创建一个实例对象,实际上分配了多少内存
为什么会有一个最少内存和一个分配内存呢?因为oc中有一个内存对齐规则。
内存对齐:简单的理解就是最终的内存大小为成员中内存最大的整数倍,不足的要对齐。

在64位的环境下oc中对象存取是以8字节来计算的,对象开辟空间的内存是以16字节来对齐的。

想要详细了解iOS中内存对齐的可以看下面两位大神的文章,建议先看第一篇文章再看第二篇。
这篇通俗易懂的讲解了iOS中的内存对齐的应用
这篇很好的讲了内存对齐的定义
先看了第一篇才能很好的理解第二篇文章中内存对齐的定义

怎么计算出NSObject最少内存为8,分配内存为16呢?

因为NSObject在c++中实际为一个结构体

struct NSObject_IMPL {
    Class isa;
};

NSObject_IMPL结构体中包含了一个isa指针,指针在64位的环境下为8个字节。结构体的内存大小所其成员所决定,又因为oc中对象存取是以8字节来对齐的NSObject_IMPL结构体成员大小为8,刚好为8的整数倍不需要补齐,所以NSObject最少内存为8。因为oc对象开辟空间是以16字节对齐的,NSObject的内存为8,不是16的整数倍,需要补齐是内存为16的整数倍,所以补齐后内存就为16了。

Person继承自NSObject,并有两个成员变量,那么Person的最少内存和实际分配内存分别是多少呢?

@interface Person : NSObject
{
    int _age;
    NSString * _name;
}

将Person转化为c++后

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    NSString *_name;
};

(本文章所说的内存都是在64位环境下的)NSObject_IMPL里是一个isa指针,为8字节,_age为4字节,_name为8字节。8+4+8=20,又因为oc中对象存取是以8字节来对齐的所以Person最少内存为24。因为oc对象开辟空间是以16字节对齐的所以实际分配内存为32。

需要注意的是苹果为了节省内存空间对内存做了重排,所以在分配内存时,并不是按你的成员变量书写顺序去分配的

4、oc对象的分类

oc中的对象主要分为3类分别为:instance对象(实例对象)、class对象(类对象)、meta-class对象(元类对象)。

1、instance对象(实例对象)
通过alloc出来的对象就是实例对象,每次alloc都会生成一个实例对象。

        NSObject * obj1 = [[NSObject alloc] init];
        NSObject * obj2 = [[NSObject alloc] init];
        
        NSLog(@"obj1:%p obj2:%p",obj1,obj2);

上面obj1和obj2就是两个不同实例对象,打印出内存地址分别是

obj1:0x10070a740 obj2:0x10070a750

内存地址不同就说明了是两个不同的对象,分别占据着两块不同的内存。
在上面对象本质的一部分,我们看到了实例对象在内存中存储了isa指针和成员变量。

2、class对象(类对象)
每个类在内存中有且只有一个class对象

        NSObject * obj1 = [[NSObject alloc] init];
        NSObject * obj2 = [[NSObject alloc] init];
        
        Class objClass1 = [obj1 class];
        Class objClass2 = [obj2 class];
        
        NSLog(@"objClass1:%p    objClass2:%p",objClass1,objClass2);

objClass1和objClass2都是类对象,打印出来的结果为

objClass1:0x7fff80670388    objClass2:0x7fff80670388

打印出来的地址是一样的,是同一块内存,所以是同一个class对象。
class对象在内存中存储的信息主要包括:isa指针、类的属性信息(@property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量(ivar)等。

3、meta-class对象(元类对象)
每个类在内存中有且只有一个meta-class对象,meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括isa指针、superclass指针、类的类方法信息(class method)

获取元类对象需要用到runtime

Class metaClass = object_getClass([NSObject class]);

需要注意的是通过下面方法获取到的不是元类对象,而是类对象

Class objClass = [[NSObject class] class];

判断一个对象是否为元类对象,可以通过下面的方法(也是runtime中的方法)

BOOL result = class_isMetaClass([NSObject class]);

5、isa指针

由上面的知识点,我们已经知道在实例对象、类对象和元类对象中都有着一个isa指针,isa指针有什么用呢?我们先看下下面1-3这张图

1-3.png

  • instanceisa指向class
    当调用对象方法时,因为对象方法是放在class对象中的,所以instance对象会通过自己的isa指针找到class,最后找到对象方法的实现进行调用。

  • classisa指向meta-class
    当调用类方法时,因为类方法是放在元对象中的,所以类对象会先通过本身的isa指针找到meta-class,最后找到类方法的实现进行调用。

  • meta-classisa指向基类的meta-calss
    这里需要注意的是meta-classisa指向基类的meta-calss,而不是父类的meta-calss
    例如有A、B、C、D四个类,A是基类,D继承C,C继承B,B继承A,那A、B、C、D四个类的meta-classisa分别指向谁呢?
    答案是都指向A的meta-class,因为meta-classisa指向基类的meta-class,A是B、C、D的基类,因为A本身就是基类,所以A的meta-calssisa指向自己的meta-class

  • 从64bit开始,isa需要进行一次位运算,才能计算出真实地址

    1-4.png

6、superclass指针

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

从1-4图中可以看到,instance中没有superclass指针,classmeta-class中都有superclass指针。
为什么instance中没有superclass制作呢,因为instance中已经包含了父类的成员变量,所以根本不需要superclass指针再去指向父类获取成员变量了。例如创建一个Student类继承自Person,那么Student的实例对象底层就是这样的。

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

对这结构不是很明白的可以滑到文章上面再看下oc底层实现这一块的知识点。

class对象的superclass指针
Person是Student的父类
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用

meta-class对象的superclass指针
Person是Student的父类
当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用

1-5是一张非常经典的图大家可以看下


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

推荐阅读更多精彩内容