普通视图

发现新文章,点击刷新页面。
昨天 — 2025年4月2日掘金 iOS

WASM I/O 2025 | MoonBit获Kotlin核心开发,Golem Cloud CEO高度评价

作者 MoonBit
2025年4月2日 18:48

图片

图片

2025 年 3 月 27 日至 28 日,为期 2 日的 WASM I/O 2025 大会在巴塞罗那圆满落幕。期间,众多 WASM 社区领袖和技术专家,共同带来了 30 余场精彩的演讲,共同探讨云原生和开源领域的前沿洞察、核心技术与最佳实践。作为一门专为 WebAssembly 优化的新兴编程语言,MoonBit 作为首个中国开发者平台受邀在本次大会上进行技术分享。

主题演讲

在本次活动中,MoonBit 负责人张宏波带来了《MoonBit & WebAssembly》主题演讲。作为一门专为 WebAssembly(Wasm)设计的编程语言,MoonBit 凭借其极致的性能优化、创新的语法设计及开发者友好的工具链,展示了在浏览器与服务器端的全栈潜力。以下是本次演讲的核心技术回顾。图片

张宏波开宗明义地指出,现有语言在 Wasm 生态中存在三大痛点:二进制体积臃肿、编译速度缓慢以及运行时性能不足。MoonBit从语言设计之初便围绕Wasm优化,实现了极简二进制体积、闪电编译速度、高性能运行时的突破。

图片

MoonBit在设计时非常重视开发者体验,在工具链设计上体现了 “开发者优先” 的理念。其云 IDE 无需本地安装,集成并行类型检查、实时错误提示及基于 ChatGPT 的代码补全功能。调试支持亦是一大亮点,开发者可直接通过 Chrome DevTools 对 Wasm 模块进行断点调试和性能分析,与传统 JavaScript 工作流无缝衔接。MoonBit 与 WasmGC 的深度整合,使其在浏览器端表现尤为亮眼。通过零成本 JS 互操作Unicode 安全算法,在浏览器端实现无缝集成与性能突破。图片

针对服务器端开发,MoonBit 借助 Wasm 组件模型(Component Model)实现了跨语言协作的模块化架构。通过标准化接口定义语言(WIT),开发者可生成跨语言绑定。在编写 HTTP 服务器的实践中,MoonBit 生成的二进制大小仅为 27KB(Rust 为 100KB,Python 为 17MB)。

在未来,MoonBit 将深度整合组件模型,将 wasm-tools 直接嵌入工具链,支持 WASI 异步流(Phase 5提案)等前沿特性。

图片

MoonBit 的潜力不仅限于 Wasm,其支持多后端编译的特性为开发者提供了灵活的选择:编译为 C 代码时可运行于嵌入式设备(MCU),而实验性 JavaScript 后端则支持 Elm 架构,用于构建全栈应用。

对于一门语言来说,生态系统和开发者体验是其获得广大社区认可的必备条件。在 2025 年,MoonBit 将达到 Beta 版本,并进一步完善语言工具链与生态,随着 Wasm 在边缘计算、微服务等领域的深化,MoonBit 或将成为开发者拥抱下一代 Web 标准的关键选择。

社区反馈

在本次演讲后,Kotlin/Wasm 作者 Zalim 在社交媒体上表示:"MoonBit 在 WebAssembly 平台实现了精彩的成果",对 MoonBit 在 WASM 方向的技术成果给予高度认可。

图片

ZivergeTech & Golem Cloud 公司 CEO John A De Goes 表示:“与张宏波在 WASM I/O 见面后,我对 MoonBit 未来更加充满期待!MoonBit 融合了 Rust 语言的精华特性,还增加了垃圾回收(GC)机制,并优先关注工具链、性能和稳定性。欢迎大家加入我,一起参加在 LambdaConf 举办的 GolemCloud 黑客松活动,届时我会使用MoonBit!” 

图片

MoonBit 在本次 WASM I/O 上的精彩亮相吸引了海外社区诸多关注,我们期待未来 MoonBit 在全球开发者社区的无限潜力!

昨天以前掘金 iOS

iOS启动优化 - 1分钟让你的启动时间降低 1s

作者 yidahis
2025年4月1日 16:01

先回答标题

原理是通过减少启动时加载的动态库数量达到目的。那一分钟就可以?是的,因为 CocoaPods 默认是会加载所有的依赖库,可以在一分钟内通过删除 Other Linker Flags 中的链接标记,取消启动时自动加载的动态库。那 300ms 是不是夸张了?没有一点点夸张,不信大家往下看。(文章结尾有惊喜!)

准备工作

如何测量启动时间?

咱们首选还是 instruments,它的 App Launch 工具 可将启动过程拆解为关键阶段(如 dyld 加载、Runtime 初始化、首帧渲染等),并量化各阶段的耗时。

image.png

如图,这里精确的量化了本次程序运行总的时长、加载可执行文件时长(包含主程序、动态连接器、动态库、各种初始化等)、加载三个动态库的运行时长。这里重点关注加载的动态库,它们的特点是描述以 Map Image + <动态库路径>

如何知道加载了哪些动态库?

如上图,其中以 Map Image 开头的描述表明这就是一个动态库。dlopen() 函数调用后也会生成一个同样的任务。 除了 instruments 还有其他手段。这里就给出一个笔者常用的一个工具。 新建一个类 LazyLoader,将以下代码拷贝到全局调用区。

// 打印动态库名称(兼容 M1/Mac 和 iOS)
void filterThirdPartyLibs(void) {
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const char* path = _dyld_get_image_name(i);
        if (!strstr(path, "/usr/lib/") &&
            !strstr(path, "/System/Library/")) {
            NSString* pathStr = [NSString stringWithCString:path encoding:NSUTF8StringEncoding];
            NSLog(@"Third-party: %@\n", pathStr.lastPathComponent);
        }
    }
}

// 在程序启动时自动调用
__attribute__((constructor)) static void runtime_init(void) {
    filterThirdPartyLibs();
}

见证奇迹的时刻即将来到,开始发车了。

首先通过 pod 新建一个私有 LoadAFDemo

pod lib create LoadAFDemo

接下来在 podfile 中添加以下依赖,并执行pod install

pod 'AFNetworking'

然后将工具 LazyLoader 拖入工程中并进行桥接。现在开始运行项目,可以得到如下结果:

image.png 可以看到 AFNetworking 在其中,这里过滤了系统的库,其他两个是主工程相关,因为开发环境被拆分为两个。 这个时候可以看到主工程的 Other Linker Flags 很简单,只有 AFNetworking

image.png

接下来打开App Launch 工具查看总的时长 242.06ms, 这个数字很重要,它代表了 pre-main 阶段的启动耗时。

image.png 如图,动态库的加载描述都是以 Map image 开头,其中第一个是笔者添加的依赖库 AFNetworking ,下面三个都是系统库的。

  1. 在 podfile 中添加一些依赖库。
pod 'SDWebImage'
pod 'Realm'
pod 'RxTheme','4.1.1'
pod 'RxGesture','3.0.2'
pod 'NSObject+Rx','5.1.0'
pod 'Moya','~> 14.0.0'
pod 'Alamofire', '~> 5.2.1'
pod 'SwiftyJSON','5.0.0'
pod 'R.swift','5.2.2'
pod 'CryptoSwift','1.4.2'
pod 'SnapKit','5.6.0'
pod 'FlexLayout','1.3.20'
pod 'PinLayout','1.9.2'
pod 'SwifterSwift/Foundation','5.2.0'
pod 'SwifterSwift/UIKit','5.2.0'
pod 'WMPageController','2.5.2'
pod 'CYLTabBarController'
pod 'KMNavigationBarTransition'
pod 'Firebase/AnalyticsWithoutAdIdSupport','9.6.0'
pod 'Firebase/Crashlytics','9.6.0'
pod 'FirebaseCoreDiagnostics','9.6.0'
pod 'FirebaseInstallations','9.6.0'
pod 'FirebasePerformance','9.6.0'
pod 'AppsFlyerFramework','6.5.2'
pod 'MoEngage-iOS-SDK','7.1.4'
pod 'MoEngageInApp', '2.1.2'
pod 'MORichNotification','5.2.0'
pod 'ZLPhotoBrowser', '4.2.5'
pod 'FCUUID','1.3.1'

此时运行pod install再观察,发现刚才添加的依赖库都被添加进来。

image.png

接下来运行项目,查看控制台输出:

image.png

总计44个三方库加载了,这里只截取部分。

接下来再次打开App Launch 工具查看总的时长为 1.47s, 比原来多出了近 1.2s。是不是很惊讶,这里的三方库加载耗时基本在 1ms 左右,共 44 个那也最多 100ms 才对。这里先卖个关子,请看接下来的表演。 image.png

接下来打开主工程,删除 Other Linker Flags 中的所有内容,再次运行项目。

image.png 发现三方库一个也没有加载。再次打开App Launch 工具查看总的时长为 197.07ms

image.png 看到这里的你,是不是惊讶,从 1.47s197.07ms,启动效能完全是飞升了。那这个做法可以直接运用到项目中去吗?接下来通过一些问题来解开谜题。

一些疑点

  1. 清空 Other Linker Flags 中的依赖后,代码能运行吗?
    答:能运行,不过相关的三方库会在 pre-main 阶段加载。请看下图, image.png 上图中首先 import AFNetworking 库,然后使用 AFHTTPSessionManager 获取其实例,且 callAF 方法没有任何地方调用,只是定义了。运行项目可以看到,控制台中输出了 AFNetworking,没有编译和运行报错。(如果只是 import 是不会触发链接操作的)。这里暂且把这种行为定义为 Real Import

  2. 那为什么会这样呢?
    其实答案很明显,获取 AFHTTPSessionManager 实例是编译期就确定的行为。而编译没有报错,那英爱是系统在 pre-main 阶段通过 LC_LOAD_DYLIB 指令加载 AFNetworking

  3. 在私有podspec中添加了 AFNetworking依赖, 还需要清除私有podspec中的 Other Linker Flags 吗?
    答:在私有库没有 Real Import 的前提下,不需要清除 Other Linker Flags 也可以实现依赖的三方库不被加载。就是说,私有podspec断开了和主工程链接关系,那么这个 AFNetworking 也就没有断开了和主工程的联系,所以即使 Other Linker Flags 添加了链接标记,也只是 AFNetworking 和当前私有库的。

  4. AFNetworking 换成 Alamofire 也是一样的吗?
    答:是的

  5. 没有加载AFNetworking的情况下,通过反射(NSClassFromString(@"AFHTTPSessionManager"))可以拿到实例吗? 答:当然不可以,需要时先动态加载 AFNetworking

  6. 说了这么多那 dyld 的加载动态库的原则是什么
    答:这个问题主要与 Mach-O 的文件结构有关。可以使用 otool -l <动态库路径> 查看文件中所以依赖的动态库。主要有两个命令LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIB, 其实上面的问题中提到的Real Import实际上是执行 LC_LOAD_DYLIB 命令将动态库加载到强依赖库列表中,也就对应在Other Linker Flags中标记为-framework的动态库。LC_LOAD_WEAK_DYLIB命令将动态库加载到弱依赖库列表中,对应于Other Linker Flags中的-weak_framework标记的动态库。而无论是强依赖还是弱依赖,正常情况情况下都会加载。

  7. 你搞这么麻烦,直接用系统支持的 -weak_framework标记不就行了? 答:这个笔者经过实测,在目前的系统版本(iOS18)是不行的。经过查阅官方文档发现最后更新日期是2013年,应该是很久没有更新了。而且现在大部分使用的是dyld3

惊喜

为了更加直观的观察依赖关系,笔者专门开发了一个脚本工具。优点是:非常直观和方便。下面以咸鱼 iOS 为例。传送门

xianyu-outputs.html.png

都2025年,你竟然还敢买iOS的源码?

作者 iOS阿玮
2025年4月1日 10:22

App Store审核问题或3.2f咨询,请公众号留言。不免费,不闲聊。iOS研究院

序言

买源码这件事情在互联网公司初创是最常见的行为,除了节约了开发成本,更为重要的是节约了开发的时间

对于想做某方面业务,但是完全不懂技术的初创者来说,是再好不过的选择了。

尤其是在2015~2020年,口罩前期不少外包公司也是雨后春笋般。那个时候没有这么多较为知名的头部产品,也是人人都是产品经理的时代

做微商的想做自己的商城App,做化妆品、做珠宝、做鞋子的,等等...

转折点

在步入2022年之后,苹果先后提高了开发者的门槛。首先就是大陆区账号注册的难度提升,增加了人脸拍摄。提高了代码查重的算法。之前矩阵的打法也就是马甲包,不断沦为苹果重点照顾的重灾区

衍生的条款

在算法一步步提升的背景下,Guideline 4.3(a) - Design - Spam中拓展了3点内容:

  • We noticed your app still shares a similar binary, metadata, and/or concept as apps previously submitted by a terminated Apple Developer Program account.

译:我们注意到你的应用程序仍然与之前被终止的苹果开发者计划帐户提交的应用程序共享类似的二进制文件、元数据和/或概念。

  • Submitting an app with the same source code or assets as other apps already submitted to the App Store

译:提交与其他已提交到app Store的应用程序具有相同源代码或资产的应用程序

  • Purchasing an app template with problematic code from a third party

译:从第三方购买有问题代码的应用模板

加强版

最近有很多同行在咨询3.2f,其中最值得关注的是

Evidence of Dishonest or Fraudulent Activity


Your account is associated with terminated developer accounts and/or accounts 
engaged in fraudulent activities. These associations may include, but are not limited to, shared account information, 
submissions with similar or identical App Store metadata, app binaries with s
hared code, resources, or assets, or the transfer of apps engaged in fraudulent activities between accounts.

包括但不限于共享账户信息、具有相似或相同App Store元数据的提交、具有共享代码、资源或资产的应用程序二进制文件

也就是说在今年之前,仅仅局限于上架被拒4.3a。苹果在最近开始了新一轮的二进制比对,在未进行迭代,上新的账户中,触发了代码关联性的查重。

所以都2025年了,你还敢买iOS的源码?我建议是算了吧,你把握不住的~

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

Flutter 性能优化:实战指南

2025年4月1日 09:46

主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。

一、首屏渲染优化

1. 骨架屏与占位符

  • 问题:首屏加载时白屏或长时间等待数据导致用户体验差。

  • 解决方案

    • 骨架屏:使用 SkeletonLoader 等组件模拟页面结构,替代空白屏4。
    • 占位符:数据未加载时返回轻量组件如 SizedBox.shrink(),降低首帧渲染时间(如减少 200ms)。
  • 代码示例

    Widget build(BuildContext context) {
      if (_isLoading) return const SizedBox.shrink(); // 轻量占位
      // 真实内容
    }
    

2. 资源预加载

  • 问题:首屏图片加载延迟。

  • 解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。

  • 代码示例

    Future<void> _precacheImages() async {
      await Future.wait([
        precacheImage(AssetImage('assets/banner.png'), context),
      ]);
    }
    

3. 数据预取与FFI优化

  • 问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。

  • 解决方案

    • Native侧发起请求:在页面路由阶段由 Native 提前请求数据,避免 Flutter 线程切换。
    • FFI(Foreign Function Interface) :通过 FFI 直接读取 Native 缓存数据,减少序列化开销。
  • 效果:详情页启动时间优化 100ms 以上9。


二、长列表与滚动性能优化

1. 懒加载与按需构建

  • 问题:一次性渲染大量列表项导致卡顿。

  • 解决方案

    • ListView.builder/GridView.builder:仅构建可见项,避免内存溢出。
    • 分帧渲染(分帧加载) :将复杂子组件分帧渲染,避免单帧耗时过长。
  • 代码示例

    ListView.builder(
      itemCount: 1000,
      itemBuilder: (context, index) => ListItem(data[index]),
    )
    

2. 复用与缓存

  • 问题:频繁切换 Tab 导致重复加载数据。

  • 解决方案

    • 状态缓存:使用 PageStorageKey 或 AutomaticKeepAliveClientMixin 缓存页面状态。
    • 数据分级管理:通过 RxMap 维护不同分类的数据状态,减少重复请求。

三、UI渲染优化

1. 减少Widget重建

  • 问题:频繁调用 setState 导致不必要的 Widget 重建。

  • 解决方案

    • const Widget:使用 const 构造函数创建静态组件,避免重复构建。
    • Provider 选择性刷新:通过 Selector 或 Consumer 局部刷新,而非全量更新。
  • 代码示例

    const MyWidget(); // 编译时确定,避免重建
    

2. 动画优化

  • 问题:复杂动画导致帧率下降。

  • 解决方案

    • AnimatedBuilder:分离动画逻辑与静态组件,仅重绘动画部分。
    • 复用Child:将不变部分作为 child 参数传入,避免重复构建。
  • 代码示例

    AnimatedBuilder(
      animation: _controller,
      child: const StaticWidget(),
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,
      ),
    )
    

四、内存与GPU优化

1. 内存泄漏检测

  • 问题:未释放资源导致内存持续增长。

  • 解决方案

    • 及时销毁监听:在 dispose 方法中取消订阅和释放资源。
    • 工具检测:使用 DevTools 的 Memory 视图分析内存泄漏。

2. GPU渲染优化

  • 问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。

  • 解决方案

    • 避免 saveLayer:使用 ClipRRect 替代复杂蒙层,减少离屏渲染。
    • 缓存静态图像:通过 checkerboardRasterCacheImages 检测未缓存的图像。

五、工具与工程化实践

1. 性能分析工具

  • DevTools:通过 Timeline 视图分析帧耗时,定位构建、布局、绘制阶段的瓶颈。
  • Profile模式:打包 Profile 版本,获取接近真实环境的性能数据。

2. 模块级混合开发

  • 问题:全 Flutter 页面启动性能差(如动态库加载耗时)。

  • 解决方案

    • Native与Flutter混合:核心模块用 Native 实现,非核心模块保留 Flutter,降低启动时间。
    • FlutterBoost 扩展:支持模块级混合容器,解决生命周期和布局问题。

六、其他高级优化

1. Isolate 并行计算

  • 问题:Dart 单线程阻塞 UI。

  • 解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。

  • 代码示例

    final result = await compute(heavyTask, data);
    

2. 资源压缩与预置

  • 问题:资源加载慢(如图片、字体)。

  • 解决方案

    • 图片压缩:使用 flutter_image_compress 优化图片体积。
    • 本地预置:关键资源(如 JSON 模板)预置到本地,减少网络依赖。

总结

  • 核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。

  • 实战案例

    • 携程酒店通过分帧渲染优化;
    • 淘宝特价版使用 FFI 和 Native 数据预取提升首屏速度;
    • 长列表懒加载和状态缓存避免卡顿。

iOS性能优化:OC和Swift实战指南

2025年4月1日 09:29

常见的iOS性能优化点,比如内存管理、UI优化、多线程处理、网络请求优化、启动优化、I/O操作、图像处理、算法优化、工具使用等。

一、内存优化

1. 循环引用处理

  • 原理:对象之间的强引用导致无法释放,内存泄漏。

  • Objective-C

    • 使用 __weak 或 __unsafe_unretained(需谨慎)打破循环:

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          // 弱引用避免循环
          [weakSelf doSomething];
      };
      
    • 对 NSTimer 使用 NSProxy 或 weak 委托模式。

  • Swift

    • 使用 [weak self] 或 [unowned self] 捕获列表:

      self.block = { [weak self] in
          guard let self = self else { return }
          self.doSomething()
      }
      
    • unowned 适用于生命周期相同或更短的场景(如父子关系)。


2. 自动释放池(Autorelease Pool)

  • 原理:批量创建临时对象时,及时释放内存。

  • Objective-C

    @autoreleasepool {
        for (int i = 0; i < 100000; i++) {
            NSString *temp = [NSString stringWithFormat:@"%d", i];
            // 临时对象会被及时释放
        }
    }
    
  • Swift

    autoreleasepool {
        for i in 0..<100000 {
            let temp = "(i)"
            // 临时对象在块结束时释放
        }
    }
    
  • 适用场景:大量临时对象生成(如解析 JSON 数组、图像处理)。


二、UI 性能优化

1. 避免离屏渲染(Offscreen Rendering)

  • 原理:离屏渲染(如圆角、阴影)触发 GPU 额外绘制,导致卡顿。

  • 优化方法

    • 预渲染圆角

      • Objective-C

        // 使用 Core Graphics 提前绘制圆角
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
        [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10] addClip];
        [image drawInRect:view.bounds];
        UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        view.layer.contents = (id)roundedImage.CGImage;
        
      • Swift

        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let roundedImage = renderer.image { context in
            UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).addClip()
            image.draw(in: view.bounds)
        }
        view.layer.contents = roundedImage.cgImage
        
    • 避免 shouldRasterize:除非复用图层,否则会触发离屏渲染。


2. Cell 复用与轻量化

  • 原理:避免频繁创建/销毁 Cell,减少 CPU 和内存压力。

  • Objective-C

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 复用 Cell
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellID"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellID"];
        }
        // 轻量配置(避免复杂计算)
        cell.textLabel.text = [self.dataArray[indexPath.row] title];
        return cell;
    }
    
  • Swift

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
        cell.textLabel?.text = dataArray[indexPath.row].title
        return cell
    }
    
  • 优化点

    • 避免在 cellForRow 中执行耗时操作(如网络请求)。
    • 使用 prepareForReuse 清理旧数据。

三、多线程优化

1. 主线程任务最小化

  • 原理:主线程阻塞导致 UI 卡顿(16ms 内未完成一帧绘制)。

  • Objective-C

    // 将耗时操作放到后台线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = [UIImage imageWithData:data];
        });
    });
    
  • Swift

    DispatchQueue.global(qos: .userInitiated).async {
        let data = try? Data(contentsOf: url)
        DispatchQueue.main.async {
            self.imageView.image = UIImage(data: data!)
        }
    }
    

2. 线程安全与锁优化

  • Objective-C

    • @synchronized 实现简单但性能较低:

      objc

      @synchronized(self) {
          // 临界区
      }
      
    • 高性能场景使用 os_unfair_lock(替代已废弃的 OSSpinLock):

      #include <os/lock.h>
      os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
      os_unfair_lock_lock(&lock);
      // 临界区
      os_unfair_lock_unlock(&lock);
      
  • Swift

    • 使用 NSLock 或 DispatchQueue 屏障:

      let queue = DispatchQueue(label: "com.example.threadSafe", attributes: .concurrent)
      queue.async(flags: .barrier) {
          // 写操作(独占访问)
      }
      

四、网络优化

1. 请求合并与缓存

  • Objective-C

    // 使用 NSURLSession 的缓存策略
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
    
  • Swift

    let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
    
  • 优化点

    • 减少重复请求(如短时间内多次刷新)。
    • 使用 HTTP/2 多路复用降低连接开销。

五、启动时间优化

1. 冷启动阶段优化

  • 原理:减少 main() 函数之前的加载时间(T1)和首帧渲染时间(T2)。

  • Objective-C

    • 减少动态库数量,合并 Category。
    • 避免在 +load 方法中执行代码。
  • Swift

    • 使用 @UIApplicationMain 减少启动代码。

    • 延迟非必要初始化:

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          // 延迟初始化第三方 SDK
      }
      

六、I/O 优化

1. 文件读写异步化

  • Objective-C

    dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], O_RDONLY, 0, queue, ^(int error) {});
    dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t data, int error) {});
    
  • Swift

    let queue = DispatchQueue.global(qos: .background)
    DispatchIO.read(fromFileDescriptor: fd, queue: queue) { data, _ in
        // 处理数据
    }
    

七、图像处理优化

1. 异步解码与降采样

  • Objective-C

    // 使用 ImageIO 进行降采样
    CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceThumbnailMaxPixelSize: @(300)});
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    
  • Swift

    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 300]
    let imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
    let image = UIImage(cgImage: imageRef)
    

八、算法与数据结构

1. 高效遍历与查询

  • Objective-C

    • 使用 NSDictionary 替代 NSArray 快速查找(O(1) vs O(n))。
  • Swift

    • 使用 lazy 延迟计算集合:

      let filteredData = data.lazy.filter { $0.isValid }.map { $0.value }
      
    • 使用 ContiguousArray 提升性能(连续内存布局)。


九、工具使用

1. Instruments 分析

  • Time Profiler:定位 CPU 热点函数。
  • Allocations:分析内存分配类型和泄漏。
  • Core Animation:检测离屏渲染和帧率。

十、Swift 特有优化

1. 减少动态派发

  • 使用 final 和 private 修饰类或方法:

    final class NetworkManager {
        private func fetchData() { ... }
    }
    

2. 值类型优先

  • 使用结构体(struct)替代类(class)减少引用计数开销:

    struct Point {
        var x: Double
        var y: Double
    }
    

总结

  • Objective-C 优化重点:手动内存管理、@autoreleasepool、避免 performSelector 潜在泄漏。
  • Swift 优化重点:值类型、协议扩展、DispatchQueue 和现代语法(如 Result 类型)。
  • 通用原则:减少主线程阻塞、复用资源、异步化耗时操作、利用工具分析瓶颈。

为什么 Swift 的反射像个“玩具”?聊聊 Mirror 背后的设计哲学

作者 JQShan
2025年3月31日 21:03

引子:当程序员想“窥探”一个对象

如果你用过 Java 或 C#,大概对「反射」这个词不陌生——它能让你在运行时像变魔术一样动态调用方法、修改属性,甚至篡改私有字段。但在 Swift 中,如果你想“窥探”一个对象的内部,苹果只给了你一个看似简陋的工具:Mirror。它不能修改属性,无法获取方法,连私有变量都藏着掖着。这不禁让人想问:反射这么重要的能力,苹果为何只做了个“玩具”出来?

今天,我们就来聊聊 Mirror 的设计逻辑,以及它背后隐藏的 Swift 语言哲学。

一、Mirror 的“克制”:安全与性能的双重防线

想象一下,你正在造一辆车。Java 的反射像是给你一套万能扳手,能拆发动机、改刹车片,甚至把车门卸下来。而 Swift 的 Mirror 更像是一把车钥匙——它能让你打开车门,看看内饰,但别想乱改零件。这种“克制”背后,是苹果对 Swift 的两大核心坚持:

  1. 安全第一 Swift 从诞生起就带着「杜绝未定义行为」的执念。反射能绕过编译检查,就像给代码开了个后门。比如,Java 中你可以用反射强行修改 final 字段,但这可能导致不可预知的崩溃。而 Mirror 的只读设计,本质上是在说: “你可以看看,但别乱动。”
  2. 速度不能妥协 Swift 被用于 iOS 系统内核、高性能游戏引擎等场景。反射的动态类型检查(as?Any)会带来运行时开销,而 Mirror 的轻量化设计,让它在需要时足够快,甚至能被编译器优化掉部分成本。

二、为什么不需要“万能扳手”?Swift 的编译时魔法

苹果似乎对动态反射兴趣缺缺,但其实他们找到了一种更“Swift 风格”的解决方案:把问题消灭在编译时。举个例子:

  • 场景:JSON 解析 在 Java 中,你可能用反射遍历字段,匹配 JSON 的 key。而在 Swift 中,Codable 协议通过编译器自动生成代码,直接映射属性——无需运行时反射,且类型安全零开销。
// 编译器自动生成编解码逻辑!
struct User: Codable {
    var name: String
    var age: Int
}
  • 场景:依赖注入 Java 的 Spring 框架依赖反射创建对象,而 Swift 可以通过泛型 + 协议,在编译时完成类型绑定:
// 编译时就知道 Container 里存了什么类型
container.register(UserService()) 
let service: UserService = container.resolve()

苹果的逻辑很明确:能通过类型系统解决的问题,绝不留到运行时。

三、Mirror 的生存空间:优雅的妥协

当然,总有些场景需要运行时信息。比如调试工具、动态生成日志,或是教学demo中展示对象结构。这时 Mirror 就派上用场了:

// 打印对象的所有属性
func debugPrint(_ value: Any) {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        print("(child.label ?? "?"): (child.value)")
    }
}

但你会发现,Mirror 的设计处处透着“小心翼翼”:

  • 不支持方法反射(避免动态派发)
  • 不暴露内存布局(防止不安全访问)
  • 对枚举和结构体的支持有限(鼓励模式匹配)

它更像是一个“安全气囊”,只在必要时弹出,而非让开发者随时飙车。

四、从 Mirror 看 Swift 的“价值观”

  1. 开发者不是敌人 Java 的反射默认允许访问私有字段,而 Swift 的 Mirror 对私有属性的可见性取决于模块边界——它假设开发者是理性的,但依然用访问控制保护代码的封装性。
  2. 工具链即力量 Swift 更倾向于通过编译器(如自动生成 Codable 代码)、Xcode 工具链(如 LLDB 调试器)来辅助开发,而非依赖运行时动态能力。
  3. 生态的统一性 在 SwiftUI 中,属性包装器(@State)、函数式编程等特性,让开发者无需反射也能实现动态 UI 和数据绑定。反射不再是必需品,而是备胎。

五、如果你真的需要“万能扳手”……

虽然苹果不鼓励,但总有极客想突破限制。比如:

  • 用 @dynamic 修饰符兼容 Objective-C 的运行时
  • 通过指针黑魔法直接操作内存布局(危险!但刺激)
  • 第三方库如 Runtime 提供元编程能力

但当你走这条路时,苹果的设计师可能会在背后叹气: “何必呢?明明有更安全的方式啊。”

结语:Mirror 是一面镜子,照出 Swift 的灵魂

Mirror 的简陋,恰恰反映了 Swift 的野心——它不想成为另一个“什么都能做,但处处是坑”的动态语言,而是试图用类型安全、编译时优化和清晰的API,重新定义现代编程的边界。就像 Swift 之父 Chris Lattner 所说: “我们希望开发者写出明显正确的代码,而非依赖运行时的小聪明。”

所以,下次当你嫌弃 Mirror 功能弱时,不妨换个角度想:或许不是苹果吝啬,而是他们相信,最好的魔法,应该发生在编译时 ✨。

Swift运行时以及与OC混编

作者 jz_study
2025年3月30日 20:44

Swift运行时

  • 在swift中如果我们要定义一个表示错误的类型非常简单,只要遵循Error协议就可以了,我们通常用枚举或结构体来表示错误类型,枚举可能用的多些,因为它能更直观的表达当前错误类型的每种错误细节。

image.png

如何抛出错误

  • 函数、方法和初始化器都可以抛出错误。需要在参数列表后面,返回值前面加throws关键字

image.png

image.png

image.png

使用Do-Catch做错误处理

  • 在Swift中我们使用do-catch块对错误进行捕获,当我们调用一个throws声明的函数或方法时,我们必须把调用语句放在do语句块中,同时do语句块后面紧接着使用catch语句块。

image.png

try?

  • try?会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发生崩溃,而返回一个nil,如果没有抛出错误则返回可选值

image.png

try!

  • 如果你确信一个函数或者方法不会抛出错误,可以使用try!来中断错误的传播。但是如果错误真的发生了,你会得到一个运行时错误。
let photo = try! loadImage(atPath:"./Resources/John Appleseed.jpg")

指定退出的清理动作

  • defer关键字:defer block里的代码会在函数return之前执行,无论函数是从哪个分支return的,还是有throw,还是自动而然走到最后一行(类似于ts的finally)

image.png

权限控制(模块和源文件)

  • 模块指的是独立的代码分发单元,框架或应用程序会作为一个独立的模块来构建和发布。在Swift中,一个模块可以使用import关键字导入另外一个模块
  • 源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

image.png

潜规则1
  • 如果一个类的访问级别是fileprivate或private那么该类的所有成员都是fileprivate或private(此时成员无法修改访问级别),如果一个类的访问级别是open、internal或者public那么它的所有成员都是internal,类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则)
潜规则2
  • 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private或fileprivate)
潜规则3
  • 在不违反1、2两条潜规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal),那么可以添加private(set)修饰符将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的)
潜规则4
  • 必要构造方法(required修改)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员时private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循潜规则1
不透明类型(why)

image.png

  • 代码是可以编译通过的,但是makeTrapezoid的返回类型有凑有偿,被暴露了出去

image.png

  • 不能将其Container用作函数的返回类型,因为该协议具有关联类型。也不能将它用作返回类型的泛型约束,因为函数体外没有足够的信息来推断泛型类型需要什么

image.png

解决问题

image.png

返回不透明类型vs返回协议类型
  • 返回opaque类型看起来非常类似于使用协议类型作为函数的返回类型,但这两种返回类型的不同之处在于它们是否保留了类型标识。opaque类型是指一种特定类型,尽管函数的调用者不能看到是哪种类型;协议类型可以指代符合协议的任何类型。一般来说,协议类型为存储值的基础类型提供了更大的灵活性,而不透明类型可以对这些基础类型做出更强有力的保证。

image.png

ARC

  • Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。
  • 引用计数只应用在类的实例。结构体(Structure)和枚举类型是值类型,并非引用类型,不是以引用的方式来存储和传递的

ARC如何工作

image.png

循环引用

  • 在两个类实例彼此保持对方的强引用,使得每个实例都使对方保持有效时会发生这种情况。我们称之为强引用环。
  • 通过用弱引用或者无主引用来取代强引用,我们可以解决强引用环问题

image.png

image.png

解决循环引用
  • 弱引用和无主引用允许引用环中的一个实例引用另外一个实例,但不是强引用。因此实例可以互相引用但是不会产生强引用环。
  • 对于生命周期中引用会变为nil的实例,使用弱引用;对于初始化时赋值之后引用再也不会赋值为nil的实例,使用无主引用

弱引用

  • 弱引用不会增加实例的引用计数,因此不会阻止ARC销毁引用的实例。这种特性使得引用不会变成强引用环。声明属性或者变量的时候,关键字weak表明引用弱引用。
  • 弱引用只能声明为变量类型,因为运行时它的值可能改变。弱引用绝对不能声明为常量。
  • 因为弱引用可以没有值,所以声明弱引用的时候必须是可选类型的。在Swift语言中,推荐用可选类型来作为可能没有值的引用的类型。

image.png

image.png

无主引用

  • 和弱引用相似,无主引用也不强持有实例。但是和弱引用不同的是,无主引用默认始终有值。因此,无主引用定义为非可选类型(non-optional-type).在属性、变量前添加unowned关键字,可以声明一个无主引用。
  • 因为是非可选类型,因此当使用无助引用的时候,不需要展开,可以直接访问。不过非可选类型变量不能赋值为nil,因此当实例被销毁的时候,ARC无法将引用赋值为nil。
  • 当实例被销毁后,试图访问该实例的无主引用会触发运行时错误。使用无主引用时请确保引用始终指向一个未销毁的实例。

image.png

闭包引用循环

  • 将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况导致了闭包使用self,从而产生了循环引用。
闭包引用循环解决
  • 定义占有列表-占有列表中的每个元素都是由weak或者unowned关键字和实例的引用(如self或someInstance)组成。每一对都在中括号中,通过逗号分开。
  • 当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。
  • 相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。

image.png

内存安全

  • 默认情况下,Swift会克服代码层面上的一些不安全的行为,如:确保一个变量被初始化完后才能被访问、确保变量在销毁后不会被访问等等安全操作。
  • Swift也会确保在多路访问内存中同一区域时不会冲突(独占访问该区域)。通常情况下,我们完全无需考虑内存访问冲突的问题,因为Swift是自动管理内存的。然而,在码代码的时候,了解那些地方可能发生内存访问冲突是非常重要的。通常情况喜爱,如果你的代码有内存访问冲突,那么Xcode会提示编译错误或者运行时错误。
  • 内存访问分为两种
  1. 即时访问:即在访问开始至结束前都不可能有其他代码来访问同一区域
  2. 长期访问:即在访问开始至结束前可能有其他代码来访问同一区域。长期访问可能和其他即时访问或者长期访问重叠
inout参数访问冲突

image.png

image.png

image.png

解决访问冲突问题:

image.png

self访问冲突

image.png

image.png

有问题

image.png

三方库

简介

使用cocoapods

image.png

  • 网络请求:Alamofire
  • JSON解析:SwiftJSON
  • 资源管理:R.swift
  • 社交分享:MonkeyKing
  • 图片缓存与加载:Kingfisher
  • 自动布局:SnapKit
  • 标准款扩展:Dollar
网络请求:Alamofire
  • Alamofire是在平果URL Loading System基础上封装的网络库,简单易用并且可扩展
  • Alamofire
基本用法
  • AF命名空间,链式调用
AF.request("https://time.geekbang.org").response { response in
 debugPrint(response)
}
request方法详解

image.png

SwiftJSON

image.png

Dollar(是对标准库的扩展,类似于js的loadash)

image.png

image.png

image.png

Snapkit

image.png

  • make初始化约束还调用
  • update更新约束的使用
  • remark有冲突约束的时候使用
图片加载和缓存-Kingfisher(类似SDWebImage)

image.png

R.Swift

image.png

image.png

image.png

R.Swift之前

image.png

R.Swift的使用

image.png

MonkeyKing

image.png

OC和Swift运行时简介

Objective-C运行时

  • 动态类型(dynamic typing)
  • 动态绑定(dynamic binding)
  • 动态加载(dynamic loading)

image.png

image.png

派发方式

  • 直接派发(Direct Dispatch)
  • 函数表派发(Table Dispatch)
  • 消息机制派发(Message Dispatch)
直接派发
  • 直接派发是最快的,不止是因为需要调用的指令集会更少,并且编译器还能够有很大的优化空间,例如函数内联等,直接派发也有人称为静态调用
  • 然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承和多继承
函数表派发

image.png

image.png

  • 查表是一种简单,易实现,而且性能可预知的方式.然而,这种派发方式比起直接派发还是慢一点,从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗,另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化(如果函数带有副作用的话)
  • 这种基于数组的实现,缺陷在于函数表无法拓展.子类会在虚数函数表的最后插入新的函数,没有位置可以让extesion安全地插入函数
消息机制派发
  • 消息机制是调用函数最动态的方式.也是Cocoa的基石,这样的机制催生了KVO,UIAppearence和CoreData等功能.这种运作方式的关键在于开发者可以在运行时改变函数的行为.不止可以通过swizzling来改变,甚至可以用isa-swizzling修改对象的继承关系,可以在面向对象的基础上实现自定义派发

Swift运行时

  • 纯Swift类的函数调用已经不再是Objective-c的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法、属性。
  • 而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过runtime拿到他的方法。这里有一点说明:老版本的Swift(如2.2)是编译期隐式的自动帮我加上了@objc,而4.0以后版本的Swift去掉了隐式特性,必须使用显式添加
  • 不管是纯Swift类还是继承自NSObject的类只要在属性和方法前面添加@objc关键字就可以使用runtime

image.png

  • 值类型总是会使用直接派发,简单易懂
  • 而协议和类的extension都会使用直接派发
  • NSObject的extension会使用消息机制进行派发
  • NSObject声明作用域里的函数都会使用函数表进行派发
  • 协议里声明的,并且带有默认实现的函数会使用函数表进行派发

image.png

Swift运行时-final @objc

  • 可以在标记为final的同时,也是用@objc来让函数可以使用消息机制派发。这么做的结果就是,调用函数的时候会使用直接派发,但也会在Objective-C的运行时里注册响应的selector.函数可以响应perfrom(selector:)以及别的Objective-C特性,但在直接调用时又可以有直接派发的性能
Swift运行时

image.png

image.png

image.png

Swift与OC的桥接

Swift调用OC

image.png

OC调用Swift

image.png

image.png

NS_SWIFT_NAME
  • 在Objective-C中,重新命名在swift中的名称
NS_SWIFT_UNAVAILABLE
  • 在Swift中不可见,不能使用
  • OC中可以调用Swift方法,要保证swift的类是继承自NSObject的

Subclass

  • 对于自定义的类而言,Objective-C的类,不能继承自Swift的类,即要混编的OC类不能是Swift类的子类。反过来,需要混编的Swift类可以继承自OC的类。

  • 定义一个常量值,后面可以方便使用;如#define TOOLBAR_HEIGHT 44;
  • 定义一个不变化的常用值,或者一个较长的对象属性;如#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
  • 定义一个会变化的常用变量值,或者一个较长的对象属性;如: #define STATUS_BAR_HEIGHT ([UIApplication sharedApplication].statusBarFrame.size.height)
  • 定义一个带参数的宏,类似于一个函数;如#define RGB_COLOR(r,g,b) [UIColor colorWithRed:r/255.f gree:g/255.f blue:b/255.f alpha:1.0]

Swift独有特性

  • Swift中有许多OC没有的特性,比如,Swift元组、为一等公民的函数、还有特有的枚举类型。所以,要使用的混编文件要注意Swift独有属性问题。

NS_REFINED_FOR_SWIFT

  • Objective-C的API和Swift的风格相差比较大,Swift调用Objective-C的API时可能由于数据类型等不一致导致无法达到预期(比如,Objective-C里的方法采用了C语言风格的多参数类型;或者Objective-C方法返回NSNotFound,在Swift中期望返回nil)。这时候就要NS_REFINED_FOR_SWIFT

image.png

SwiftUI-国际化

作者 YungFan
2025年3月30日 20:13

介绍

  • 如果 App 需要提供给不同国家的用户使用,则需要进行国际化处理。
  • SwiftUI 项目的国际化主要包括:Info.plist 文件国际化、文本国际化等。

配置国际化语言

在进行国际化之前,必须要添加需要的国际化语言,选中国际化的项目 —> PROJECT —> Info —> Localizations,点击+添加需要的国际化语言(默认已经存在英文)。

Info.plist文件国际化

  1. 新建一个Strings File,必须命名为InfoPlist.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. InfoPlist.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File,里面存放的是形如Key = Value的键值对。
  4. 在不同语言的Strings File中设置需要国际化的内容,如 App 名称等。
// 英文App名
"CFBundleName" = "I18N";
// 中文App名
"CFBundleName" = "国际化";

文本国际化

  1. 新建一个Strings File,必须命名为Localizable.strings
  2. 选中InfoPlist.strings,在 Xcode 的右侧文件检查器中找到Localization,点击Localize...,然后勾选配置的国际化语言。
  3. Localizable.strings左侧多了一个箭头,点击箭头展开后可以看见不同语言的Strings File
  4. 在不同语言的Strings File中设置需要国际化的文本键值对。
// 英文
"title" = "Reminder";
"message" = "Weather Information";
// 插值
"Weather is %@" = "Today is %@";
"Temperature is %lld" = "The temperature is %lld";
// 中文
"title" = "提示";
"message" = "今日天气";
// 插值
"Weather is %@" = "今天 %@";
"Temperature is %lld" = "气温 %lld 度";
  1. SwiftUI 文本国际化非常简单,开箱即用,因为大多数 View 与 Modifier 的构造方法中都将LocalizedStringKey作为参数类型,该参数的值为文本键值对中的键。
import SwiftUI

struct ContentView: View {
    let weather = "Sunny"
    let temperature = 10

    var body: some View {
        VStack {
            // 纯文本,有3种方式
            Text(title)
            
            Text(LocalizedStringKey("title"))

            Text("title", comment: "The title of the dialog.")
            
            // 自定义View
            MessageView("message")
            
            // 插值
            Text("Weather is \(weather)")
            
            Text("Temperature is \(temperature)")   
        }
    }
}

struct MessageView: View {
    var messaege: LocalizedStringKey

    init(_ messaege: LocalizedStringKey) {
        self.messaege = messaege
    }

    var body: some View {
        Text(messaege)
    }
}

注意:插值格式参考 String Format Specifiers

测试

默认情况下,App 的语言随着系统语言的变化而变化。但在开发阶段,如果才能快速测试 App 的国际化效果?主要有以下几种方式。

  1. 运行 App 之后在设备/模拟器通过设置(Settings)—> 通用(General)—> 语言与地区(Languages & Region) 切换系统语言以查看 App 的国际化效果。
  2. 通过 Xcode 菜单 —> Product —> Scheme —> Edit Scheme... —> Run —> Options —> App Language,选择需要测试的国际化语言之后再运行 App。
  3. 通过 Xcode 菜单 —> Product —> Scheme —> Manage Scheme... —> 选择需要复制的 Scheme —> 点击下方的圆形...图标 —> Duplicate —> 重命名 Scheme,然后将复制的 Scheme 按照方式 2 将 App Language 设置为需要测试国际化语言,最后运行时选择对应国际化语言的 Scheme。

效果

  • 英文。

英文.png

  • 中文。

中文.png

AutoreleasePool:iOS 内存管理乐章中的隐秘旋律

作者 布多
2025年3月30日 20:04

揭开 AutoreleasePool 的面纱

AutoreleasePool(中文也叫自动释放池)是 iOS 内存管理机制中的一个重要组成部分。它优雅地解决了对象生命周期管理的问题 - 通过延迟对象的释放时机,在合适的时间点统一回收内存资源。

这个精妙的设计背后有着怎样的实现原理呢?让我们一起深入 objc4 开源项目的源码,去探索 AutoreleasePool 的底层实现细节。

📝 本文使用的 Runtime 版本是 objc4-906。为了方便阅读,我对代码样式和排版略作了修改,并删减了一些不影响主逻辑的冗余代码。

🔧 我在 这里 维护了一个可以直接运行调试的 Runtime 项目,欢迎大家下载调试源码。

深入底层:AutoreleasePool 的实现机制

在 ARC 环境下,我们作为开发者能够接触到 AutoreleasePool 的场景主要有两种:

  1. 使用 __autoreleasing 修饰符

    • 将对象注册到自动释放池中
    • 实现对象的延迟释放机制
  2. 使用 @autoreleasepool {} 语法块

    • 精确控制内存的释放时机
    • 有效控制内存峰值

让我们一起深入源码和汇编层面,揭开这两种使用场景背后的技术实现细节。

__autoreleasing 修饰符的内部实现

从断点的汇编代码中我们可以发现,当我们使用 __autoreleasing 修饰一个对象时,系统会自动将其转换为对 objc_autorelease 函数的调用。这个操作等同于在 MRC 环境下手动调用对象的 autorelease 方法,它们在底层实现上是完全一致的。

让我们深入 Runtime 源码,一探 objc_autorelease 的内部实现。以下是经过整理的核心代码(如果觉得代码太长可以先跳过,后面会有详细解释):

id objc_autorelease(id obj) {
    return obj->autorelease();
}

id objc_object::autorelease() {
     /*
     检查该对象是否有自定义的引用计数(RR = Retain/Release)实现,
     ARC 肯定没有,MRC 一般也不会自定义这些方法实现。
     */
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootAutorelease();
    }

    return objc_msgSend(this, @selector(autorelease));
}

id objc_object::rootAutorelease() {
    // 如果对象正在释放,不要加入到 AutoreleasePool 中。
    if (isa().isDeallocating()) return (id)this;

    /*
      如果对象有使用返回值优化的话,不要加入到 AutoreleasePool 中。
      后面的章节《性能优化:TLS 机制解析》会对该机制进行说明。
    */
    if (prepareOptimizedReturn((id)this, true, ReturnAtPlus1)) return (id)this;
    
    // 如果是类对象,不要加入到 AutoreleasePool 中。
    if (slowpath(isClass())) return (id)this;
    
    return rootAutorelease2();
}

id objc_object::rootAutorelease2() {
    return AutoreleasePoolPage::autorelease((id)this);
}

// 注意:从这里开始,后面都是 AutoreleasePoolPage 的内部函数。
static id autorelease(id obj) {
    autoreleaseFast(obj);
    return obj;
}

id *autoreleaseFast(id obj) {
    AutoreleasePoolPage *page = hotPage();
    
    if (page && !page->full()) {
        // 当前页未满,直接添加
        return page->add(obj);
    } else if (page) {
        // 当前页已满,创建新页添加
        return autoreleaseFullPage(obj, page);
    } else {
        // 当前线程中还没有 Page,创建一个并添加对象。
        return autoreleaseNoPage(obj);
    }
}

id *add(id obj) {
    id *ret;
    /*
    下面的这段代码的逻辑大致是:如果启用了对象合并优化方案,
    则将重复调用 autorelease 的对象进行合并,
    这么做的目的是为了减少同一个对象被多次重复的添加到自动释放池。
    
    一般这种场景会出现在循环中,例如以下代码,
    如果未优化的话就会在 Page 中存放 10 个 Person 对象;
    优化后只会存放 1 个 Person 对象。
    
    for (int i = 0; i < 10; i++) {
        __autoreleasing Person *per = [[Person alloc] init];
    }
    */
    if (!DisableAutoreleaseCoalescing || 
        !DisableAutoreleaseCoalescingLRU) {
        if (!DisableAutoreleaseCoalescingLRU) {
            if (!empty() && (obj != POOL_BOUNDARY)) {
                /*
                获取 Page 最后的存储对象,检查与当前对象是否相同,
                如果相同则进行合并。
                */
                AutoreleasePoolEntry *topEntry = (AutoreleasePoolEntry *)next - 1;
                // 向前最多查找 4 个对象
                for (uintptr_t offset = 0; offset < 4; offset++) {
                    AutoreleasePoolEntry *offsetEntry = topEntry - offset;
                    // 检查是否越界或遇到池边界(POOL_BOUNDARY)
                    if (offsetEntry <= (AutoreleasePoolEntry*)begin() || 
                        *(id *)offsetEntry == POOL_BOUNDARY) {
                        break;
                    }
                    // 找到相同的对象且计数未达上限
                    if (offsetEntry->getPointer() == (uintptr_t)obj && 
                        offsetEntry->getCount() < AutoreleasePoolEntry::maxCount) {
                        // 通过内存移动,将匹配的对象移动到池顶。
                        if (offset > 0) {
                            AutoreleasePoolEntry found = *offsetEntry;
                            memmove(offsetEntry, offsetEntry + 1, offset * sizeof(*offsetEntry));
                            *topEntry = found;
                        }
                        
                        // 增加这个对象的计数。
                        topEntry->incrementCount();
                        ret = (id *)topEntry;
                        goto done;
                    }
                }
            }
        } else {
            if (!empty() && (obj != POOL_BOUNDARY)) {
                // 仅检查池顶前一个对象和当前对象是否一致。
                AutoreleasePoolEntry *prevEntry = (AutoreleasePoolEntry *)next - 1;
                
                // 直接合并到前一个对象。
                if (prevEntry->getPointer() == (uintptr_t)obj && 
                    prevEntry->getCount() < AutoreleasePoolEntry::maxCount) {
                    prevEntry->incrementCount();
                    ret = (id *)prevEntry;
                    goto done;
                }
            }
        }
    }

    ret = next;
    *next++ = obj;

 done:
    return ret;
}

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    /*
    获取下一个 Page 对象,
    如果它不存在或者存满了就创建一个新的。
    */
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    
    // 将当前 Page 设置为 hot。
    setHotPage(page);
    
    // 将对象添加到 Page 中。
    return page->add(obj);
}

id *autoreleaseNoPage(id obj) {
    bool pushExtraBoundary = false;
    /*
     首次创建自动释放池时,会用一个占位符表示空池。
     此时若添加对象,需先补一个边界。
     */
    if (haveEmptyPoolPlaceholder()) {
        pushExtraBoundary = true;
    }
    // 直接使用空占位符,避免创建真实的池结构。
    else if (obj == POOL_BOUNDARY) {
        return setEmptyPoolPlaceholder();
    }

    // 创建一个 Page,并设置为 hot。
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    return page->add(obj);
}

上面的函数调用流程大致如下:

objc_autorelease
└── autorelease
    └── rootAutorelease
        └── rootAutorelease2
            └── AutoreleasePoolPage::autorelease
                └── AutoreleasePoolPage::autoreleaseFast

从上面的代码分析中我们可以看到,当一个对象被标记为 autoreleasing 时,系统会通过一系列函数调用最终将其加入到 AutoreleasePoolPage 中。这个过程主要由 autoreleaseFast 函数完成,它负责管理对象的具体存储工作。在 autoreleaseFast 函数内部,系统会获取当前线程的 hotPage(所谓的 hotPage,其实就是双向链表的尾节点),然后根据 page 状态决定执行逻辑:

场景 状态 执行逻辑
逻辑一 page 存在且未满 直接调用 add 函数将对象添加到 Page 中
逻辑二 page 存在但已满 调用 autoreleaseFullPage 创建新 Page,然后添加对象
逻辑三 page 不存在 直接创建新 Page,然后添加对象

这里有一个有趣的优化细节:系统并不是直接存放对象的地址,而是将其包装成一个 AutoreleasePoolEntry 对象。这样设计的原因在于系统采用了 "对象合并优化" 方案 - 当多个相同对象被重复加入 Page 时,系统只会保留第一个对象,并通过 count 值记录该对象被重复加入的次数。这种优化可以有效减少内存占用(具体细节请阅读上面的 add 函数)。

至此,autoreleasing 对象就完成了它进入 AutoreleasePool 的全过程。

在上述实现中,有一个核心类型值得我们特别关注 - AutoreleasePoolPage。作为存储数据的底层结构,它在整个自动释放池机制中扮演着至关重要的角色。接下来,让我们深入分析这个关键组件。

AutoreleasePoolPage:内存管理的基石

让我们一起来看看 AutoreleasePoolPage 这个核心类的内部结构。为了便于理解,我对源码进行了精简和注释,保留了最关键的部分:

class AutoreleasePoolPage {
    magic_t magic;                // 魔数,用于校验 Page 的有效性和完整性
    __unsafe_unretained id *next; // 指向当前 Page 中下一个可用的存储位置
    objc_thread_t thread;         // 当前 Page 所属的线程,每个线程都有自己的 AutoreleasePool
    AutoreleasePoolPage *parent;  // 指向双向链表中的前一个 Page
    AutoreleasePoolPage *child;   // 指向双向链表中的后一个 Page
    uint32_t depth;               // 当前 Page 在双向链表中的深度,从 0 开始
    /*
     用于性能调试和监控。记录 AutoreleasePool 历史存储对象数量的最大值。
     目前的逻辑是当对象数量超过阈值(256)时,会触发日志记录,包含:
     - 当前线程信息
     - 对象存储数量
     - 完整调用栈
     这些信息有助于排查内存使用异常的情况。
     */
    uint32_t hiwat;
    
    // Page 大小固定为 4KB(4096字节),与系统内存页大小对齐
    static size_t const SIZE = 1 << 12;
    
    // 重写 new 操作符,确保分配的内存按 SIZE 对齐
    static void * operator new(size_t size) {
        void *result = 0;
        int r = posix_memalign(&result, SIZE, SIZE);
        ASSERT(r == 0);
        return result;
    }
}

为了更直观地理解 AutoreleasePoolPage 的内存结构,我绘制了一张简图:

AutoreleasePoolPage内存结构图.png

结合 AutoreleasePoolPage 的内部结构和前面分析的函数执行流程,我们可以更清晰地理解 AutoreleasePool 的底层实现。它本质上是一个普通的双向链表结构,其中每个节点都是一个 AutoreleasePoolPage 对象。这些对象通过 parent 和 child 指针相互连接,形成了一个完整的内存管理链条。每个 Page 不仅承担着节点的角色,还肩负着数据存储的重任,是整个自动释放池机制的核心载体。

在内存管理的层面上,AutoreleasePoolPage 的设计也体现了深思熟虑。通过查看其 new 方法的实现,我们可以看到每个 Page 的大小被设定为 4096 字节。这个看似随意的数字其实暗含玄机 - 它与现代操作系统的内存页大小完美对齐。这种设计带来了多重优势:

  • 减少内存碎片,提高内存利用效率;
  • 实现更高效的内存分配;
  • 确保内存访问的连续性,降低系统中断频率;
  • 优化缓存命中率,提升整体性能。

这些细节的优化,让 AutoreleasePool 在实现优雅的同时,也保持了极高的运行效率。

@autoreleasepool 的优雅之道

@autoreleasepool {} 是我们在项目中经常使用的另一个重要特性。这个看似简单的语法块背后,隐藏着编译器的巧妙转换和 Runtime 的精密配合。让我们揭开它的神秘面纱,一探其实现原理。

// 原代码:
@autoreleasepool {    
__autoreleasing Person *per = [Person createPerson]
}

// 编译后:
atautoreleasepoolobj = objc_autoreleasePoolPush(); // 对应 @autorelease {
__autoreleasing Person *per = [Person createPerson];
objc_autoreleasePoolPop(atautoreleasepoolobj); // 对应 }

通过查看下面的汇编代码截图,我们可以清晰地看到编译器确实将 @autoreleasepool 语法块转换成了 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的函数调用。这种转换不仅保证了代码的执行效率,还为开发者提供了一种优雅的内存管理方式:

@autorelease原理.png

让我们来看看 objc_autoreleasePoolPush 函数的内部实现。为了便于理解,我将相关代码进行了精简和整理:

void * objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void *push() {
    ReturnAutoreleaseInfo info = getReturnAutoreleaseInfo();
    // 将 TLS 中的对象转移至当前释放池。
    moveTLSAutoreleaseToPool(info);
    // 插入一个池边界。
    id *dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}

这段逻辑非常简洁优雅:它通过调用 autoreleaseFast 函数,在当前的 AutoreleasePool 中插入一个边界标记(POOL_BOUNDARY)。这个边界标记就像是一个书签,标记着当前自动释放池的范围起点。

让我们继续探索 objc_autoreleasePoolPop 的实现细节。下面是经过精简的核心代码,我们将分步骤详细解析其工作原理(如果觉得代码太长可以先跳过,稍后我们会详细解释其工作原理):

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

void pop(void *token) {
    // 清理 TLS 中的残留对象,确保其生命周期与当前池同步。
    while (releaseReturnAutoreleaseTLS());
    
    AutoreleasePoolPage *page;
    
    // EMPTY_POOL_PLACEHOLDER 表示清空 Pool 中的所有对象。
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        page = hotPage();
        if (!page) {
            // 从未使用的池直接清除占位符
            return setHotPage(nil);
        }
        
        /*
        获取 Pool 的起始页。内部逻辑如下:
         
        AutoreleasePoolPage *coldPage() {
            // 先拿到尾节点。
            AutoreleasePoolPage *result = hotPage();
            if (result) {
                // 不断获取链表中的前一个节点,直到第一个。
                while (result->parent) {
                    result = result->parent;
                    result->fastcheck();
                }
            }
            return result;
        }
        */
        page = coldPage();
        
        // 把 stop 设置为第一页的起点。
        token = page->begin();
    } else {
        page = pageForPointer(token);
    }
    
    id *stop = (id *)token;
    /*
     这段逻辑的意思是:
     如果 stop 不是池边界(POOL_BOUNDARY)就需要进行检查:
     如果 stop 是这一页的起点,并且这一页已经是第一页了。
     说明需要清理链表中的所有数据,这是正常情况。
     反之,这是不正常的,调用 badPop。
     */
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
        } else {
            return badPop(token);
        }
    }
    
    return popPage(token, page, stop);
}

static void popPage(void *token, AutoreleasePoolPage *page, id *stop) {
    /*
     从尾部一直往前释放对象,直到遇到 stop。
     这一步执行完后,只是释放了 Page 内的数据,Page 对象并未销毁。
    */
    page->releaseUntil(stop);
    
    /*
     如果这一页的数据被删完了,就把这个 Page 对象释放掉,
     并把它的父 Page 设为 hotPage。
     */
    if (page->empty()) {
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (page->child) {
        /*
         如果当前 Page 上的数据小于 Page 总容量的一半,
         则把下一个 Page 释放掉(因为这个 Page 还能存很多数据,
         可能很久都用不到下一页);
         反之,把下下一个 Page 释放掉(这一页快存满了,
         可能很快就要用到下一页了)。
         */
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

void releaseUntil(id *stop) {
    do {
        // 从 next 开始,一直往前释放对象,直到遇到 stop。
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();
            // 如果当前 Page 为空,则往前找父 Page。
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
            
            // 获取当前 Page 的最后一个对象。
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
            // 获取对象。
            id obj = (id)entry->getPointer();
            // 获取对象的引用计数。
            int count = (int)entry->getCount();

            /* 
            将原先存放数据的那块空间赋值为一个魔数 SCRIBBLE,
            这么做的目的是方便后期检查 Page 是否被破坏。
            */
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));

            // 如果 obj 不是边界标记,则执行释放操作。
            if (obj != POOL_BOUNDARY) {
                // 执行指定次数的释放操作。
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
            }
        }

      // 清空 TLS 中的临时对象,确保它们和 Page 的生命周期是相同的。
    } while (releaseReturnAutoreleaseTLS());
    setHotPage(this);
}

释放的逻辑稍微复杂一些,我们来梳理一下。当我们调用 pop 函数时,系统会按照以下流程进行对象的释放操作:

  1. 首先,objc_autoreleasePoolPop 函数会被调用,它是整个释放流程的入口。
  2. 然后,pop 函数会被调用,它负责处理具体的释放逻辑。
  3. 接着,popPage 函数会被调用,它负责管理 Page 的释放。
  4. 最后,releaseUntil 函数会被调用,它负责执行实际的对象释放操作。

完整的函数调用流程如下:

objc_autoreleasePoolPop
└── pop
    └── popPage
        └── releaseUntil

在这些函数中,releaseUntil 和 popPage 扮演着尤为关键的角色:

releaseUntil 函数负责对象的具体释放工作。它会按照先进后出(LIFO)的顺序,将 Page 中的对象逐一释放。在这个过程中,它不仅要处理普通对象的释放,还要考虑对象合并优化带来的特殊情况,确保每个对象都能得到正确的释放次数。同时,它还会通过魔数(SCRIBBLE)标记已释放的内存空间,这么做的目的是方便后期进行内存完整性检查。

popPage 函数则专注于 Page 对象本身的管理和释放。它维护着 AutoreleasePool 的整体结构,确保当 Page 中所有的对象都被释放完后,Page 本身能够被正确地释放,从而保持整个池结构的高效运转。

通过上述分析,我们已经深入理解了 AutoreleasePool 的核心工作原理:系统通过精心设计的入池(Push)和出池(Pop)机制,在保证内存管理安全性的同时,也实现了极高的运行效率。这种双向链表结构不仅让对象的生命周期管理变得优雅,还通过诸如对象合并等优化手段提升了运行性能。

在掌握了这些底层实现细节后,我们将进一步探讨 AutoreleasePool 在多线程环境下的行为特征,以及在实际开发中的最佳实践方案。这些知识将帮助我们更好地驾驭这个强大的内存管理工具。

线程与 AutoreleasePool:纠缠的双螺旋

AutoreleasePool 和线程是一一对应的关系,每个线程都拥有自己独立的 AutoreleasePool。从实现上看,它是一个由 AutoreleasePoolPage 对象组成的双向链表结构,线程通过 TLS (Thread Local Storage) 机制持有这个链表的尾节点(也就是 hotPage)。

关于 AutoreleasePool 的释放时机,这是一个经常被开发者讨论的话题。特别是被 __autoreleasing 修饰的对象,它们具体在什么时候被释放?

经过深入研究,我发现不同场景下对象的释放时机是不同的。让我们逐一分析:

场景一:主线程中的 autorelease 对象

  • 由 RunLoop 来管理释放时机
  • 通常在当前任务执行完成后释放
  • 具体时机可以通过下图来理解:

mainthread.png

场景二:GCD 创建的子线程中的对象

  • 由于 GCD 采用线程池机制,线程可能会被复用
  • 释放时机是在线程任务完成、即将被放回线程池时
  • 通过下图可以看到具体流程:

gcd.png

值得注意的是,_dispatch_worker_thread2 函数在处理线程任务时遵循这样的逻辑:从队列中获取并执行任务,当队列为空时让线程进入休眠状态。在进入休眠之前,会调用 _dispatch_last_resort_autorelease_pool_pop 来释放自动释放池中的对象。

场景三:通过 NSThread 等方式创建的非复用线程(无 RunLoop)

  • 对象会在线程销毁时被释放
  • 释放过程如图所示:

pthread.png

场景四:通过 NSThread 等方式创建的非复用线程(有 RunLoop)

  • 释放机制类似于主线程
  • 在当前任务执行完成后释放
  • 具体流程如下:

pthread_runloop.png

场景四中使用的 NSThread 是我开发的一个开源库 WXLThread。这个库实现了一个优雅的常驻线程机制,并提供了简洁而强大的任务调度接口,让线程管理变得更加便捷和高效。

需要特别说明的是,这些释放时机并非固定不变。因为 autoreleasing 对象的释放本质上是由 pop 函数的调用时机决定的。随着系统版本的更新和优化,这些调用时序可能会发生变化。比如在早期版本中,系统是通过在 RunLoop 中注册 Observer 来处理释放操作的,释放时机是在线程即将进入休眠状态之前。

另外,如果对象被 @autoreleasepool {} 语法块包裹,那么它的释放时机就很明确了 - 就是在退出这个语法块作用域的时候。这提供了一种更精确的内存管理方式。

性能优化:TLS 机制解析

- (Person *)createPerson {
    Person *per = [[Person alloc] init];
    return per;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *per = [self createPerson];
    NSLog(@"%@", per);
}

在上面的例子中,如果没有任何优化,系统会在 return 之前对 per 调用 release 方法,这会导致对象过早释放。为了避免这种情况,编译器会在方法返回前将对象加入到自动释放池中,确保调用方能够正常使用这个对象。但这种方案存在一个问题:即使对象马上就会被调用方使用,也要经过「加入自动释放池 -> 从自动释放池取出」这个过程,这无疑会带来一些性能开销。

为了解决这个问题,系统引入了基于 TLS (Thread Local Storage) 的返回值优化机制。具体来说:

  1. 在返回对象的方法中,编译器会插入 objc_autoreleaseReturnValue 调用;
  2. 在接收返回值的地方,编译器会插入 objc_retainAutoreleasedReturnValue 调用;
  3. 这两个函数会配合工作:如果检测到它们的调用配对,就会把对象暂存在线程的 TLS 中,直接传递给调用方,完全跳过自动释放池的过程。

这种优化极大地提升了返回对象的性能,尤其是在频繁方法调用的场景下。

通过调试查看汇编代码,我们可以清晰地看到这些优化函数的调用过程,当编译器检测到返回值优化的场景时,会跳过传统的自动释放池流程,直接通过 TLS 机制传递对象提升了性能。让我们通过下面的图来直观地体会这个优化过程:

调用方.png
方法调用方
返回方.png
方法返回方

常见误区与最佳实践

在实际开发中,我们经常会遇到一些对 AutoreleasePool 使用的误区。其中最常见的就是在循环中过度使用 @autoreleasepool,例如:

for (int i = 0; i < 10; i++) {
    @autoreleasepool {
        Person *per = [[Person alloc] init];
        // 对 per 进行的操作……
    }
}

这段代码中的 per 对象是 __strong 类型,它会在每次循环结束时立即释放,无需等待自动释放池的清理。这种情况下添加 @autoreleasepool 并不会带来任何性能优势,反而会因为频繁创建和销毁自动释放池而增加开销。

因此,@autoreleasepool 的使用需要遵循以下原则:

  1. 循环中创建了大量 autoreleasing 对象时才使用。这种情况下才能有效降低内存峰值,避免内存持续增长。

  2. 调用未知方法时需要谨慎评估。因为方法内部可能会创建 autoreleasing 对象,此时需要使用 @autoreleasepool 包裹,否则也会导致内存峰值过高。

注意:错误的使用 @autoreleasepool 不仅不会带来性能优势,反而会因为频繁创建和销毁自动释放池而产生额外开销。所以在使用前,建议先分析代码中的对象创建情况,再决定是否需要手动管理自动释放池。

技术总结

通过对 AutoreleasePool 底层实现的深入分析,我们可以总结出以下几个关键点:

  1. 底层数据结构

    • AutoreleasePool 本质是由 AutoreleasePoolPage 对象组成的双向链表;
    • 每个 Page 目前的大小为 4KB,按系统内存页对齐以优化性能;
    • 每个线程的 TLS 中存储着链表的尾节点(hotPage);
    • Page 中除了存储对象指针外,还包含 magic、next、thread 等重要信息。
  2. 对象管理机制

    • 添加对象时,系统首先获取 hotPage,根据其状态执行不同逻辑:

      • Page 存在且未满:直接添加对象
      • Page 存在但已满:创建新 Page 后添加
      • Page 不存在:创建首个 Page 后添加
    • 清理时,系统会从链表末尾往前遍历,逐个释放对象直到遇到指定的边界标记(POOL_BOUNDARY)。

  3. 性能优化方案

    • 对象合并优化:相同对象重复入池时,通过 count 计数避免重复存储;
    • TLS 返回值优化:方法返回对象时,通过 TLS 机制避免自动释放池的中转;
    • 内存对齐:Page 大小与系统页对齐,提高内存访问效率;
    • 双向链表:支持快速的正向和反向遍历,适应不同的使用场景。

这些精妙的设计不仅保证了 AutoreleasePool 的正确性,还在性能和内存使用效率上都做到了极致的优化。

参考资料

iOS 中的 RunLoop 详解

作者 90后晨仔
2025年3月30日 20:03

1. 什么是 RunLoop?

RunLoop(运行循环) 是 iOS/macOS 开发中用于管理线程事件和消息的核心机制。它通过一个循环不断监听和处理输入事件(如触摸、定时器、网络数据),并在没有任务时让线程休眠以节省资源。

  • 本质:基于 mach port 的事件循环机制。
  • 核心作用:让线程在有任务时执行任务,无任务时休眠,避免资源浪费。

2. RunLoop 与线程的关系

  • 主线程:默认自动创建并启动 RunLoop(UIApplicationMain 函数中启动)。
  • 子线程:默认不开启 RunLoop,需手动创建和启动。
  • 生命周期:线程与 RunLoop 一一对应,存储在全局字典中(线程销毁时 RunLoop 也释放)。

3. RunLoop 的核心组成

组件 作用
Modes(模式) 定义 RunLoop 在不同场景下监听的事件源(Source)和观察者(Observer)。
Sources(事件源) - Source0:处理 App 内部事件(如触摸、PerformSelector)。
- Source1:基于 mach port 的系统事件(如硬件事件)。
Timers(定时器) 基于 RunLoop 的定时器(如 NSTimer),受 RunLoop Mode 影响。
Observers(观察者) 监听 RunLoop 的状态变化(如即将处理事件、即将休眠等),用于性能监控。

4. RunLoop 的 Mode(模式)

模式 描述
Default(NSDefaultRunLoopMode 默认模式,处理大多数事件(如普通触摸、网络请求)。
Tracking(UITrackingRunLoopMode 界面滚动时的模式(如 UIScrollView 滑动),优先保证滑动流畅。
Common(NSRunLoopCommonModes 一个虚拟模式,包含多个模式的集合(Default + Tracking),用于通用事件监听。

5. RunLoop 的典型应用场景

(1) 保持线程存活

通过 RunLoop 让子线程持续处理任务而不退出:

class BackgroundThread {
    private var thread: Thread?
    private var shouldKeepRunning = true
    
    func start() {
        thread = Thread { [weak self] in
            // 创建并启动 RunLoop
            let runLoop = RunLoop.current
            runLoop.add(NSMachPort(), forMode: .default)
            while self?.shouldKeepRunning == true {
                runLoop.run(mode: .default, before: .distantFuture)
            }
        }
        thread?.start()
    }
    
    func stop() {
        shouldKeepRunning = false
        thread?.cancel()
    }
}

(2) 优化滚动性能

将耗时任务放在 Default 模式,避免滚动时执行(如 UIScrollView 滑动):

DispatchQueue.main.async {
    // 在 Default 模式下执行任务
    RunLoop.current.perform(inMode: .default) {
        // 处理耗时任务(如数据解析)
    }
}

(3) 监听 RunLoop 状态

通过 CFRunLoopObserver 监控主线程卡顿:

let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { observer, activity in
    switch activity {
    case .beforeWaiting:
        print("RunLoop 即将休眠")
    case .afterWaiting:
        print("RunLoop 被唤醒")
    default: break
    }
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)

(4) 解决定时器失效问题

在滚动模式下(如 UIScrollView 滑动时),将 NSTimer 添加到 CommonModes

let timer = Timer(timeInterval: 1.0, repeats: true) { _ in
    print("Timer fired")
}
RunLoop.current.add(timer, forMode: .common)

6. RunLoop 的工作流程

  1. 接收事件:通过 mach port 接收系统事件(Source1)或 App 内部事件(Source0)。
  2. 通知 Observers:即将处理事件(kCFRunLoopBeforeTimerskCFRunLoopBeforeSources)。
  3. 处理事件:执行触发的 Source0、Source1 和 Timer 事件。
  4. 进入休眠:若没有事件,调用 mach_msg 让线程休眠,等待新事件唤醒。
  5. 唤醒处理:被唤醒后处理新到达的事件,循环往复。

7. 注意事项

  • 避免阻塞主线程:主线程 RunLoop 负责 UI 更新,长时间阻塞会导致卡顿。
  • 子线程 RunLoop 管理:手动创建的 RunLoop 需自行控制生命周期(如添加保活 Source)。
  • 性能优化:合理使用 Mode 隔离不同任务(如滑动时暂停后台任务)。

总结

场景 RunLoop 的作用 关键技术点
线程保活 让子线程持续处理任务 添加保活 Source(如 NSMachPort
滑动性能优化 隔离耗时任务与滚动事件 使用 CommonModes 或模式切换
定时器管理 确保定时器在滚动时仍有效 注册到 CommonModes
卡顿监控 监听主线程 RunLoop 状态变化 CFRunLoopObserver

RunLoop 是 iOS 系统高效管理线程和事件的核心机制,合理利用可优化性能、实现复杂任务调度。在大多数情况下,开发者无需直接操作 RunLoop,但理解其原理对解决卡顿、线程管理等问题至关重要。

Swift基础知识(三)

作者 90后晨仔
2025年3月29日 19:21

Swift 与 Objective-C 中的自省(Introspection)对比


1. 核心概念

  • 自省(Introspection):在运行时检查对象的类型或是否符合特定协议的能力。
  • Objective-C:基于 NSObject 的类体系,主要用于检查类继承关系。
  • Swift:支持所有类型(类、结构体、枚举),不依赖 NSObject,且涵盖协议检查。

2. 类型检查语法与行为

特性 Objective-C Swift
检查类继承关系 isKindOfClass:(判断类或子类)
isMemberOfClass:(仅判断自身类)
is 操作符(判断类型或子类型)
支持类型 NSObject 子类 所有类型(类、结构体、枚举)
协议一致性检查 conformsToProtocol: is(检查协议类型)
as?(转换并检查协议)
值类型支持 不支持(仅类) 支持(结构体、枚举)
泛型支持 支持(结合泛型类型检查)

3. 具体用法与示例

Objective-C
// 类继承检查
BOOL isView = [obj isKindOfClass:[UIView class]];       // YES(obj 是 UIView 或其子类实例)
BOOL isExactView = [obj isMemberOfClass:[UIView class]]; // NO(obj 必须是 UIView 实例)

// 协议检查
BOOL conforms = [obj conformsToProtocol:@protocol(NSCopying)];
Swift
// 类型检查
let obj: Any = "Hello"
if obj is String { // true
    print("是 String 类型")
}

// 协议检查
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() { } }

let shape: Any = Circle()
if shape is Drawable { // true
    print("符合 Drawable 协议")
}

// 值类型检查
let value: Any = 100
if value is Int { // true
    print("是 Int 类型")
}

// 类型转换 + 检查
if let drawableShape = shape as? Drawable {
    drawableShape.draw()
}

4. 核心区别总结

维度 Objective-C Swift
类型体系依赖 必须继承自 NSObject 不依赖任何基类,支持所有类型
检查范围 仅类(包括子类) 类、结构体、枚举、协议
协议检查 需显式调用 conformsToProtocol: 直接通过 isas? 检查
语法简洁性 方法调用(冗长) 操作符(简洁直观)
泛型支持 支持泛型类型检查

5. 高级特性(Swift 独有)

  • 模式匹配:结合 switch 语句进行类型和协议匹配。
    func checkType(_ value: Any) {
        switch value {
        case is String: print("字符串类型")
        case let num as Int where num > 0: print("正整数")
        case is Drawable: print("可绘制对象")
        default: break
        }
    }
    
  • 元类型检查:通过 .Type 获取类型元信息。
    let type: Int.Type = Int.self
    let instance = type.init(10) // 创建 Int 实例
    

6. 使用建议

  • Objective-C:在维护旧项目或与 Cocoa 框架交互时使用,注意仅适用于 NSObject 子类。
  • Swift:在新项目中优先使用 isas?,充分利用其对值类型和协议的支持,提升代码灵活性和安全性。

三、Swift 闭包(Closures)与 Objective-C Block 的对比

1. 内存分配与结构

特性 Swift 闭包 Objective-C Block
底层数据结构 闭包是 捕获上下文的函数,本质是结构体 Block 是 封装函数指针的结构体对象
内存位置 默认栈分配,逃逸闭包自动提升到堆 默认栈分配,需显式 copy 到堆
结构体布局 闭包结构体包含函数指针和捕获的上下文数据 Block 结构体包含 isa 指针、函数指针、捕获变量等
生命周期管理 通过 引用计数(ARC) 管理堆内存 手动 copy/release 或 ARC 管理堆内存

2. 变量捕获机制

特性 Swift 闭包 Objective-C Block
值类型捕获 捕获值类型的副本(深拷贝) 默认捕获值类型变量的 原始值(需 __block 修饰允许修改)
引用类型捕获 捕获引用类型的强引用(需通过捕获列表弱化) 捕获对象的强引用(需 __weak 弱化)
可变性支持 通过 varinout 参数捕获可变变量 __block 修饰符实现变量可变性

3. 函数执行与上下文

特性 Swift 闭包 Objective-C Block
函数指针 闭包的函数指针直接指向编译生成的函数代码 Block 的函数指针通过 invoke 成员指向代码
上下文管理 闭包通过结构体存储捕获的变量(值或引用) Block 通过结构体的 descriptor 管理捕获变量引用计数
逃逸性处理 编译器自动检测逃逸闭包,并生成堆分配逻辑 需手动调用 copy 将 Block 从栈复制到堆

4. 底层实现细节

Swift 闭包
  1. 结构体表示

    // 伪代码表示闭包结构体
    struct Closure<T> {
        var function: (T) -> Void  // 函数指针
        var capturedValues: [Any]  // 捕获的上下文数据
    }
    
  2. 内存管理

    • 非逃逸闭包:栈分配,函数返回后销毁。
    • 逃逸闭包:自动复制到堆,由 ARC 管理生命周期。
  3. 捕获列表优化

    • 显式声明 [weak self][unowned self],避免隐式强引用。
    • 编译器生成代码时,捕获列表直接修改闭包结构体的成员引用类型。
Objective-C Block
  1. 结构体表示

    // 伪代码表示 Block 结构体
    struct Block_layout {
        void *isa;                  // 指向 Block 类型(栈/堆/全局)
        int flags;                  // 状态标记(是否被拷贝等)
        int reserved;               // 保留字段
        void (*invoke)(void *, ...); // 函数指针
        struct Block_descriptor *descriptor; // 描述符(引用计数、捕获变量等)
        // 捕获的变量数据...
    };
    
  2. 内存管理

    • 栈 Block:默认创建在栈上,函数返回后失效。
    • 堆 Block:通过 copy 操作复制到堆,由引用计数管理。
  3. 变量捕获

    • __block 修饰的变量会被包装为 Block_byref 结构体,允许跨 Block 修改。
    • 对象类型变量通过 retain/release 管理引用计数(ARC 下自动处理)。

5. 性能对比

维度 Swift 闭包 Objective-C Block
内存开销 更小(结构体直接存储捕获数据) 较大(包含 isaflags 等元数据)
执行速度 更快(直接函数指针调用) 稍慢(需通过 invoke 间接调用)
捕获变量修改 灵活(值类型副本独立修改) __block 包装,性能开销较大

6. 总结

  • Swift 闭包

    • 更轻量:基于结构体和值语义,减少内存开销。
    • 更安全:编译时检查捕获列表,避免循环引用。
    • 更高效:栈分配优化 + 直接函数调用。
  • Objective-C Block

    • 更底层:直接操作结构体和引用计数,灵活性高。
    • 兼容性:与 Cocoa 框架深度集成,适合传统代码维护。
    • 显式控制:需手动管理 copy__block 修饰符。

底层核心差异

  • Swift 闭包是 值类型结构体,通过编译优化实现高效捕获和内存管理。
  • Objective-C Block 是 对象,依赖运行时元数据(isadescriptor)管理生命周期和捕获变量。

Swift基础知识(二)

作者 90后晨仔
2025年3月29日 18:53

一、Swift 属性观察器(Property Observers)

1. 核心概念

属性观察器用于监听存储属性值的变化,在值被修改前后执行自定义逻辑。包含两种观察器:

  • willSet:在值即将被设置前调用,默认提供 newValue 参数(新值)。
  • didSet:在值已被设置后调用,默认提供 oldValue 参数(旧值)。

2. 使用条件

  • 适用对象:仅用于 lazyvar 存储属性
    var score: Int = 0 {  // 正确:非 lazy 的存储属性
        willSet { print("新值:\(newValue)") }
        didSet { print("旧值:\(oldValue)") }
    }
    
    // lazy var data: [Int] = loadData() { willSet { } } // 错误:lazy 属性不支持
    
  • 不适用场景
    • 常量(let 属性)。
    • 计算属性(需通过 set 方法监听)。
    • 延迟存储属性(lazy var)。

3. 注意事项

  • 初始化不触发:属性在初始化赋值时不会调用观察器。
    struct User {
        var name: String = "Guest" { // 初始化赋值不会触发观察器
            didSet { print("名字被修改") }
        }
    }
    
  • 自定义参数名:可显式命名参数(如 willSet(newScore))。
  • 避免循环修改:在 didSet 中修改属性本身会再次触发观察器。

4. 使用场景

  • 数据验证:确保值在合法范围内。
    var age: Int = 0 {
        didSet {
            age = min(max(age, 0), 120)  // 限制年龄在 0~120
        }
    }
    
  • UI 同步更新:值变化时刷新界面。
    var isLoggedIn: Bool = false {
        didSet {
            updateLoginUI()
        }
    }
    
  • 日志记录:跟踪属性变化历史。
    var temperature: Double = 25.0 {
        willSet { print("温度将从 \(temperature) 变为 \(newValue)") }
        didSet { print("温度已从 \(oldValue) 更新为 \(temperature)") }
    }
    

5. 与计算属性的区别

特性 属性观察器 计算属性
用途 监听存储属性变化 通过计算动态获取值
触发时机 属性值被修改前后 每次访问时计算
存储能力 需有存储空间(必须初始化) 无存储空间(依赖其他属性)
语法 willSet/didSet get + set(或只读 get

6. 总结

  • 核心作用:在属性值变化前后注入逻辑,提升代码灵活性和可维护性。
  • 适用场景:数据验证、日志记录、UI 同步等。
  • 限制:仅适用于非 lazyvar 存储属性,初始化不触发。

二、Swift 中的异常捕获

Swift 通过 Error 协议和 throws 关键字实现异常处理,提供多种方式捕获和处理错误。以下是主要的异常捕获方法:

1. do-catch 语句(详细错误处理)

  • 用法:捕获特定错误类型并处理。
  • 适用场景:需要根据不同错误类型执行不同逻辑。
  • 示例
    enum NetworkError: Error {
        case invalidURL
        case timeout(seconds: Int)
    }
    
    func fetchData() throws {
        throw NetworkError.timeout(seconds: 30)
    }
    
    do {
        try fetchData()
    } catch NetworkError.invalidURL {
        print("URL 无效")
    } catch NetworkError.timeout(let seconds) {
        print("请求超时:\(seconds) 秒")
    } catch {
        print("未知错误:\(error)")
    }
    

2. try?(转换为可选类型)

  • 用法:忽略具体错误,返回 nil
  • 适用场景:不关心错误细节,只需判断是否成功。
  • 示例
    let result = try? someThrowingFunction()
    if let data = result {
        // 成功
    } else {
        // 失败
    }
    

3. try!(强制解包,慎用)

  • 用法:假设函数不会抛出错误,若出错则触发运行时崩溃。
  • 适用场景:确信代码不会失败(如本地静态数据加载)。
  • 示例
    let data = try! loadLocalConfig() // 确定本地文件存在时使用
    

4. 向上传递错误(throws 关键字)

  • 用法:函数声明 throws,错误由调用者处理。
  • 适用场景:将错误传递给上层调用链。
  • 示例
    func processFile() throws {
        let content = try String(contentsOfFile: "path/to/file")
        // 处理内容
    }
    
    // 调用处需处理或继续传递
    do {
        try processFile()
    } catch {
        print("文件处理失败:\(error)")
    }
    

5. rethrows(传递闭包中的错误)

  • 用法:函数本身不产生错误,但可能传递闭包的异常。
  • 适用场景:高阶函数(如 mapfilter)中处理闭包可能抛出的错误。
  • 示例
    func customMap<T>(_ array: [T], _ transform: (T) throws -> T) rethrows -> [T] {
        var result = [T]()
        for item in array {
            try result.append(transform(item))
        }
        return result
    }
    
    let numbers = [1, 2, 3]
    let doubled = try? customMap(numbers) { num in
        if num == 2 { throw NetworkError.invalidURL }
        return num * 2
    }
    

6. 异步错误处理(async/await

  • 用法:在异步函数中使用 throwstry
  • 适用场景:异步操作中的错误处理。
  • 示例
    func downloadData() async throws -> Data {
        let url = URL(string: "https://example.com")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
    
    Task {
        do {
            let data = try await downloadData()
        } catch {
            print("下载失败:\(error)")
        }
    }
    

总结

方法 关键字 特点 适用场景
详细错误处理 do-catch 精准捕获错误类型 需要区分不同错误逻辑
可选值简化 try? 忽略错误,返回 nil 不关心错误细节
强制解包(高危) try! 假设成功,崩溃风险高 确定不会失败的场景
向上传递错误 throws 将错误传递给调用者 多层调用链中的错误处理
闭包错误传递 rethrows 传递闭包中的错误 高阶函数中的闭包操作
异步错误处理 async throws 结合 async/await 处理异步错误 网络请求、文件读写等异步操作

最佳实践

  • 优先使用 do-catch 处理可预见的错误。
  • 谨慎使用 try!,确保代码绝对安全。
  • 异步操作中结合 async/await 提升可读性。

三、Swift 中 defer 关键字的详解

1. 基本概念

defer 用于定义一个代码块(延迟执行块),该代码块会在 当前作用域结束前 执行,无论作用域是通过正常返回、抛出错误还是其他控制流(如 returnbreak)结束的。
核心作用:确保清理或收尾逻辑(如资源释放)一定会执行,避免资源泄漏。


2. 使用场景

  • 资源管理:文件操作、网络请求、锁的获取与释放等。
  • 错误处理:在抛出错误前确保清理逻辑执行。
  • 代码可读性:将清理代码紧跟在初始化代码后,提高可维护性。

3. 基本用法

func readFile() {
    let file = openFile()
    defer {
        closeFile(file) // 作用域结束前执行
    }
    // 处理文件...
    if someCondition {
        return // 触发 defer
    }
    // 更多操作...
} // 函数结束,触发 defer

4. 执行规则

  • 逆序执行:同一作用域内的多个 defer定义顺序的逆序 执行(类似栈结构)。
    defer { print("1") }
    defer { print("2") }
    // 输出:2 → 1
    
  • 作用域限制defer 仅在 当前作用域 有效(如函数、循环、条件语句)。
    func example() {
        if condition {
            defer { print("if 块结束") }
            // ...
        } // 此处执行 defer
        // 函数后续代码...
    }
    

5. 常见应用示例

(1) 文件操作
func readFile(path: String) throws -> String {
    let file = try openFile(path)
    defer {
        closeFile(file) // 确保文件关闭
    }
    return try parseFile(file)
}
(2) 加锁与解锁
let lock = NSLock()
func criticalSection() {
    lock.lock()
    defer { lock.unlock() } // 确保锁释放
    // 执行关键代码...
}
(3) 循环中的资源清理
for url in urls {
    let data = try downloadData(from: url)
    defer { 
        cleanupTemporaryFiles() // 每次循环结束清理
    }
    process(data)
}

6. 注意事项

  • 禁止控制流操作defer 块中不能使用 breakreturn 或抛出错误。
  • 避免副作用:修改外部变量可能导致逻辑混乱。
    var value = 0
    func example() {
        defer { value += 1 } // 不推荐
        // ...
    }
    
  • 性能影响:频繁使用 defer 可能影响性能(极少数情况)。

7. 错误处理中的 defer

即使函数抛出错误,defer 仍会执行:

func riskyOperation() throws {
    let resource = allocateResource()
    defer { releaseResource(resource) } // 错误抛出前执行
    try mayThrowError()
}

8. 总结

场景 示例 作用
文件/网络资源释放 defer { file.close() } 防止资源泄漏
锁的获取与释放 defer { lock.unlock() } 避免死锁
错误处理中的清理 defer { cleanup() } 确保异常时资源释放
临时数据清理 defer { removeTempFiles() } 提升代码健壮性

最佳实践

  • defer 紧跟在资源获取代码后,提高可读性。
  • 避免在 defer 中执行耗时操作或修改外部状态。
  • 优先用于必须执行的清理逻辑,而非复杂业务代码。

通过合理使用 defer,可以显著提升代码的健壮性和可维护性,确保资源安全和逻辑清晰。


四、Swift 与 Objective-C 中的 Protocol 区别

1. 基本概念

  • Objective-C:Protocol 是一种定义方法列表的方式,用于声明接口,实现类必须遵守这些接口(除非标记为 @optional)。
  • Swift:Protocol 不仅定义接口,还支持属性、方法、关联类型、默认实现等,是面向协议编程(POP)的核心工具。

2. 核心区别

特性 Objective-C Protocol Swift Protocol
可选方法 使用 @optional 标记可选方法 默认无可选方法,可通过 @objc optional 兼容 OC,或通过协议扩展提供默认实现
默认实现 不支持 支持通过 协议扩展 提供默认实现
关联类型 不支持 支持 associatedtype,实现泛型协议
值类型支持 仅适用于类(Class) 适用于类、结构体(Struct)、枚举(Enum)
协议继承与组合 单继承(只能继承一个协议) 支持多继承(ProtocolA & ProtocolB
泛型支持 支持泛型约束(where 子句)
属性定义 只能定义方法 可定义属性(需指定 { get }{ get set }
类型检查 运行时检查(conformsToProtocol: 编译时检查 + 运行时检查(isas?

3. 使用场景与示例

Objective-C Protocol
@protocol DataSource <NSObject>
@required
- (NSInteger)numberOfItems;
@optional
- (NSString *)titleForItemAtIndex:(NSInteger)index;
@end

// 类遵循协议
@interface ViewController : UIViewController <DataSource>
@property(nonatomic,weak)id <DataSource> delegate;
@end
  • 特点:主要用于委托模式(Delegate)、数据源模式(DataSource)。
Swift Protocol
protocol Drawable {
    func draw()
}

// 协议扩展提供默认实现
extension Drawable {
    func draw() { print("默认绘制") }
}

struct Circle: Drawable {} // 结构体遵循协议
class Square: Drawable {}  // 类遵循协议

// 关联类型
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

// 泛型约束
func process<T: Container>(container: T) where T.Item: Equatable {
    // ...
}
  • 特点:支持面向协议编程,适用于值类型和引用类型,灵活扩展功能。

4. 协议扩展(Swift 独有)

Swift 允许通过扩展为协议添加默认实现,而 Objective-C 无法实现:

protocol Loggable {
    func log()
}

extension Loggable {
    func log() { print("日志记录") }
}

struct User: Loggable {} // 自动获得默认 log() 实现

5. 可选方法的实现方式

  • Objective-C:显式标记 @optional,调用时需检查 respondsToSelector:

    @protocol NetworkDelegate <NSObject>
    @optional
    - (void)didReceiveData:(NSData *)data;
    @end
    
    // 调用前检查
    if ([delegate respondsToSelector:@selector(didReceiveData:)]) {
        [delegate didReceiveData:data];
    }
    
  • Swift:通过协议扩展或 @objc optional(仅限类)实现可选方法。

    @objc protocol NetworkDelegate {
        @objc optional func didReceiveData(_ data: Data)
    }
    
    class Handler: NetworkDelegate {} // 可选实现方法
    
    // 调用时检查
    delegate?.didReceiveData?(data)
    

6. 总结

场景 Objective-C Swift
接口定义 委托、数据源等简单场景 面向协议编程、泛型抽象、功能扩展
类型支持 仅类 类、结构体、枚举
灵活性 基础功能,依赖运行时检查 编译时安全,支持默认实现和关联类型
跨平台设计 主要用于 iOS/macOS 开发 跨平台(iOS/macOS/服务器/开源项目)

选择建议

  • Objective-C:在传统项目或需要与 OC 代码交互时使用,适合简单接口定义。
  • Swift:在新项目或需要高度抽象、复用和类型安全时使用,充分发挥面向协议编程的优势。

五、Swift 与 Objective-C 初始化方法(init)有什么不一样?

1. 核心设计理念

  • Swift:强调 安全性严格性,通过两段式初始化编译时检查确保对象完整初始化。
  • Objective-C灵活性优先,依赖开发者自觉管理初始化过程,缺少编译时强制约束。

2. 主要区别

特性 Swift Objective-C
初始化阶段 两段式初始化(属性初始化 → 自定义操作) 无明确阶段划分
安全检查 强制所有非可选(non-optional)存储属性初始化 无强制检查,未初始化的属性可能为 nil 或默认值
可选属性 必须显式初始化或声明为 Optional 默认允许 nil(引用类型)
初始化器类型 支持 指定初始化器(Designated)和 便利初始化器(Convenience) 无明确分类,但可通过 NS_DESIGNATED_INITIALIZER 标记指定初始化器
修饰符 convenience(便利初始化器)、required(强制子类实现)、override(重写父类初始化器) 无类似关键字
可失败初始化器 支持 init?init! 返回 nil 表示失败
继承与重写规则 子类必须重写父类的指定初始化器或自动继承,规则严格 子类可自由重写初始化器,无强制要求
值类型支持 结构体(struct)、枚举(enum)可定义初始化器 仅类(class)支持初始化器

3. 关键机制详解

(1) 两段式初始化(Swift 独有)
  • 阶段 1:确保所有存储属性被初始化。
  • 阶段 2:在属性初始化完成后,进一步自定义实例(如调用方法、访问 self)。
  • 优势:避免属性未初始化就被使用,提升安全性。
  • 示例
    class Person {
        var name: String
        var age: Int
        
        // 指定初始化器
        init(name: String, age: Int) {
            self.name = name // 阶段1:初始化属性
            self.age = age
            // 阶段2:可调用方法
            setup()
        }
        
        convenience init() {
            self.init(name: "Unknown", age: 0) // 必须调用指定初始化器
        }
        
        private func setup() { /*...*/ }
    }
    
(2) 强制属性初始化(Swift)
  • 所有非可选存储属性必须在初始化完成前赋值,否则编译器报错。
  • 示例
    class User {
        var id: Int    // 错误:未初始化
        var name: String?
        
        init(id: Int) {
            self.id = 1234 // 正确:非可选属性必须赋值
        }
    }
    
(3) 初始化器修饰符(Swift)
  • convenience:定义便利初始化器,必须调用同类中的指定初始化器。
    class Rectangle {
        var width: Double
        var height: Double
        
        init(width: Double, height: Double) {
            self.width = width
            self.height = height
        }
        
        convenience init(side: Double) {
            self.init(width: side, height: side) // 调用指定初始化器
        }
    }
    
  • required:强制子类实现该初始化器。
    class Vehicle {
        required init() { /*...*/ }
    }
    
    class Car: Vehicle {
        required init() { // 必须实现
            super.init()
        }
    }
    
(4) 可失败初始化器
  • Swift:通过 init? 返回可选实例,init! 返回隐式解包实例。
    struct Temperature {
        let celsius: Double
        init?(celsius: Double) {
            guard celsius >= -273.15 else { return nil }
            self.celsius = celsius
        }
    }
    
  • Objective-C:返回 nil 表示失败。
    @interface MyClass : NSObject
    - (instancetype)initWithValue:(NSInteger)value;
    @end
    
    @implementation MyClass
    - (instancetype)initWithValue:(NSInteger)value {
        if (value < 0) return nil;
        self = [super init];
        return self;
    }
    @end
    

4. 初始化器继承规则

  • Swift
    • 子类默认不继承父类初始化器。
    • 若子类未定义任何指定初始化器,则自动继承父类所有指定初始化器。
    • 若子类实现了父类所有指定初始化器,则自动继承父类便利初始化器。
  • Objective-C:子类自动继承父类所有初始化器,除非显式重写。

5. 总结

场景 Swift Objective-C
安全性 高(编译时强制检查) 低(依赖开发者自觉)
灵活性 较低(严格规则限制) 高(自由定义初始化逻辑)
代码复杂度 高(需遵循两段式、修饰符规则) 低(简单直接)
适用类型 类、结构体、枚举 仅类
错误处理 可失败初始化器、异常抛出 返回 nilNSError

选择建议

  • Swift:在需要高安全性、复杂类型设计的场景下使用,遵循严格初始化规则。
  • Objective-C:在维护旧项目或需要快速灵活初始化时使用,但需注意潜在风险。

Swift 基础知识(一)

作者 90后晨仔
2025年3月29日 17:56

一、Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点?

核心区别

特性 类(Class) 结构体(Struct)
类型 引用类型(传递时共享内存) 值类型(传递时复制新实例)
存储位置 堆内存 栈内存(若包含引用类型属性,其数据仍在堆内存)
继承 支持(除非用 final 修饰) 不支持
类型转换 支持运行时类型检查(is/as 仅编译时类型检查
内存管理 引用计数(ARC) 自动栈内存释放(无需引用计数)
方法派发 动态派发(运行时确定方法地址) 静态派发(编译时确定方法地址)
析构函数 支持 deinit 不支持
线程安全 需手动处理(共享内存) 天然线程安全(值隔离)

赋值行为的本质

  • 类(浅拷贝)
    赋值时复制指针,新旧变量指向同一内存地址。修改任一变量会影响所有引用。

    class Person {
        var name: String
        init(name: String) { self.name = name }
    }
    let p1 = Person(name: "Alice")
    let p2 = p1
    p2.name = "Bob"
    print(p1.name) // 输出 "Bob"
    
  • 结构体(深拷贝)
    赋值时复制值,新旧变量独立存储。修改一个不会影响另一个。

    struct Point {
        var x: Int
        var y: Int
    }
    var p1 = Point(x: 1, y: 2)
    var p2 = p1
    p2.x = 3
    print(p1.x) // 输出 1
    

优缺点分析

类(Class)的优点
  1. 共享与可变状态:适合需要多个对象共享同一数据的场景(如网络请求管理器)。
  2. 继承与多态:支持面向对象设计,复用代码逻辑。
  3. 运行时动态性:支持类型检查、方法重写和 KVO。
类(Class)的缺点
  1. 内存开销:堆内存分配和引用计数管理增加性能损耗。
  2. 线程安全隐患:共享内存需额外同步机制(如锁、GCD)。
  3. 复杂性:需处理循环引用(weak/unowned)。
结构体(Struct)的优点
  1. 高性能:栈内存分配速度快,无引用计数开销。
  2. 线程安全:值隔离特性天然避免共享内存冲突。
  3. 清晰所有权:深拷贝行为减少意外副作用。
结构体(Struct)的缺点
  1. 无法继承:不支持继承,复用代码需依赖协议和组合。
  2. 不适合复杂对象:频繁深拷贝大对象可能导致性能问题。

使用场景建议

场景 推荐类型 理由
轻量级数据模型(如坐标、颜色) 结构体 值类型更安全高效,无共享风险
需要继承或共享状态的组件 利用面向对象特性实现复用和动态行为
高频创建的临时对象 结构体 栈内存快速分配释放,减少堆内存压力
需线程安全的数据容器 结构体 值隔离避免竞态条件

补充说明

  1. 结构体中包含引用类型
    若结构体的属性是类实例,深拷贝时仅复制引用(指针),数据仍共享:

    struct Container {
        var ref: NSMutableArray // 引用类型属性
    }
    var c1 = Container(ref: NSMutableArray())
    var c2 = c1
    c2.ref.add("Test") // c1.ref 也会被修改
    
  2. 协议与扩展
    结构体和类均可遵循协议并使用扩展,但结构体无法通过继承实现多态。

  3. Copy-on-Write 优化
    Swift 标准库中的集合类型(如 ArrayDictionary)对结构体实现了写时复制,避免不必要的深拷贝。


官方建议

Apple 在 Swift 官方文档 中明确推荐:

优先使用结构体,除非你需要类独有的特性(如继承、析构函数或引用语义)。

二、 Swift 中什么是可选类型?

1. 什么是可选类型?

可选类型(Optional)是 Swift 中一种特殊的数据类型,用于表示一个变量可能有值(如 IntString 等),也可能没有值(nil)。它的核心设计目的是强制开发者显式处理值缺失的情况,从而避免空指针异常(Null Pointer Exception),提升代码的安全性。


2. 可选类型的语法

  • 声明可选类型:在类型后添加 ?

    var name: String?  // 可能为 String 或 nil
    var age: Int?      // 可能为 Int 或 nil
    
  • 隐式解包可选类型:在类型后添加 !

    var forcedValue: String!  // 声明时允许为 nil,但使用时假设已赋值
    

3. 可选类型的本质

可选类型是一个泛型枚举,定义如下:

public enum Optional<Wrapped> {
    case none       // 无值(nil)
    case some(Wrapped) // 有值(Wrapped 类型)
}

例如,String? 实际上是 Optional<String> 的简写。


4. 可选类型的解包方式

(1) 强制解包(Force Unwrap)

使用 ! 强制解包,但如果值为 nil 会触发运行时错误。

let name: String? = "Alice"
print(name!) // 输出 "Alice"

let age: Int? = nil
print(age!) // 运行时崩溃!
(2) 可选绑定(Optional Binding)

通过 if let 或 guard let 安全解包:

if let unwrappedName = name {
    print("Name is (unwrappedName)")
} else {
    print("Name is nil")
}
(3) 空合并运算符(Nil-Coalescing Operator)

提供默认值:

let name = optionalName ?? "Unknown"
(4) 可选链式调用(Optional Chaining)

安全访问属性或方法:

let length = user?.name?.count // 若任一环节为 nil,返回 nil

5. 隐式解包可选类型(Implicitly Unwrapped Optional)

  • 用途:用于变量初始化后一定会被赋值,但声明时可能为 nil 的场景(如 IBOutlet)。
  • 风险:若访问时仍为 nil,会触发运行时错误。
@IBOutlet weak var label: UILabel! // 隐式解包类型

6. 可选类型的优势

  • 安全性:编译器强制处理 nil 情况,减少崩溃风险。
  • 清晰性:明确标记可能缺失的值,提升代码可读性。
  • 灵活性:与 Swift 的强类型系统结合,支持泛型和模式匹配。

7. 使用场景

  • 网络请求结果:可能成功(有数据)或失败(nil)。

  • 用户输入处理:如文本框内容可能为空。

  • 类型转换:尝试将 Any 转换为具体类型时可能失败。

    let value: Any = "123"
    let number = value as? Int // Int?
    

8. 注意事项

  • 避免滥用 ! :强制解包应仅在确定值非 nil 时使用。
  • 优先使用可选绑定if let 和 guard let 更安全。
  • 合理使用隐式解包:仅用于生命周期明确的变量(如 UI 控件)。

9. 示例代码

// 定义一个可能为 nil 的可选类型
var optionalNumber: Int? = 42

// 安全解包
if let number = optionalNumber {
    print("The number is (number)")
} else {
    print("The number is nil")
}

// 空合并运算符
let validNumber = optionalNumber ?? 0

// 可选链式调用
struct Person {
    var address: Address?
}

struct Address {
    var street: String?
}

let person: Person? = Person(address: Address(street: "Main St"))
let street = person?.address?.street // 类型为 String?

10. 总结

可选类型是 Swift 语言的核心特性之一,通过编译时的严格检查,显著提升了代码的健壮性。合理使用可选类型及其解包方式,能有效避免空指针异常,同时使代码逻辑更加清晰可靠。

三、Swift 中什么 是 泛型?

泛型是 Swift 语言中的核心特性之一,允许开发者编写灵活、可重用且类型安全的代码。通过泛型,可以定义适用于多种数据类型的函数、类、结构体或枚举,而无需重复编写相同逻辑的代码。


1. 泛型的基本概念

泛型通过类型参数化实现,类型参数在定义时作为占位符(如 <T>),在使用时由具体类型替换。例如:

// 泛型函数:交换任意类型的两个值
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 5, y = 10
swapTwoValues(&x, &y) // T 被推断为 Int

var str1 = "Hello", str2 = "World"
swapTwoValues(&str1, &str2) // T 被推断为 String

2. 泛型的核心作用

  • 代码复用:同一套逻辑可处理不同类型的数据。
  • 类型安全:编译器在编译时检查类型,避免运行时错误。
  • 性能优化:泛型在编译时生成具体类型的代码,无运行时开销。

3. 泛型的使用场景

(1) 泛型函数

适用于需要处理多种类型的算法(如排序、交换):

func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
    for (index, item) in array.enumerated() {
        if item == value { return index }
    }
    return nil
}

let names = ["Alice", "Bob"]
let index = findIndex(of: "Bob", in: names) // 返回 1
(2) 泛型类型

定义可存储任意类型数据的容器(如自定义集合):

struct Stack<Element> {
    private var elements = [Element]()
    mutating func push(_ element: Element) {
        elements.append(element)
    }
    mutating func pop() -> Element? {
        elements.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)

var stringStack = Stack<String>()
stringStack.push("Swift")
(3) 泛型协议

通过 associatedtype 定义协议中的关联类型,允许遵循协议的类型自定义具体类型:

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntList: Container {
    typealias Item = Int
    private var items = [Int]()
    var count: Int { items.count }
    mutating func append(_ item: Int) {
        items.append(item)
    }
    subscript(i: Int) -> Int { items[i] }
}

4. 泛型约束

限制泛型类型必须满足特定条件(如遵循协议、继承类或实现特定方法):

// 约束 T 必须遵循 Comparable 协议
func maxValue<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// 约束 Key 必须遵循 Hashable,Value 可以是任意类型
struct Dictionary<Key: Hashable, Value> {
    private var storage = [Key: Value]()
    // ...
}

5. 关联类型(Associated Types)

在协议中使用关联类型,定义泛型需求:

protocol NetworkService {
    associatedtype Response: Decodable
    func fetchData(completion: @escaping (Result<Response, Error>) -> Void)
}

struct UserService: NetworkService {
    typealias Response = User // 指定具体类型
    func fetchData(completion: @escaping (Result<User, Error>) -> Void) {
        // 网络请求逻辑
    }
}

6. 类型擦除(Type Erasure)

处理需要隐藏具体泛型类型的场景(如返回泛型协议的实例):

struct AnyPrinter<T>: Printer {
    private let _print: (T) -> Void
    init<U: Printer>(_ printer: U) where U.T == T {
        _print = printer.print
    }
    func print(_ value: T) {
        _print(value)
    }
}

7. 泛型与标准库

Swift 标准库广泛使用泛型,例如:

  • 集合类型Array<Element>Dictionary<Key, Value>
  • 可选类型Optional<Wrapped>
  • 错误处理Result<Success, Failure>

8. 泛型的优势

  • 减少重复代码:无需为不同类型编写相同逻辑。
  • 增强类型安全:编译器在编译时检查类型错误。
  • 提升性能:无运行时类型转换开销。

9. 总结

场景 示例 核心作用
函数逻辑复用 swapTwoValues<T> 处理多种类型数据
自定义容器 Stack<Element> 存储任意类型元素
协议抽象 Container 协议 + 关联类型 定义灵活的类型需求
类型约束 T: Equatable 确保类型满足特定条件

通过合理使用泛型,开发者可以显著提升代码的灵活性和健壮性,同时减少冗余代码。


四、 Swift 中的 strongweakunowned 详解

1、基本概念

在 Swift 中,strongweakunowned 是用于管理对象引用计数的关键字,与 自动引用计数(ARC) 机制密切相关。它们的核心作用是 控制对象生命周期,避免内存泄漏或野指针问题。

关键字 行为 引用计数 是否可为 nil 安全性
strong 默认修饰符,持有对象时增加引用计数,对象不会被释放。 增加 否(非可选类型) 安全
weak 不增加引用计数,对象释放后自动置为 nil 不增加 是(可选类型) 安全(自动置空)
unowned 不增加引用计数,对象释放后仍保留悬垂指针,访问时可能导致崩溃。 不增加 否(非可选类型) 不安全(需谨慎)

2、核心区别

(1). strong
  • 特点:默认修饰符,持有对象时会增加引用计数,对象生命周期由引用计数控制。
  • 适用场景:大多数情况下使用,表示明确的“拥有关系”。
  • 示例
    class Person {
        var pet: Dog? // 默认是 strong
    }
    
(2). weak
  • 特点
    • 不增加引用计数,对象释放后自动置为 nil
    • 必须声明为可选类型(var + ?)。
    • 它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不会出现崩溃。
  • 适用场景:打破循环引用,尤其用于 非父子关系 的相互引用(如 delegate)。
  • 示例
    class ViewController: UIViewController {
        weak var delegate: DataDelegate? // 弱引用避免循环
    }
    
(3). unowned
  • 特点
    • 不增加引用计数,但假设对象在生命周期内始终有效,不会为 nil
    • 非可选类型,访问已释放的对象会导致崩溃。
  • 适用场景:两个对象生命周期 严格同步,且被引用对象不会先于引用者释放。
  • 示例
    class CreditCard {
        unowned let owner: Customer // Customer 一定比 CreditCard 生命周期长
        init(owner: Customer) {
            self.owner = owner
        }
    }
    

何时使用 unowned

1. 适用场景
  • 对象生命周期严格绑定
    被引用对象(如 owner)的生命周期 长于或等于 引用者(如 CreditCard)。
  • 避免循环引用且无法使用 weak
    当需要非可选类型且确保对象不会提前释放时。
2. 使用示例
class Customer {
    var card: CreditCard?
}

class CreditCard {
    unowned let owner: Customer
    init(owner: Customer) {
        self.owner = owner
    }
}

// 使用
let customer = Customer()
customer.card = CreditCard(owner: customer) // 无循环引用
3. 注意事项
  • 绝对不要滥用:必须确保被引用对象不会被提前释放。
  • 优先选择 weak:若无法确保生命周期同步,优先使用 weak
  • unowned 与弱引用本质上一样。不同的是,unowned 无主引用 实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
  • weak unowned 只能用在 类实例上面
  • weakunowned 都能解决 循环引用,unowned 要比 weak 性能 稍高
    • 在生命周期中可能会 变成 nil 的使用 weak
    • 初始化赋值以后再也不会变成 nil 使用 unowned

对比 weakunowned

特性 weak unowned
是否可选 必须声明为可选类型(? 非可选类型
安全性 自动置 nil,访问安全 可能触发野指针崩溃
生命周期假设 允许被引用对象提前释放 假设被引用对象始终存在
内存管理 无需手动置 nil 需确保引用对象生命周期

常见问题

1. 循环引用是如何产生的?

当两个对象通过 strong 相互引用时,引用计数无法归零,导致内存泄漏:

class A {
    var b: B?
}

class B {
    var a: A?
}

let a = A()
let b = B()
a.b = b // A 强引用 B
b.a = a // B 强引用 A → 循环引用!
2. 如何打破循环引用?
  • 将其中一个引用改为 weakunowned
    class B {
        weak var a: A? // 弱引用打破循环
    }
    
3. 闭包中的循环引用

闭包捕获外部变量时默认是 strong,需使用捕获列表:

class NetworkManager {
    var completion: (() -> Void)?
    
    func fetchData() {
        // 使用 [weak self] 避免循环引用
        someAsyncTask { [weak self] in
            self?.handleData()
        }
    }
}

总结

场景 推荐修饰符 理由
默认对象引用 strong 明确所有权关系
打破父子对象外的循环引用 weak 安全自动置空
严格同步生命周期的对象引用 unowned 避免可选类型解包,需确保对象存活

通过合理使用 strongweakunowned,可以避免内存泄漏和野指针问题,写出更健壮的 Swift 代码。


五、Swift 中 staticclass 关键字对比

核心区别

特性 static class
适用类型 classstructenum class
修饰存储属性 ✅ 允许 ❌ 禁止
修饰计算属性/方法 ✅ 允许 ✅ 允许
是否支持子类重写 ❌ 不可重写(隐含 final ✅ 可重写(需用 override
协议中的使用 协议中统一用 static,所有类型实现 协议中不可用,仅类实现时可选 class

使用场景

  1. static

    • 通用场景:适用于所有类型(classstructenum)。
    • 存储属性:定义类级别的常量或变量。
    • 禁止重写:明确不希望子类重写的类型方法或属性。
    • 协议定义:协议中声明类型方法或属性时,必须用 static
  2. class

    • 类专用:仅用于 class 类型。
    • 允许重写:希望子类重写父类的类型方法或计算属性。
    • 计算属性:定义可被子类重写的类计算属性。

示例代码

// 协议定义
protocol MyProtocol {
    static func protocolMethod()  // 协议中必须用 static
}

// 类实现
class Parent: MyProtocol {
    static var storageProperty = "Parent"  // 类存储属性
    class var computedProperty: String { "Parent" }  // 可重写的类计算属性
    class func classMethod() { }          // 可重写的类方法
    static func protocolMethod() { }      // 实现协议方法(隐含 final)
}

class Child: Parent {
    override class var computedProperty: String { "Child" }
    override class func classMethod() { }  // ✅ 允许重写
    // override static func protocolMethod() { }  // ❌ 禁止重写(static隐含final)
}

// 结构体实现
struct MyStruct: MyProtocol {
    static func protocolMethod() { }      // 结构体必须用 static
}

注意事项

  • 存储属性class 不能修饰存储属性(仅 static 可以)。
  • 协议兼容性:协议中声明类型方法/属性时,使用 static;类实现时可用 classstaticstatic 会隐含 final)。
  • 性能优化static 方法/属性在编译期绑定,效率略高于 class

六、Swift 访问控制总结

Swift 提供了 5 种访问控制级别,用于限制代码中实体(类、属性、方法等)的可见性,确保代码的封装性和安全性。以下是各访问级别的核心区别及使用场景:


1. 访问级别从高到低

关键字 作用范围 允许继承/重写 适用场景
open 跨模块可见,且允许其他模块继承或重写 框架或库的公开 API(如 UIKit)
public 跨模块可见,但禁止其他模块继承或重写(类和成员默认不可继承) 暴露无需继承的工具类、方法
internal 仅当前模块内可见(默认级别) - 模块内部实现细节
fileprivate 仅当前文件内可见 - 文件内共享的辅助函数或类型
private 仅当前作用域(类型或扩展)内可见 - 类型内部的私有实现细节

2. 核心规则

  • 默认访问级别:未显式指定时,默认为 internal
  • 成员访问限制:成员的访问级别不能高于其所属类型(如 public classprivate 属性合法,但 private classpublic 属性非法)。
  • 协议一致性:遵循协议的类型必须满足协议要求的访问级别(如协议声明为 public,则遵循类型也需 public)。
  • 扩展中的访问控制:扩展默认继承原始类型的访问级别,但可显式指定(如 private extension)。

3. 使用示例

框架开发(跨模块)
// Framework 模块
open class NetworkManager {        // 允许其他模块继承
    public static let shared = NetworkManager()
    internal func log() { }        // 仅模块内部可见
    private var token: String?     // 仅当前类可见
}

public struct APIError: Error {    // 其他模块可用,不可继承
    public let code: Int
}
模块内部
// App 模块
internal struct User {             // 默认 internal,仅模块内可见
    var name: String
    fileprivate var id: Int        // 同一文件内可见
}

private extension String {         // 扩展内所有成员默认 private
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespaces)
    }
}

4. 特殊场景

  • 单元测试:通过 @testable import ModuleName 可访问模块的 internal 实体。
  • 子类重写:子类方法的访问级别不能低于父类方法(如父类方法为 open,子类可重写为 public,但不能是 internal)。
  • 泛型约束:泛型类型的访问级别需与其类型参数一致或更高。

5. 选择指南

场景 推荐级别 示例
开发第三方框架或库 open/public 公开工具类、核心功能方法
模块内部工具类 internal 数据模型、网络请求封装
文件内共享的辅助函数 fileprivate 同一文件内的 JSON 解析工具
类型内部私有实现 private 缓存属性、敏感数据处理方法

通过合理使用访问控制,可以提升代码的可维护性,减少耦合,同时保护关键实现细节不被外部误用。

七、Swift 写时复制(Copy-on-Write)核心总结

1. 核心机制

  • 延迟复制:值类型(如 ArrayString)在赋值时不立即复制内存,而是共享同一份数据。
  • 写入触发:仅当修改副本时,才真正创建新内存,确保原数据不受影响。

2. 实现原理

  • 内部引用:值类型内部通过引用类型(如类)存储实际数据。
  • 引用计数检查:写入前调用 isKnownUniquelyReferenced,检测数据引用是否唯一:
    • 唯一引用:直接修改原数据。
    • 多引用:创建新副本后再修改,避免共享数据意外变更。

3. 优势

  • 性能优化:减少不必要的数据复制,提升内存效率。
  • 值类型安全:保留值语义(独立修改),同时兼顾性能。

4. 适用场景

  • 高频赋值的大数据:如集合操作、字符串处理。
  • 标准库类型ArrayDictionarySetString 默认支持 COW。

5. 示例

var a = [1, 2, 3]  // 内部引用计数为 1
var b = a           // 引用计数 +1(共享数据)
b.append(4)         // 检测到多引用,复制新内存再修改(a 仍为 [1,2,3])

总结

COW 通过共享数据 + 写入时复制,平衡了值类型的安全性与性能,是 Swift 高效处理大数据的核心机制。

八、Swift 将集合类型设计为值类型的原因

1. 值类型的核心优势

  • 线程安全:值类型在传递时通过复制确保数据独立性,避免多线程同时修改同一内存。
  • 无副作用:方法调用不会意外修改原始数据,代码行为更易推理。
  • 内存高效:结合 写时复制(Copy-on-Write, COW),减少不必要的内存分配。

2. 具体设计动机

(1) 安全性优先
  • 避免隐式共享:引用类型多个变量指向同一内存,修改一处会影响所有引用(如 NSMutableArray)。
  • 值类型赋值即复制:每个变量持有独立数据,修改副本不影响原数据。
    var a = [1, 2, 3]
    var b = a
    b.append(4)
    print(a) // [1, 2, 3](a 未被修改)
    
(2) 性能优化
  • 栈内存分配:值类型默认在栈上分配,操作高效(指针移动即可)。
  • 写时复制(COW):延迟实际内存复制,仅在修改时创建新副本。
    var largeArray = [Int](repeating: 0, count: 1000000)
    var copy = largeArray // 不立即复制内存(共享存储)
    copy[0] = 1          // 修改时触发复制(独立存储)
    
(3) 不可变性的真正保证
  • let 语义严格:值类型声明为 let 后,内容完全不可变。
  • 引用类型的缺陷:即使使用 let 修饰引用类型(如 NSArray),内部数据仍可能被修改。
    let nsArray: NSArray = NSMutableArray()
    let swiftArray = [1, 2, 3]
    // nsArray 可被其他引用修改(不安全),swiftArray 完全不可变
    

3. 与引用类型的对比

特性 值类型(Array/String/Dictionary) 引用类型(NSArray/NSString)
内存分配 栈 + COW 优化堆内存 堆内存
线程安全 天然安全(独立内存) 需手动同步(如锁、GCD)
赋值行为 深拷贝(COW 延迟实际复制) 浅拷贝(共享内存)
不可变性 let 完全不可变 let 仅保证引用不变,内容可能被其他引用修改

4. 写时复制(COW)的实现原理

  1. 内部引用类型存储:值类型内部使用类(如 _ContiguousArrayStorage)存储实际数据。
  2. 引用计数检测:通过 isKnownUniquelyReferenced 检查数据是否被多引用。
  3. 按需复制:修改数据时,若引用非唯一,则创建新副本再修改。
    struct MyArray<T> {
        private var storage: Storage<T> // 内部引用类型
        mutating func append(_ element: T) {
            if !isKnownUniquelyReferenced(&storage) {
                storage = storage.copy()
            }
            storage.append(element)
        }
    }
    

5. 总结

  • 安全第一:值类型 + COW 解决了引用类型在多线程和共享数据中的安全隐患。
  • 性能平衡:栈内存和 COW 机制将复制开销降至最低。
  • 开发友好:严格的不可变性和可预测行为,减少代码副作用。

通过将 ArrayStringDictionary 设计为值类型,Swift 在内存效率、线程安全和代码可靠性之间取得了最佳平衡,同时借助 COW 技术避免了传统值类型的性能缺陷。

iOS 中的 `@autoreleasepool` 详细解析

作者 90后晨仔
2025年3月29日 16:10

1. 什么是 @autoreleasepool

@autoreleasepoolObjective-CSwift 中用于管理 自动释放对象(Autoreleased Objects) 的机制。它的核心作用是 延迟对象的释放时机,避免内存峰值过高,尤其在需要频繁创建临时对象的场景下优化内存使用。


2. 为什么需要 @autoreleasepool

在 iOS 内存管理中:

  • MRC(手动引用计数):开发者需手动调用 retain/release 管理对象生命周期。
  • ARC(自动引用计数):编译器自动插入内存管理代码,但 自动释放池 依然用于管理某些场景下的临时对象。

核心问题
当一个对象被标记为 autorelease(如通过工厂方法 [NSString stringWithFormat:] 创建的对象),它会被添加到当前的 autoreleasepool 中,直到池子销毁时才释放。若未及时释放池子,大量临时对象可能导致内存峰值。


3. @autoreleasepool 的工作原理

  • 创建与销毁
    • 进入 @autoreleasepool 作用域时,系统会创建一个池子。
    • 退出作用域时,池子中的所有对象会收到 release 消息,若引用计数为 0 则释放。
  • 延迟释放
    对象不会立即释放,而是延迟到池子销毁时统一处理。
@autoreleasepool {
    // 临时对象会被添加到当前池子
    NSString *tempStr = [NSString stringWithFormat:@"Hello, %@", @"World"];
    // 池子销毁时,tempStr 会被释放(若引用计数为 0)
}

4. 使用场景

(1) 循环中创建大量临时对象

在循环中频繁生成临时对象时,使用 @autoreleasepool 及时释放内存:

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *temp = [NSString stringWithFormat:@"Index: %d", i];
        // 每次循环结束后,temp 会被释放
    }
}
(2) 主线程 RunLoop 的自动释放池
  • 主线程 的 RunLoop 在每次事件循环(Event Loop)中会自动创建和销毁 autoreleasepool
  • 子线程 默认不自动创建池子,需手动添加:
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @autoreleasepool {
            // 子线程中的代码
        }
    });
    
(3) 优化内存敏感操作
  • 解析大型 JSON/XML 文件。
  • 处理图像或视频数据时生成临时缓冲对象。

5. 底层机制

(1) NSAutoreleasePool@autoreleasepool
  • MRC 时代:通过 NSAutoreleasePool 类手动管理池子:
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // 创建对象
    [pool drain]; // 释放池子
    
  • ARC 时代:使用 @autoreleasepool 语法糖,编译器会将其转换为高效的底层代码。
(2) 自动释放池的栈结构
  • 自动释放池以 形式管理,后进先出(LIFO)。
  • 嵌套使用 @autoreleasepool 时,内层池子优先释放:
    @autoreleasepool {
        // 外层池子
        @autoreleasepool {
            // 内层池子(优先释放)
        }
    }
    
(3) 对象如何被添加到池子?
  • 通过 autorelease 方法标记对象(ARC 下由编译器自动插入):
    // MRC 下显式调用
    NSString *str = [[[NSString alloc] initWithFormat:@"Test"] autorelease];
    

6. ARC 下的注意事项

  • 无需手动调用 autorelease:ARC 会自动插入内存管理代码。
  • 推荐使用 @autoreleasepool 语法:比 NSAutoreleasePool 更高效,且可读性更好。
  • 避免过度嵌套:仅在必要时使用,频繁创建池子可能影响性能。

7. 性能优化建议

  • 减少临时对象数量:尽量复用对象,避免循环内频繁创建。
  • 合理控制池子作用域:在内存敏感代码段精确包裹 @autoreleasepool
  • 避免主线程阻塞:耗时操作放在子线程,并为其添加池子。

8. 总结

场景 解决方案
循环中创建大量临时对象 在循环内部使用 @autoreleasepool
子线程中操作 手动添加 @autoreleasepool
解析大型数据文件 分段处理数据并包裹池子
优化内存敏感代码 精确控制池子作用域,及时释放临时对象

通过合理使用 @autoreleasepool,开发者可以有效管理内存峰值,提升应用性能和稳定性。

__block 与 __weak的区别是什么?

作者 90后晨仔
2025年3月29日 15:57

两者在功能上的区别?

__block会持有该对象,即使超出了该对象的作用域,该对象还是会存在的,直到block对象从堆上销毁;

__weak仅仅是将该对象赋值给weak对象,当该对象销毁时,weak对象将指向nil;

__block可以让block修改局部变量,而__weak不能。
MRC中__block是不会引起retain;但在ARC中__block则会引起retain。所以ARC中应该使用__weak。

循环引用的问题?

block下循环引用的问题

__block本身并不能避免循环引用,避免循环引用需要在block内部把__block修饰的obj置为nil

__weak可以避免循环引用,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

什么是KVO 、什么是 KVC ?

作者 90后晨仔
2025年3月29日 15:43

KVO(Key-Value Observing,键值观察)

是什么?

KVO 是 Objective-C/Swift 中的一种 观察者模式,允许对象监听另一个对象的属性变化。当被监听属性的值被修改时,观察者会自动收到通知。

核心机制

  1. 动态子类

    • 当对象首次被监听时,Runtime 会动态生成一个子类(如 NSKVONotifying_ClassName)。
    • 重写被监听属性的 setter 方法,插入 willChangeValueForKey: 和 didChangeValueForKey: 方法。
  2. 消息转发

    • 修改对象的 isa 指针,指向动态子类,隐藏实现细节。
    • 当属性被修改时,通过 didChangeValueForKey: 触发观察者的回调。

用途

  • 数据与 UI 同步:例如,模型数据变化时自动更新界面。
  • 跨组件通信:解耦不同模块间的依赖关系。

示例

// 监听 person 的 age 属性
[person addObserver:self 
        forKeyPath:@"age" 
           options:NSKeyValueObservingOptionNew 
           context:nil];

// 回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"新年龄:%@", change[NSKeyValueChangeNewKey]);
    }
}

KVC(Key-Value Coding,键值编码)

是什么?

KVC 是一种通过 字符串键(Key)间接访问对象属性 的机制,无需直接调用 getter/setter 方法,甚至可以直接访问私有成员变量。

核心机制

  1. 方法优先

    • 赋值setValue:forKey:)时,优先查找 setKey: 或 _setKey: 方法。
    • 取值valueForKey:)时,优先查找 getKeykeyisKey_key 方法。
  2. 成员变量次之

    • 若未找到方法,且 +accessInstanceVariablesDirectly 返回 YES,则按顺序访问成员变量:
      _key → _isKey → key → isKey

用途

  • 动态配置属性:通过字符串灵活读写属性(如解析 JSON 数据)。
  • 访问私有变量:调试或特殊场景下访问私有成员。
  • 简化代码:避免硬编码大量的 getter/setter 调用。

示例

// 通过 KVC 设置属性
[person setValue:@25 forKey:@"age"];

// 通过 KVC 读取属性
NSNumber *age = [person valueForKey:@"age"];

KVO 与 KVC 的区别

特性 KVO KVC
核心功能 监听属性变化 通过字符串键间接访问属性
底层机制 动态生成子类,重写 setter 方法查找 + 成员变量访问
主要用途 数据同步、跨组件通信 动态读写属性、访问私有变量
触发条件 必须通过 setter 或 KVC 修改属性 直接通过 setValue:forKey: 操作
复杂度 需手动管理观察者生命周期 无需额外管理

常见问题

1. 为什么 KVO 监听属性必须用字符串 KeyPath?

  • 动态性:字符串 KeyPath 允许运行时动态绑定属性,但容易拼写错误(编译器不检查)。

2. KVC 能修改私有变量吗?

  • 可以:若 +accessInstanceVariablesDirectly 返回 YES,且私有变量命名符合 KVC 规范(如 _key)。

3. KVO 不移除观察者会崩溃吗?

  • :观察者释放后,若被观察对象仍发送通知,会导致野指针访问(EXC_BAD_ACCESS)。

总结

  • KVO 是 iOS 中实现 属性监听 的核心机制,依赖 Runtime 动态派发。

  • KVC 提供了一种 灵活访问属性 的方式,但需注意命名规范和安全性。

  • 适用场景

    • KVO:数据驱动 UI、跨层级通信。
    • KVC:动态配置、反射、访问私有变量(谨慎使用)。

KVC(Key-Value Coding)的底层实现分析

作者 90后晨仔
2025年3月29日 15:37

一、KVC(Key-Value Coding)的底层行为

1. KVC 赋值流程(setValue:forKey:

graph TD
A[调用 setValue:forKey:] --> B{是否存在 setKey: 或 _setKey: 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则赋值]
D -->|NO| G[抛出 NSUnknownKeyException]

2. KVC 取值流程(valueForKey:

graph TD
A[调用 valueForKey:] --> B{是否存在 getKey/key/isKey/_key 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key -> _isKey -> key -> isKey]
E --> F[找到则返回值]
D -->|NO| G[抛出 NSUnknownKeyException]

二、KVC 的底层行为与实现细节

1. setValue:forKey: 赋值流程详解

当调用 setValue:forKey: 时,KVC 按以下顺序查找并执行赋值操作:

方法优先查找

  1. set<Key>:
    查找与属性名匹配的 setter 方法(如属性 age 对应 setAge:)。
  2. _set<Key>:
    若未找到 set<Key>:,则查找带下划线的 setter 方法(如 _setAge:)。

成员变量次之

若未找到上述方法,检查 +accessInstanceVariablesDirectly 方法的返回值:

  • 返回 YES:按顺序查找成员变量:
    • _<key>_is<Key><key>is<Key>
      (例如 _age_isAgeageisAge)。
  • 返回 NO:直接抛出 NSUnknownKeyException 异常。

赋值结果处理

  • 找到成员变量:直接赋值。
  • 未找到:抛出异常。
// 示例:通过 KVC 设置私有成员变量
@interface Person : NSObject {
    @private
    NSString *_nickname;
}
@end

Person *p = [[Person alloc] init];
[p setValue:@"Jack" forKey:@"nickname"]; // 成功赋值 _nickname

2. valueForKey: 取值流程详解

当调用 valueForKey: 时,KVC 按以下顺序查找并返回值:

方法优先查找

  1. get<Key>
    查找与属性名匹配的 getter 方法(如 getName)。
  2. <key>
    直接查找与属性名相同的方法(如 name)。
  3. is<Key>
    查找布尔类型的 getter(如 isActive 对应属性 active)。
  4. _<key>
    查找带下划线的 getter(如 _name)。

成员变量次之

若未找到上述方法,检查 +accessInstanceVariablesDirectly 方法的返回值:

  • 返回 YES:按顺序查找成员变量:
    • _<key>_is<Key><key>is<Key>
      (例如 _age_isAgeageisAge)。
  • 返回 NO:抛出 NSUnknownKeyException 异常。

取值结果处理

  • 找到成员变量:返回其值。
  • 未找到:抛出异常。
// 示例:通过 KVC 获取私有成员变量
NSString *nickname = [p valueForKey:@"nickname"]; // 返回 _nickname 的值

3. KVC 方法查找与成员变量访问对比

操作类型 方法查找顺序 成员变量查找顺序 权限控制方法
赋值 setKey:_setKey: _key_isKeykeyisKey +accessInstanceVariablesDirectly
取值 getKeykeyisKey_key _key_isKeykeyisKey +accessInstanceVariablesDirectly

4. 常见场景与陷阱

场景 1:布尔类型属性的特殊处理

若属性为布尔类型(如 isActive),valueForKey:@"active" 会优先调用 isActive 方法:

@interface User : NSObject
@property (nonatomic, assign) BOOL isActive;
@end

User *user = [[User alloc] init];
user.isActive = YES;
NSNumber *isActive = [user valueForKey:@"active"]; // 调用 isActive 方法

场景 2:集合类型属性的 KVC 访问

若属性为集合(如 NSArray),valueForKey: 可能触发以下方法:

  • countOf<Key>:返回集合元素数量。
  • objectIn<Key>AtIndex::根据索引获取元素。
@interface Team : NSObject
- (NSUInteger)countOfMembers;
- (id)objectInMembersAtIndex:(NSUInteger)index;
@end

Team *team = [[Team alloc] init];
NSArray *members = [team valueForKey:@"members"]; // 触发上述方法

陷阱:直接访问私有成员变量

若未实现 getter/setter 且 accessInstanceVariablesDirectly 返回 NO,直接访问成员变量会崩溃:

@interface SecretData : NSObject {
    @private
    NSString *_password;
}
@end

@implementation SecretData
+ (BOOL)accessInstanceVariablesDirectly {
    return NO; // 禁止 KVC 访问成员变量
}
@end

SecretData *data = [[SecretData alloc] init];
[data setValue:@"123456" forKey:@"password"]; // 抛出 NSUnknownKeyException

5. 性能优化建议

  • 避免频繁使用 KVC:方法查找和消息转发会带来额外开销,性能敏感场景优先使用直接方法调用。
  • 缓存 Key Paths:若需多次访问同一属性,可将 Key Path 转换为指针或常量减少字符串解析开销。
  • 合理控制访问权限:通过重写 +accessInstanceVariablesDirectly 限制不必要的成员变量暴露。

三、面试题扩展

Q:valueForKey:objectForKey: 有何区别?

  • valueForKey:
    属于 KVC 方法,通过属性名或键路径查找值,支持方法调用和成员变量访问,可触发 KVO。
  • objectForKey:
    NSDictionary 的方法,直接从字典中根据键取值,不涉及 KVC/KVO 机制。

Q:如何阻止 KVC 访问私有成员变量?

重写 +accessInstanceVariablesDirectly 返回 NO

+ (BOOL)accessInstanceVariablesDirectly {
    return NO; // 禁止 KVC 直接访问成员变量
}

iOS 开发中KVO 核心机制与底层原理解析

作者 90后晨仔
2025年3月29日 15:10

一、KVO 的本质:动态子类与消息转发

  1. 动态生成子类

    • 当对象首次被添加 KVO 监听时,Runtime 会动态创建名为 NSKVONotifying_ClassName 的子类(如 NSKVONotifying_JCAnimal)。
    • 修改对象的 isa 指针,使其指向该子类(并非修改对象的类,而是重定向方法调用路径)。
  2. 重写关键方法

    • 子类重写被监听属性的 setter 方法,插入 willChangeValueForKey:didChangeValueForKey: 调用。
    • 子类还重写 class 方法以隐藏自身存在(返回原始类名),避免外部感知。
    // 伪代码:动态子类 NSKVONotifying_JCAnimal 的实现
    - (void)setAge:(int)age {
        [self willChangeValueForKey:@"age"];
        [super setAge:age]; // 调用原始类的 setter
        [self didChangeValueForKey:@"age"];
    }
    
    - (Class)class {
        return JCAnimal.class; // 伪装成原始类
    }
    
  3. 通知触发链路

    graph LR
    A[修改属性值] --> B[调用子类重写的 setter]
    B --> C[willChangeValueForKey]
    C --> D[原始类 setter 方法]
    D --> E[didChangeValueForKey]
    E --> F[通知所有观察者]
    

二、KVO 的触发条件与限制

  1. 自动触发场景

    • 通过 Setter 方法 修改属性(如 obj.age = 20)。
    • 通过 KVC 修改属性(如 [obj setValue:@20 forKey:@"age"])。
  2. 无法触发的情况

    • 直接修改成员变量:如 obj->_age = 20(未调用 setter)。
    • 未遵循 KVC 规范:例如属性未声明 @dynamic 或未实现访问器方法。
  3. 手动触发技巧

    // 手动通知属性变化(即使直接修改成员变量)
    [obj willChangeValueForKey:@"age"];
    obj->_age = 20;
    [obj didChangeValueForKey:@"age"];
    

三、KVC 的底层行为与 KVO 联动

1. KVC 赋值流程 (setValue:forKey:)

graph TD
A[调用 setValue:forKey:] --> B{是否存在 setKey: 或 _setKey: 方法?}
B -->|是| C[调用对应方法]
B -->|否| D[检查 +accessInstanceVariablesDirectly]
D -->|YES| E[按顺序查找成员变量 _key, _isKey, key, isKey]
E --> F[找到则赋值]
D -->|NO| G[抛出 NSUnknownKeyException]

2. KVC 触发 KVO 的原因

  • KVC 内部默认调用属性的 Setter 方法(若存在),从而触发 KVO。
  • 若直接赋值成员变量,需依赖 accessInstanceVariablesDirectly 返回 YES,但此时 不会自动触发 KVO,需手动调用 will/didChange

四、关键面试题深度解析

Q1:KVO 如何实现属性监听?

  • 动态子类:Runtime 生成子类并重写 setter,插入通知逻辑。
  • 消息转发:修改 isa 指针,使方法调用指向子类。
  • 通知链路:通过 didChangeValueForKey: 触发观察者回调。

Q2:直接修改成员变量会触发 KVO 吗?

  • 不会。KVO 依赖 setter 方法或手动触发 willChangeValueForKey:didChangeValueForKey:,直接修改变量绕过了这些路径。

Q3:如何手动触发 KVO?

  • 显式调用:在修改变量前后添加 willChangeValueForKey:didChangeValueForKey:

Q4:KVC 修改属性会触发 KVO 吗?

  • 。KVC 默认调用 setter 方法,与直接使用 setter 效果相同。

Q5:KVC 的赋值过程是怎样的?

  • 方法优先:查找 setKey:_setKey: 方法。
  • 成员变量次之:若允许访问变量,按 _key_isKeykeyisKey 顺序查找。

五、实战技巧与陷阱规避

  1. 自动与手动模式切换

    • 重写 +automaticallyNotifiesObserversForKey: 控制是否自动通知:
      + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
          if ([key isEqualToString:@"age"]) {
              return NO; // 关闭 age 属性的自动通知
          }
          return [super automaticallyNotifiesObserversForKey:key];
      }
      
  2. 避免野指针崩溃

    • 移除观察者前检查:确保 removeObserver:forKeyPath: 调用次数不超过添加次数。
    • 使用关联对象管理观察者(Swift 中推荐闭包 API 自动管理生命周期)。
  3. 多线程安全

    • KVO 通知在属性修改的线程触发,需在主线程更新 UI 时手动派发:
      dispatch_async(dispatch_get_main_queue(), ^{
          [self.tableView reloadData];
      });
      

六、KVO 与替代方案对比

方案 优势 劣势 适用场景
KVO 自动监听、跨组件解耦 需手动移除、字符串 KeyPath 易出错 数据模型与 UI 同步
Delegate 类型安全、一对一精准通知 需定义协议、代码冗余 父子组件定制化交互
Notification 全局广播、一对多监听 数据传递类型受限(无强类型) 系统事件(如键盘弹出)
Combine 链式处理、线程调度、类型安全 仅限 Swift、学习成本高 复杂数据流响应式处理

七、总结

KVO 的核心价值在于其 隐式监听能力,通过 Runtime 动态派发实现无侵入式观察。理解其底层机制(如动态子类、方法重写)有助于规避内存泄漏和多线程问题。在 Swift 中,优先使用 NSKeyValueObservation 的闭包 API 简化生命周期管理,而在需要精细控制时(如性能敏感场景),可结合手动触发模式优化通知频率。

❌
❌