乐于分享
好东西不私藏

iOS内存管理篇(一)

iOS内存管理篇(一)
前提
本小节会先从ios的内存存储的原理开始介绍,后续再继续介绍内存的检测等。
在iOS的内存区域一共有几大区域包含: 内核区栈区堆区数据区(它包含全局静态区和常量区)代码区保留区,其中内核区和保留区由操作系统管理,区域的划分按照从高地址到低地址的分布如下, 每个区对应的存储数据类型如图所示。
一. 栈区(Stack)
它是用来存储局部变量,方法参数,以及方法返回值的, 注意变量并不包括static变量,static的变量在数据区中。 它是一段连续的存储空间,从高地址到低地址的存储顺序, 遵循后进先出(先进后出)的规则。
栈区是的内存管理是由系统来管理, 声明的变量超出作用域范围后内存就会自动的回收;
在iOS中站的存储地址开头都是以0x7开头;
栈区的优点: 不会产生内存碎片,存储效率高。
缺点: 在多线程中,各个线程都独立的栈区,iOS默认给主线程分配的栈是1MB,其它子线程是512KB。
二. 堆区(Heap)
堆区是一段并不连续的区域,它的存储顺序是从低地址到高地址,跟栈的方式正好相反, 它的内存管理是由程序员手动分配管理的, 遵守“谁创建,谁释放,谁引用,谁管理”的原则。通过引用计数管理, 开发需要关注内存的分配与释放,避免造成内存泄漏,或不及时释放而引起的OOM(内存溢出)问题
堆的内存空间在iOS通常是以0x6开头;
OC中是通过alloc方式释放,底层调用是allocWithZone方法
C++语言中是使用的malloccalloc分配内存,oc底层最终都会调用到malloc函数来分配, C++的使用deletefree进行释放
堆区优点: 分配内存大; 缺点: 需手动管理,容易产生内存碎片,需关注内存释放的问题。
三. 数据区(_Data)
数据区分为: 常量区和全局静态区;
常量区(.rodata)就是编译时分配的内存空间,在Mach-O加载阶段,这些常量数据会常驻在App的内存中,直到App被系统回收。
存放的常量类型: 整型浮点字符字符串等。
常量区的数据存储在Mach-O文件的Data段中的Section中,可以通过MachOView工具查看。
全局静态区: 同样也是在常驻在App内存中直到被系统回收,它由分为:BSS区(存储未初始化的全局变量和静态变量)
数据区(存储已初始化的全局变量和静态变量)
四. 代码区
程序编译的Mach-O中的二进制代码, 同样也是常驻内存直到App被系统回收,代码区的加载过程,在上一篇的开机启动中有介绍细节,可以参考rebase/binding的过程。
此部分的代码区不需要程序员处理。
五. 系统---内存管理机制
ios早起的版本使用的MRC,现在基本使用的都是ARC,但本质都是使用的retaincount引用计数的管理方式;
alloc和copy会分配一块新的内存空间; strong会让引用计数+1;
weak表示弱引用;
Autorelase的介绍:
intmain(int argc, constchar * argv[]) {    @autoreleasepool {    }    return 0;}
@autorelease{}分析,可以通过执行clang -rewrite-objc main.m -o main.cpp命令得到main.cpp文件, 代码被转换为C++文件:
intmain(int argc, constchar * argv[]) {    /* @autoreleasepool */ {         __AtAutoreleasePool __autoreleasepool;     }    return 0;}

可以看到@autoreleasepool实际就是由__AtAutoreleasePool类创建管理的,作用域开始时(创建时)会调用这个类的构造函数,作用域结束时,会调用这个类的析构函数

struct __AtAutoreleasePool {   // 构造函数--调用push方法  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}   // 析构函数--调用pop方法  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}  void * atautoreleasepoolobj;};

构造函数会调用objc_autoreleasePoolPush函数,并返回atautoreleasepoolobj对象

析构函数会调用objc_autoreleasePoolPop函数,并传入atautoreleasepoolobj对象

这部分的细节,可以通过查看objc的源码,查看objc_autoreleasePoolPushobjc_autoreleasePoolPop函数:

void * objc_autoreleasePoolPush(void){    return AutoreleasePoolPage::push();}void objc_autoreleasePoolPop(void *ctxt){   AutoreleasePoolPage::pop(ctxt);}

关于AutoreleasePoolPage类,可以通过objc的源码中查看到类的结构,这里总结一下:

  • 自动释放池是由多个AutoreleasePoolPage组成的双向链表结构,AutoreleasePoolPage中有2个指针分配是parent和child指针,对应指向上一页和下一页(Page)。

  • 当一个Page的空间(一页4096字节)占满时, 会创建一个新的AutoreleasePoolPage对象并添加到链表中,

哨兵对象(POOL_BOUNDARY)

在一个AutoreleasePoolPage的结构体中有一个边界对象(哨兵对象):POOL_BOUNDARY这个边界对象实际一个起的一个别名(本质还是nil),它就是起到一个标识的作用。

当调用objc_autoreleasePoolPush方法,会将POOL_BOUNDARY放到当前的page的栈顶,并且返回这个哨兵对象。

当调用objc_autoreleasePoolPop方法,会将此哨兵对象以参数传入,这样会在自动释放池对这个池中的对象发送release消息,直到找到第一个哨兵对象为止。

对于有大量临时内存对象创建时,比如一个超大的for循环中alloc了一堆对象时, 我们就需要主动添加@autoreleasepool{}方法来管理,

这也就是手动干预释放情况,另外@autoreleasepool{}是可以相互嵌套的, 实际对应上边的page链表来说,就是插入了多个的哨兵对象。

手动创建的@autoreleasepool{}会在“}”结束时释放对象。

另外,autorelease对象释放还有系统干预释放的情况,上面介绍的是手动干预释放的情况。

对于系统干预释放是不指定@autoreleasepool,所有的autoreleasepool对象都是由主线程的RunLoop创建的@autoreleasepool来管理。在Runloop事件进入Entry时,创建AutoreleasePoolPage对象,也就是调用

objc_autoreleasePoolPush方法,在即将休息时,也就是RunLoop进入BeforeWaiting阶段,会调用objc_autoreleasePoolPop方法来释放内存。并创建一个新的AutoreleasePoolPage对象, 最后在RunLoop进入到exit时,会调用objc_autoreleasePoolPop方法来释放内存。

六. 系统内存优化的细节---Tagged Pointer
苹果自动iphone5s之后,就开始使用了arm64架构,实际我们的内存地址用不到这么大的数值, 比如以NSNumber为例,
NSNumber *n1 = @1
 按照常规理解,需要在栈上分配一个n1的指针地址,指向一个@1的堆内存,这样就需要2块内存来处理。
苹果为了节省内存和提高执行效率, 就引入了tagged point的机制, tagged point是一种特殊的指针, 它存储的并不是内存地址,而是具体的数据和附加的信息。
当然这个机制还包含有NSString的短字符串,还有NSDate;
这种机制的存储并不存储在堆中, 所以也就不需要malloc和free,它的内存读取效率要快3倍。
这里有一个小细节,例如我们通常使用%p来打印内存地址,
NSString *str = [NSString stringWithFormat:@"Hi"];NSLog(@"%p - %@ - %@ - 0x%lx",str,str,str.class,ssl_objc_decodeTaggedPointer(str));
得到并不是我们看到真正的内存地址,它是由于苹果通过混淆算法后的一个地址, 需要通过ssl_objc_decodeTaggedPointer函数才能获取到解密后的真实内存地址。
如果开发过程中,也可以通过修改Xcode->Run-> Arguments中的Enviroment Variables中,配置环境变量OBJC_DISABLE_TAG_OBFUSCATION为YES,关闭数据混淆。
简述: Tagged Pointer存储的是小内存,系统在通过objc_msgSend函数调用时,可以识别是Tagged Pointer类型, 比如NSNumber的intValue方法,直接从指针中提取数据。
后续:
本小节主要是整体介绍一下iOS内存存储的各个区域,每个区域中的存储的数据类型,另外涉及到堆区的内存管理方式,包括release和autorelease的逻辑等, 后续将会在此基础的知识点,重点讲解一下内存泄漏,大内存检测的一些逻辑。