iOS八股文之 内存管理

一、iOS的内存管理是啥

一说起内存管理,其实是一个很大的概念,但在任何系统中都有着重要的意义;这里只是基于参考资料和个人的理解大体框定一个概念

  • 对于iOS来说,内存管理的核心是 "管控对象的生命周期",即确保 "有用的对象被持有、无用的对象及时释放";避免内存泄漏(无用对象未释放)和野指针(对象被提前释放后仍被访问)。其底层依赖 "引用计数" 机制,结合编译器(ARC)、运行时(runtime)、RunLoop 等协同工作,形成一套完整的内存管控体系。
  • 谁持有,谁释放 ;无论 MRC(手动引用计数)还是 ARC(自动引用计数),内存管理的核心原则不变:
    持有(Retain):当需要使用一个对象时,通过 "持有" 操作增加其引用计数,确保对象不被释放。
    释放(Release):当不再使用对象时,通过 "释放" 操作减少其引用计数,当计数归 0 时,对象被销毁(调用 dealloc)。
    禁止野指针:对象销毁后,需确保不再访问其指针(否则触发 EXC_BAD_ACCESS 崩溃)。

二、内存管理有啥东西

1. 引用计数(Reference Counting, RC)

引用计数是 iOS 内存管理的底层核心,本质是给每个对象分配一个 "计数器",记录当前持有该对象的 "引用者数量"。

a. 原理概述

  • 计数增加(+1):表示有新的引用者持有对象,常见场景:
    MRC 下调用 [obj retain]、[obj alloc](alloc 会默认将计数设为 1)、[obj copy](拷贝新对象,计数为 1)。
    ARC 下用 __strong 指针赋值(如 self.obj = [NSObject new],编译器自动插入 retain)。
  • 计数减少(-1):表示有引用者放弃持有对象,常见场景:
    MRC 下调用 [obj release](计数减 1,若归 0 则销毁)、[obj autorelease](延迟减 1,由自动释放池处理)。
    ARC 下 __strong 指针被置为 nil、超出作用域(如局部变量销毁),编译器自动插入 release。
  • 对象销毁:当引用计数从 1 减至 0 时,系统会调用对象的 dealloc 方法(开发者可重写以释放资源,如移除监听、关闭文件);回收对象占用的内存(归还给系统,指针变为野指针)。

b. 底层实现

isa 与 SideTable 的协同

引用计数的存储分两种情况,取决于对象是否为 "非 tagged pointer"(如小整数、短字符串等特殊对象,直接存储在 isa 中,无引用计数):

  • 64 位系统下的 isa 优化:isa 指针共 64 位,其中 extra_rc 字段(占 19 位)可直接存储引用计数(最多存储 2^19 - 1 = 524,287)。若计数未超过上限,直接存在 extra_rc 中,无需额外内存。
  • SideTable 兜底:当引用计数超过 extra_rc 上限,或对象需支持弱引用(__weak)时,引用计数会存储在 SideTable 中。
    SideTable 是一个全局哈希表,结构包含:
    spinlock_t:自旋锁,保证线程安全(多线程操作引用计数时避免竞争);
    RefcountMap:存储对象的引用计数(key 为对象指针,value 为计数);
    weak_table_t:存储对象的弱引用列表(当对象销毁时,自动将所有弱引用置为 nil,避免野指针)。

c. 强引用 and 弱引用

引用计数的核心是 "强引用" 和 "弱引用" 的区分,这是避免循环引用的关键;

类型 关键字 对引用计数的影响 对象销毁后的行为 适用场景
强引用 __strong 增加计数(持有) 指针仍指向原地址(野指针) 需长期持有对象(如属性、变量)
弱引用 __weak 不影响计数(不持有) 指针自动置为 nil 避免循环引用(如代理、block)
不安全弱引用 __unsafe_unretained 不影响计数(不持有) 指针仍为野指针(危险) 兼容旧版本,现已极少使用

默认规则:ARC 下,所有局部变量、属性(未指定关键字)默认是 __strong(强引用)。

2. ARC(Automatic Reference Counting)

ARC 是 iOS 5 引入的核心特性,本质是 "编译器自动插入引用计数操作(retain/release/autorelease)",开发者无需手动管理计数,大幅降低内存问题概率。

a. 原理概述(编译器和运行时协同)

  • 编译期:编译器分析代码中对象的 "持有范围",在合适的位置自动插入 retain(如赋值给强引用时)、release(如强引用超出作用域时)、autorelease(如方法返回对象时)。

    // 以一个简单的str举例:
    NSString *str = [NSString stringWithFormat:@"test"];
    // 编译后自动插入的代码(MRC 等价逻辑)
    NSString *str = [[NSString stringWithFormat:@"test"] retain]; // 强引用持有,retain+1
    // ... 使用 str
    [str release]; // str 超出作用域,release-1

  • 运行时 :runtime 辅助处理特殊场景,如:

    • __weak 引用的自动置 nil(对象销毁时,runtime 遍历 SideTable 中的弱引用列表,将所有弱指针置空);
    • autorelease 对象的延迟释放;

b. ARC下开发中需注意的点

(1)无需调用 retain/release/autorelease/dealloc 这些方法,否则编译会报错;

(2)使用@autoreleasepool 语法,而非 NSAutoreleasePool;

(3)不建议使用retainCount,因为ARC 会优化计数存储,retainCount获取的计数可能不准确;

(4)block 会自动捕获外部变量,若捕获 __strong 的 self,可能导致循环引用(需用 __weak 打破)。

3. 自动释放池(AutoreleasePool)

自动释放池是引用计数的 "补充机制",核心作用是 "延迟对象的释放时机",避免短时间内频繁创建 / 释放对象导致的性能开销;

  • 批量释放:将多个 autorelease 对象集中在池销毁时统一释放,减少 release 调用次数;
  • 主线程自动管理:由 RunLoop 自动创建 / 销毁池(进入时 push,休眠前 pop),开发者无需手动处理;
  • 子线程手动管理:子线程无默认池,需手动用 @autoreleasepool 包裹任务,避免 autorelease 对象泄漏。

ps:整理了下有点多,这玩意以后单开一篇( ̄▽ ̄)~*;

4. RunLoop

RunLoop 虽不直接管理引用计数,但通过 "管控自动释放池的生命周期""调度任务执行时机" ,间接影响内存释放。有关RunLoop的详述见这里:iOS八股文之 RunLoop

这里简述其对内存管理的影响:

  • 主线程自动释放池的 "管家":RunLoop 每次循环(处理 UI 事件、定时器等)都会创建新池,休眠前释放旧池,确保临时对象(如 UI 绘制产生的临时数据)及时释放;
  • 避免主线程内存峰值:通过 "分批次释放",防止大量临时对象堆积在单次 RunLoop 循环中,导致内存骤升。

三、内存管理实践踩坑集

1. 循环引用:内存泄漏十个里面八个是它

循环引用是指 "两个或多个对象互相强引用",导致它们的引用计数永远无法归 0,最终内存泄漏。

举几个小🌰:

  • Block 捕获 self;Block 会强引用捕获的 self,若 self 又强引用 Block(如属性持有 Block),形成循环引用:

    // 错误示例:self 强引用 block,block 强引用 self
    self.myBlock = ^{
    [self doSomething]; // block 捕获 self(强引用)
    };
    // 解决方案:用 __weak 修饰 self,让 Block 弱引用 self;
    // 当然一些三方库的弱引用宏写起来更简洁,但注意多个库都有时的冲突;
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf; // 避免 block 执行中 self 被销毁
    if (strongSelf) {
    [strongSelf doSomething];
    }
    };

  • 代理(Delegate)用强引用;若代理属性用 strong 修饰,代理对象(如 VC)会强引用被代理对象(如 View),被代理对象又强引用代理,形成循环;

    // 错误示例:代理用 strong
    @interface CustomView : UIView
    @property (nonatomic, strong) id<CustomViewDelegate> delegate; // 强引用代理
    @end
    // VC 中:self(VC)强引用 view,view 强引用 self(delegate)
    self.customView.delegate = self;
    // 解决方案:代理属性必须用 weak 修饰:
    @property (nonatomic, weak) id<CustomViewDelegate> delegate; // 弱引用代理,打破循环

  • NSTimer 未及时释放;NSTimer 会强引用其 target(如 VC),若 VC 又强引用 NSTimer,且未在 VC 销毁前停止 Timer,会形成循环:

    // 错误示例:VC 强引用 timer,timer 强引用 VC
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    解决方案:VC 销毁前停止并置空 Timer:

    • (void)dealloc {
      [self.timer invalidate]; // 停止 Timer,解除对 self 的强引用
      self.timer = nil; // 解除 VC 对 Timer 的强引用
      }

2. 关于内存泄漏的多说几句:

  • 常见的内存泄漏还有一些点如:
    • 单例持有 VC / 大对象 单例生命周期与 App 一致,持有对象后永不释放;单例中避免持有 VC / 大对象,改用弱引用或临时持有
    • 未移除通知监听 通知中心强引用监听者,未移除则监听者无法释放;在 dealloc 中调用removeObserver:self
    • 大对象未及时释放 如 UIImage/NSData 占用大量内存,未置空;用完后立即将强引用置为 nil(ARC 下立即释放)
    • WebView 未正确销毁 UIWebView/WKWebView 持有周期长,未释放;销毁前停止加载(stopLoading),置空代理;
  • 排查工具:Instruments(核心工具)
    • Leaks 模板:实时检测内存泄漏,标记泄漏对象的类型、创建栈,帮助定位泄漏代码;
    • Allocations 模板:记录所有对象的内存分配情况,查看对象的 "存活数量""内存占用",分析是否有异常堆积;
    • Memory Graph(内存快照):Xcode 内置功能(Debug → Memory Graph),可视化展示对象的引用关系,快速定位循环引用(如红色标记的循环引用链)。

3. 内存警告与处理

当 App 内存占用过高时,系统会发送 内存警告(Memory Warning),若不及时释放内存,App 会被杀死。处理流程如下:

  • 接收内存警告的回调
    UIViewController:- (void)didReceiveMemoryWarning;
    AppDelegate:- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application;
  • 处理策略(核心:释放 "可重建" 的资源)
    释放缓存:如图片缓存(SDImageCache 的 clearMemory)、网络请求缓存(NSURLCache 的 removeAllCachedResponses);
    释放未显示的资源:如隐藏的视图、未播放的视频数据;
    释放临时对象:如内存中的大列表数据(可重新从数据库 / 网络加载);
    避免在警告回调中创建新对象(防止内存进一步升高)。

四、关于内存管理的其他补充

1. 不同对象类型的内存管理差异

  • OC 对象(引用类型):依赖引用计数管理,ARC 自动处理;
  • Swift 值类型(struct/enum):不依赖引用计数,存储在栈上(栈内存自动回收,无需管理),赋值时会拷贝(深拷贝);
  • Swift 引用类型(class):与 OC 对象类似,ARC 管理,但 Swift 的 ARC 更严格(如引入 "所有权" 概念,避免跨模块的计数混乱);
  • Tagged Pointer 对象:如 OC 的 NSNumber(小整数)、NSString(短字符串)、Swift 的 Int(小值),直接存储在 isa 指针中,无引用计数,销毁时无需释放内存(性能极高)。

2. iOS 版本对内存管理的优化

iOS 7+:UIWebView 优化内存占用,同时推出 WKWebView(内存管理更高效,推荐替代 UIWebView);

iOS 10+:系统支持 "内存压缩"(Memory Compression),当内存紧张时,系统会压缩部分内存页,而非直接杀死 App;

iOS 13+:UIScene 多窗口架构下,每个窗口的内存管理独立,某窗口销毁时其资源可单独释放。

3. 大内存对象的特殊处理

  • 图片加载:避免直接加载原图(如 10MB 的图片),需按显示尺寸缩放(用 UIGraphicsImageRenderer 或 SDWebImage 自动缩放),减少内存占用;
  • NSData 处理:大文件数据(如视频、压缩包)避免一次性读入内存,改用流(NSInputStream/NSOutputStream)分块处理;
  • 缓存策略:大对象优先用磁盘缓存(如 NSCachesDirectory),而非内存缓存,内存缓存需设置上限(如 SDImageCache 的 maxMemoryCost)。

对于内存管理认识,应该渗透在整个iOS开发相关的各个细节里;理解透了内存管理,那也掌握了iOS的大部分;so,这里我肯定也写不全,越写越多,先到这( ̄▽ ̄)~*。

相关推荐
2501_915106329 小时前
iOS 26 APP 性能测试实战攻略:多工具组合辅助方案
android·macos·ios·小程序·uni-app·cocoa·iphone
开开心心loky9 小时前
[iOS] KVC 学习
学习·ios·objective-c·cocoa
00后程序员张18 小时前
iOS混淆与IPA文件加固全流程实战 防止苹果应用被反编译的工程级方案
android·ios·小程序·https·uni-app·iphone·webview
胖虎118 小时前
iOS 推送证书 P8 介绍及生成流程
ios·个推·p8证书·极光推送·ios推送
白熊18819 小时前
【图像大模型】ms-swift 深度解析:一站式多模态大模型微调与部署框架的全流程使用指南
开发语言·ios·swift
2501_9151063219 小时前
iOS 应用加固与苹果软件混淆指南,如何防止 IPA 被反编译与二次打包?
android·ios·小程序·https·uni-app·iphone·webview
用户347475478332820 小时前
把SwiftUI View 转为图片
ios·swiftui
mit6.8241 天前
[FSCalendar] 可定制的iOS日历组件 | docs | Interface Builder
ios
2501_915921431 天前
iOS 应用加固与苹果软件混淆全解析 IPA 文件防反编译、混淆加密与无源码加固策略
android·macos·ios·小程序·uni-app·cocoa·iphone