前言:内存管理可以说一直都是比较热门的话题,也是面试的时候几乎必问的话题,那么从今天开始针对内存管理这一块,进行阶段性学习以及总结,以后忘了也可以过来回顾一下😄
一.内存五大区
首先分享一张关于内存五大区的示意图,如下:
- 1.栈区:函数,方法,指针,参数等 存储局部变量,当局部变量的作用域被执行完毕之后,这个局部变量就会被系统立即回收
- 2.堆区:手动申请的字节空间,alloc, malloc, calloc,realloc 函数,block copy等
- 3.Bss段:存储未被初始化的全局变量,静态变量,一旦初始化就回收,并转存到数据段中
- 4.数据段:存储已被初始化的全局变量,静态变量,直到程序结束才会被回收
- 5.代码段: 存储代码,直到程序结束才会被回收
其实通过这张图也能看出来,各个内存区的顺序,又高到低分别是:栈区->堆区 ->未初始化数据 .bbs ->已初始化数据 .data5 ->代码段 .text
下面分享2道面试题,比较简单,相信大家都会😄
-
1.如何定位栈区?为什么栈区访问快?
通过sp寄存器,其实如果对汇编有所了解应该都清楚,sp寄存器对应着栈底,而栈区在高位,所以通过它可以快速定位,同样的就是因为有sp寄存器,寄存器的读取速度那必然是最快的,所以栈区访问速度自然很快 -
2.全局变量和局部变量的区别,谁先创建?
一个在全局区,一个在栈区,全局变量在编译的时候已经确定的,局部变量是在函数调用时创建,所以先后显而易见
二.static与extern
说到全局变量,很容易想到一个修饰词,那就是static,那么我们也稍稍坐下总结
1:为什么说全局变量不安全,需要用static修饰?
因为如果在文件内部声明的全局变量,不用static修饰修饰的话,是可以通过extern修饰访问到全局变量,并且进行更改,这样文件内部的全局变量就变了,而如果是用static修饰的全局变量,放在文件内部也就是.m文件外部无法访问,用extern也不行,而放在.h文件,外面也是无法访问,如果外面引用了,其实是copy了一份新的全局静态变量而已,而作用域就在引用的文件范围,总结一下几点:
- 1.static关键字修饰的变量是无法改变它的作用域:
- 2.当static关键字修饰局部变量时,只会初始化一次且在程序中只有一份内存;
- 3.关键字static不可以改变局部变量的作用域,但可延长局部变量的生命周期(直到程序结束才销毁)。
- 4.当static关键字修饰全局变量时,作用域仅限于当前文件,外部类是不可以访问到该全局变量的(即使在外部使用extern关键字也无法访问)。
2.Static和extern的区别?
- 1.想要访问全局变量可以使用extern关键字,但是全局变量定义不能有static修饰。
- 2.extern修饰的全局变量默认是有外部链接的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern全局变量的声明,就可以使用全局变量。
- 3.static修饰的全局静态变量,作用域是声明此变量所在的文件。
为了更好的理解,附上几个测试的图
解释:局部变量c无法在下面的方法中引用
上面三幅图,主要是想让大家看一下不同内存区的变量对应的地址有什么不同,大家可以自己去尝试,第三幅图也就应证了我前所说的结论,帮助大家更好的理解static
这个大家可以测试一下哦,答案是可以的
三.关于NSString
说到NSString,可能收到想到的是修饰词copy,这个必要要讲的😄,但是NSString到底有哪几个子类呢?
- 1.NSTaggedPointerString 存放在栈区,小对象 ,按照低于10位采用NSTaggedPointerString来优化存储的方式. 前提是数字和英文字母,其他语言直接是__NSCFString
- 2.__NSCFConstantString 存放在数据常量区,也就是所谓的常量的一种而已
- 3.__NSCFString 存放在堆区,也就是我们用到的最多的类型了
那么问题就来了,这三种字符串分别在什么时候会出现呢?__NSCFConstantString我相信大家一看就明白,关键是NSTaggedPointerString和__NSCFString的辨别方式,具体怎么区分,先卖个关子,请看下文😄
补充几个小知识点:
-
1.__NSCFConstantString类型的字符串,存储在数据区,即使当前控制器被dealloc释放了,存在于这个控制器的该字符串所在内存仍然不会被销毁。通过快捷方式创建的字符串,无论字符串多长或多短,都是__NSCFConstantString类型,存储在数据区。
-
2.NSString +copy ( 浅拷贝,返回不可变对象) NSString+mutableCopy(深拷贝,返回可变对象) NSMutableString+copy(深拷贝,返回不可变对象) NSMutableString+mutableCopy(深拷贝,返回可变对象)
-
3.关于修饰符Copy影响的就是setter方法,只要是copy属性,不关拷贝还是不拷贝,setter方法返回的都是不可变对象
-
4.对于NSStringFromClass()方法,字符串较短的class,系统会对其进行比较特殊的内存管理,NSObject字符串比较短,直接存储在栈区,类型为NSTaggedPointerString,不论n你NSStringFromClass多少次,得到的都是同一个内存地址的string,但对于较长的class,则为__NSCFString类型,而NSCFString存储在堆区,每次NSStringFromClass都会得到不同内存地址的string
关于第四点可能不太理解,这里我给大家测试一下
可以看到,每次取ycx类的字符串时,地址都是一样的!!!!为什么!!!!答案很简单,因为NSTaggedPointerString是存在栈区的,而它的值就是存在指针里面,没出栈之前,怎么会变呢?趁热打铁再看一个面试题!
大家觉得运行之后点击频幕,会不会出问题呢,如果会,会在哪里出问题?
算了,我也不卖关子了,崩那是必然的,问题是在哪里崩的?就执行顺序而言,肯定是上面的方法先执行,不然队列都没创建,其实这里面涉及了2个知识点?
- 1.线程安全问题,其实我在前面的篇章也分享了关于GCD的面试题,应该都知道,这种方式,读写不安全,也就是在setter方式执行时,会进行对旧数据的release和新数据的retain,但是由于这是并发队列异步函数执行,就可能存在同时release的情况,就是造成对空数据的release,也就出现了上面的坏地址的报错!!!那么崩的地方应该就是上面喽,因为上面先执行已经崩了,没机会点击了,BUT!!!!!
- 2.真正崩的地方是在下面,原因前面的题目已经说了哦,上面的字符串“ycx”,属于小对象数据,所以self.nameStr是NSTaggedPointerString,是存在栈区的,根本不存在release和retain这一说法,所以怎么会出现读写异常呢?😄
相信通过上面的例子的讲解,大家都对这个TaggedPointer产生了很大的好奇,没关系下面就会讲到了
三.TaggedPointer
以下是三种系统针对内存管理优化做出的方案
首先就是我们说到的TaggedPointer,推荐大家去看下官方提供的视频WWDC 2020,里面有详细解释为什么会使用到TaggedPointer,总结以下几点
- 1.专门用来存小数据 ,通过高位直接判断数据类型,例如NSNumber,NSDate,包括前面提到的NSTaggedPointerString字符串
- 2.小对象并没有进堆区,值直接存在指针,而不是想常规数据,指针存地址,所以实际上它不是一个对象,只是一个普通变量而已,没存在堆中,自然也不需要malloc和free
- 3.内存读取3倍效率,创建速度快106倍
这是使用TaggedPointer的大致范围,如图
为了验证官方给的优化,我们一起来做一下测试,这是模拟器下执行的结果,代码如下
这个kc_objc_decodeTaggedPointer函数是从objc源码中提取出来,进行了自定义写法,目的就是为了把指针中的跟数据本身无关的信息去掉而去做的一些位运算,如果想知道详情,可以查阅objc的源码去学习,这里我就不仔细去讲解了,大家只需要知道通过这个函数可以取出,指针上存放的二进制数据,函数如下
uintptr_t
kc_objc_decodeTaggedPointer(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
这是objc源码给出的各种oc小对象数据类型的定义
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
取上面其中一个结果取验算,先附上小部分ASCII码对照表
就以第一个kc为例子,$4=0b1-0100…省略0…-01100011-01101011-0010,首先看类型0100十进制就是2,对应上面的OBJC_TAG_NSString类型,0010对应十进制2,表示字符串长度位2,然后01100011对应小写字母c,01101011对应小写字母k,完美契合,有木有!!!当然呢剩下的几个大家也可以去演算,我这里就演示一个就行了,容我偷个懒😄
切记:真机和模拟器数据表示存储方式不同,跟视频中所讲的也不一样哦,毕竟视频是20年的,模拟器数据类型是在二进制数据前面,真机是在末尾,真机后3位表示类型,在之前4位是表示长度,在之前才是数据 ,当然这些都是一步步测出来的,官方并没有给出定义相关的文档,如果大家感兴趣也可以去测试一下,这里我就不一一去测试了
今天的分享也就到这里了,后面会通过源码去查看底层的内部实现,敬请期待!!!