重学iOS系列之底层基础(三)内存管理-内存的5大区与TaggedPointer

前言 

        在前文重学iOS系列之APP启动(四)Mach-O中,我们对二进制在内存中的存储有了一个大概的轮廓,但是并没有深入分析程序在内存中的具体存储情况。本文将会补齐这部分知识,并且以TaggedPointer为例子,进行实战演练。

        开门见山,程序在内存中的布局分为5大区:栈区、堆区、全局区、常量区、代码区。下图将内存的分区进行了非常详细的描述

之前分析mach-o的时候,已经了解 .text 段为低地址,所以上图底部为低地址,顶部为高地址。


栈区--stack

特点:

1、栈是系统数据结构,其对应的进程或者线程是唯一的

2、栈是向低地址扩展的数据结构

3、栈是一块连续的内存区域,遵循先进后出(FILO)原则

4、栈的地址空间在iOS中是以0X7开头

5、栈区一般在运行时分配

存储内容

1、栈区是由编译器自动分配并释放的,主要用来存储局部变量

2、函数的参数,例如函数的隐藏参数(id self,SEL _cmd)

优缺点

优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效

缺点:栈的内存大小有限制(其实绝大部分情况都不会达到最大值)

iOS主线程栈大小是1MB,其他主线程是512KB,MAC只有8M

注意:传入函数的参数值、函数体内声明的局部变量等,由编译器自动分配释放,通常在函数执行结束后就释放了(不包括static修饰的变量,static意味该变量存放在全局/静态区)。

Threading Programming Guide中有相关内存大小的说明,如下:


堆区--heep

特点

1、堆是向高地址扩展的数据结构

2、堆是不连续的内存区域,类似于链表结构(便于增删,不便于查询),

3、遵循先进先出(FIFO)原则

4、堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态的

5、堆区的分配一般是在运行时分配

存储内容

1、堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收

2、OC中使用alloc或者使用new开辟空间创建对象

3、C语言中使用malloc、calloc、realloc分配的空间,需要free释放

优缺点

优点:灵活方便,数据适应面广泛

缺点:需手动管理,速度慢、容易产生内存碎片

注意:当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区。因为现在iOS默认使用ARC来进行内存管理,所以也不需要手动释放。


全局区(静态区)(BSS段)

BSS段(bss segment)通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域,数据段属于静态内存分配。

 全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放

1、未初始化的全局变量和静态变量,即BSS区(.bss)

2、已初始化的全局变量和静态变量,即数据区(.data)

由static修饰的变量会成为静态变量,该变量的内存由全局/静态区在编译阶段完成分配,且仅分配一次。

static可以修饰局部变量也可以修饰全局变量。


常量区(数据段)

常量区是编译时分配的内存空间,在iOS中的地址一般以0x1开头,比如:0x100000000011038a,在程序结束后由系统释放

通常是指用来存放程序中已经初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。字符串常量等,是放在只读数据段中,结束程序时才会被收回。

代码区(代码段)

代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存

代码区需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

注:除了以上内存区域外,系统还会保留一些内存区域。


说了这么多理论,没有实战演示没有说服力,我们进入实战环节

上图可以发现全局区的地址非常小,堆区的地址相对来说更高,栈区地址就非常高了,间接的验证了本文开头的内存分区图。

为了更直观的观察这些区域,我们再打开MachOView来查看刚刚运行的工程mach-o。


看上图常量字符串的地址 00003F72,这个地址相对来说已经是很低了,因为比这个更低的位置存储的是代码数据

常量字符串存储在 .text 末段


****全局区****  a = 0x100008808

这个打印的地址在上图    8800  偏移 8个字节 处,如图划红线位置,有一个值为9。

但是后续的 b 地址为 0x100008818,但是上图 8818 位置没有了。并且请看下图

后面这个段的起始位置为 C000 ,中间缺了一大段的空间。这些空间就是保存空间。

再看看 BSS 在 mach-o 中的起始位置和size

是不是很有意思,起始地址就是消失的 8818 ,然后Size为 16 字节,这不就是 8828吗?然后 str1 地址起始位置为 8820 ,占用8个字节,末尾不就是 8828吗?

BSS 里面只存储了2个静态变量 b 和 str1 的地址, b 和 str1 都是未初始化的静态变量,与上文的描述完全一致  。


已经初始化的静态变量存储在 (__DATA,__data) 段中

mach-o 在(__DATA,__data)后面的地址空间预留了一段内存,这段内存包含了 BSS 段,存储着未初始化的静态变量。


我们看看 mach-o 中最大的地址为多少

最大的地址为:00012330。远远小于堆区对象 objc 的地址 0x101a05be0 。可以发现堆区和栈区都不在 mach-o 中 ,由系统控制管理。

由堆区和栈区打印的地址可以得出,堆的地址 比 栈的地址要小。


说了那么多,和TaggedPointer有什么关系?为什么要提TaggedPointer?TaggedPointer又是什么鬼东西?

TaggedPointer

        如果有读者阅读过重学iOS系列之APP启动(三)objc(runtime)这篇文章,则会对 TaggedPointer 有个大概的了解。TaggedPointer 是苹果为了优化内存而创新的新技术。

从前文重学iOS系列之底层基础(一)OC对象的本质中,我们了解到OC对象其实是个指针,数据都存储到指针指向的地址中。

简单来说 TaggedPointer 是会将某些类型的短小数据直接存储到 对象指针中 ,而不是存储到指针指向的地址中,也就是说 如果这个对象是 TaggedPointer 对象,则该对象的地址里存储的内容就包含了该对象引用的值。

那么哪些类型支持 TaggedPointer 呢?

在 objc-internal.h  文件中有如下定义 , 

定义比较抽象,不容易理解,我们来通过代码直观的来验证下。

咦,发现地址好像没问题啊,看起来都像是栈空间的地址是!为什么呢?

既然是 TaggedPointer 难道不是应该将数据存储在地址中吗?莫非是我打印的姿势有问题?

带着这些疑问,我们打开objc818.2 的源码,全局搜索下 TaggedPointer

在 objc-internal.h 文件中找到一堆的关于 TaggedPointer 的函数。

那么我们从哪开始呢?我们是不是要找出为什么 TaggedPointer 的对象地址会被伪装成栈空间地址的样子对吧?

那肯定有关于编码encode相关的操作吧,刚好我们的截图中有如下3个函数:

_objc_encodeTaggedPointer(uintptr_t ptr)

_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)

_objc_decodeTaggedPointer(const void * _Nullable ptr)

很显然是 _objc_encodeTaggedPointer 函数内部对对象指针进行了伪装,我们具体分析下是怎么伪装的

第一句代码就直接对传入的对象指针 ptr 做了异或 ^ 操作。

那么 objc_debug_taggedpointer_obfuscator 是什么呢?从单词翻译来猜想应该是一个混淆器,

当然文件找不到该 变量的赋值操作,我们全局搜索下,找到 initializeTaggedPointerObfuscator 函数

初始化 taggedPointer 混淆器,那么 objc_debug_taggedpointer_obfuscator 就是用于 taggedPointer 对象进行地址混淆的,实锤了!

那么这个混淆器的初始化函数是什么时候调用的呢?

在 _read_images 的时候就已经调用了。

回到 _objc_encodeTaggedPointer 函数来,那么

uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

这句代码其实就已经是对 ptr 进行了编码了。

但是从 objc_debug_taggedpointer_obfuscator 初始化的过程中,应该是随机数据啊,不应该伪装成栈地址那么完美的啊。

继续分析后续逻辑代码

uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);

value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);

value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;

然后又发现在 调用 _objc_encodeTaggedPointer 之前还对 ptr 做了一次转换操作

经过这么多次的转换,显然无法通过静态分析来拿到真实的 ptr 值了。

那么我们怎么才能拿到混淆前的真实 ptr 地址呢?我们又不能直接调用 decode 函数来解码!

大家不知道还记不记得 initializeTaggedPointerObfuscator 进入就有一个 if 判断,这个判断就是是否开启混淆的。那么我们是不是可以直接关闭这个开关呢,让 runtime 在运行时不混淆

看 DisableTaggedPointerObfuscation 的定义,这不就是Xcode 的环境变量吗?

OK,我们打开Xcode的环境变量设置,将 DisableTaggedPointerObfuscation 设置为YES,如下图

但是笔者的Xcode出了点问题,环境变量设置不生效(可能和Value的值设置错误有关)。

那么我们换另外一种方案。

既然我们已经了解到源码是怎么 encode 的,那么我们可以自己实现一个解码函数,将 decode 的相关代码 copy 出来,代码如下:

extern  uintptr    _tobjc_debug_taggedpointer_obfuscator;

uintptr_t

_objc_decodeTaggedPointer(id ptr)

{

    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;

}

读者可以直接copy到你们自己的工程中使用

那么怎么用呢?非常简单

那么我们运行下看看 s1 解码后的值是什么

0xa000000000000371,展开成64位

1010000000000000000000000000000000000000000000000000001101110001

那么我们怎么把真实的7取出来呢?在这之前我们先看下 NSString 类型的 TaggedPoint 位视图

然后再将展开成64位的值带入到位视图的相关位

1010000000000000000000000000000000000000000000000000001101110001

先看最左侧的第 63 位,为1,说明是 Tagged Point

然后看第 60 ~ 62 位,为 010 = 2 , 对应 tag 枚举值为 OBJC_TAG_NSString (忘记的读者请翻到上面查看所有类型的枚举),说明是 NSString 类型。

再看第 0 ~ 3 位, 0001 = 1,字符串长度为 1 。

最后看存储数据的部分,第 4 ~ 59 位,(前面的0可以省略)110111 = 55 , 而 7 的 ASCII码 就是55。

完美分解 Tagged Point 。但是你以为你就了解到 Tagged Point 的所有吗?

并不是的,请再看下面 一 幅图

如上图所示,如果类型是NSNumber,则前 4 位 代表的是NSNumber 类型中存储具体的数据类型。比如 int ,char, long 等。那么这些数据类型的具体值是多少呢?

0        char

1        short

2        int

3        long

4        float

5        double


照旧,我们写代码来验证下

注意,这里 n1 的类型打印虽然依旧是 __NSCFNumber ,实际却可以使用解码函数进行解码,说明真实的类型是 Tagged Point 类型。n2 的解码结果明显是错的。

n1 的 Tagged Point 值为 0xb000000000000072

展开为64位的值为 1011000000000000000000000000000000000000000000000000000001110010

第 63 位,为1,说明是 Tagged Point

第 60 ~ 62 位,为 011 = 3 , 对应 tag 枚举值为 OBJC_TAG_NSNumber (忘记的读者请翻到上面查看所有类型的枚举),说明是 NSNumber 类型。

第 0 ~ 3 位, 0010 = 2,对应上面的类型映射则为 int。

第 4 ~ 59 位,(前面的0可以省略)111 = 7。

完美验证!


不知道大家有没有发现我们在打印 NSString 类型的时候,直接使用常量字符串进行初始化的时候是不会生成 Tagged Point 类型的,为什么呢?

我们先看看 __NSCFConstantString 和 __NSCFString 有什么区别和联系 :

__NSCFConstantString

1. 常量字符串,存储在字符串常量区,继承于 __NSCFString。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例,可以通过 == 判断字符串内容是否相同。

2. 这种对象一般通过字面值@"XXX"创建。如果使用 __NSCFConstantString 来初始化一个字符串,那么这个字符串也是相同的 __NSCFConstantString。

__NSCFString

1. 存储在堆区,需要维护其引用计数,继承于 NSMutableString。

2. 通过stringWithFormat:等方法创建的NSString对象(且字符串值过大无法使用Tagged Pointer存储)一般都是这种类型。

由于我们使用字面量 @"XXX" 来对变量进行初始化,说明该变量的值已经固定的存储在字符串常量区了,所以不会生成 Tagged Point ,而是会创建 isa 指针将指针指向常量区。所以即便我们初始化的字面量的长度没有超过7字节能存储的大小也不会生成 Tagged Point 。 

在演示验证 NSNumber 类型的时候 NSConstantIntegerNumber 其实也是同样的道理。


那么在源码中,苹果是怎么判断是否是Tagged Point 呢?是将第63位读取出来判断01吗?其实不是的

将指针值通过 & 运算符和_OBJC_TAG_MASK 运算后,判断是否和 _OBJC_TAG_MASK 相等。那么_OBJC_TAG_MASK 又是什么呢?

在 MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位,而iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。

说明 MacOS 和 iOS 的位图是不一样的!!!

在 MacOS 下 NSString 和 NSNumber 的位图如上图所示,感兴趣的读者可以按照前文的方法自行验证。

那么都在什么情况下会进行 Tagged Point 类型判断呢?

我们搜索下哪些函数有调用到 isTaggedPointer 

很多函数内部都有做判断,笔者整理如下:

1、判断class 

2、获取 isa指针的时候

3、设置关联对象的时候会判断

4、自动释放池在释放对象的时候会判断

5、引用计数加减的时候会进行判断

6、对象释放的时候会进行判断(好像有个经典崩溃面试题用到了这个知识点)


到此,Tagged Point 也分析完毕,如果有错误的地方,可以评论或者私聊笔者进行修改。

PS:

读源码的时候有时候会看到fastpath, slowpath, 解释下这2个的作用

fastpath 和 slowpath 宏定义是编译器特性,fastpath 的意思是告诉编译器括号中的代码很高概率为 1,即后续的判断大概率为ture;而slowpath 的意思是告诉编译器括号中的代码很高概率为 0,即后续的判断大概率为false 。

编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降;

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

推荐阅读更多精彩内容