iOS ARC 本质:__strong / __weak / __unsafe_unretained / __autoreleasing 深度解析

在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句话,覆盖所有高频考点:

  1. ARC本质:编译器自动插入retain、release、autorelease操作,底层依赖引用计数管理对象生命周期;
  2. __strong:默认修饰符,强持有对象,引用计数+1,是循环引用的核心原因;
  3. __weak:弱引用,不持有对象,对象释放后自动置nil,是解决循环引用的核心方案;
  4. __unsafe_unretained废弃慎用,__autoreleasing隐式使用,日常开发重点关注前两种修饰符。
相关推荐
humors2212 小时前
全平台日常使用的国外应用
android·ios·app·安卓·应用·国外
pop_xiaoli3 小时前
【iOS】锁的原理
ios·objective-c·cocoa
秋雨梧桐叶落莳4 小时前
iOS——MVC架构学习
学习·ui·ios·架构·mvc·objective-c
代码的小搬运工20 小时前
UITableView
开发语言·ui·ios·objective-c
互联网行业信息差21 小时前
iOS开发常见问题与最新工具使用心得
macos·ios·cocoa
MonkeyKing1 天前
iOS Tagged Pointer 原理、判断方式、适用场景与避坑指南
ios
wuxianda10301 天前
Object-C/Swift/UniApp项目苹果商店上架3天极速解决方案汇报总结
ios·uni-app·objective-c·cocoa·苹果上架
鹤卿1231 天前
UI----多界面传值
ui·ios
UnicornDev1 天前
从零开始学iOS开发(第四十七篇):Core Haptics 触感反馈 —— 让应用拥有真实的触觉体验
ios