)
内存管理(一)文章目录内存管理(一)内存五大区栈区函数栈堆区全局区(.bss .data)常量区代码区内存管理内存优化方案Tagged PointerNONPOINTER_ISASideTableMRC和ARCMRCARC内存五大区栈区栈区(stack):编译器自动分配由系统管理在不需要的时候自动清除。局部变量函数的参数储存在这里。栈区的内存地址一般是0x7开头从高地址到低地址分配内存空间在iOS中栈是每个线程独立的如果创建了一个子线程子线程也有它自己的栈。因此线程之间不能简单的通过局部变量来直接共享数据voidA{[selfB];}voidB{[selfC];}voidC{}执行过程是C-回调B-回调C这就是栈后进先出不断递归iOS主线程大小为1MB 其他线程为512KB MAC为8MB通过上面的数据我们发现栈区的空间其实比较小因此递归层级过深或者过多的局部变量因此我们需要防止栈区内存溢出函数栈函数栈在内存中从高地址网低地址分配与堆区相对栈帧是指函数(运行中未完成)占用的一块独立的连续内存区域函数共享线程的这个栈空间每个函数使用的栈空间是一个栈帧所有的栈帧组成这个线程完整的栈函数调用发生在栈上每个函数的相关信息(局部变量函数参数调用记录等)都储存在这个栈帧中每执行一次函数调用就会生成一个与其相关的栈帧然后栈帧压入函数栈当函数执行结束就将函数对应的栈帧出栈释放掉Caller:调用者(图中的main函数)callee被调用者(图中的func1函数)如图中所示在执行func1前目前只有main栈帧,此时可以简单理解为高地址- main栈帧【argc argv i 10 j 5】 - 低地址然后main调用func1此时CPU从main跳到func1但并不能简单的跳转因为func1执行完后还要知道应该回到main的哪里继续执行所以函数的调用需要保存各种上下文PCPC就是一个程序计数器它表示CPU当前正在执行/即将执行哪里的指令还是拿图中举例当执行main函数时我们在哪个指令PC就指向哪个指令当我们执行func1时CPU做了两件事将返回地址也就是fun1();的下一个指令压入栈然后将func1的入口地址写入PC然后就跳到函数中执行当函数执行完后从栈里弹出之前保存的返回地址把这个地址写入PCLRLR就是储存返回地址的指针但是LR只有一个当我们有多重函数调用时一个LR就保存不过来了因此在多重函数调用时我们的逻辑时如果LR中没有返回地址那么就将返回地址存入如果已经有了那么现将这个LR压入栈再将新地址写入CPU中的LRSPSP保存当前栈顶的地址在ARM64栈模型中栈往低地址方向增长所以当需要更多栈空间时SP向低地址移动SP 的变化反映了当前函数需要的栈空间。FPSP可能因为保存寄存器分配局部变量等发生变化此时如果依赖SP定位可能比较麻烦此时可以使用FP当前栈帧的一个固定参考位置去定位栈帧中的相关数据并且FP还会保存上一个FP所在的位置堆区堆区堆是从低地址向高地址扩展的数据结构堆是不连续的一片内存区域类似与链表(便于增删不便于查询)遵循先进先出原则堆的地址空间在iOS是以0x6开头其空间的内存分配是动态的堆区的分配一般在运行时分配堆区是由程序员动态分配和释放的如果程序员不释放可能由操作系统回收OC中用alloc或者使用new开辟空间创建对象C语言中使用malloccallocrealloc分配的空间需要free释放优点灵活方便数据适应面广泛缺点需手动管理速度慢容易产生内存碎片当需要访问堆中内存时一般需要先通过对象读取到栈区的指针地址然后通过指针地址访问堆区全局区(.bss .data)全局区是编译时分配的内存空间在iOS中一般以0x1开头在程序运行过程中此内存中的数据一直存在程序结束后由系统释放主要存放未初始化的全局变量和静态变量即BSS区.bss已初始化的全局变量和静态变量即数据区.data其中全局变量是指变量值可以在运行时被动态修改而静态变量是 static 修饰的变量包含静态局部变量和静态全局变量常量区常量区是 编译时分配的内存空间在程序结束后由系统释放主要存放 已经使用了的且没有指向的字符串常量字符串常量因为可能在程序中被多次使用所以在程序运行之前就会提前分配内存代码区代码区是 编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的内存管理移动端的内存管理技术主要有GC(垃圾回收)的标记清除算法以及苹果公司使用的引用计数方法相比较于 GC 标记清除算法引用计数法可以及时地回收引用计数为 0 的对象减少查找次数。但是引用计数会带来循环引用的问题比如当外部的变量强引用 Block 时Block 也会强引用外部的变量就会出现循环引用。我们需要通过弱引用来解除循环引用的问题。另外在 ARC自动引用计数之前一直都是通过 MRC手动引用计数这种手写大量内存管理代码的方式来管理内存因此苹果公司开发了 ARC 技术由编译器来完成这部分代码管理工作。但是ARC 依然需要注意循环引用的问题。当 ARC 的内存管理代码交由编译器自动添加后有些情况下会比手动管理内存效率低所以对于一些内存要求较高的场景我们还是要通过 MRC 的方式来管理、优化内存的使用内存优化方案Tagged Pointer在使用Tagged Pointer前如果声明一个NSNumber number 10;变量需要8个字节存指针变量number,需要16个字节存NSNumber对象这样需要24个字节的内存空间而使用Tagged Pointer后NSNumber指针里存储的数据变成了:Tag Data, 也就是将数据直接存入了指针直接将数据10保存在指针变量number中这样仅仅占用8个字节但是当指针不够存储数据时就会使用动态分配内存的方法来存储数据dispatch_queue_t queuedispatch_get_global_queue(0,0);for(inti0;i1000;i){dispatch_async(queue,^{self.name[NSString stringWithFormat:asdasdefafdfa];});}NSLog(end);dispatch_queue_t queuedispatch_get_global_queue(0,0);for(inti0;i1000;i){dispatch_async(queue,^{self.name[NSString stringWithFormat:abc];});}NSLog(end);上面两段代码的结果是第一段会崩溃但第二段不会创建多个线程来对name进行操作时name的引用计数会不断的retain和release此时就会出现资源竞争而崩溃但第二段却不会崩溃说明在Tagged Pointer下较小的值不会调用set和get方法可以直接修改,关于字符串什么时候是用Tagged Pointer可以见我之前的文章NSString类簇通过源码我们也可以比较直观的看出Tagged Pointer类型对象是直接返回的。在retain和release的源码中我们都可以发现第一句就是这个它表示Tagged Pointer根本不进引用计数逻辑直接返回自己不操作isaif(slowpath(isTaggedPointer()))return(id)this;Tagged Pointer是专⻔⽤来存储⼩的对象例如NSNumberNSDate等。Tagged Pointer指针的值不再是地址了⽽是真正的值。所以实际上它不再是⼀个对象了它只是⼀个披着对象⽪的普通变量⽽已。所以它的内存并不存储在堆中也不需要malloc和free。当指针不够存储数据时就会使用动态分配内存的方式来存储数据。NONPOINTER_ISANONPOINTER_ISA也是苹果内存优化的一种方案用八字节来存储一个内存地址显然有些浪费于是可以用一部分额外空间存储其他内容isa指针第一位为1即表示使用优化的isa指针unionisa_t{isa_t(){}isa_t(uintptr_t value):bits(value){}Class cls;uintptr_t bits;#ifdefined(ISA_BITFIELD)struct{uintptr_t nonpointer:1;uintptr_t has_assoc:1;uintptr_t has_cxx_dtor:1;uintptr_t shiftcls:44;/*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/uintptr_t magic:6;uintptr_t weakly_referenced:1;uintptr_t deallocating:1;uintptr_t has_sidetable_rc:1;uintptr_t extra_rc:8};#endif};下面解释一下isa结构体中各个变量的含义nonpointer表示是否对isa指针开启指针优化 0:纯isa指针1:不只是类对象地址isa中包含了类信息对象的引用计数等has_assoc关联对象标志位0没有1存在has_cxx_dtor该对象是否有C或者Objc的析构函数有要做析构逻辑没有可以更快的释放对象shiftcls储存类指针的值开启指针优化的情况下在arm架构有33位用来储存magic用于调试器判断当前对象是真的对象还是没有初始化的空间weakly_referenced指对象是否被指向或者曾经指向一个ARC的弱变量没有弱引用的对象可以更快释放deallocating标志对象是否正在释放内存has_sidetable_rc:当对象引用计数大于10时则需要借用该变量存储进位extra_rc当表示该对象的引用计数值实际上是引用计数值减1例如如果对象的引用计数值为10则extra_rc为9.如果引用计数大于10则要用到has_sidetable_rcSideTable在NONPOINTER_ISA有两个成员变量hsa_sidetable_rc和extra_rc当extra_r的八位不够存储引用计数时hsa_sidetable_rc的值就会变成1那么此时引用计数会存储在SideTable中SideTable可以理解为一个全局的hash数组里面存储了SideTable类型的数据长度为64也就是里面有64个SideTablestructSideTable{spinlock_t slock;// 自旋锁保护这张表的读写RefcountMap refcnts;// 引用计数表专门存那些塞不下的计数weak_table_t weak_table;// 弱引用表存所有 __weak 指针的地址};slock因为多线程可能同时操作需要一把锁来保证线程安全。用自旋锁是因为大多数操作都很快忙等一会儿比让出 CPU 更划算。refcnts一个哈希表。Key 是对象的地址Value 是引用计数的具体数值。当isa里的extra_rc溢出时一半的计数会存到这里。weak_table同样是个哈希表。Key 是被弱引用的对象地址Value 是一个数组里面装着所有指向该对象的__weak指针的地址。对象销毁时就靠这张表把这些弱引用指针都置为nilMRC和ARCMRC在MRC时代程序员需要手动管理内存创建一个对象时需要在set和get方法内部添加释放对象的代码并且在对象的dealloc里面添加dealloc里面添加释放的代码property(nonatomic,retain)NSString*name;-(void)setName:(NSString*)name{if(_name!name){[_name release];// 释放旧值_name[name retain];// 持有新值}}-(NSString*)name{return_name;}-(void)dealloc{[_name release];[_age release];[superdealloc];// 必须最后调用}ARC在ARC环境中我们不再像以前一样自己手动管理内存系统帮助我们做了release或者autorelease等事情。 ARC是LLVM编译器和RunTime协作的结果。其中LLVM编译器自动生成release、reatin、autorelease的代码像weak弱引用这些则靠RunTime在运行时释放