底层研究 - 对象的底层探索(上)

前言

  1. 本文底层使用的是objc 4-838.1版本
  2. 汇编指令
  • b bl 跳转指令,函数的调用
  • ret 函数的返回
  • ; 注释

1、alloc方法在底层的调用流程

alloc 示例

从上图的例子中我们可以发现,p1和p2两个指针的地址是一模一样的,那么我们可以得出一个结论:

p1 = p ,p2 = p
那么说明init方法并没有开辟内存空间,内存空间在alloc时已经开辟完成

那么alloc内具体干了啥呢?我们接下来结合源码和汇编来进行探索。

打开objc源码,经过搜索可以发现,alloc方法的调用是alloc -> _objc_rootAlloc -> callAlloc

alloc

_objc_rootAlloc
callAlloc

但实际上,通过断点调试,会发现实际上的并没有直接调用alloc方法,这个时候我们通过汇编的方式来查看具体原因


alloc方法断点

可以发现,在调用alloc方法后,实际上是调用了objc_alloc方法,在源码中搜索,可以发现当有调用alloc方法时,实际上会调用objc_alloc

objc_alloc调用原因

继续汇编的探索,打断点进入objc_alloc,可以发现,调用了objc_msgSend,而这个消息发送函数发送的就是alloc方法

objc_alloc方法汇编

打断点进入alloc方法,发现它实际上又去调用了_objc_rootAlloc方法

alloc汇编

进入到_objc_rootAlloc后,可以发现跳转到了_objc_rootAllocWithZone

_objc_rootAlloc汇编

在源码中搜索_objc_rootAllocWithZone,发现方法返回了一个id类型

_objc_rootAllocWithZone

进入_class_createInstanceFromZone,可以发现返回的是一个object

_class_createInstanceFromZone

在汇编中进入_objc_rootAllocWithZone,通过打印可以发现返回的就是一开始例子中的Person类对象

_objc_rootAllocWithZone汇编

整理下上面的探索流程,可以发现具体的alloc底层调用方法顺序为:


alloc调用顺序

经过上面的探索,我们能进一步的得出一个结论:

alloc方法开辟内存空间并返回了实例化对象

那么这就引出另一个问题了,alloc把对象都创建完返回了,那init方法又干了啥呢,它有何作用呢?在源码中可以发现,init方法啥都没干,直接返回了对象。

init

那苹果设计init方法的作用是啥呢?其实这是一种工厂模式,苹果的初衷是让开发者重写init方法,在内部进行一些初始化等相关操作。

2、编译器的优化

在上面的汇编探索中,我们可以发现实际上有一些方法在汇编中并没有显示出来,比如callAlloc,_class_createInstanceFromZone,这里就涉及到编译器的优化问题了。

xcode优化等级

在Build Settings中的Optimization Level选项中,可以看到编译器在Release下是Fastest,Smallest,当Debug模式下也是Fastest,Smallest时,再进行汇编探索会发现代码少了很多,这就是编译器的优化。None的优化等级并不代表编译器不会优化,只是说None的优化等级没有那么高而已。
从此也可以联想到方法的消失本质上就是被编译优化掉了。

关于编译器的优化,除了xcode的设置,还有代码方面:


代码编译优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))

fastpath 和 slowpath 都是一个宏定义,这个代码的作用是告诉编译器fastpath内的语句的执行可能性更大,这时候编译器就会预先读取fastpath这个条件下的语句,系统在运行时就会减少重新取指,从而减少指令跳转带来的性能上的下降。

3、对象的内存对齐方式

那么接下来我们探索下被优化掉的_class_createInstanceFromZone方法具体干了啥。

_class_createInstanceFromZone

首先可以发现调用了instanceSize方法,代码中有一句if (size < 16) size = 16,也就是说当对象被alloc出来后,其初始化的大小就是16个字节。

方法内有一个缓存判断方法,

  • 没缓存时,调用了方法alignedInstanceSizealignedInstanceSize又直接调用word_align,可以发现,alignedInstanceSize本质就是个字节对齐算法。(x + 7) & ~7,低位清零,8字节对齐。

    instanceSize

    word_align

  • 有缓存时,调用了方法fastInstanceSize,该方法调用了align16,可以发现align16是个16字节对齐算法

    fastInstanceSize

    align16

综上所述,我们可以知道,在64位操作系统下创建对象开辟内存是以8字节对齐,如果没缓存时是以16字节对齐,我们通过_class_createInstanceFromZone后续的代码可以发现,系统实际为对象分配大小依赖的的是callocinstanceSize实际上只是计算对象开辟需要的内存大小,而calloc函数本身就是以16字节对齐

继续往下看_class_createInstanceFromZone,obj调用了initInstanceIsa方法,这个方法实际上将obj绑定到相应的类上,我们可以通过打印来证明

initInstanceIsa执行前后obj类型

总结一下,_class_createInstanceFromZone方法主要做了以下几个操作:

  • 先计算出需要的内存空间大小,cls -> instanceSize
  • 向系统申请开辟内存,返回地址指针,calloc
  • 关联到相应的类,obj->initInstanceIsa

那苹果为啥要这么设计呢,我们接下来看下对象的本质

4、对象本质

首先创建一个mac app项目,创建一个类

@interface Person : NSObject

@property (nonatomic ,assign) int age;
@property (nonatomic ,copy) NSString *name;
- (void)test;

@end

@implementation Person

- (void)test {
}
@end

然后打开终端进入当前项目下输入clang -rewrite-objc main.m 指令,这样我们在文件夹下就得到了一个.cpp的文件,打开.cpp文件搜索Person我们可以发现对象的本质是一个objc_object的结构体,其结构体内存储的是 isa指针 + 成员变量的值

Person类定义

NSObject_IMPL

那么为什么要字节对齐呢?
字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以
“块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不
对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节,
这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就
是字节对⻬。

那为什么对象成员变量是8字节对齐,而实际分配的内存是16字节对齐?
这其实是一种空间换时间的做法。苹果采取16字节对⻬,是因为OC的对象中,第⼀位叫isa指针,它是必然存在的,
⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8
位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空
间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块
读取就可以,效率更⾼,同时还不容易混乱。

5、结构体的内存对齐方式

通过上文我们已经知道对象的本质是一个结构体,那接下来我们看下结构体对象对齐方式


结构体内存大小

通过上面的例子,我们可以发现在结构体内,变量位置不同或变量类型不同都会对结构体的大小产生影响。

具体规则如下:

  1. 数据成员对⻬规则
    结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)。
  2. 结构体作为成员
    如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)。
    注意:结构体作为成员时,不是看做整体计算,而是以内部成员计算
  3. 收尾⼯作
    结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。

那么看回上面的例子,我们可以分析为

  • struct1
struct Struct1 {
    double a;//8字节,0~7
    char b;//1字节,8
    int c;//4字节,4字节的整数倍,12~15
    short d;//2字节,16~17
}struct1;//最大成员变量的整数倍,也就是double,3x8=24
  • struct2
struct Struct2 {
    double a;//8字节,0~7
    int b;//4字节,8~11
    char c;//1字节,12
    short d;//2字节,2字节的整数倍,14~15
}struct2;//最大成员变量的整数倍,也就是double,2x8=16
  • struct3
struct Struct1 {
    double a;//8字节,24~31
    char b;//1字节,32
    int c;//4字节,4字节的整数倍,36~39
    short d;//2字节,40~41
}struct1;

struct Struct3 {
    double a;//8字节,0~7
    char b;//1字节,8
    int c;//4字节,4字节的整数倍,12~15
    short d;//2字节,16~17
    int e;//4字节,4字节整数倍,20~23
    struct Struct1 str;
}struct3;//最大成员变量的整数倍,也就是double,6x8=48

6、总结

  1. alloc方法底层调用流程:
    alloc -> objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone
  2. cls -> instanceSize() - 计算对象需要的内存大小
    calloc() - 系统实际为对象分配内存的大小
  3. 字节对齐是为了让CPU更高效的读取数据,因为CPU是按块读取,因此对象内部的成员变量是以8字节对齐,系统实际分配的内存以16字节对齐。
  4. 对象的本质是objc_object结构体,⾥⾯存储isa指针和成员变量的值。
  5. 结构体的对齐方式
  1. 数据成员对⻬规则
    结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)。
  2. 结构体作为成员
    如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)。
    注意:结构体作为成员时,不是看做整体计算,而是以内部成员计算
  3. 收尾⼯作
    结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容