在iOS开发中,ARC(Automatic Reference Counting,自动引用计数)是Objective-C(OC)和Swift的核心内存管理机制,它替代了手动管理引用计数(MRC)的繁琐操作,极大降低了内存泄漏、野指针崩溃的风险。但很多开发者对ARC的认知仅停留在"不用手动写retain/release"的表层,不清楚其底层本质,更无法灵活区分__strong、__weak、__unsafe_unretained、__autoreleasing四种修饰符的用法与差异,导致开发中仍会出现内存问题。
本文将从ARC的底层本质出发,结合objc4-818.2源码(适配iOS 13+),逐一对四种修饰符的底层逻辑 、使用场景 、核心差异进行拆解,每个知识点都搭配可直接在Xcode中运行的实战示例,全程无冗余、重点突出,既适合新手入门ARC内存管理,也适合开发者查漏补缺、深化理解,轻松应对面试中的高频考点。
前置说明:本文聚焦OC中的ARC机制及四种修饰符,Swift中虽有类似概念(如weak、unowned),但底层实现略有差异,暂不展开;所有示例均基于64位架构(32位已淘汰),涉及的源码均做简化处理,保留核心逻辑,便于理解;文中涉及的内存地址、运行结果,可直接复制代码到Xcode中验证。
一、ARC 本质:不是"自动管理内存",而是"自动插入引用计数操作"
很多人误以为ARC是"自动管理内存",无需关注引用计数,但实际上,ARC的本质是编译器自动在合适的位置插入retain、release、autorelease等引用计数操作 ,底层依然依赖"引用计数"机制来管理对象的生命周期------当对象的引用计数为0时,系统会自动调用dealloc方法释放对象。
核心原则:ARC会跟踪每个OC对象的引用次数,当有新的强引用指向对象时,引用计数+1;当强引用消失时,引用计数-1;引用计数为0时,对象立即被释放(特殊情况除外,如 autorelease 池管理的对象)。
关键补充:ARC仅管理OC对象(继承自NSObject的对象),基本数据类型(int、float、bool等)和结构体(struct)不受ARC管理,无需担心其内存问题。
二、四大修饰符深度解析(附实战示例)
ARC环境下,OC对象的引用修饰符分为四种,核心差异在于"是否持有对象(增加引用计数)"、"对象释放后是否自动置nil",下面逐一拆解,结合示例说明用法与避坑点。
1. __strong:默认修饰符,强引用(核心)
核心逻辑
__strong是ARC环境下的默认修饰符 (不写任何修饰符时,默认就是__strong),它会强持有对象,即给对象的引用计数+1;当强引用变量生命周期结束(如出作用域、被置nil)时,会自动给对象的引用计数-1;只有当所有强引用都消失,对象的引用计数才会降为0,进而被释放。
底层源码简化(objc4):当用__strong修饰变量时,编译器会自动插入retain操作;变量销毁时,自动插入release操作。
实战示例1:__strong的默认行为
objectivec
#import <UIKit/UIKit.h>
@interface Person : NSObject
@end
@implementation Person
// 析构函数,验证对象是否被释放
- (void)dealloc {
NSLog(@"Person dealloc");
}
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// 不写修饰符,默认__strong
Person *p1 = [[Person alloc] init];
// p1是强引用,p1指向的Person对象引用计数=1
__strong Person *p2 = p1;
// p2也是强引用,引用计数+1,此时引用计数=2
p1 = nil; // 释放p1的强引用,引用计数-1,此时引用计数=1
NSLog(@"p1置nil后,p2依然有效");
// 当p2出@autoreleasepool作用域时,自动释放强引用,引用计数-1=0,对象被释放
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行结果:
go
p1置nil后,p2依然有效
Person dealloc
结论:__strong修饰的变量会强持有对象,只要有一个强引用存在,对象就不会被释放;变量出作用域或被置nil时,自动释放强引用。
避坑点
__strong是循环引用的"罪魁祸首"------当两个对象相互用__strong修饰引用对方时,会形成"对象A→对象B,对象B→对象A"的强引用闭环,导致两者引用计数都无法降为0,进而造成内存泄漏(后续结合__weak讲解解决方案)。
2. __weak:弱引用,避免循环引用(高频)
核心逻辑
__weak是弱引用 ,它不会持有对象,即不会给对象的引用计数+1;当对象被所有强引用释放(引用计数为0)时,对象会被立即释放,同时__weak修饰的变量会自动置为nil,避免野指针崩溃。
核心作用:解决循环引用问题,适用于"不需要强持有对象,但需要访问对象"的场景(如Block内部访问self、代理模式等)。
实战示例2:__weak避免野指针
objectivec
#import <UIKit/UIKit.h>
@interface Person : NSObject
@end
@implementation Person
- (void)dealloc {
NSLog(@"Person dealloc");
}
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
__strong Person *p1 = [[Person alloc] init]; // 强引用,引用计数=1
__weak Person *p2 = p1; // 弱引用,引用计数不变,仍为1
NSLog(@"p2未释放前:%@", p2); // 能正常访问,打印Person对象地址
p1 = nil; // 释放强引用,引用计数=0,Person对象被释放
NSLog(@"p1置nil后,p2:%@", p2); // p2自动置为nil,打印(null)
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行结果:
csharp
p2未释放前:<Person: 0x6000000100008000>
Person dealloc
p1置nil后,p2:(null)
实战示例3:__weak解决Block循环引用
结合前面Block的知识,当ViewController的strong属性持有Block,Block内部强引用self时,会形成循环引用,用__weak修饰self即可打破闭环:
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (strong, nonatomic) void (^myBlock)(void); // strong修饰Block,self强引用Block
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// __weak修饰self,Block弱引用self,打破循环引用
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
// Block内部访问weakSelf(弱引用,不增加self的引用计数)
NSLog(@"Block内部访问self:%@", weakSelf);
};
}
- (void)dealloc {
NSLog(@"ViewController dealloc"); // 能打印,说明无循环引用,对象正常释放
}
@end
运行结果:当ViewController被pop/dismiss后,打印"ViewController dealloc",说明循环引用已解决。
避坑点
__weak修饰的变量不能直接用于异步操作(如GCD延迟执行)------因为异步操作执行前,对象可能已被释放,weakSelf会置为nil,导致异步操作中无法访问对象。解决方案:在Block内部用__strong修饰weakSelf(即"weak-strong dance"),临时强持有对象,确保异步操作执行完成。
scss
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.myBlock = ^{
// 临时强持有weakSelf,避免异步操作中self被释放
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // 防止self已释放
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"异步操作执行:%@", strongSelf);
});
};
self.myBlock();
}
3. __unsafe_unretained:不安全弱引用(废弃慎用)
核心逻辑
__unsafe_unretained也是弱引用,不持有对象 ,不增加引用计数;但与__weak最大的区别是:当对象被释放后,__unsafe_unretained修饰的变量不会自动置为nil,会变成野指针------此时访问该变量,会导致程序崩溃(EXC_BAD_ACCESS)。
历史背景:__unsafe_unretained是iOS 4之前的弱引用方式,iOS 5引入__weak后,__unsafe_unretained已基本废弃,仅在兼容低版本系统时可能用到,日常开发不推荐使用。
实战示例4:__unsafe_unretained的野指针问题
objectivec
#import <UIKit/UIKit.h>
@interface Person : NSObject
@end
@implementation Person
- (void)dealloc {
NSLog(@"Person dealloc");
}
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
__strong Person *p1 = [[Person alloc] init]; // 强引用,引用计数=1
__unsafe_unretained Person *p2 = p1; // 不安全弱引用,引用计数不变
NSLog(@"p2未释放前:%@", p2); // 能正常访问
p1 = nil; // 释放强引用,Person对象被释放,p2变成野指针
NSLog(@"p1置nil后,p2:%@", p2); // 访问野指针,可能崩溃(行为不确定)
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行结果(可能):
go
p2未释放前:<Person: 0x6000000100008000>
Person dealloc
p1置nil后,p2:<Person: 0x6000000100008000> // 野指针,地址未变,但对象已释放,后续访问崩溃
结论:__unsafe_unretained存在野指针风险,日常开发优先使用__weak,避免使用该修饰符。
4. __autoreleasing:自动释放引用(隐式使用,极少手动写)
核心逻辑
__autoreleasing用于修饰"自动释放的对象",它会将对象加入到最近的 autorelease 池中,当 autorelease 池被销毁(如出@autoreleasepool作用域)时,自动给对象的引用计数-1;若此时对象的引用计数为0,对象会被释放。
核心特点:日常开发中极少手动写__autoreleasing,编译器会自动对某些场景(如方法返回值、out参数)插入__autoreleasing修饰,我们只需了解其底层逻辑即可。
实战示例5:__autoreleasing的隐式使用(方法out参数)
OC中,方法的out参数(如NSError **),编译器会自动给参数添加__autoreleasing修饰,将错误对象加入autorelease池:
objectivec
#import <UIKit/UIKit.h>
// 模拟一个带out参数的方法
- (void)doSomethingWithError:(NSError **)error {
// 编译器会自动将error修饰为__autoreleasing,即NSError * __autoreleasing *error
if (error) {
*error = [NSError errorWithDomain:@"com.test.error" code:100 userInfo:@{NSLocalizedDescriptionKey:@"操作失败"}];
}
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
NSError *error = nil;
[self doSomethingWithError:&error]; // 传入error的地址,编译器自动处理为__autoreleasing
NSLog(@"Error:%@", error); // 能正常访问,error在autorelease池中
}
// 出@autoreleasepool作用域,autorelease池销毁,error对象被释放
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
补充说明:手动写__autoreleasing的场景极少,仅在需要手动将对象加入autorelease池时使用(如MRC迁移到ARC的兼容代码),日常开发无需关注。
三、四大修饰符核心对比(面试必记)
为了方便记忆和区分,整理四大修饰符的核心差异,结合表格直观呈现,覆盖面试高频考点:
| 修饰符 | 是否持有对象(引用计数+1) | 对象释放后,变量是否自动置nil | 核心作用/场景 | 是否安全 |
|---|---|---|---|---|
| __strong | 是 | 否(变量本身不置nil,对象已释放) | 默认修饰,强持有对象,日常开发主流 | 安全(无野指针,可能有循环引用) |
| __weak | 否 | 是(自动置nil) | 解决循环引用,弱引用对象 | 安全(无野指针) |
| __unsafe_unretained | 否 | 否(变成野指针) | 兼容低版本系统,已废弃 | 不安全(有野指针风险) |
| __autoreleasing | 否(加入autorelease池) | 否(autorelease池销毁后释放) | 隐式用于out参数、方法返回值 | 安全(由autorelease池管理) |
四、实战避坑:ARC开发中常见内存问题及解决方案
1. 循环引用(最常见)
场景:两个对象相互用__strong修饰引用对方(如ViewController持有Block,Block持有self;代理模式中,代理双方相互强引用)。
解决方案:用__weak修饰其中一方的引用(如Block内部用__weak修饰self;代理属性用__weak修饰)。
2. 野指针崩溃
场景:使用__unsafe_unretained修饰变量,对象释放后仍访问该变量;或__weak变量未做判空,直接用于异步操作。
解决方案:优先使用__weak修饰弱引用变量;异步操作中用"weak-strong dance"临时强持有对象,并做判空处理。
3. 不必要的强引用
场景:对无需长期持有的对象使用__strong修饰,导致对象无法及时释放(如缓存对象、临时对象)。
解决方案:根据需求选择合适的修饰符,临时对象可使用__weak或让其自动出作用域释放;缓存对象可结合__strong和__weak实现内存优化。
五、总结:ARC核心要点(面试必记)
结合前面的底层解析和实战示例,ARC及四大修饰符的核心要点可总结为4句话,覆盖所有高频考点:
- ARC本质:编译器自动插入retain、release、autorelease操作,底层依赖引用计数管理对象生命周期;
- __strong:默认修饰符,强持有对象,引用计数+1,是循环引用的核心原因;
- __weak:弱引用,不持有对象,对象释放后自动置nil,是解决循环引用的核心方案;
- __unsafe_unretained废弃慎用,__autoreleasing隐式使用,日常开发重点关注前两种修饰符。