【iOS】MRC和ARC

📅 2026/7/22 9:10:07
【iOS】MRC和ARC
文章目录MRC多个对象的内存管理自动释放池注意事项循环引用ARCretain函数rootRetain函数release函数rootRelease函数ARC规则下的autoreleaseARC在编译期和运行期都做了什么MRC手动引用计数管理通过调用retain,release和autorelease等的方法来控制对象生命周期对象操作和方法生成并持有对象alloc/new/copy/mutableCopy等方法持有对象retain方法释放对象release废弃对象dealloc对于单个对象的内存管理自己生成的对象自己持有非自己生成的对象自己也可以持有NSMutableArray*ary[NSMutableArray array];[ary retain];不再需要自己持有的对象时释放非自己持有的对象无法释放多个对象的内存管理一个对象不持有另一个对象此时两个实例对象没有关联可以各自释放一个对象持有另一个对象持有的对象要在set方法中引用计数加一释放时候也要把持有的对象销毁-(void)setMyObject:(MyObject*)myObj{// 引用计数器 1[myObj retain];_myObjmyObj;}-(void)dealloc{// 人释放了, 那么房间也需要释放[_myObj release];NSLog(%s,__func__);[superdealloc];}一个对象持有并且释放掉一个对象后持有另一个对象此时要在更换原持有前释放第一个对象-(void)setMyObject:(MyObject*)myObj{[_myObj release];// 引用计数器 1[myObj retain];_myObjmyObj;}一个对象持有一个对象且释放该对象后再次持有该对象将那个对象赋值给该对象后该对象对那个对象的引用计数加一再将那个对象引用计数减一此时两个对象的引用计数相同此时再次进行赋值会导致那个对象的引用计数先减一此时为0变成了野指针此时再对野指针进行retain操作就会报错此时有两种方法一种是先retain再release还有一种实际应用方法就是先在setter方法中判断是否重复赋值如果是同一个实例对象就不需要重复进行release和retain- (void)setObj:(NSObject *)obj { [obj retain]; //先持有新的 [_obj release]; //再释放旧的 _obj obj; //赋值 } - (void)setMyObject:(MyObject*)myObj { if (_myObj ! myObj) {//先判断 [_myObj release]; // 引用计数器 1 [myObj retain]; _myObj myObj; } }property参数在成员变量前加property系统会帮我们自动生成setter/getter方法但是不会生成内存管理相关代码(默认后面什么都不写就是assign)property(nonatomic)intval;如果在property后边加上retain系统就会自动帮我们生成getter/setter方法内存管理的代码但是仍需要我们自己重写dealloc方法property(nonatomic,retain)MyObject*myObj;自动释放池autorelease是一种支持引用计数的内存管理方式主要用于延迟对象的释放只要给对象发送一条autorelease消息会将对象注册到一个自动释放池中当自动释放池被销毁时会对池子里面的所有对象做一次release操作。autorelease方法会返回对象本身且调用完autorelease方法之后对象的计数器不变创建第一种使用NSAutoreleasePool创建NSAutoreleasePoo*pool[[NSAutoreleasePool alloc]init];//创建自动释放池[pool release];[pool drain];//销毁自动释放池第二种使用autoreleasepool创建autoreleasepool{// 开始代表创建自动释放池}// 结束代表销毁自动释放池使用对象调用autorelease方法要在创建autoreleasepool之后否则他无法将其注册到autoreleasepoolNSAutoreleasePool*autoreleasePool[[NSAutoreleasePool alloc]init];Person*p[[[Person alloc]init]autorelease];[autoreleasePool drain];autoreleasepool{// 创建一个自动释放池Person*p[[Person new]autorelease];// 将代码写到这里就放入了自动释放池}// 销毁自动释放池(会给池子中所有对象发送一条 release 消息)//以上两种使用方法相同自动释放池的嵌套自动释放池是以栈的形式存在的所以调用autorelease会把对象放到栈顶的自动释放池autoreleasepool{// 栈底自动释放池autoreleasepool{autoreleasepool{// 栈顶自动释放池Person*p[[[Person alloc]init]autorelease];}Person*p[[[Person alloc]init]autorelease];}}注意事项自动释放池中不易放占用内存比较大的对象因为自动释放池时延迟释放机制只有清空池子时才会把注册到池子的对象销毁此时如果有多个内存较大的对象就会造成短时间内的内存暴涨不把大量循环操作放到同一个autoreleasepool之间这样会造成内存峰值的上升若一定要循环则// 内存暴涨autoreleasepool{for(inti0;i99999;i){Person*p[[[Person alloc]init]autorelease];}}// 内存不会暴涨,在循环内部创建自动释放池for(inti0;i99999;i){autoreleasepool{Person*p[[[Person alloc]init]autorelease];}}不连续调用autorelease也不调用后又调用releaseautoreleassepool{// 会导致过度释放Person*p[[[[Person alloc]init]autorelease]autorelease];}autoreleasepool{Person*p[[[Person alloc]init]autorelease];[p release];//过度释放}循环引用两个对象互相持有对方A对象的销毁依赖于BB对象的销毁依赖于A自循环引用若有一个对象内部强持有他的成员变量obj若此时我们给obj赋值为原对象时就是自循环引用相互循环引用对象A内部强持有obj对象B内部强持有obj若此时对象A的obj指向对象A就是相互引用多循环引用假如类中有对象1…对象N每个对象中都强持有一个obj若每个对象obj都指向下个对象就产生了多循环引用ARCARC的全称Auto Reference Counting. 也就是自动引用计数。使用MRC时开发者不得不花大量的时间在内存管理上并且容易出现内存泄漏或者release一个已被释放的对象导致crash。后来Apple引入了ARC。使用ARC开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码再结合Objective-C的runtime实现自动引用计数ARC管理的三种类型OC对象类型id等Block由 __ attribute __ ((NSObject))标记的类型(把c类型伪装成OC对象)ARC只管理“对象指针”不管理如int,struct等的普通C类型CF对象也默认不归ARC管ARC的工作原理:当我们编译源码的时候编译器会分析源码里每个对象的生命周期然后基于这些对象的生命周期来添加对应的引用计数操作代码所以ARC是工作在编译期的一种技术方案其主要依赖这三个方法retain增加引用计数release降低引用计数引用计数为0的时候释放对象autorelease在当前的autoreleasepool结束之后降低引用计数retain函数inline id objc_object::retain() { assert(!isTaggedPointer()); if (fastpath(!ISA()-hasCustomRR())) { return rootRetain(); } return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain); }首先断言检查是否为标签指针不是的话判断是否具有定制的retain/release行为。如果没有那么调用rootRetain()函数。这是一个更快的内部函数直接更新引用计数避免了消息发送的开销。如果类有定制的retain/release行为那么使用objc_msgSend发送retain消息到对象。rootRetain函数ALWAYS_INLINE id objc_object::rootRetain() { return rootRetain(false, false); //传递false和false参数。 }这里会调用重载的rootRetain函数并传入两个false参数C 中的函数重载Function Overloading机制指的是同名函数通过参数列表的不同参数类型或数量来区分不同的实现。ALWAYS_INLINE bool objc_object::rootTryRetain() { return rootRetain(true, false) ? true : false; // 调用rootRetain参数为true和false。 }这里是尝试性调用重载的rootRetain()并传入ture和false两个参数返回true如果成功增加引用计数否则返回false。ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { // Inline函数用于核心的retain操作接受两个bool参数 // tryRetain决定是否为尝试性retain // handleOverflow决定是否处理引用计数溢出。 if (isTaggedPointer()) return (id)this; // 如果当前对象是标记指针直接返回this因为标记指针有自己的内存管理机制。 bool sideTableLocked false; // 初始化侧边表锁定状态为未锁定。 bool transcribeToSideTable false; // 初始化是否需要将数据转录到侧边表为否。 isa_t oldisa; // 定义oldisa变量用于存储对象的原始ISA信息。 isa_t newisa; // 定义newisa变量用于存储新的ISA信息。 do { transcribeToSideTable false; // 每次循环前重置是否需要转录到侧边表的状态。 oldisa LoadExclusive(isa.bits); // 使用LoadExclusive获取ISA的bits字段保证原子操作。 // LoadExclusive允许我们独占地读取内存这样在我们读取之后和写入之前其他线程不能修改这个内存位置。 newisa oldisa; // 复制旧的ISA信息到新ISA中准备修改。 if (slowpath(!newisa.nonpointer)) { // 检查ISA的nonpointer标志判断是否有侧边表。 // slowpath宏用于指示编译器此条件在正常情况下很少为真用于优化。 ClearExclusive(isa.bits); // 清除独占状态因为接下来要处理侧边表。 if (!tryRetain sideTableLocked) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定解锁侧边表。 if (tryRetain) // 尝试性retain操作调用侧边表的尝试性retain方法。 return sidetable_tryRetain() ? (id)this : nil; else // 非尝试性retain操作调用侧边表的常规retain方法。 return sidetable_retain(); } // 以上处理有侧边表的情况接下来处理没有侧边表的情况。 // 不检查newisa.fast_rr因为我们已经调用了任何RR覆盖。 // fast_rr是用于快速引用计数的字段当有侧边表时fast_rr字段可能无效。 if (slowpath(tryRetain newisa.deallocating)) { // 如果是尝试性retain且对象正在dealloc中。 ClearExclusive(isa.bits); // 清除独占状态因为对象正在dealloc中。 if (!tryRetain sideTableLocked) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定解锁侧边表。 return nil; // 返回nil因为尝试性retain失败。 } // 上面的条件判断确保我们不会在对象dealloc时尝试增加引用计数。 uintptr_t carry; // 定义carry变量用于保存进位信息。 // 使用addc函数原子地增加引用计数同时检查是否溢出。 newisa.bits addc(newisa.bits, RC_ONE, 0, carry); if (slowpath(carry)) { // 如果addc操作产生了进位即发生了溢出。 if (!handleOverflow) { // 如果不处理溢出。 ClearExclusive(isa.bits); // 清除独占状态。 // 下面调用rootRetain_overflow函数处理溢出情况参数为tryRetain。 return rootRetain_overflow(tryRetain); } // 如果需要处理溢出准备将一半的引用计数转移到侧边表。 if (!tryRetain !sideTableLocked) sidetable_lock(); // 如果不是尝试性retain且侧边表未锁定锁定侧边表。 sideTableLocked true; // 设置侧边表锁定状态为已锁定。 transcribeToSideTable true; // 设置需要转录到侧边表的状态为是。 newisa.extra_rc RC_HALF; // 设置extra_rc字段为RC_HALF表示一半的引用计数。 newisa.has_sidetable_rc true; // 设置has_sidetable_rc标志表示侧边表中有引用计数。 } } while (slowpath(!StoreExclusive(isa.bits, oldisa.bits, newisa.bits))); // StoreExclusive尝试原子地更新ISA的bits字段如果更新失败其他线程修改了bits则继续循环。 if (slowpath(transcribeToSideTable)) { // 如果需要将数据转录到侧边表。 // 转录额外的一半引用计数到侧边表无需锁定。 sidetable_addExtraRC_nolock(RC_HALF); } if (slowpath(!tryRetain sideTableLocked)) sidetable_unlock(); // 如果不是尝试性retain且侧边表已锁定解锁侧边表。 // 此处的sidetable_unlock调用确保我们释放了侧边表的锁除非正在进行尝试性retain。 return (id)this; // 成功增加引用计数后返回this指针。 }这段代码主要为四个部分对象是否是标记指针、对象是否有侧边表、引用计数是否会溢出、是否是尝试性retain。当引用计数降到0时对象会被标记为正在析构中并最终调用dealloc方法进行析构。如果对象有侧边表它会尝试从侧边表借用引用计数以避免下溢。如果侧边表为空或对象已经在析构中它会处理overrelease错误或执行析构操作。独占访问某一时刻只有一个线程能访问特定资源或内存位置这是为了避免并发访问导致的数据竞争和不一致的状态。并发编程中独占访问主要通过锁或者原子操作实现LoadExclusive函数开始独占访问和ClearExclusive函数就是通过原子操作来进行与独占访问有关的操作。当对象的引用计数超过了一定的阈值通常是因为被多个引用持有runtime会将一部分引用计数信息移动到侧边表中以避免在对象的isa字段中存储过大的数值从而节省空间和提高效率。typedef objc::DenseMapDisguisedPtrobjc_object,size_t,true RefcountMap; struct SideTable { spinlock_t slock; RefcountMap refcnts; weak_table_t weak_table; //省略其他实现... };这个数据结构就是存储了一个自旋锁一个引用计数map。在实现上这个引用计数的map以对象的地址作为key引用计数作为value。这意味着每个对象在侧边表中最多只有一个条目对象的地址提供了唯一性便于查找和更新。release函数inline void objc_object::release() { assert(!isTaggedPointer()); if (fastpath(!ISA()-hasCustomRR())) { rootRelease(); return; } ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release); }与retain函数的逻辑几乎一致是否实现定制的retain/releaserootRelease函数ALWAYS_INLINE bool objc_object::rootRelease() { return rootRelease(true, false); // 调用rootRelease的重载版本参数为true和false。 }ALWAYS_INLINE bool objc_object::rootReleaseShouldDealloc() { return rootRelease(false, false); // 调用rootRelease的重载版本参数为false和false。 }ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { if (isTaggedPointer()) return false; // 如果是标记指针直接返回false因为标记指针有自己的内存管理。 bool sideTableLocked false; // 初始化侧边表锁定状态为未锁定。 isa_t oldisa; // 存储对象的原始ISA信息。 isa_t newisa; // 存储新的ISA信息用于修改。 retry: do { oldisa LoadExclusive(isa.bits); // 使用LoadExclusive获取ISA的bits字段开始独占访问。 newisa oldisa; // 复制旧的ISA信息到新ISA中准备修改。 if (slowpath(!newisa.nonpointer)) { // 如果有侧边表。 ClearExclusive(isa.bits); // 清除独占状态。 if (sideTableLocked) sidetable_unlock(); // 如果侧边表已锁定解锁。 return sidetable_release(performDealloc); // 调用侧边表的release方法。 } // 以下处理没有侧边表的情况。 uintptr_t carry; newisa.bits subc(newisa.bits, RC_ONE, 0, carry); // 原子递减引用计数并检查是否产生借位。 if (slowpath(carry)) { // 如果产生了借位即发生了下溢。 // 不ClearExclusive保留独占访问状态。 goto underflow; // 跳转到underflow标签处理下溢情况。 } } while (slowpath(!StoreReleaseExclusive(isa.bits, oldisa.bits, newisa.bits))); // 使用StoreReleaseExclusive尝试更新ISA的bits字段如果更新失败继续循环。 if (slowpath(sideTableLocked)) sidetable_unlock(); // 如果侧边表已锁定解锁。 return false; // 如果没有下溢直接返回false。 underflow: // 发生了下溢从侧边表借用引用计数或析构对象。 // abandon newisa以撤销递减操作。 newisa oldisa; if (slowpath(newisa.has_sidetable_rc)) { // 如果有侧边表引用计数。 if (!handleUnderflow) { // 如果不处理下溢。 ClearExclusive(isa.bits); // 清除独占状态。 return rootRelease_underflow(performDealloc); // 调用处理下溢的函数。 } // 从侧边表转移引用计数到内联存储。 if (!sideTableLocked) { // 如果侧边表未锁定。 ClearExclusive(isa.bits); // 清除独占状态。 sidetable_lock(); // 锁定侧边表。 sideTableLocked true; // 设置侧边表锁定状态为已锁定。 goto retry; // 重新开始循环防止竞态条件。 } // 尝试从侧边表移除一些引用计数。 size_t borrowed sidetable_subExtraRC_nolock(RC_HALF); // 从侧边表借入一半的引用计数。 if (borrowed 0) { // 如果成功借入。 // 侧边表引用计数减少。 // 尝试将它们添加到内联计数。 newisa.extra_rc borrowed - 1; // 重新递减引用计数。 bool stored StoreReleaseExclusive(isa.bits, oldisa.bits, newisa.bits); // 尝试更新ISA的bits字段。 if (!stored) { // 如果更新失败。 // 内联更新失败。 // 将借来的引用计数放回侧边表。 sidetable_addExtraRC_nolock(borrowed); // 将引用计数放回侧边表。 goto retry; // 重新开始循环。 } // 递减成功后从侧边表借用。 // 这个递减不可能是析构递减 - 侧边表锁和has_sidetable_rc标志确保如果所有其他线程都试图在我们工作时-release最后一个会阻塞。 sidetable_unlock(); // 解锁侧边表。 return false; // 返回false。 } else { // 侧边表为空。 } } // 真正析构对象。 if (slowpath(newisa.deallocating)) { // 如果对象正在析构中。 ClearExclusive(isa.bits); // 清除独占状态。 if (sideTableLocked) sidetable_unlock(); // 如果侧边表已锁定解锁。 return overrelease_error(); // 返回overrelease错误。 } newisa.deallocating true; // 设置对象为正在析构中。 if (!StoreExclusive(isa.bits, oldisa.bits, newisa.bits)) goto retry; // 尝试更新ISA的bits字段如果失败重新开始循环。 if (slowpath(sideTableLocked)) sidetable_unlock(); // 如果侧边表已锁定解锁。 __sync_synchronize(); // 确保所有线程可见状态的更新。 if (performDealloc) { // 如果需要执行析构。 ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); // 发送-dealloc消息。 } return true; // 返回true表示对象应该被析构。 }这段代码也可以分为四部分对象是否是标记指针对象是否有侧边表引用计数是否会下溢通过侧边表或析构处理下溢。当引用计数降到0时对象会被标记为正在析构中并最终调用dealloc方法进行析构。如果对象有侧边表它会尝试从侧边表借用引用计数以避免下溢。如果侧边表为空或对象已经在析构中它会处理overrelease错误或执行析构操作。内联计数不足时从侧边表借用或触发析构。ARC规则下的autorelease在ARC规则下alloc/init/new/copy/mutableCopy开头的方法返回的对象不是autorelease对象。我们可以改写delloc方法判断作用域结束后对象会不会被立刻释放来判断是否为autorelease对象。#importFoundation/Foundation.h#importCustomObject.hintmain(intargc,constchar*argv[]){__weak CustomObject*weakRef;{CustomObject*temp[CustomObject object];//此处不是autorelease对象改为alloc init则是。主要判断在于weakRef是否立刻销毁。weakReftemp;}NSLog(%,weakRef);}对于Cocoa框架来说提供了两种方式来把对象显式的放入AutoReleasePool.NSAutoreleasePool(只能在MRC下使用)autoreleasepool {}代码块(ARC和MRC下均可以使用)autorelease函数inline id objc_object::autorelease() { if (isTaggedPointer()) return (id)this; if (fastpath(!ISA()-hasCustomRR())) return rootAutorelease(); return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease); }先断言再判断是否有定制的retain/release行为inline id objc_object::rootAutorelease() { if (isTaggedPointer()) return (id)this; if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this; return rootAutorelease2(); }先判断是否为标签指针是的话返回this指针即可。标签指针内置生命管理周期。函数调用 prepareOptimizedReturn(ReturnAtPlus1) 函数。这个函数检查是否可以在当前调用栈深度 1 的地方直接返回从而跳过自动释放池的操作。这通常在某些优化场景下发生比如当对象在当前作用域结束时就会被销毁那么就没有必要将其放入自动释放池中等待稍后的释放。如果可以优化函数再次直接返回 this 指针。如果上述两种情况都无法应用那么函数会调用 rootAutorelease2() 函数。rootAutorelease2() 是 autorelease 操作的核心实现负责将对象放入当前线程的自动释放池中以便在适当的时候通常是作用域结束时释放对象。rootAutorelease2__attribute__((noinline,used)) id objc_object::rootAutorelease2() { assert(!isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this); }首先判断对象是否为taggedPointer如果不是就调用AutoreleasePoolPage::autorelease这个方法把该对象作为参数。AutoreleasePoolPage::autorelease函数public: static inline id autorelease(id obj) { // 断言确保传入的对象非空。 assert(obj); // 确保对象不是标记指针标记指针不需要自动释放。 assert(!obj-isTaggedPointer()); // 调用快速自动释放函数尝试将对象加入当前热自动释放池页面。 id *dest __unused autoreleaseFast(obj); // 断言检查确保dest要么是空要么是空池占位符要么指向obj。 // 这里用于验证autoreleaseFast是否正确执行。 assert(!dest || dest EMPTY_POOL_PLACEHOLDER || *dest obj); // 返回原始对象autorelease操作完成后对象仍然可用。 return obj; } // 快速自动释放实现尝试将对象加入到当前线程的热自动释放池页面。 static inline id *autoreleaseFast(id obj) { // 获取当前线程的热自动释放池页面。 AutoreleasePoolPage *page hotPage(); // 如果页面存在并且没有满则尝试将对象加入到页面中。 if (page !page-full()) { return page-add(obj); } // 如果页面存在但已满调用特殊处理函数处理满页情况。 else if (page) { return autoreleaseFullPage(obj, page); } // 如果页面不存在调用无页面自动释放处理函数。 else { return autoreleaseNoPage(obj); } } // 自动释放池页面的add方法用于向页面中添加一个对象。 id *AutoreleasePoolPage::add(id obj) { // 确保页面没有满。 assert(!full()); // 临时解除保护允许修改页面。 unprotect(); // 记录下一个要写入的位置这里使用next指针。 // 注意返回的是next-1的地址但通过直接返回next避免了指针偏移。 id *ret next; // 将对象写入到next指向的位置然后递增next。 *next obj; // 重新保护页面防止其他线程修改。 protect(); // 返回对象在页面中的地址用于验证。 return ret; }autoreleaseFast函数尝试将对象加入到当前线程的“热”自动释放池页面。如果页面存在并且没有满对象将被直接加入到页面中。如果页面已满或者页面不存在将分别调用autoreleaseFullPage和autoreleaseNoPage函数分别进行满页和无页的处理。AutoreleasePoolPage::add 方法负责将对象实际添加到页面中。它先解除页面保护将对象写入到指定位置然后递增页面的写入指针最后重新保护页面。这里的保护和解除保护操作是为了确保在多线程环境下页面数据的一致性和安全性。autorelease方法会把对象存储到AutoreleasePoolPage的双向链表里。等到autorelease pool被drain的时候把链表内存储的对象删除。所以AutoreleasePoolPage就是自动释放池的内部实现。对于自动释放池的总结1、自动释放池一个关于指针的栈2、其中的指针是指要释放的对象或者 pool_boundary 哨兵现在经常被称为 边界3、自动释放池是一个页的结构虚拟内存中提及过 而且这个页是一个双向链表表示有父节点 和 子节点在类中提及过即类的继承链4、自动释放池和线程有关系autoreleasepool { // 外层池 id a ...; // A id b ...; // B autoreleasepool { // 内层池 id c ...; // C id d ...; // D } // 内层池销毁 id e ...; // E id f ...; // F } // 外层池销毁ARC在编译期和运行期都做了什么ARC本质上是一套由编译期和Runtime协同实现的自动内存管理机制在编译期Clang 根据所有权规则自动插入 retain、release、autorelease 代码同时把能互相抵消的操作优化掉开发者不用手动管理。在运行期Runtime 主要负责三件事第一真正执行引用计数操作第二维护 weak 弱引用表对象释放时自动置 nil第三管理 AutoreleasePool 的 push/pop并通过返回值优化减少多余的 autorelease 和 retain返回值优化具体怎么做的objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue配合通过全局标志位跳过不必要的autoreleaseweak怎么做到自动置nildealloc时走weak_clear_no_lock遍历weak表把指向该对象的所有weak指针置nil编译期优化能举个例子吗连续retain两次中间无其他操作编译器会合并成一次