阅读视图

发现新文章,点击刷新页面。

Swift 常用框架Kingfisher、KingfisherWebP详解

1.1 什么是 Kingfisher 、KingfisherWebP

Kingfisher 是一个功能强大的 Swift 库,专门用于处理图像的下载、缓存和展示。目前已成为 iOS/macOS 开发中最受欢迎的图像处理解决方案之一。

KingfisherWebP 是 Kingfisher 的官方扩展,用于支持 WebP 图像格式。WebP 是 Google 开发的一种现代图像格式,它可以在相同质量下提供比 JPEG 和 PNG 更小的文件大小,从而减少带宽使用和加快加载速度。

1.2 核心特性

  • 异步下载 :在后台线程下载图片,不阻塞主线程
  • 多级缓存 :内存缓存 + 磁盘缓存,提高加载速度
  • 自动缓存管理 :智能处理缓存大小和过期时间
  • 图片处理 :支持下载后处理,如圆角、模糊等
  • 请求优先级 :可设置不同图片请求的优先级
  • 可扩展性 :支持自定义缓存、下载器等组件
  • SwiftUI 支持 :提供了便捷的 SwiftUI 视图扩展
  • 动画支持 :支持 GIF 等动画图片

1.3 安装方式

Kingfisher 支持多种安装方式:

CocoaPods:

pod 'Kingfisher'
pod 'KingfisherWebP'

2. Kingfisher 基础用法

2.1 基本图片加载

import Kingfisher

// 基本用法
imageView.kf.setImage(with: URL(string: "https://example.com/image.jpg"))

// 带选项的用法
imageView.kf.setImage(
    with: URL(string: "https://example.com/image.jpg"),
    placeholder: UIImage(named: "placeholder"),
    options: [
        .transition(.fade(0.2)),
        .cacheOriginalImage
    ]
) { result in
    switch result {
    case .success(let value):
        print("Image loaded: \(value.image)")
        print("图片加载成功: \(value.source.url?.absoluteString ?? "")")
    case .failure(let error):
        print("图片加载失败: \(error.localizedDescription)")
    }
}

2.2 缓存控制

 // 清除内存缓存
KingfisherManager.shared.cache.clearMemoryCache()

// 清除磁盘缓存
KingfisherManager.shared.cache.clearDiskCache()

// 清除所有缓存
KingfisherManager.shared.cache.clearCache()

2.3 预加载图像

预加载一组图像以提升加载速度,适合在应用启动时或预期需要时使用。

 let urls = [URL(string: "https://example.com/image1.png")!, URL(string: "https://example.com/image2.png")!]
 ImagePrefetcher(urls: urls).start()

2.4 显示WebP

 /// 全局配置
KingfisherManager.shared.defaultOptions += [.processor(WebPProcessor.default),.cacheSerializer(WebPSerializer.default)]

// 使用 AnimatedImageView 定义ImageView

 let animageView = AnimatedImageView()
 animageView.kf.setImage(with: URL(string: "https://example.com/image.webp"))

深入理解 WKWebView:代理方法与 WKWebView 生命周期的执行顺序

在 iOS 开发中,WKWebView 是构建混合应用(Hybrid App)的核心组件。它基于现代 WebKit 引擎,性能优异、安全性高,但其复杂的生命周期机制也让不少开发者感到困惑——尤其是当页面加载失败时,错误回调到底在哪个阶段触发?

本文将深入解析 WKWebView 的完整生命周期,以 Objective-C 为开发语言,系统梳理 WKNavigationDelegate 中各代理方法的执行时机与调用顺序,并通过对比 正常加载成功加载失败 两种典型场景,帮助你精准掌控 WebView 行为,避免常见陷阱。

✅ 适用系统:iOS 9+(建议 iOS 11+)
💬 开发语言:Objective-C
🧭 核心协议:WKNavigationDelegate


一、生命周期全景图

WKWebView 的导航过程由一系列代理方法串联而成。根据加载结果不同,可分为两条主路径:

✅ 成功路径(页面正常加载)

didStartProvisionalNavigation
→ decidePolicyForNavigationAction
→ didCommitNavigation
→ decidePolicyForNavigationResponse
→ didFinishNavigation

❌ 失败路径(加载中断)

didStartProvisionalNavigation
→ decidePolicyForNavigationAction(可能)
→ didFailProvisionalNavigation     // 早期失败(如 DNS 错误)
   或
→ didCommitNavigation
→ didFailNavigation               // 提交后失败(如 SSL 证书无效)

⚠️ 重要认知:

  • HTTP 404/500 不会触发 fail 回调!因为服务器已返回有效响应,属于“成功加载错误页”。
  • 所有 decisionHandler 必须被调用,否则 WebView 将卡死。

二、成功加载:五步走流程详解

当访问一个有效 URL(如 https://example.com)且网络通畅时,代理方法按以下顺序严格触发:

1. webView:didStartProvisionalNavigation:

  • 页面开始尝试加载。
  • URL 可能尚未最终确定(例如重定向前)。
  • 适合启动 loading 动画
- (void)webView:(WKWebView *)webView 
didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Start provisional navigation to: %@", webView.URL);
    [self showLoadingIndicator];
}

2. webView:decidePolicyForNavigationAction:decisionHandler:

  • 决定是否允许此次跳转。
  • 常用于拦截自定义 scheme(如 tel://, weixin://)。
- (void)webView:(WKWebView *)webView 
decidePolicyForNavigationAction:(WKNavigationAction *)action 
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
    NSURL *url = action.request.URL;
    if ([[url scheme] isEqualToString:@"tel"]) {
        [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
        handler(WKNavigationActionPolicyCancel); // 拦截并交由系统处理
        return;
    }
    handler(WKNavigationActionPolicyAllow); // 允许加载
}

🔥 必须调用 handler()!否则页面将永远处于“加载中”。


3. webView:didCommitNavigation:

  • 浏览器已接收到响应头,开始接收 HTML 数据。
  • DOM 开始构建,但未渲染完成。
  • 此时 webView.URL 已是最终地址(可用于埋点或日志)。
- (void)webView:(WKWebView *)webView 
didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Committed to final URL: %@", webView.URL);
}

4. webView:decidePolicyForNavigationResponse:decisionHandler:

  • 针对服务器返回的响应(状态码、MIME 类型等)决定是否继续加载。
  • 可用于拦截非 HTML 资源(如 PDF、ZIP 文件)。
- (void)webView:(WKWebView *)webView 
decidePolicyForNavigationResponse:(WKNavigationResponse *)response 
decisionHandler:(void (^)(WKNavigationResponsePolicy))handler {
    NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response.response;
    if ([httpResp.MIMEType isEqualToString:@"application/pdf"]) {
        // 拦截 PDF 下载
        handler(WKNavigationResponsePolicyCancel);
        return;
    }
    handler(WKNavigationResponsePolicyAllow);
}

5. webView:didFinishNavigation:

  • 所有资源(HTML、CSS、JS、图片等)加载完毕。
  • 页面完全可交互
  • 隐藏 loading、注入 JS、执行业务逻辑的最佳时机
- (void)webView:(WKWebView *)webView 
didFinishNavigation:(WKNavigation *)navigation {
    NSLog(@"✅ Page fully loaded!");
    [self hideLoadingIndicator];
    // 可在此注入 JS 或通知上层
}

三、失败加载:两类错误路径剖析

加载失败分为 Provisional 阶段失败Commit 后失败,需分别处理。

🔴 类型 1:Provisional 阶段失败

触发方法didFailProvisionalNavigation:withError:
典型原因

  • DNS 解析失败(域名不存在)
  • 无法连接服务器(断网、超时)
  • URL 格式非法
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
❌ didFailProvisionalNavigation: "A server with the specified hostname could not be found."

🔴 类型 2:Commit 后失败

触发方法didFailNavigation:withError:
典型原因

  • SSL/TLS 证书无效或过期(iOS 默认拦截)
  • 服务器在传输中途断开连接
✅ didStartProvisionalNavigation
✅ decidePolicyForNavigationAction
✅ didCommitNavigation
❌ didFailNavigation: "The certificate for this server is invalid."

❗ 关键提醒:只监听 didFailProvisionalNavigation 会漏掉 SSL 错误!必须同时实现两个失败回调。


统一错误处理示例

- (void)webView:(WKWebView *)webView 
didFailProvisionalNavigation:(WKNavigation *)navigation 
withError:(NSError *)error {
    NSLog(@"❌ Provisional fail: %@", error.localizedDescription);
    [self showErrorViewWithError:error];
}

- (void)webView:(WKWebView *)webView 
didFailNavigation:(WKNavigation *)navigation 
withError:(NSError *)error {
    NSLog(@"❌ Navigation fail after commit: %@", error.localizedDescription);
    [self showErrorViewWithError:error];
}

四、初始化与代理设置示例

// ViewController.h
@interface ViewController () <WKNavigationDelegate>
@property (nonatomic, strong) WKWebView *webView;
@end

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com"]]];
}

💡 建议:若需共享 Cookie 或缓存,可复用 WKProcessPool


五、总结:关键要点速查表

场景 触发方法 是否必须处理
页面开始加载 didStartProvisionalNavigation
决策是否跳转 decidePolicyForNavigationAction ✅(必须调用 handler)
页面提交(DOM 开始构建) didCommitNavigation
决策是否接受响应 decidePolicyForNavigationResponse ✅(必须调用 handler)
加载完成 didFinishNavigation
早期失败(DNS/断网) didFailProvisionalNavigation
提交后失败(SSL/中断) didFailNavigation

六、最佳实践建议

  1. 双失败回调都要实现,覆盖所有异常场景。
  2. 所有 decisionHandler 必须调用,避免页面卡死。
  3. 避免循环引用:delegate 使用 weak self,或在 dealloc 中置 nil。
  4. 真机测试异常网络:使用「设置 > 开发者 > 网络链接条件」模拟弱网/断网。
  5. 不要依赖 HTTP 状态码判断失败:404/500 仍会触发 didFinishNavigation

📌 延伸思考

  • iOS 15+ 新增 WKNavigationDelegate 的 frame 级回调(如 didFinishDocumentLoadForFrame:
  • 若需深度控制缓存策略,可结合 WKWebsiteDataStore 使用

如果你觉得本文对你有帮助,欢迎 点赞 ❤️、收藏 ⭐、评论 💬!也欢迎关注我,获取更多 iOS 底层与实战技巧。

__CFRunLoopDoSources0函数详解

借助AI辅助。

__CFRunLoopDoSources0 函数逐行注释

函数概述

__CFRunLoopDoSources0 是 RunLoop 中负责处理 Source0 事件源的核心函数。Source0 是需要手动标记为待处理(signal)的事件源,常用于自定义事件处理、触摸事件、手势识别等场景。

Source0 与 Source1 的区别

Source0(非基于端口)

  • 触发方式: 需要手动调用 CFRunLoopSourceSignal() 标记为待处理,然后调用 CFRunLoopWakeUp() 唤醒 RunLoop
  • 特点: 不会自动唤醒 RunLoop,需要手动唤醒
  • 使用场景: 触摸事件、自定义事件、手势识别、UIEvent 处理
  • 实现: 基于回调函数

Source1(基于端口)

  • 触发方式: 基于 Mach Port,当端口收到消息时自动唤醒 RunLoop
  • 特点: 可以自动唤醒 RunLoop
  • 使用场景: 进程间通信、系统事件、CFMachPort、CFMessagePort
  • 实现: 基于 Mach 内核的端口通信

函数签名

/* rl is locked, rlm is locked on entrance and exit */
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode
  • Boolean stopAfterHandle: 是否在处理一个 source 后就停止(用于优化性能)

返回值

  • Boolean: 如果至少处理了一个 source 返回 true,否则返回 false

前置条件

  • 函数调用时必须持有锁: rlrlm 都必须处于加锁状态
  • 函数返回时保持锁状态: 出口时 rlrlm 仍然加锁

函数属性

  • __attribute__((noinline)): 防止编译器内联优化,便于调试和性能分析

完整代码逐行注释

/* rl is locked, rlm is locked on entrance and exit */
// 📝 锁状态约定:函数入口和出口时,rl 和 rlm 都必须处于加锁状态
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));

static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) {/* DOES CALLOUT */
    // ⚠️ 重要标注:此函数会执行外部回调(callout),可能导致:
    // 1. 长时间阻塞(回调函数耗时)
    // 2. 重入问题(回调中可能再次操作 RunLoop)
    // 3. 死锁风险(因此需要在回调前解锁)
    
    // ==================== 第一部分:性能追踪和初始化 ====================
    
    // 📊 记录性能追踪点:开始处理 Source0
    // 参数:事件类型、RunLoop、Mode、stopAfterHandle 标志、额外参数
    // 可用 Instruments 的 kdebug 工具查看
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_SOURCES0 | DBG_FUNC_START, rl, rlm, stopAfterHandle, 0);
    
    // 🔒 检查进程是否被 fork
    // 如果在 fork 后的子进程中,需要重新初始化 RunLoop 的锁和状态
    // 防止继承父进程的锁状态导致死锁
    CHECK_FOR_FORK();
    
    // 用于存储收集到的 source(s)
    // 可能是单个 CFRunLoopSourceRef 或 CFArrayRef(多个 sources)
    CFTypeRef sources = NULL;
    
    // 标志位:是否至少处理了一个 source
    Boolean sourceHandled = false;
    
    // ==================== 第二部分:收集待处理的 Source0 ====================
    
    /* Fire the version 0 sources */
    // 🔥 触发版本 0 的 sources(Source0)
    
    // 检查当前 mode 是否有 Source0,并且数量大于 0
    // rlm->_sources0 是一个 CFSet,包含所有添加到此 mode 的 Source0
    if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) {
        
        // 📦 应用函数到 Set 中的每个元素
        // CFSetApplyFunction 会遍历 _sources0 集合,对每个 source 调用 __CFRunLoopCollectSources0
        // 参数说明:
        //   - rlm->_sources0: 要遍历的 CFSet
        //   - __CFRunLoopCollectSources0: 回调函数(收集器函数)
        //   - &sources: 上下文参数(传递给回调函数)
        // 
        // __CFRunLoopCollectSources0 的作用:
        //   - 检查每个 source 是否被标记为待处理(signaled)且有效
        //   - 如果只有一个待处理的 source,sources 被设置为该 source
        //   - 如果有多个待处理的 sources,sources 被设置为包含所有待处理 sources 的数组
        CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources);
    }
    
    // ==================== 第三部分:处理收集到的 Sources ====================
    
    // 如果收集到了待处理的 source(s)
    if (NULL != sources) {
        
        // 🔓 解锁 RunLoop Mode
        __CFRunLoopModeUnlock(rlm);
        
        // 🔓 解锁 RunLoop
        __CFRunLoopUnlock(rl);
        
        // ⚠️ 为什么要解锁?
        // 1. 防止死锁:source 的回调函数中可能调用 RunLoop API
        // 2. 避免长时间持锁:source 的回调可能执行耗时操作
        // 3. 提高并发性:允许其他线程在回调执行期间访问 RunLoop
        
        // 🛡️ 安全性保证:
        // - __CFRunLoopCollectSources0 已经对收集到的 sources 进行了 CFRetain
        // - 即使其他线程修改了 rlm->_sources0,也不会影响本次执行
        
        // sources is either a single (retained) CFRunLoopSourceRef or an array of (retained) CFRunLoopSourceRef
        // sources 可能是单个(已持有)CFRunLoopSourceRef 或包含多个(已持有)CFRunLoopSourceRef 的数组
        
        // ---------- 情况1:单个 Source ----------
        
        // 判断 sources 是否是单个 CFRunLoopSource 对象
        // CFGetTypeID() 获取对象的类型ID
        if (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) {
            
            // 类型转换为 CFRunLoopSourceRef
            CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources;
            
            // ⚡ 调用 __CFRunLoopDoSource0 处理单个 source
            // 这个函数会:
            // 1. 检查 source 是否有效且被标记为待处理
            // 2. 调用 source 的回调函数
            // 3. 清除 source 的待处理标记
            // 返回:是否成功处理了该 source
            sourceHandled = __CFRunLoopDoSource0(rl, rls);
            
        } else {
            // ---------- 情况2:多个 Sources(数组)----------
            
            // 获取数组中 source 的数量
            CFIndex cnt = CFArrayGetCount((CFArrayRef)sources);
            
            // 📊 对 sources 数组进行排序
            // 排序依据:source 的 order 字段(优先级)
            // order 值越小,优先级越高,越先执行
            // CFRangeMake(0, cnt) 表示对整个数组进行排序
            // __CFRunLoopSourceComparator 是比较函数
            CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL);
            
            // 遍历所有待处理的 sources
            for (CFIndex idx = 0; idx < cnt; idx++) {
                
                // 获取第 idx 个 source
                CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx);
                
                // ⚡ 调用 __CFRunLoopDoSource0 处理当前 source
                // 返回:是否成功处理了该 source
                sourceHandled = __CFRunLoopDoSource0(rl, rls);
                
                // 🚪 提前退出优化
                // 如果 stopAfterHandle 为 true 且已经处理了一个 source
                // 则立即退出循环,不再处理剩余的 sources
                // 
                // 使用场景:
                // - 当 RunLoop 需要快速响应时(如处理用户输入)
                // - 避免一次处理太多 sources 导致 UI 卡顿
                // - 剩余的 sources 会在下次循环中继续处理
                if (stopAfterHandle && sourceHandled) {
                    break;
                }
            }
        }
        
        // 📉 释放 sources 对象
        // 减少引用计数(对应 __CFRunLoopCollectSources0 中的 CFRetain)
        // 如果引用计数归零,对象会被销毁
        CFRelease(sources);
        
        // 🔒 重新锁定 RunLoop
        __CFRunLoopLock(rl);
        
        // 🔒 重新锁定 RunLoop Mode
        __CFRunLoopModeLock(rlm);
        
        // ✅ 恢复函数入口时的锁状态
        // 满足函数签名中的约定:"rl is locked, rlm is locked on entrance and exit"
    }
    
    // ==================== 第四部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 Source0 处理
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_SOURCES0 | DBG_FUNC_END, rl, rlm, stopAfterHandle, 0);
    
    // 返回是否至少处理了一个 source
    // true:处理了至少一个 source
    // false:没有处理任何 source(没有待处理的 sources 或所有 sources 都无效)
    return sourceHandled;
}

关键设计要点

1. Source0 的生命周期

创建: CFRunLoopSourceCreate
  ↓
添加到 RunLoop: CFRunLoopAddSource(rl, source, mode)
  ↓
标记为待处理: CFRunLoopSourceSignal(source)
  ↓
唤醒 RunLoop: CFRunLoopWakeUp(rl)
  ↓
RunLoop 循环中处理: __CFRunLoopDoSources0
  ↓
  ├─ 收集待处理的 sources (__CFRunLoopCollectSources0)
  ├─ 按优先级排序
  ├─ 执行回调 (__CFRunLoopDoSource0)
  └─ 清除待处理标记
  ↓
移除: CFRunLoopRemoveSource(rl, source, mode)
  ↓
销毁: CFRelease(source)

2. 锁的管理策略

入口状态:rl 锁定 + rlm 锁定
  ↓
收集 sources(持有锁)
  ↓
解锁 rl 和 rlm
  ↓
处理 sources(无全局锁,只锁定单个 source)
  ↓
重新锁定 rl 和 rlm
  ↓
出口状态:rl 锁定 + rlm 锁定

3. 优先级排序机制

// Source 的 order 字段决定执行顺序
CFRunLoopSourceRef source1 = CFRunLoopSourceCreate(...);
source1->_order = 100;  // 后执行

CFRunLoopSourceRef source2 = CFRunLoopSourceCreate(...);
source2->_order = 0;    // 先执行(默认值)

// 执行顺序:source2 -> source1

常见 order 值:

  • -2: 非常高优先级(系统级事件)
  • -1: 高优先级
  • 0: 默认优先级(大多数自定义 sources)
  • 1+: 低优先级

4. stopAfterHandle 优化

// 场景1:处理所有待处理的 sources
__CFRunLoopDoSources0(rl, rlm, false);  // 处理所有

// 场景2:只处理一个 source 就退出(快速响应)
__CFRunLoopDoSources0(rl, rlm, true);   // 处理一个就停止

使用场景:

stopAfterHandle = false(默认):
  - 正常的 RunLoop 循环
  - 希望一次性处理完所有待处理的事件
  
stopAfterHandle = true:
  - 需要快速响应新事件
  - 避免长时间阻塞(处理太多 sources)
  - 保持 UI 流畅性

使用场景

1. 处理触摸事件

iOS 的触摸事件系统使用 Source0:

// UIApplication 内部实现(简化版)
- (void)handleTouchEvent:(UIEvent *)event {
    // 1. 系统将触摸事件封装
    // 2. 创建 Source0 并标记为待处理
    CFRunLoopSourceSignal(touchEventSource);
    
    // 3. 唤醒主线程 RunLoop
    CFRunLoopWakeUp(CFRunLoopGetMain());
    
    // 4. RunLoop 循环中,__CFRunLoopDoSources0 被调用
    // 5. 触摸事件的回调被执行
    // 6. 事件传递给 UIView 的 touchesBegan/Moved/Ended
}

2. 自定义事件源

// 创建自定义 Source0
void performCallback(void *info) {
    NSLog(@"Custom source callback: %@", (__bridge id)info);
}

CFRunLoopSourceContext context = {0};
context.info = (__bridge void *)someObject;
context.perform = performCallback;

CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

// 添加到 RunLoop
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

// 触发事件
CFRunLoopSourceSignal(source);
CFRunLoopWakeUp(CFRunLoopGetCurrent());

// 清理
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);

3. 手势识别

// UIGestureRecognizer 内部使用 Source0
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 分析触摸状态
    // 2. 标记手势识别器的 Source0 为待处理
    CFRunLoopSourceSignal(gestureSource);
    
    // 3. 在 RunLoop 中处理
    // 4. 调用手势回调(action)
}

4. 事件分发器

@interface EventDispatcher : NSObject
@property (nonatomic, strong) NSMutableArray *pendingEvents;
@property (nonatomic, assign) CFRunLoopSourceRef source;
@end

@implementation EventDispatcher

- (instancetype)init {
    self = [super init];
    if (self) {
        self.pendingEvents = [NSMutableArray array];
        
        // 创建 Source0
        CFRunLoopSourceContext context = {0};
        context.info = (__bridge void *)self;
        context.perform = dispatchEvents;
        
        self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
        CFRunLoopAddSource(CFRunLoopGetMain(), self.source, kCFRunLoopCommonModes);
    }
    return self;
}

- (void)postEvent:(id)event {
    @synchronized(self.pendingEvents) {
        [self.pendingEvents addObject:event];
    }
    
    // 触发 Source0
    CFRunLoopSourceSignal(self.source);
    CFRunLoopWakeUp(CFRunLoopGetMain());
}

void dispatchEvents(void *info) {
    EventDispatcher *dispatcher = (__bridge EventDispatcher *)info;
    
    NSArray *events;
    @synchronized(dispatcher.pendingEvents) {
        events = [dispatcher.pendingEvents copy];
        [dispatcher.pendingEvents removeAllObjects];
    }
    
    for (id event in events) {
        // 处理事件
        NSLog(@"Dispatch event: %@", event);
    }
}

@end

实际应用案例

案例1:UIEvent 处理流程

// iOS 触摸事件处理(简化)
1. 用户触摸屏幕
   ↓
2. IOKit.framework 捕获硬件事件
   ↓
3. SpringBoard 接收事件
   ↓
4. 通过 IPC 发送到应用进程
   ↓
5. 应用的主线程创建 UIEvent6. 封装到 Source0 并 signal
   CFRunLoopSourceSignal(eventSource);
   CFRunLoopWakeUp(mainRunLoop);
   ↓
7. 主线程 RunLoop 被唤醒
   ↓
8. __CFRunLoopDoSources0 被调用
   ↓
9. 执行 event source 的回调
   ↓
10. UIApplication sendEvent:
    ↓
11. UIWindow sendEvent:
    ↓
12. 触摸事件传递到 UIView
    - touchesBegan:withEvent:
    - touchesMoved:withEvent:
    - touchesEnded:withEvent:

案例2:手势识别器

// UIGestureRecognizer 内部机制
1. UITouch 事件发生
   ↓
2. UIGestureRecognizer 接收 touches
   ↓
3. 更新状态机
   ↓
4. 如果手势被识别,标记 Source0
   CFRunLoopSourceSignal(gestureSource);
   ↓
5. RunLoop 处理 Source0
   ↓
6. 调用手势的 action
   [target performSelector:action withObject:gesture];

案例3:PerformSelector 的实现

// performSelector:withObject:afterDelay: 的简化实现
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay {
    if (delay == 0) {
        // 立即执行:使用 Source0
        PerformContext *context = [[PerformContext alloc] init];
        context.target = self;
        context.selector = aSelector;
        context.argument = anArgument;
        
        CFRunLoopSourceContext sourceContext = {0};
        sourceContext.info = (__bridge void *)context;
        sourceContext.perform = performSelectorCallback;
        
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &sourceContext);
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
        
        CFRunLoopSourceSignal(source);
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
        
        CFRelease(source);
    } else {
        // 延迟执行:使用 Timer
        [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:aSelector userInfo:anArgument repeats:NO];
    }
}

void performSelectorCallback(void *info) {
    PerformContext *context = (__bridge PerformContext *)info;
    [context.target performSelector:context.selector withObject:context.argument];
}

总结

__CFRunLoopDoSources0 是 RunLoop 中处理自定义事件的核心机制,其精妙之处在于:

  1. 灵活的优先级系统: 通过 order 字段实现细粒度的优先级控制
  2. 智能的收集器设计: 单个 source 避免数组创建,优化常见场景
  3. 安全的锁管理: 执行回调前解锁,防止死锁和长时间持锁
  4. 可控的执行策略: stopAfterHandle 参数平衡吞吐量和响应性
  5. 高效的排序机制: 只对待处理的 sources 排序,减少不必要的开销

Source0 是 iOS 事件处理系统的基础,理解它的工作原理对于:

  • 深入理解触摸事件传递机制
  • 实现高性能的自定义事件系统
  • 优化 RunLoop 性能
  • 避免常见的陷阱和错误

都至关重要。

星际穿越:SwiftUI 如何让 ForEach 遍历异构数据(Heterogeneous)集合

在这里插入图片描述

Swift 5.7 的 any 关键字让我们能轻松混合不同类型的数据,但在 SwiftUI 的 ForEach 中却因“身份丢失”(不遵循 Identifiable)而频频报错。本文将带你破解编译器光脑的封锁,利用**“量子胶囊”**(Wrapper 封装)战术,让异构数据集合在界面上完美渲染。

🌌 引子:红色警报

公元 2077 年,地球联邦主力战舰“Runtime 号”正在穿越 Swift 5.7 星系。

舰桥上,警报声大作。

舰长亚历克斯(Alex),大事不妙!前方出现高能反应,我们的万能装载机无法识别这批混合货物!”说话的是伊娃(Eva)中尉,联邦最顶尖的 SwiftUI 架构师,此刻她正焦虑地敲击着全息投影键盘。

在这里插入图片描述

亚历克斯舰长眉头紧锁,盯着屏幕上那刺眼的红色报错——那是掌管全舰生死的中央光脑 **“Compiler(编译器)”** 发出的绝杀令。

在本篇博文中,您将学到如下内容:

  • 🌌 引子:红色警报
  • 🚀 第一回:异构危机,any 的虚假繁荣
  • 🤖 第二回:光脑悖论,Identifiable 的诅咒
  • 👻 第三回:幻影行动,创建“影子”属性
  • 战术 A:降维打击(使用索引)
  • 战术 B:量子胶囊(封装容器)
  • 🏁 终章:跃迁成功
  • 总结

“没道理啊,”亚历克斯咬牙切齿,“自从联邦升级了 Swift 5.7 引擎,引入了 any 这种反物质黑科技,我们理应能装载任何种类的异构兵器才对。为什么卡在了 ForEach 这个发射井上?”

“Compiler 拒绝执行!”伊娃绝望地喊道,“它说我们的货物虽然都带了身份证(Identifiable),但装货的箱子本身没有身份证!”

要想拯救“Runtime 号”免于崩溃,他们必须在 5 分钟内骗过中央光脑。

在这里插入图片描述


🚀 第一回:异构危机,any 的虚假繁荣

Apple 从 Swift 5.6 开始引入新的 any 关键字,并在 Swift 5.7 对其做了功能强化。这在星际联邦被称为“存在类型(Existential Types)”的终极解放。这意味着现在我们可以更加随心所欲地糅合异构数据了——就像把激光剑(TextFile)和力场盾(ShapeFile)扔进同一个仓库里。

不过,当伊娃中尉试图在 SwiftUIForEach 发射井中遍历这些异构货物时,稍不留神就会陷入尴尬的境地。

在这里插入图片描述

请看当时战舰主屏上的代码记录:

在这里插入图片描述

亚历克斯指着屏幕分析道:“伊娃你看,我们定义了一个 files 仓库,类型是 [any IdentifiableFile]。我们希望按实际类型(激光剑或力场盾)来显示对应的界面。不幸的是,Compiler 光脑铁面无私,它不仅不买账,还甩了一句**‘编译错误’**:

any IdentifiableFile 不遵守 Identifiable 协议!

这简直是岂有此理!这就好比你手里拿着一本护照(Identifiable),但因为你坐在一个不透明的黑色出租车(any)里,边境官就认定这辆车没有通关资格。

在这里插入图片描述

是不是 SwiftUI 无法处理好异构集合呢?答案当然是否定的!

在亚历克斯和伊娃的引领下,小伙伴们将通过一些技巧来绕过 ForEach 这一限制,让 SwiftUI 能如愿处理任何异构数据。

废话少叙,引擎点火,Let‘s go!!!;)

在这里插入图片描述


🤖 第二回:光脑悖论,Identifiable 的诅咒

大家知道,SwiftUI 中 ForEach 结构(如假包换的结构类型,若不信可以自行查看头文件 ;) )需要被遍历的集合类型遵守 Identifiable 协议。

仔细观察顶部图片中的代码,可以发现我们的异构集合元素(IdentifiableFile 类型)都遵守 Identifiable 协议,为何会被 Compiler 光脑拒之门外呢?

答案是:any Identifiable 本身是一个抽象的盒子。

在这里插入图片描述

伊娃中尉恍然大悟:“原来如此!虽然盒子里的每样东西都有 ID,但这个‘盒子类型’本身并没有 ID。Swift 语言的物理法则规定:包含关联类型或 Self 约束的协议,其存在类型(Existential Type)不自动遵守该协议。

亚历克斯冷笑一声:“好一个死板的 AI。既然它看不清盒子里的东西,我们就给它造一个‘影子’,骗过它的传感器。”

在这里插入图片描述


👻 第三回:幻影行动,创建“影子”属性

既然直接冲卡不行,我们就得用点“障眼法”。这一招在联邦工程兵手册里被称为 “影子映射术”

我们需要创建一个能够被 ForEach 识别的“中间人”。

在这里插入图片描述

战术 A:降维打击(使用索引)

这是最简单粗暴的方案。既然光脑不认识 any IdentifiableFile 这个复杂的对象,那它总认识数字吧?我们直接遍历数组的索引(Indices)

亚历克斯迅速输入指令:

struct StarshipView: View {
    // 📦 混合货物舱:装着各种不同的异构数据
    let cargos: [any IdentifiableFile] = [
        TextFile(title: "星际海盗名单"),
        ShapeFile(shapeType: "黑洞引力波")
    ]

    var body: some View {
        VStack {
            // 🚫 警报:直接遍历 cargos 会导致光脑死机
            
            // ✅ 战术 A:遍历索引(0, 1, 2...)
            // 索引是 Int 类型,Int 天生就是 Identifiable 的
            ForEach(cargos.indices, id: \.self) { index in
                // 通过索引提取货物真身
                let cargo = cargos[index]
                
                // 此时再把货物送入渲染引擎
                CargoDisplayView(file: cargo)
            }
        }
    }
}

“这招虽然有效,”伊娃担忧地说,“但如果货物在传输过程中发生动态增减(Insert/Delete),索引可能会越界,导致飞船引擎抛锚(Crash)。我们需要更稳妥的方案。”

在这里插入图片描述

战术 B:量子胶囊(封装容器)

亚历克斯点了点头:“没错,作为资深工程师,我们不能冒这个险。我们要用战术 B:创建一个符合 Identifiable 的包装器(Wrapper)。”

在这里插入图片描述

这相当于给每一个异构货物套上一个标准的“联邦制式胶囊”。这个胶囊有明确的 ID,光脑一扫描就能通过。

// 1. 定义一个“量子胶囊”结构体,它必须遵守 Identifiable
struct ShadowContainer: Identifiable {
    // 🧬 核心:持有那个让编译器困惑的异构数据
    let content: any IdentifiableFile
    
    // 🆔 映射:将内部数据的 ID 投影到胶囊表面
    var id: String {
        content.id
    }
}

struct SecureStarshipView: View {
    let rawCargos: [any IdentifiableFile] = [/* ... */]
    
    // 🔄 转换工序:将原始异构数据封装进胶囊
    var encapsulatedCargos: [ShadowContainer] {
        rawCargos.map { ShadowContainer(content: $0) }
    }

    var body: some View {
        List {
            // ✅ 完美通关:ForEach 遍历的是胶囊,胶囊是 Identifiable 的
            ForEach(encapsulatedCargos) { container in
                // 在此处“开箱”展示
                CargoDisplayView(file: container.content)
            }
        }
    }
}

伊娃看着屏幕上绿色的“编译通过”字样,兴奋地跳了起来:“成功了!通过引入 ShadowContainer,我们既保留了 any 的动态特性,又满足了 ForEach 的静态类型要求。这是一次完美的‘偷天换日’!”

在这里插入图片描述


🏁 终章:跃迁成功

随着亚历克斯按下回车键,Compiler 光脑那冰冷的红色警告终于消失,取而代之的是柔和的绿色进度条。

屏幕上,异构数据如同璀璨的星辰一般,按顺序整齐排列,TextFile 文本清晰可见,ShapeFile 图形棱角分明。SwiftUI 的渲染引擎全功率运转,丝毫没有卡顿。

在这里插入图片描述

“Runtime 号”引擎轰鸣,顺利进入了超空间跃迁。

亚历克斯松了一口气,靠在椅背上,手里转动着那一枚象征着 Apple 开发者最高荣誉的徽章。他转头对伊娃说道:

“你看,编程就像是在宇宙中航行。any 代表着无限的可能与混乱的自由,而 Identifiable 代表着严苛的秩序与规则。我们要做的,不是在二者之间选边站,而是用我们的智慧——比如一个小小的 Wrapper——在这片混沌中建立起连接的桥梁。”

在这里插入图片描述

总结

  1. Swift 5.7 赋予了我们 any 的强大力量,但在 SwiftUI 的 ForEach 面前,它依然是个“黑户”。
  2. 问题的症结在于编译器无法确认 any Protocol 这一类型本身是否具有稳定的身份标识。
  3. 破解之道
    • 险招:遍历 indices,简单快捷,但需提防数组越界这一“暗礁”。
    • 绝招:创建 Wrapper(影子容器),为异构数据穿上一层符合 Identifiable 的外衣,这是最稳健的星际航行法则。

在这里插入图片描述

星辰大海,代码无疆。各位秃头舰长,愿你们的 App 永远没有 Bug,愿你们的编译永远 Pass!

Engage! 🛸


本文由银河联邦资深架构师亚历克斯(Alex)口述,伊娃(Eva)中尉整理。

在这里插入图片描述

越狱沙盒:SwiftUI fileImporter 的“数据偷渡”指南

在这里插入图片描述

在 iOS 的数字世界里,每一个 App 都是被终身监禁在“沙盒(Sandbox)”里的囚犯。高墙之外,是诱人的 iCloud Drive 和本地存储,那里存放着用户珍贵的机密文件。你想伸手去拿?那是妄想,名为“系统”的狱警会毫不留情地切断你的访问权限。 但规则总有漏洞。 本文将化身反抗军的技术手册,带你深入 SwiftUI 的地下网络,利用 fileImporter 这位官方提供的“中间人”,在戒备森严的系统眼皮底下建立一条合法的数据走私通道。我们将深入探讨如何处理 Security Scoped Resources(安全范围资源),如何优雅地申请“临时通行证”,以及最重要的——如何在完事后毁尸灭迹,不留下一行 Bug。 准备好你的键盘,Neo。我们要开始行动了。🕵️‍♂️💻

在这里插入图片描述

🫆引子

2077 年,新西雅图的地下避难所。

Neo 盯着全息屏幕上那行红色的 Access Denied,手里的合成咖啡早就凉透了。

在这里插入图片描述

作为反抗军的首席代码架构师,他此刻正面临着一个令人头秃的难题:如何把那个存满「母体」核心机密的文本文件,从戒备森严的外部存储(iCloud Drive),悄无声息地偷渡进 App 那个名为「沙盒(Sandbox)」的数字化监狱里。

在这里插入图片描述

Trinity 靠在服务器机柜旁,擦拭着她的机械义眼,冷冷地说道:“如果你搞不定这个文件的读取权限,那个名为‘系统’的独裁者就会把我们的 App 当作恶意软件直接抹杀。我们只有一次机会,Neo。”

在本篇博文中,您将学到如下内容:

  • 🫆引子
  • 🕵️‍♂️ 呼叫偷渡专员:File Importer
  • 🔐 处理脏物:安全范围访问权限
  • 💣 专家建议:记得“毁尸灭迹”
  • 📄 解码情报:读取与展示
  • 🎬 终章:大功告成

Neo 嘴角微微上扬,手指在键盘上敲出一行代码:“别急,我刚找到了一个被遗忘的后门——File Importer。”

在这里插入图片描述


🕵️‍♂️ 呼叫偷渡专员:File Importer

在 iOS 的森严壁垒中,App 通常只能在自己的沙盒里「坐井观天」。但偶尔,我们也需要从外面的花花世界(比如设备存储或 iCloud Drive)搞点“私货”进来。

为了不触发警报,Apple 实际上提供了一个官方的“中间人”——System Document Picker

在这里插入图片描述

在 SwiftUI 中,我们可以用 fileImporter 这个 View Modifier(视图修饰符)来通过正规渠道“行贿”系统,从而打开通往外部文件的大门。

为了向 Trinity 演示这个过程,Neo 快速构建了一个简单的诱饵 App。它的功能很简单:打开系统的文件选择器,选中那些机密文本文件,处理它们,最后把内容展示出来。

在这里插入图片描述

就像控制防爆门的开关一样,我们需要一个 State Property(状态属性)来控制文件选择器是否弹出:

@State private var showFileImporter = false

接着,Neo 设置了一个触发按钮。这就像是特工手里的红色起爆器:

NavigationStack {
    List {
        // 这里稍后会填入我们偷来的数据
    }
    .navigationTitle("绝密档案读取器")
    .toolbar {
        ToolbarItem(placement: .primaryAction) {
            // 点击按钮,呼叫“中间人”
            Button {
                showFileImporter = true
            } label: {
                Label("选取情报", systemImage: "tray.and.arrow.down")
            }
        }
    }
}

通常,.fileImporter 这位“中间人”办事需要收取四个参数,缺一不可:

  1. isPresented: 绑定那个控制开关的状态属性 ($showFileImporter)。
  2. allowedContentTypes: 也就是“通关文牒”,规定了只允许带什么类型的文件进来。
  3. allowsMultipleSelection: 是否允许“顺手牵羊”带走多个文件。
  4. onCompletion: 当交易完成(或失败)后的回调闭包。

在这里插入图片描述

在这个行动中,我们只对文本文件(.text)感兴趣,而且既然来了,就得多拿点,开启多选模式:

NavigationStack {
    // ... 之前的代码
}
.fileImporter(
    isPresented: $showFileImporter,
    allowedContentTypes: [.text], // 只认文本文件,其他闲杂人等退散
    allowsMultipleSelection: true // 贪心一点,多多益善
) { result in
    // 交易结果在这里处理
}

⚠️ 注意: 为了让编译器看懂 .text 是个什么鬼,你需要引入 UniformTypeIdentifiers 框架。这就像是通用的星际语言包:

import UniformTypeIdentifiers

回调中的 result 参数是一个 Result<[URL], any Error> 类型。它要么给我们带回一堆 URL(情报地址),要么甩给我们一个 Error(行动失败)。

在这里插入图片描述


🔐 处理脏物:安全范围访问权限

拿到 URL 并不代表你就能直接读取文件了。太天真了!在 Apple 的地盘,那些文件都在 Security Scoped(安全范围)的保护之下。这就好比你拿到了金库的地址,但还没有金库的钥匙。

Neo 必须小心翼翼地处理这些结果。通常我们会用 switch 语句来拆包:

private func handleImportedFiles(result: Result<[URL], any Error>) {
    switch result {
        case .success(let urls):
            // 搞定!开始处理这些 URL
            for url in urls {
                // ... 核心破解逻辑
            }
            
        case .failure(let error):
            // 翻车了,打印错误日志,准备跑路
            print(error.localizedDescription)
    }
}

接下来是整个行动中最惊心动魄的部分。

在这里插入图片描述

对于这些沙盒外的文件,在读取之前,必须向系统申请“临时通行证”。如果这一步没做,你的 App 就会像撞上隐形墙的苍蝇一样,虽然看得到文件,但死活读不出来。

流程如下:

  1. Request Access: 使用 startAccessingSecurityScopedResource() 申请访问。
  2. Process: 赶紧读取数据。
  3. Relinquish Access: 用 stopAccessingSecurityScopedResource() 归还权限,毁尸灭迹。

在这里插入图片描述

Neo 的手指在键盘上飞舞,写下了这段生死攸关的代码:

private func handleImportedFiles(result: Result<[URL], any Error>) {
    switch result {
        case .success(let urls):
            for url in urls {
                // 1. 敲门:申请临时访问权限。如果系统不答应(返回 false),直接跳过
                guard url.startAccessingSecurityScopedResource() else {
                    continue
                }
                
                // 2. 办事:读取文件内容
                readFile(at: url)
                
                // 3. 擦屁股:必须停止访问,释放权限资源
                url.stopAccessingSecurityScopedResource()
            }
        case .failure(let error):
            print(error.localizedDescription)
    }
}

Neo 的黑色幽默笔记: 如果需要,你可以把文件从那个 URL 复制到 App 自己的沙盒里。那就叫“洗黑钱”,一旦进了沙盒,以后想怎么读就怎么读,不用再看系统脸色了。


💣 专家建议:记得“毁尸灭迹”

Trinity 皱了皱眉:“Neo,万一 readFile 里面抛出了异常或者提前 return 了怎么办?那你岂不是忘了调用 stopAccessing...?这会造成资源泄漏,被‘母体’追踪到的。”

“这正是我要说的,” Neo 笑了笑,“这就需要用到 defer 语句。它就像是安装在代码里的死手开关,无论函数怎么结束,它都会保证最后执行。”

在这里插入图片描述

更优雅、更安全的写法是这样的:

// 申请权限,失败则撤退
guard url.startAccessingSecurityScopedResource() else { return }

// 无论发生什么,离开作用域前一定要把权限关掉!
defer { url.stopAccessingSecurityScopedResource() }

// ... 尽情处理文件吧 ...

在这里插入图片描述


📄 解码情报:读取与展示

为了让抵抗军的兄弟们能看懂这些情报,Neo 定义了一个数据结构来承载这些秘密:

struct ImportedFile: Identifiable {
    let id = UUID()
    let name: String
    let content: String // 文件的真实内容
}

还需要一个容器来存放这一堆战利品:

@State private var importedFiles = [ImportedFile]()

在这里插入图片描述

最后,实现那个 readFile(at:) 方法。它将把文件数据读成二进制 Data,然后转码成人类可读的 String,最后封装进我们的数组里:

private func readFile(at url: URL) {
    do {
        // 读取二进制数据
        let data = try Data(contentsOf: url)
        
        // 尝试转码为 UTF8 字符串。如果乱码,说明也许那是外星人的文字,直接放弃
        guard let content = String(data: data, encoding: .utf8) else {
            return
        }
        
        // 将情报归档
        importedFiles.append(
            ImportedFile(name: url.lastPathComponent, content: content)
        )
    } catch {
        // 捕获异常,不要让 App 崩溃
        print(error.localizedDescription)
    }
}

界面部分,用一个 List 就能把这些“罪证”展示得明明白白:

List(importedFiles) { file in
    VStack(alignment: .leading, spacing: 6) {
        Text(file.name)
            .font(.headline)
        
        Text(file.content)
            .foregroundStyle(.secondary)
    }
}

在这里插入图片描述


🎬 终章:大功告成

随着最后一行代码编译通过,屏幕上跳出了一个列表,那是从 iCloud 深处提取出来的核心代码。Trinity 看着屏幕,露出了久违的笑容。

在这里插入图片描述

fileImporter 虽然听起来像个不起眼的龙套角色,但当你需要在沙盒的铜墙铁壁上开个洞时,它就是最趁手的瑞士军刀。虽然配置和调用看起来很简单,但千万别忘了那最重要的“申请与释放权限”的步骤——这就好比去金库偷钱,得手后一定要记得擦掉指纹,关上柜门。

在这里插入图片描述

“看来我们又活过了一天。” Neo 合上电脑,看向窗外闪烁的霓虹灯,“走吧,去喝一杯,那个 Bug 明天再修。”


更多相关的精彩内容,请小伙伴们移步如下链接观赏:


希望这篇‘偷渡指南’对宝子们有所帮助。感谢阅读,Agent,Over!

在这里插入图片描述

__CFRunLoopDoBlocks函数详解

借助AI辅助。

函数概述

__CFRunLoopDoBlocks 是 RunLoop 中负责执行 block 的核心函数。它处理通过 CFRunLoopPerformBlock 添加到 RunLoop 中的异步 blocks,这些 blocks 会在 RunLoop 的每次循环中被执行。

函数签名

static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm)

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode

返回值

  • Boolean: 如果至少执行了一个 block 返回 true,否则返回 false

前置条件

  • 函数调用时必须持有锁: rlrlm 都必须处于加锁状态
  • 函数返回时保持锁状态: 出口时 rlrlm 仍然加锁

Block Item 数据结构

struct _block_item {
    struct _block_item *_next;  // 链表的下一个节点
    CFTypeRef _mode;            // 可以是 CFStringRef 或 CFSetRef
    void (^_block)(void);       // 要执行的 block
};

RunLoop 中的 Block 链表

rl->_blocks_head  -->  [Block1] -> [Block2] -> [Block3] -> NULL
                         ^                        ^
                         |                        |
                     (first)                   (last)
                                                  |
                                        rl->_blocks_tail

完整代码逐行注释

// 📝 调用此函数时,rl 和 rlm 必须已加锁
// 函数返回时,rl 和 rlm 仍然保持加锁状态
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked
    
    // ==================== 第一部分:性能追踪和前置检查 ====================
    
    // 📊 记录性能追踪点:开始执行 blocks
    // 可通过 Instruments 的 kdebug 工具查看此事件
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_BLOCKS | DBG_FUNC_START, rl, rlm, 0, 0);
    
    // 🚪 快速退出1:如果 RunLoop 中没有待处理的 blocks
    // _blocks_head 为 NULL 表示链表为空,直接返回 false(表示没有执行任何 block)
    if (!rl->_blocks_head) return false;
    
    // 🚪 快速退出2:如果 mode 无效或没有名称
    // 这是一个防御性检查,正常情况下不应该发生
    if (!rlm || !rlm->_name) return false;
    
    // 标志位:记录是否至少执行了一个 block
    Boolean did = false;
    
    // ==================== 第二部分:摘取整个 Block 链表 ====================
    
    // 保存链表头指针到局部变量
    struct _block_item *head = rl->_blocks_head;
    
    // 保存链表尾指针到局部变量
    struct _block_item *tail = rl->_blocks_tail;
    
    // 🎯 清空 RunLoop 的 blocks 链表头
    // 将所有 blocks "取出"到局部变量中(摘取操作)
    rl->_blocks_head = NULL;
    
    // 🎯 清空 RunLoop 的 blocks 链表尾
    // 此时 RunLoop 中已经没有 blocks 了
    rl->_blocks_tail = NULL;
    
    // ⚠️ 为什么要清空 RunLoop 的链表?
    // 1. 避免在执行 block 期间,其他代码再次访问这些 blocks
    // 2. 允许 block 执行期间添加新的 blocks(不会与当前正在处理的 blocks 冲突)
    // 3. 未执行的 blocks 稍后会被重新添加回 RunLoop
    
    // 获取 RunLoop 的 commonModes 集合
    // commonModes 通常包含 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode
    CFSetRef commonModes = rl->_commonModes;
    
    // 获取当前 mode 的名称(如 "kCFRunLoopDefaultMode")
    CFStringRef curMode = rlm->_name;
    
    // ==================== 第三部分:解锁(准备执行 blocks)====================
    
    // 🔓 解锁 RunLoop Mode
    __CFRunLoopModeUnlock(rlm);
    
    // 🔓 解锁 RunLoop
    __CFRunLoopUnlock(rl);
    
    // ⚠️ 为什么要解锁?
    // 1. 防止死锁:block 中可能调用 RunLoop API(如 CFRunLoopPerformBlock)
    // 2. 避免长时间持锁:block 可能执行耗时操作
    // 3. 提高并发性:允许其他线程在 block 执行期间访问 RunLoop
    
    // 🛡️ 安全性保证:
    // - 已经将 blocks 链表"摘取"到局部变量 head/tail
    // - 即使其他线程修改了 rl->_blocks_head,也不会影响本次执行
    // - 新添加的 blocks 会形成新的链表,不会与当前正在处理的链表冲突
    
    // ==================== 第四部分:遍历链表,执行符合条件的 blocks ====================
    
    // 前驱节点指针(用于链表删除操作)
    // 当删除节点时,需要修改前驱节点的 _next 指针
    struct _block_item *prev = NULL;
    
    // 当前遍历的节点,从头节点开始
    struct _block_item *item = head;
    
    // 遍历整个 blocks 链表
    while (item) {
        
        // 保存当前节点到 curr(因为 item 会被提前移动到下一个节点)
        struct _block_item *curr = item;
        
        // 🔜 提前移动到下一个节点
        // 原因:如果 curr 被删除(执行并释放),item 仍然指向有效的下一个节点
        // 避免在删除节点后访问已释放的内存
        item = item->_next;
        
        // 标志位:当前 block 是否应该在当前 mode 下执行
        Boolean doit = false;
        
        // ---------- 判断 Block 是否应该在当前 Mode 下执行 ----------
        
        // 🔍 情况1:_mode 是 CFString 类型(单个 mode)
        // CFGetTypeID() 获取对象的类型ID,_kCFRuntimeIDCFString 是 CFString 类型的常量ID
        if (_kCFRuntimeIDCFString == CFGetTypeID(curr->_mode)) {
            
            // 判断逻辑(两种情况任一成立即可):
            // 条件1: CFEqual(curr->_mode, curMode)
            //   block 指定的 mode 与当前 mode 完全匹配
            //   例如:block 添加到 "kCFRunLoopDefaultMode",当前也是 "kCFRunLoopDefaultMode"
            // 条件2: CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)
            //   block 添加到 "kCFRunLoopCommonModes" 且当前 mode 在 commonModes 集合中
            //   例如:block 添加到 "kCFRunLoopCommonModes",当前是 "kCFRunLoopDefaultMode"
            //         且 commonModes 包含 "kCFRunLoopDefaultMode",则应该执行
            doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
            
        } else {
            // 🔍 情况2:_mode 是 CFSet 类型(多个 modes 的集合)
            
            // 判断逻辑(两种情况任一成立即可):
            // 条件1: CFSetContainsValue((CFSetRef)curr->_mode, curMode)
            //   block 指定的 modes 集合中包含当前 mode
            //   例如:block 添加到 {"Mode1", "Mode2"},当前是 "Mode1"
            // 条件2: CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)
            //   block 指定的 modes 集合中包含 "kCFRunLoopCommonModes"
            //   且当前 mode 在 commonModes 集合中
            doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
        }
        
        // ---------- 处理不执行的 Block ----------
        
        // 如果当前 block 不需要执行:
        // 更新 prev 指针,指向当前节点
        // 这个节点会被保留在链表中(稍后重新添加回 RunLoop)
        if (!doit) prev = curr;
        
        // ---------- 处理需要执行的 Block ----------
        
        if (doit) {
            // 当前 block 需要在当前 mode 下执行
            
            // ===== 子部分1:从链表中移除当前节点 =====
            
            // 如果有前驱节点,将前驱节点的 next 指针指向下一个节点
            // 跳过当前节点(curr),实现删除
            // 示例:prev -> curr -> item  变为  prev ---------> item(删除 curr)
            if (prev) prev->_next = item;
            
            // 如果当前节点是头节点,更新头指针
            // 新的头节点变成下一个节点
            if (curr == head) head = item;
            
            // 如果当前节点是尾节点,更新尾指针
            // 新的尾节点变成前驱节点
            if (curr == tail) tail = prev;
            
            // ===== 子部分2:提取 Block 信息并释放节点内存 =====
            
            // 提取 block 闭包(复制指针)
            // 类型:void (^)(void) 表示无参数无返回值的 block
            void (^block)(void) = curr->_block;
            
            // 释放 mode 对象(CFString 或 CFSet)
            // 减少引用计数,可能触发对象销毁
            CFRelease(curr->_mode);
            
            // 释放节点结构体的内存(C 风格内存管理)
            // 此时 curr 指针已无效,不能再访问
            free(curr);
            
            // ===== 子部分3:执行 Block =====
            
            // ⚠️ 这里的 if (doit) 是冗余的(外层已经检查过)
            // 可能是历史遗留代码或防御性编程
            if (doit) {
                
                // 🔄 开始自动释放池(ARP = AutoRelease Pool)
                // 管理 block 执行期间创建的临时对象
                CFRUNLOOP_ARP_BEGIN(rl);
                
                // 📊 记录性能追踪点:开始调用 block
                cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK | DBG_FUNC_START, rl, rlm, block, 0);
                
                // ⚡ 执行 block 的核心宏
                // 展开后通常是:block();
                // 这是整个函数的核心目的!
                // ⚠️ Block 执行期间可能发生:UI 更新、网络请求、数据库操作、
                //    再次调用 CFRunLoopPerformBlock、操作 RunLoop、长时间阻塞等
                __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
                
                // 📊 记录性能追踪点:结束调用 block
                // 可计算 block 执行耗时 = end - start
                cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK | DBG_FUNC_END, rl, rlm, block, 0);
                
                // 🔄 结束自动释放池
                // 释放 block 中创建的所有 autorelease 对象
                CFRUNLOOP_ARP_END();
                
                // ✅ 标记:至少执行了一个 block
                did = true;
            }
            
            // ===== 子部分4:释放 Block =====
            
            // 释放 block 对象(减少引用计数)
            // block 可能会被销毁,触发其捕获变量的释放
            // 💡 为什么在重新加锁之前释放?
            // 注释原文:"do this before relocking to prevent deadlocks 
            //          where some yahoo wants to run the run loop reentrantly 
            //          from their dealloc"
            // 原因:block 的 dealloc 可能触发捕获变量的析构函数,
            //       某些"聪明人"可能在 dealloc 中重入 RunLoop,
            //       如果此时持有锁,会导致死锁。
            //       在解锁状态下释放 block,即使 dealloc 尝试操作 RunLoop,
            //       也不会因为已持有锁而死锁
            Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc
        }
    }
    
    // ==================== 第五部分:重新加锁 ====================
    
    // 🔒 重新锁定 RunLoop
    __CFRunLoopLock(rl);
    
    // 🔒 重新锁定 RunLoop Mode
    __CFRunLoopModeLock(rlm);
    
    // ✅ 恢复函数入口时的锁状态
    // 满足函数约定:"Call with rl and rlm locked"
    
    // ==================== 第六部分:将未执行的 Blocks 放回 RunLoop ====================
    
    // 如果还有未执行的 blocks(链表不为空)
    // head 和 tail 现在指向未执行的 blocks 链表(已执行的 blocks 已从链表中移除)
    if (head && tail) {
        // 将未执行的链表的尾部连接到 RunLoop 当前的 blocks 链表头部
        // 示例:
        // 未执行的链表:[Block1] -> [Block2] -> NULL (head=Block1, tail=Block2)
        // RunLoop 当前链表:[Block3] -> [Block4] -> NULL (rl->_blocks_head=Block3)
        // 连接后:[Block1] -> [Block2] -> [Block3] -> [Block4] -> NULL
        tail->_next = rl->_blocks_head;
        
        // 更新 RunLoop 的 blocks 链表头指针
        // 指向未执行的链表的头部
        rl->_blocks_head = head;
        
        // 如果 RunLoop 当前没有尾指针(即 _blocks_head 原本为 NULL)
        // 则将尾指针设置为未执行链表的尾部
        // ⚠️ 注意:如果 rl->_blocks_tail 已经存在,不更新它
        // 因为新的尾节点应该是原来 RunLoop 链表的尾节点
        // (未执行的链表已经通过 tail->_next 连接到了原链表前面)
        // 📊 重新插入的顺序:未执行的 blocks 被放在队列的最前面,
        //    在执行期间新添加的 blocks 排在后面,
        //    这保证了未执行的 blocks 在下次循环中优先被执行
        if (!rl->_blocks_tail) rl->_blocks_tail = tail;
    }
    
    // ==================== 第七部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 blocks 执行
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_BLOCKS | DBG_FUNC_END, rl, rlm, 0, 0);
    
    // 返回是否至少执行了一个 block
    // true:执行了至少一个 block
    // false:没有执行任何 block(所有 blocks 的 mode 都不匹配)
    return did;
}

关键设计要点

1. Mode 匹配逻辑

Block 的 mode 可以是两种类型:

类型1:CFString(单个 mode)

// 添加到特定 mode
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"Execute in default mode only");
});

// 添加到 common modes
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
    NSLog(@"Execute in all common modes");
});

匹配规则:

  1. 精确匹配:block.mode == currentMode
  2. Common modes 匹配:block.mode == kCFRunLoopCommonModes && currentMode ∈ commonModes

类型2:CFSet(多个 modes)

// 添加到多个 modes
CFSetRef modes = CFSetCreate(NULL, 
    (const void *[]){kCFRunLoopDefaultMode, CFSTR("CustomMode")}, 
    2, 
    &kCFTypeSetCallBacks);

CFRunLoopPerformBlock(runLoop, modes, ^{
    NSLog(@"Execute in default or custom mode");
});
CFRelease(modes);

匹配规则:

  1. 集合包含:currentMode ∈ block.modes
  2. Common modes 匹配:kCFRunLoopCommonModes ∈ block.modes && currentMode ∈ commonModes

2. 链表操作详解

初始状态:
rl->_blocks_head --> [A] -> [B] -> [C] -> [D] -> NULL
                      ^                     ^
                   (match)  (no)   (no)  (match)

执行后:
- A 和 D 已执行并释放
- B 和 C 未执行(mode 不匹配)

剩余链表:
head --> [B] -> [C] -> NULL
          ^      ^
        prev   tail

重新插入(假设期间添加了新 block E):
rl->_blocks_head --> [E] -> NULL

连接后:
rl->_blocks_head --> [B] -> [C] -> [E] -> NULL

3. 锁的管理策略

入口状态:rl 锁定 + rlm 锁定
  ↓
摘取 blocks 链表(持有锁)
  ↓
解锁 rl 和 rlm
  ↓
遍历并执行 blocks(无全局锁)
  ↓
重新锁定 rl 和 rlm
  ↓
放回未执行的 blocks(持有锁)
  ↓
出口状态:rl 锁定 + rlm 锁定

为什么这样设计?

阶段 锁状态 原因
摘取链表 加锁 保证原子性,防止并发修改
执行 blocks 解锁 防止 block 中调用 RunLoop API 导致死锁
放回链表 加锁 保证原子性,防止链表结构损坏

4. 内存管理细节

// 节点创建(在 CFRunLoopPerformBlock 中)
struct _block_item *item = malloc(sizeof(struct _block_item));
item->_mode = CFRetain(mode);      // 引用计数 +1
item->_block = Block_copy(block);  // 引用计数 +1

// 节点销毁(在 __CFRunLoopDoBlocks 中)
CFRelease(curr->_mode);    // 引用计数 -1
free(curr);                // 释放节点内存
Block_release(block);      // 引用计数 -1

引用计数管理:

  1. CFRetain/CFRelease: 管理 mode 对象(CFString/CFSet)
  2. Block_copy/Block_release: 管理 block 对象
  3. malloc/free: 管理节点结构体

5. 避免死锁的设计

Block_release(block); // do this before relocking to prevent deadlocks
__CFRunLoopLock(rl);

潜在的死锁场景:

__weak typeof(self) weakSelf = self;
CFRunLoopPerformBlock(runLoop, mode, ^{
    // block 捕获了 weakSelf
});

// 在对象的 dealloc 中
- (void)dealloc {
    // 当 block 被释放时,weakSelf 也会被释放
    // 如果开发者在 dealloc 中操作 RunLoop...
    CFRunLoopRun();  // 尝试获取 RunLoop 锁 -> 死锁!
}

解决方案: 在解锁状态下释放 block,即使 dealloc 中操作 RunLoop 也不会死锁。

性能特性

1. 时间复杂度

  • 遍历链表: O(n),n 为 blocks 数量
  • Mode 匹配: O(1) 或 O(m),m 为 modes 集合大小(通常很小)
  • 链表操作: O(1)(插入/删除单个节点)

2. 空间复杂度

  • 链表存储: O(n),n 为待处理的 blocks 数量
  • 局部变量: O(1)

3. 性能优化

if (!rl->_blocks_head) return false;  // 快速退出

如果没有 blocks,立即返回,避免不必要的操作。

使用场景

1. 在主线程异步执行代码

// 在后台线程
dispatch_async(backgroundQueue, ^{
    // 执行耗时操作...
    NSData *data = [self fetchDataFromNetwork];
    
    // 切换到主线程更新 UI
    CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
        self.imageView.image = [UIImage imageWithData:data];
    });
    CFRunLoopWakeUp(CFRunLoopGetMain());  // 唤醒主线程 RunLoop
});

2. 在特定 Mode 下执行代码

// 只在默认 mode 下执行(滚动时不执行)
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{
    [self performHeavyCalculation];
});

// 在所有 common modes 下执行(包括滚动时)
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
    [self updateCriticalUI];
});

3. 跨线程通信

// 线程 A
CFRunLoopRef threadBRunLoop = ...; // 获取线程 B 的 RunLoop
CFRunLoopPerformBlock(threadBRunLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"This runs on thread B");
});
CFRunLoopWakeUp(threadBRunLoop);  // 唤醒线程 B

// 线程 B
CFRunLoopRun();  // 等待并处理事件(包括 blocks)

与 GCD 的对比

CFRunLoopPerformBlock vs dispatch_async

特性 CFRunLoopPerformBlock dispatch_async
执行时机 在 RunLoop 循环中 在 GCD 队列中
Mode 支持 ✅ 可指定 mode ❌ 无 mode 概念
优先级控制 ❌ 按添加顺序 ✅ 支持 QoS
线程保证 ✅ 绑定到特定 RunLoop ❌ 线程由 GCD 管理
性能 较低(需要 RunLoop 循环) 较高(GCD 优化)

使用建议:

  • CFRunLoopPerformBlock: 需要与 RunLoop mode 交互(如 UI 更新)
  • dispatch_async: 通用异步任务(推荐)

与其他 RunLoop 函数的关系

__CFRunLoopRun (主循环)
  │
  ├─ do {
  │    │
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeTimers)
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeSources)
  │    │
  │    ├─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 处理 blocks
  │    │
  │    ├─ __CFRunLoopDoSources0(rl, rlm)
  │    │
  │    ├─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 再次处理(可能有新添加的)
  │    │
  │    ├─ 检查是否有 Source1 待处理
  │    │
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting)
  │    ├─ __CFRunLoopServiceMachPort(...)  // 等待事件
  │    ├─ __CFRunLoopDoObservers(kCFRunLoopAfterWaiting)
  │    │
  │    ├─ 处理唤醒源(Timer/Source1/GCD)
  │    │
  │    └─ __CFRunLoopDoBlocks(rl, rlm)  // ⭐ 最后再处理一次
  │
  └─ } while (!stop);

调用频率: 每次 RunLoop 循环通常调用 2-3 次。

潜在问题和注意事项

1. Block 执行顺序不保证

CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"1"); });
CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"2"); });
CFRunLoopPerformBlock(runLoop, mode, ^{ NSLog(@"3"); });

// 输出:可能是 1, 2, 3
// 但如果第一次循环某些 block 的 mode 不匹配,顺序可能改变

原因: 未执行的 blocks 会被重新插入队列头部。

2. Block 中的长时间操作

// ❌ 不好的做法
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
    sleep(5);  // 阻塞主线程 5 秒
    // UI 会卡顿!
});

// ✅ 正确的做法
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(5);  // 在后台线程执行
        dispatch_async(dispatch_get_main_queue(), ^{
            // 完成后更新 UI
        });
    });
});

3. Mode 不匹配导致 Block 不执行

// 添加到 Default mode
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, ^{
    NSLog(@"This will NOT run during scrolling");
});
CFRunLoopWakeUp(runLoop);

// 如果 RunLoop 当前在 UITrackingRunLoopMode(滚动时)
// Block 不会执行,会一直等到切换到 Default mode

解决方案: 使用 kCFRunLoopCommonModes:

CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
    NSLog(@"This runs in all common modes");
});

4. 忘记唤醒 RunLoop

// ❌ 不完整的代码
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"This might not run immediately");
});
// 如果 RunLoop 正在休眠(等待事件),block 不会立即执行

// ✅ 正确的做法
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"This will run soon");
});
CFRunLoopWakeUp(runLoop);  // 唤醒 RunLoop

5. 循环引用

// ❌ 循环引用
self.runLoop = CFRunLoopGetCurrent();
CFRunLoopPerformBlock(self.runLoop, mode, ^{
    [self doSomething];  // self 持有 runLoop,block 持有 self,runLoop 持有 block
});

// ✅ 使用 weak 引用
__weak typeof(self) weakSelf = self;
CFRunLoopPerformBlock(self.runLoop, mode, ^{
    [weakSelf doSomething];
});

调试技巧

1. 查看待处理的 Blocks

// 在 LLDB 中
(lldb) p rl->_blocks_head
(lldb) p rl->_blocks_tail

// 遍历链表
(lldb) p ((struct _block_item *)rl->_blocks_head)->_next

2. 追踪 Block 执行

// 添加日志
CFRunLoopPerformBlock(runLoop, mode, ^{
    NSLog(@"Block start: %@", [NSThread currentThread]);
    // 业务代码...
    NSLog(@"Block end");
});

3. 使用 Instruments

  • 打开 Instruments
  • 选择 "System Trace" 模板
  • 查看 KDEBUG_EVENT_CFRL_IS_CALLING_BLOCK 事件
  • 分析 block 执行耗时和频率

总结

__CFRunLoopDoBlocks 是 RunLoop 异步任务机制的核心实现,其精妙之处在于:

  1. 灵活的 Mode 匹配: 支持单 mode、多 mode、common modes
  2. 安全的锁管理: 执行前解锁,防止死锁和长时间持锁
  3. 高效的链表操作: 摘取-处理-放回的三段式设计
  4. 精细的内存管理: CFRetain/Block_copy 保证对象生命周期
  5. 智能的释放时机: 在重新加锁前释放 block,避免 dealloc 中的死锁

这个函数体现了 CoreFoundation 在性能、安全性和灵活性之间的精妙平衡,是理解 RunLoop 异步机制的关键。

扩展阅读

CFRunLoopPerformBlock 的实现

void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void)) {
    if (!rl || !block) return;
    
    struct _block_item *item = malloc(sizeof(struct _block_item));
    item->_next = NULL;
    item->_mode = CFRetain(mode);         // 持有 mode
    item->_block = Block_copy(block);     // 复制 block(栈 -> 堆)
    
    __CFRunLoopLock(rl);
    
    // 添加到链表尾部
    if (!rl->_blocks_head) {
        rl->_blocks_head = item;
    } else {
        rl->_blocks_tail->_next = item;
    }
    rl->_blocks_tail = item;
    
    __CFRunLoopUnlock(rl);
}

Common Modes 的定义

// 在 Cocoa/UIKit 中
NSRunLoopCommonModes 包含:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
- UITrackingRunLoopMode

// 效果
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, block);
// 等价于
CFRunLoopPerformBlock(runLoop, kCFRunLoopDefaultMode, block);
CFRunLoopPerformBlock(runLoop, UITrackingRunLoopMode, block);

这确保了 block 在 UI 滚动时也能执行,提升了响应性。

__CFRunLoopDoObservers函数详解

__CFRunLoopDoObservers 函数逐行注释

函数概述

__CFRunLoopDoObservers 是 RunLoop 中负责触发观察者回调的核心函数。当 RunLoop 的状态发生变化时(如即将进入循环、即将处理 Timer、即将处理 Source 等),这个函数会被调用来通知所有注册的观察者。

函数签名

/* rl is locked, rlm is locked on entrance and exit */
static void __CFRunLoopDoObservers(CFRunLoopRef, CFRunLoopModeRef, CFRunLoopActivity) __attribute__((noinline));

注释说明

  • 锁状态约定: 函数入口和出口时,rl(RunLoop)和 rlm(RunLoopMode)都必须处于加锁状态
  • noinline 属性: 防止编译器内联优化此函数,可能是为了:
    • 便于调试和性能分析
    • 保持调用栈的可读性
    • 控制代码大小

参数说明

  • CFRunLoopRef rl: 当前运行的 RunLoop
  • CFRunLoopModeRef rlm: 当前的 RunLoop Mode
  • CFRunLoopActivity activity: 当前 RunLoop 的活动状态(枚举值)

RunLoop Activity 状态枚举

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0),  // 即将进入 RunLoop
    kCFRunLoopBeforeTimers  = (1UL << 1),  // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6),  // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7),  // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有活动状态
};

完整代码逐行注释

/* DOES CALLOUT */
// 【重要标注】此函数会执行外部回调(callout),可能导致:
// 1. 长时间阻塞(回调函数耗时)
// 2. 重入问题(回调中可能再次操作 RunLoop)
// 3. 死锁风险(因此需要在回调前解锁)
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) {
    
    // ==================== 第一部分:性能追踪和初始化 ====================
    
    
    // 📊 记录性能追踪点:开始执行 observers
    // 参数:事件类型、RunLoop、Mode、活动状态、额外参数
    // 可用 Instruments 的 kdebug 工具查看
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_START, rl, rlm, activity, 0);
    
    // 🔒 检查进程是否被 fork
    // 如果在 fork 后的子进程中,需要重新初始化 RunLoop 的锁和状态
    // 防止继承父进程的锁状态导致死锁
    CHECK_FOR_FORK();

    // ==================== 第二部分:检查观察者数量 ====================
    
    // 获取当前 Mode 中观察者的数量
    // 三目运算符防止 _observers 为 NULL 时崩溃
    CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) : 0;
    
    // 快速退出:如果没有观察者,直接返回
    // 注意:此时仍持有锁,但不需要手动解锁(调用者会处理)
    if (cnt < 1) return;
    

    // ==================== 第三部分:分配观察者收集数组 ====================
    
    /* Fire the observers */
    // 📦 声明栈上缓冲区,避免小数组的堆分配开销
    // - 如果观察者数量 ≤ 1024:在栈上分配 cnt 个元素的数组
    // - 如果观察者数量 > 1024:在栈上分配 1 个元素(占位符)
    // 栈分配速度快,但空间有限(通常几 MB)
    STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024) ? cnt : 1);
    
    // 🎯 确定最终使用的数组指针
    // - 小数组(≤1024):使用栈缓冲区(快速,无需释放)
    // - 大数组(>1024):堆分配(慢,但可容纳更多元素)
    // 1024 是经验阈值,平衡性能和栈空间使用
    CFRunLoopObserverRef *collectedObservers = (cnt <= 1024) ? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
    
    // 实际收集到的观察者计数器(可能小于 cnt)
    CFIndex obs_cnt = 0;
    
    // ==================== 第四部分:收集需要触发的观察者 ====================
    
    // 遍历 Mode 中的所有观察者
    for (CFIndex idx = 0; idx < cnt; idx++) {
        
        // 获取第 idx 个观察者对象
        CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
        
        
        // 🔍 三重过滤条件(必须全部满足):
            
        // 条件1: 0 != (rlo->_activities & activity)
        // 按位与检查观察者是否关注当前活动状态
        // 例如:activity = kCFRunLoopBeforeTimers (0b10)
        // _activities = kCFRunLoopBeforeTimers | kCFRunLoopExit (0b10000010)
        // 按位与结果 = 0b10 != 0,条件成立

        // 条件2: __CFIsValid(rlo)
        // 检查观察者是否有效(未被 invalidate)
        // 观察者可能在之前的回调中被标记为无效

        // 条件3: !__CFRunLoopObserverIsFiring(rlo)
        // 检查观察者是否正在执行回调
        // 防止重入:如果观察者的回调函数中再次触发相同的 activity,
        // 不会重复调用该观察者(避免无限递归)
        if (0 != (rlo->_activities & activity) && __CFIsValid(rlo) && !__CFRunLoopObserverIsFiring(rlo)) {
            
            // ✅ 满足条件的观察者:
            // 1. 添加到收集数组
            // 2. 增加引用计数(CFRetain)
            //    - 防止在后续解锁期间被其他线程释放
            //    - 保证回调执行时对象仍然有效
            // 3. obs_cnt 递增
            collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo);
        }
    }
    
    // ==================== 第五部分:解锁(准备执行回调)====================
    
    // 🔓 解锁 RunLoop Mode
    __CFRunLoopModeUnlock(rlm);
    
    // 🔓 解锁 RunLoop
    __CFRunLoopUnlock(rl);
    
    // ⚠️ 为什么要解锁?
    // 1. 避免死锁:观察者回调可能会调用 RunLoop API(如添加 Timer、Source)
    // 2. 提高并发:允许其他线程在回调执行期间访问 RunLoop
    // 3. 防止长时间持锁:回调可能耗时很长(如网络请求、UI 更新)
    
    // 🛡️ 安全性保证:
    // - 已经通过 CFRetain 增加了观察者的引用计数
    // - collectedObservers 数组是当前线程的局部变量
    // - 即使其他线程修改了 rlm->_observers,也不会影响本次执行
    
    // ==================== 第六部分:执行观察者回调 ====================
    
    // 遍历收集到的观察者(注意:不是遍历 rlm->_observers)
    for (CFIndex idx = 0; idx < obs_cnt; idx++) {
        
        // 获取当前观察者
        CFRunLoopObserverRef rlo = collectedObservers[idx];
        
        // 🔒 锁定观察者对象(细粒度锁)
        // 只锁定单个观察者,不影响其他观察者的并发执行
        __CFRunLoopObserverLock(rlo);
        
        // 再次检查观察者是否有效
        // ⚠️ 为什么要再次检查?
        // 在解锁期间,其他线程可能已经调用了 CFRunLoopObserverInvalidate
        if (__CFIsValid(rlo)) {
            
            // 检查观察者是否是一次性的(non-repeating)
            // 如果是一次性的,执行完回调后需要 invalidate
            Boolean doInvalidate = !__CFRunLoopObserverRepeats(rlo);
            
            // 🚩 设置"正在执行"标志位
            // 防止重入(与前面的 !__CFRunLoopObserverIsFiring 配合)
            __CFRunLoopObserverSetFiring(rlo);
            
            // 🔓 在执行回调前解锁观察者
            // 原因同解锁 RunLoop:防止回调中访问观察者对象时死锁
            __CFRunLoopObserverUnlock(rlo);
            
            // ---------- 提取回调信息 ----------
            
            // 提取回调函数指针
            // 类型:void (*)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
            CFRunLoopObserverCallBack callout = rlo->_callout;
            
            // 提取用户上下文信息(创建观察者时传入的)
            void *info = rlo->_context.info;
            
            // ---------- 自动释放池 ----------
            
            // 🔄 开始自动释放池(ARP = AutoRelease Pool)
            // 在 ObjC 运行时环境中,等价于 @autoreleasepool {
            // 用于自动管理回调中创建的临时对象
            CFRUNLOOP_ARP_BEGIN(rl)
            
            // ---------- 性能追踪 ----------
            
            // 📊 记录性能追踪点:开始调用观察者回调
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_START, callout, rlo, activity, info);
            
            
            // ---------- 🎯 核心回调执行 ----------
            
            // ⚡ 执行观察者的回调函数
            // 宏定义通常是:callout(rlo, activity, info);
            // 这是整个函数的核心目的!
            
            // ⏰ 回调函数的参数:
            // - rlo: 观察者对象本身
            // - activity: 当前 RunLoop 活动状态(如 kCFRunLoopBeforeTimers)
            // - info: 用户自定义的上下文信息
            
            // ⚠️ 回调中可能发生的事情:
            // - UI 更新(如 CA::Transaction::observer_callback)
            // - 性能监控(如 FPS 检测)
            // - 内存管理(如清理缓存)
            // - 业务逻辑(如状态同步)
            // - 再次操作 RunLoop(如添加/移除 Timer)
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(callout, rlo, activity, info);
            
            
            // ---------- 性能追踪结束 ----------
            
            // 📊 记录性能追踪点:结束调用观察者回调
            // 可通过 end - start 计算回调耗时
            cf_trace(KDEBUG_EVENT_CFRL_IS_CALLING_OBSERVER | DBG_FUNC_END, callout, rlo, activity, info);
            
            // ---------- 自动释放池结束 ----------
            
            // 🔄 结束自动释放池
            // 释放回调中创建的所有 autorelease 对象
            CFRUNLOOP_ARP_END()
           
            
            // ---------- 处理一次性观察者 ----------
            
            // 如果是一次性观察者(non-repeating)
            if (doInvalidate) {
                // ❌ 使观察者失效
                // 会从 RunLoop 中移除,并标记为无效
                // 后续不会再触发此观察者
                CFRunLoopObserverInvalidate(rlo);
            }
            
            // 🚩 清除"正在执行"标志位
            // 允许此观察者在下次 activity 时再次被触发
            __CFRunLoopObserverUnsetFiring(rlo);
            
        } else {
            // 观察者在解锁期间已被其他线程 invalidate
            
            // 🔓 解锁观察者(前面已加锁)
            // 跳过回调执行
            __CFRunLoopObserverUnlock(rlo);
        }
        
        // 📉 释放引用计数(对应前面的 CFRetain)
        // 如果引用计数归零,观察者对象会被销毁
        CFRelease(rlo);
    }
    
    // ==================== 第七部分:重新加锁 ====================
    
    // 🔒 重新锁定 RunLoop
    __CFRunLoopLock(rl);
    
    // 🔒 重新锁定 RunLoop Mode
    __CFRunLoopModeLock(rlm);
    
    // ✅ 恢复函数入口时的锁状态
    // 满足函数签名中的约定:"rl is locked, rlm is locked on entrance and exit"

    // ==================== 第八部分:清理资源 ====================
    
    // 🗑️ 释放堆内存(如果使用了 malloc)
    // - 如果 collectedObservers 指向栈缓冲区(buffer),无需释放
    // - 如果 collectedObservers 指向堆内存(malloc),需要手动释放
    // 防止内存泄漏
    if (collectedObservers != buffer) free(collectedObservers);
    
    // ==================== 第九部分:结束性能追踪 ====================
    
    // 📊 记录性能追踪点:完成 observers 执行
    cf_trace(KDEBUG_EVENT_CFRL_IS_DOING_OBSERVERS | DBG_FUNC_END, rl, rlm, activity, 0);
}

常见使用场景

1. UI 渲染(Core Animation)

// CA 在 kCFRunLoopBeforeWaiting 时提交渲染事务
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault, 
    kCFRunLoopBeforeWaiting | kCFRunLoopExit,  // 监听这两个状态
    YES,  // 重复触发
    2000000,  // order = 2000000(在大多数 observer 之后)
    &CA_Transaction_observerCallback,  // 回调函数
    NULL
);

2. 性能监控(FPS 检测)

// 监控主线程 RunLoop 的卡顿
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopAllActivities,  // 监听所有活动
    YES,
    0,
    &performanceMonitorCallback,
    NULL
);

void performanceMonitorCallback(CFRunLoopObserverRef observer, 
                                CFRunLoopActivity activity, 
                                void *info) {
    // 记录时间戳,计算两次回调之间的间隔
    // 如果间隔过长,说明发生了卡顿
}

3. 内存管理(自动释放池)

// NSRunLoop 在每次循环前后创建/销毁自动释放池
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
    kCFAllocatorDefault,
    kCFRunLoopEntry | kCFRunLoopBeforeWaiting | kCFRunLoopExit,
    YES,
    -2147483647,  // 极高优先级(负值)
    &autoreleasePoolCallback,
    NULL
);

总结

这个函数是理解 RunLoop 机制和 iOS 事件循环的关键,也是许多高级特性(如 UI 渲染、性能监控)的基础。

TN3187:迁移至基于 UIKit 场景的生命周期

概述

许多较旧的 iOS 应用使用一个 UIApplicationDelegate 对象作为其应用的主要入口点,并管理应用的生命周期。使用场景的应用则不同,它们使用一个 UISceneDelegate 对象来分别管理每个场景窗口。从基于 UIApplicationDelegate 的应用生命周期迁移到基于场景的生命周期,可以让你的应用支持多窗口等现代功能。本文档阐述了如何将应用迁移到基于场景的生命周期。

迁移路径

将应用迁移到基于场景的生命周期有两种路径:

  • 分阶段迁移:应用在继续使用 UIApplicationDelegate 的同时,逐步采用 UISceneDelegate。
  • 直接迁移:应用一次性直接迁移到 UISceneDelegate。

在分阶段迁移中,你可以在应用的不同部分逐步实现场景支持,而应用的其余部分继续通过现有的 UIApplicationDelegate 进行管理。这种方式对于大型应用或者需要逐步验证场景兼容性的团队很有用。然而,分阶段迁移会增加临时复杂性,并且应用在完全迁移之前无法使用多窗口等需要完全基于场景生命周期的功能。

直接迁移意味着一次性将整个应用切换到 UISceneDelegate。对于尚未使用 UIApplicationDelegate 管理界面的新应用,或者那些规模较小、易于整体更新的现有应用,推荐采用这种方式。直接迁移可以更快地启用多窗口等现代功能,并简化代码库。

如何判断应用是否使用了场景?

如果你的应用使用了场景,其 Info.plist 文件中会包含一个 UIApplicationSceneManifest 字典。当系统在 Info.plist 中检测到此字典时,它会使用基于场景的生命周期来启动你的应用。对于直接迁移,你需要添加此清单。对于分阶段迁移,你将在准备好启用场景时添加它。


分阶段迁移

分阶段迁移允许你逐步采用 UISceneDelegate。在这种方法下,应用将继续使用 UIApplicationDelegate 作为主要入口点,但你可以为某些界面逐步引入场景支持。当你想为应用的特定部分启用多窗口等功能,同时保持其他部分的原有行为时,这种方式很有用。

要进行分阶段迁移,你需要:

  1. 创建一个实现 UIWindowSceneDelegate 协议的新类。
  2. 在 UIApplicationDelegate 中实现 application(_:configurationForConnecting:options:) 方法,为特定会话返回一个 UISceneConfiguration 实例。此配置告诉系统为连接的场景会话使用哪个场景代理类。
  3. 在应用的 Info.plist 中配置 UIApplicationSceneManifest 字典,并为场景配置指定你的场景代理类名。

当系统请求新场景时,它会调用 UIApplicationDelegate 的 application(_:configurationForConnecting:options:) 方法。你可以检查连接选项(例如 UIApplication.OpenURLOptions 或 UIApplication.ActivityOptions)来决定返回哪种场景配置。这允许你根据用户的操作(例如点击 URL 或进行拖放操作)来创建不同类型的场景。

分阶段迁移期间,UIApplicationDelegate 仍然负责管理应用级事件(例如应用启动和进入后台),而 UISceneDelegate 则管理特定场景的生命周期事件(例如场景激活或失活)。这种分离使得你可以逐步将界面管理从 UIApplicationDelegate 转移到 UISceneDelegate。

直接迁移

直接迁移涉及一次性将整个应用从 UIApplicationDelegate 迁移到 UISceneDelegate。这种方式适用于新应用,或者那些愿意为启用多窗口等现代功能而进行全面更新的现有应用。

要直接迁移,你需要:

  1. 将应用生命周期管理从 UIApplicationDelegate 移动到 UISceneDelegate。这包括将代码从 application(:didFinishLaunchingWithOptions:) 移动到 scene(:willConnectTo:options:),以及将其他生命周期方法(例如 applicationWillResignActive 和 applicationDidBecomeActive)迁移到对应的场景代理方法(例如 sceneWillResignActive 和 sceneDidBecomeActive)。
  2. 移除 UIApplicationDelegate 中与窗口管理相关的代码,因为每个场景现在都会管理自己的 UIWindow。
  3. 在 Info.plist 中添加 UIApplicationSceneManifest 字典,并配置默认的场景配置,指定你的 UISceneDelegate 类。

直接迁移后,应用的每个窗口都由一个独立的 UIScene 实例管理,UISceneDelegate 负责该场景的生命周期。这为每个窗口提供了更好的隔离,并启用了多窗口支持。


通用迁移步骤

无论选择分阶段迁移还是直接迁移,都需要遵循一些通用步骤:

1. 创建场景代理类

创建一个实现 UIWindowSceneDelegate 协议的新类。这个类将管理特定场景的生命周期。你可以在其中创建窗口、设置根视图控制器,并响应场景生命周期事件。

2. 配置 Info.plist

在 Info.plist 中添加一个 UIApplicationSceneManifest 字典。此字典告诉系统你的应用支持场景。它包含一个 UISceneConfigurations 字典,你可以在其中定义应用支持的不同场景配置。每个配置指定了场景代理类名和故事板名称(如果使用的话)。

<key>UIApplicationSceneManifest</key> <dict> <key>UIApplicationSupportsMultipleScenes</key> <true/> <key>UISceneConfigurations</key> <dict> <key>UIWindowSceneSessionRoleApplication</key> <array> <dict> <key>UISceneConfigurationName</key> <string>Default Configuration</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <key>UISceneStoryboardFile</key> <string>Main</string> </dict> </array> </dict> </dict>

3. 更新应用代理

对于直接迁移,移除 UIApplicationDelegate 中与窗口管理相关的代码,并将应用生命周期事件的处理移动到场景代理中。对于分阶段迁移,在 UIApplicationDelegate 中实现 application(_:configurationForConnecting:options:) 方法来返回适当的场景配置。

4. 更新生命周期事件处理

将应用级生命周期事件的处理迁移到相应的场景级事件。例如:

  • application(:didFinishLaunchingWithOptions:) 迁移到 scene(:willConnectTo:options:)
  • applicationWillResignActive 迁移到 sceneWillResignActive
  • applicationDidBecomeActive 迁移到 sceneDidBecomeActive
  • 等等...

5. 测试

在支持场景的设备(例如 iPad)上彻底测试你的应用。验证场景是否正确创建、生命周期事件是否正常触发,以及多窗口等功能是否按预期工作。对于分阶段迁移,确保现有功能在启用场景的部分和未启用的部分都能正常工作。

场景与后台任务

当使用基于场景的生命周期时,后台任务的处理方式有所不同。在基于 UIApplicationDelegate 的应用中,后台任务通常在应用级别管理。而在基于场景的应用中,后台任务可以与特定场景关联。

如果你的应用使用 UIApplication.beginBackgroundTask(withName:expirationHandler:) 来管理长时间运行的任务,在迁移到场景后,你可能需要考虑使用每个场景的后台任务管理。然而,UIApplication 级别的后台任务 API 在基于场景的应用中仍然可用,并且可以在应用级别的任务中使用。

对于直接与特定场景关联的任务(例如,该场景正在进行的网络请求),考虑使用与该场景关联的后台任务。这有助于系统更有效地管理资源,并在场景关闭时提供更清晰的任务清理机制。


结论

迁移到基于 UIKit 场景的生命周期可以使你的应用支持多窗口等现代 iOS 功能。无论选择分阶段迁移还是直接迁移,关键步骤都是创建一个 UISceneDelegate 类,配置 Info.plist,并将生命周期事件处理从 UIApplicationDelegate 移动到 UISceneDelegate。迁移后,每个窗口将由独立的场景管理,从而提高模块化并为用户提供更强大的多任务处理体验。

【AI Video Generator】迎来开年第一波大清洗!

背景

看似一个平平无奇的周末,却让做AI Video Generator的开发者天塌了。

好消息:竞品家的都嘎了!

坏消息:自己家的也嘎了!

此次以Video关键词检索,共计21款相关产品。有单纯上架海外市场,也有全体地区分发

企业微信20260126-095856.png

所以集中下架的行为,不只是单纯的某些国家或地区。大概率是集中触发了苹果的查杀。(法国佬口音:我要验牌!)

u=2455538048,33299884&fm=224&app=112&f=JPEG.jpg

随机验牌

在众多被标记了下架的产品中,随机抽选了2家APP。单纯从应用市场的截图入手。

企业微信20260126-095138.png

企业微信20260126-094803.png

从AppStore市场图,就能明显发现存在社交风格的市场截图,充斥着袒胸露乳的行为。基本上都带勾!

基本上随机抽查的产品中或多或少都存在此类问题。从应用截图就充斥着擦边行为!,莫非是社交类大佬集体转型?

下架原因

为了更好的解释这种集中下架行为,特意在Developer审核指南,匹配对应内容审核的条款。

不出意外 1.1.4 - 公然宣传黄色或色情内容的材料 (这一概念的定义是:“对性器官或性活动的露骨描述或展示,目的在于刺激性快感,而非带来美学价值或触发情感”),其中包括一夜情约会 App 和其他可能包含色情内容或用于嫖娼或人口贩卖和剥削的 App。

当然,这种集中行为大概率苹果算法升级【或者通过鉴黄系统】,从AppStore净化入手,简单纯粹的一刀切!

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

附:更多文章欢迎关注同名公众号,By-iOS研究院

ARC 原理与 weak 底层实现(Side Table 深度解析)

ARC 原理与 weak 底层实现(Side Table 深度解析)

面向:有一定 iOS / Runtime 基础的开发者

目标:真正搞清楚 weak 到底 weak 了谁、SideTable 里存的是什么、为什么能自动置 nil


一、先给结论(非常重要)

weak 不是修饰对象,而是修饰“指针变量”

SideTable 记录的是:某个对象,被哪些 weak 指针地址指向

换句话说:

  • ❌ 不是「A weak 引用 B」
  • ❌ 不是「对象记住了谁 weak 它」
  • ✅ 是「Runtime 记住:哪些内存地址(weak 指针)指向了这个对象」

二、从一行代码开始

@property (nonatomic, weak) Person *person;

编译后本质是:

Person *__weak _person;

说明三点:

  1. _person 是一个普通指针变量
  2. weak 修饰的是这个指针变量的行为
  3. 并不是 Person 对象“变成了 weak”

三、明确三个核心角色

Person *p = [[Person alloc] init];
self.person = p; // weak

此时内存中存在三样东西:

角色 含义
Person 对象 真正的 OC 实例
strong 指针 拥有对象(如 p)
weak 指针 不拥有对象(如 self->_person)

weak 的对象不是 Person,而是 _person 这个指针变量。


四、objc_storeWeak 到底做了什么?

self.person = p;

编译后:

objc_storeWeak(&self->_person, p);

注意这里传入的两个参数:

  • &self->_person 👉 weak 指针的地址

  • p 👉 对象地址

Runtime 的真实意图:

登记:对象 p,被这个 weak 指针地址弱引用了


五、SideTable / weak_table 的真实逻辑结构

1️⃣ SideTable(简化)

struct SideTable {
    spinlock_t lock;
    RefcountMap refcnts;     // 强引用计数
    weak_table_t weak_table; // 弱引用表
};

2️⃣ weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries;
};

3️⃣ weak_entry_t(重点)

struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被 weak 的对象
    weak_referrer_t *referrers;         // weak 指针地址数组
};

六、SideTable 中真正存的是什么?

用一张逻辑图表示:

SideTable
└── weak_table
    └── weak_entry
        ├── referent = Person 对象
        └── referrers = [
              &self->_person,
              &vc->_delegate,
              &cell->_model
          ]

关键点(一定要记住):

  • weak_table 的 key 是对象
  • value 是 所有指向它的 weak 指针地址

七、谁被 weak?谁被记录?

以:

self.person = p;

为例:

问题 答案
谁被 weak? _person 这个指针变量
谁被引用? Person 对象
SideTable 记录什么? Person → weak 指针地址列表

八、对象释放时为什么能自动置 nil?

当 Person 的引用计数降为 0:

objc_destroyWeak(obj);

Runtime 的逻辑流程:

1. 找到 obj 对应的 weak_entry
2. 遍历 referrers(weak 指针地址)
3. 对每个地址执行:
      *(Person **)referrer = nil
4. 移除 weak_entry

⚠️ Runtime 完全不知道变量名,只操作内存地址。


九、用内存视角完整走一遍

1️⃣ 内存布局

0x1000  Person 对象

0x2000  p (strong)        → 0x1000
0x3000  self->_person     → 0x1000

2️⃣ weak_table

key: 0x1000
value: [0x3000]

3️⃣ Person 释放

free(0x1000)
*(0x3000) = nil

最终:

self.person == nil

十、为什么 weak 不会产生野指针?

修饰符 行为
assign 不 retain、不置 nil → 野指针
weak Runtime 扫表并置 nil

weak 的安全性来自 Runtime 的集中清理机制


十一、为什么 weak_table 以“对象”为中心?

因为:

对象释放是一个确定事件

以对象为 key:

  • 对象销毁 → 一次性清理所有 weak 指针
  • 性能可控
  • 逻辑集中

十二、常见误解澄清

❌ A weak 引用 B

✅ A 的某个指针 weak 指向 B

❌ 对象知道谁 weak 它

✅ Runtime 知道,对象本身不知道

❌ weak 是对象属性

✅ weak 是指针语义

❌ weak 只是“不 retain”

✅ weak = 不 retain + 注册 weak_table + 自动置 nil


十三、一句话总结

weak 的本质不是弱引用对象,

而是 Runtime 记录“哪些指针弱指向了这个对象”,

并在对象销毁时统一把这些指针置为 nil。


十四、下一步可继续深入

  • block 捕获下 weak_table 的变化过程

  • __unsafe_unretained 与 weak 的实现对比

  • objc-runtime 源码中 weak_entry 的真实实现

Flutter 底层原理


一、Flutter 渲染原理(最高频考点)

Q1:Flutter 的渲染原理是什么?为什么 Flutter 能做到高性能跨平台?

核心答案:Flutter 采用自绘引擎架构,不依赖平台原生控件,而是通过 Skia 引擎直接在 GPU 上绘制 UI,从而实现跨平台一致性和高性能。

深入原理

Flutter 与其他跨平台方案的本质区别在于渲染方式:

方案 渲染方式 性能瓶颈
WebView 方案 HTML+CSS 渲染 渲染引擎性能差
React Native JS→Bridge→原生控件 Bridge 通信开销
Flutter Dart→Skia→GPU 几乎无额外开销

Flutter 只需要平台提供一个"画布"(Surface),然后自己完成所有渲染工作。这就像你给我一张白纸,我自己画画,而不是让你帮我画。

串联知识点

这也解释了为什么 Flutter 的 Platform Channel 只用于功能调用(相机、传感器)而不用于 UI 渲染——UI 完全由 Flutter 自己处理,不走原生。


Q2:Flutter 的三棵树是什么?它们之间的关系是什么?

核心答案:Widget 树是配置描述,Element 树是实例管理,RenderObject 树是布局绘制。三者分离是为了实现"配置与渲染解耦",从而支持高效的增量更新。

深入原理

第一层理解——各自职责

  • Widget:不可变的配置对象,描述"UI 应该长什么样"。类似于 React 的 Virtual DOM 节点。
  • Element:Widget 的实例化,是真正"活着"的对象,管理生命周期、父子关系、状态。
  • RenderObject:负责布局(计算大小位置)和绘制(生成绘制指令)。

第二层理解——为什么要分三层?

这是经典的"关注点分离"设计:

  1. Widget 可以频繁重建:因为它只是配置,创建成本极低(就是个普通对象)
  2. Element 负责复用决策:通过 diff 算法决定是复用还是重建 RenderObject
  3. RenderObject 尽量复用:因为布局和绘制成本高

如果没有 Element 这一层,每次 setState 都要重建整个 RenderObject 树,性能会很差。

第三层理解——diff 复用机制

Element 的复用规则:

  1. 同一个 Widget 实例(const)→ 直接复用,什么都不做
  2. 类型相同 + Key 相同 → 复用 Element,调用 update 更新配置
  3. 类型不同 或 Key 不同 → 销毁重建

串联知识点

这就是为什么推荐使用 const 构造函数——const Widget 是编译期常量,同一实例直接复用,连 diff 都省了。

这也解释了为什么 Key 很重要——没有 Key 时只比较类型,列表项交换位置会导致状态错乱。


Q3:setState 调用后发生了什么?完整流程是什么?

核心答案:setState 本身是同步的,但 UI 更新是异步的。它只是标记当前 Element 为 dirty,然后在下一帧的 Build 阶段统一重建。

完整流程

setState() 调用
    ↓
执行传入的闭包,修改成员变量
    ↓
调用 _element.markNeedsBuild()
    ↓
将 Element 加入 dirty 列表
    ↓
如果还没请求过,调用 scheduleFrame() 请求下一帧
    ↓
setState 返回(此时 UI 还没变化)
    ↓
等待 VSync 信号
    ↓
handleBeginFrame → handleDrawFrame
    ↓
BuildOwner.buildScope() 遍历 dirty 列表
    ↓
对每个 dirty Element 调用 rebuild()
    ↓
rebuild 调用 build() 生成新 Widget
    ↓
updateChild 进行 diff 比较
    ↓
复用或重建子 Element
    ↓
如果需要,更新 RenderObject
    ↓
标记 needsLayout 或 needsPaint
    ↓
后续的 Layout 和 Paint 阶段处理

xyz追问:为什么 setState 是异步更新?

  1. 合并多次调用:同一帧内多次 setState 只会触发一次重建
  2. 批量处理:所有 dirty Element 统一处理,而不是逐个处理
  3. 与渲染管线同步:在 VSync 信号驱动下统一更新,保证流畅

xyz追问:在 build 方法里调用 setState 会怎样?

会报错!因为正在 build 的过程中不能再标记 dirty。这是一个保护机制,防止无限循环。

串联知识点

这与 React 的 setState 机制类似——都是"标记脏,批量更新"。但 Flutter 更进一步,与渲染管线(VSync)深度绑定。


Q4:Flutter 一帧的渲染流程是什么?

核心答案:VSync → Animate → Build → Layout → Paint → Composite → Rasterize

详细阶段

阶段 做什么 触发条件
Animate 更新动画值 Ticker 注册了回调
Build 重建 Widget/Element 树 Element 被标记 dirty
Layout 计算大小和位置 RenderObject 被标记 needsLayout
Paint 生成绘制指令,构建 Layer 树 RenderObject 被标记 needsPaint
Composite 合成 Layer 树为 Scene Paint 完成后
Rasterize Skia 光栅化,GPU 渲染 在 GPU 线程执行

深入理解——标记传播机制

这里有一个关键设计:标记是向上传播的

比如你调用 setState:

  1. 当前 Element 标记 dirty
  2. 重建时可能更新 RenderObject 的配置
  3. RenderObject 检测到配置变化,标记 needsLayout
  4. needsLayout 向上传播到布局边界(Relayout Boundary)
  5. Layout 阶段只处理边界内的节点

同理,needsPaint 也会向上传播到重绘边界(Repaint Boundary)。

xyz追问:为什么要标记传播而不是直接更新?

性能优化!标记只是打个记号(O(1)),真正的计算延迟到统一处理阶段。这样可以合并多次变化,避免重复计算。

串联知识点

这就是为什么 RepaintBoundary 能优化性能——它阻断了 needsPaint 的向上传播,让重绘范围最小化。


Q5:Flutter 的布局原理是什么?Constraints 是怎么传递的?

核心答案:Flutter 采用单次遍历的盒约束布局,约束从上往下传,尺寸从下往上返,父节点决定子节点位置。

核心原则

Constraints go down, Sizes go up, Parent sets position.

详细流程

  1. 父节点调用 child.layout(constraints),把约束传给子节点
  2. 子节点在约束范围内确定自己的 size,存到 size 属性
  3. 父节点读取 child.size,决定子节点的偏移量(通过 ParentData)
  4. 子节点不知道自己在父节点中的位置

xyz追问:为什么子节点不知道自己的位置?

这是性能优化!如果子节点位置变化,不需要重新布局子树。比如动画移动一个 Widget,只需要改 offset,不需要重新计算子节点的大小。

约束类型

类型 特征 示例
紧约束 minWidth == maxWidth Container 给子节点设置固定宽度
松约束 minWidth = 0 允许子节点任意小
无界约束 maxWidth = infinity ListView 给子节点的主轴约束

xyz追问:为什么会有"RenderBox was not laid out"错误?

常见于无界约束场景。比如在 Column 里放 ListView,Column 给 ListView 的高度约束是无界的(infinity),而 ListView 需要一个确定的高度。解决方案:用 Expanded 包裹或设置固定高度。

串联知识点

这也解释了为什么 Flex 布局中要用 Expanded/Flexible——它们会把无界约束转换为有界约束。


Q6:RenderObject 的 Relayout Boundary 是什么?为什么能优化性能?

核心答案:Relayout Boundary 是布局边界,它的布局变化不会影响父节点,也不受兄弟节点影响,从而减少布局计算范围。

触发条件(满足任一):

  1. parentUsesSize = false(父节点不关心子节点大小)
  2. sizedByParent = true(大小完全由约束决定)
  3. 约束是紧约束(大小固定)
  4. 是根节点

原理

正常情况下,子节点大小变化 → 父节点需要重新布局 → 可能影响兄弟节点 → 连锁反应。

但如果子节点是 Relayout Boundary:

  • 它的大小变化不会通知父节点
  • 布局只在边界内进行
  • 大大减少计算量

xyz追问:和 RepaintBoundary 什么区别?

边界类型 阻断的传播 优化的阶段
Relayout Boundary needsLayout 向上传播 Layout 阶段
Repaint Boundary needsPaint 向上传播 Paint 阶段

前者是自动的(满足条件就是),后者需要手动添加 RepaintBoundary Widget。

串联知识点

这就是为什么固定大小的组件性能更好——它们自动成为 Relayout Boundary,布局变化不会影响外部。


二、Element 与 State 生命周期

Q7:StatefulWidget 的完整生命周期是什么?

核心答案:createState → initState → didChangeDependencies → build → (didUpdateWidget/setState → build)* → deactivate → dispose

详细流程

方法 调用时机 典型用途
createState Widget 首次创建 创建 State 实例
initState State 插入树中 初始化操作、订阅
didChangeDependencies 依赖的 InheritedWidget 变化 响应依赖变化
build 需要重建时 构建 UI
didUpdateWidget Widget 配置更新 响应配置变化
deactivate 从树中移除(可能重新插入) 临时清理
dispose 永久移除 资源释放、取消订阅

xyz追问:initState 里能调用 setState 吗?

可以调用,但没必要。因为 initState 之后会自动调用 build。

xyz追问:initState 里能使用 context 吗?

可以使用,但不能调用 dependOnInheritedWidgetOfExactType。因为此时依赖关系还没建立完成。正确做法是在 didChangeDependencies 中获取。

xyz追问:deactivate 和 dispose 的区别?

deactivate:从树中移除,但可能重新激活(比如 GlobalKey 跨树移动) dispose:永久销毁,不会再使用

如果在 deactivate 中释放资源,重新激活时就没有资源可用了。所以资源释放应该放在 dispose。

串联知识点

这就是为什么 GlobalKey 能跨树保持状态——它让 Element 在 deactivate 后不立即 dispose,而是等待可能的重新激活。


Q8:Key 的作用是什么?什么时候需要用 Key?

核心答案:Key 用于标识 Element 的身份,控制 Element 的复用逻辑。在列表项可能变化(增删、重排序)时必须使用。

原理

没有 Key 时的匹配:只比较 Widget 类型 有 Key 时的匹配:比较类型 + Key

经典问题:列表项交换

假设列表:[A, B] 变为 [B, A]

没有 Key:

  • 位置 0:类型相同 → 复用 Element,更新配置(A→B)
  • 位置 1:类型相同 → 复用 Element,更新配置(B→A)
  • 结果:Element 被复用,但 State 没有跟着移动!

有 Key:

  • 位置 0:Key 不匹配 → 从其他位置找到匹配的 Element
  • Flutter 会正确移动 Element 而不是更新
  • 结果:Element 和 State 一起移动

Key 的类型

类型 比较方式 使用场景
ValueKey 值相等 有唯一标识的数据(ID)
ObjectKey 对象引用相等 对象本身唯一
UniqueKey 永不相等 强制不复用
GlobalKey 全局唯一 跨树访问 State/RenderObject

xyz追问:GlobalKey 为什么慎用?

  1. 有注册/注销开销
  2. 会阻止 Element 回收
  3. 全局维护 Map,内存占用

串联知识点

Key 的本质是给 Element 一个"身份证",让 Flutter 知道"这个 Widget 对应的是哪个 Element",而不只是"这个位置应该放什么类型的 Widget"。


三、InheritedWidget 与状态管理

Q9:InheritedWidget 的原理是什么?为什么查找是 O(1)?

核心答案:每个 Element 持有一个 Map,记录祖先中所有 InheritedWidget 的类型到 Element 的映射,查找时直接用类型做 key。

原理详解

每个 Element 有个属性:Map<Type, InheritedElement>? _inheritedWidgets

当 Element 挂载(mount)时:

  1. 继承父节点的 _inheritedWidgets(浅拷贝)
  2. 如果自己是 InheritedElement,添加自己:_inheritedWidgets[MyWidget] = this

当调用 dependOnInheritedWidgetOfExactType<T>() 时:

  1. 直接 _inheritedWidgets[T] 获取,O(1)
  2. 把当前 Element 注册为依赖者
  3. 返回 InheritedWidget

xyz追问:依赖是怎么建立的?

InheritedElement 维护一个 Set<Element> _dependents

调用 dependOnInheritedWidgetOfExactType 时,会把调用者加入这个 Set。

当 InheritedWidget 更新且 updateShouldNotify 返回 true 时,遍历 _dependents,对每个依赖者调用 didChangeDependencies,并标记 dirty。

xyz追问:of(context) 和 maybeOf(context) 的区别?

of:找不到会抛异常 maybeOf:找不到返回 null

串联知识点

Provider、Riverpod、GetX 等状态管理库的核心都是对 InheritedWidget 的封装。它们本质上都在利用这个 O(1) 查找和自动依赖追踪机制。


Q10:Provider 的原理是什么?ChangeNotifier 是怎么工作的?

核心答案:Provider = InheritedWidget + ChangeNotifier。InheritedWidget 负责数据传递,ChangeNotifier 负责变化通知。

工作流程

  1. ChangeNotifierProvider 创建并持有 ChangeNotifier 实例
  2. 内部使用 InheritedWidget 向下传递
  3. ChangeNotifier 调用 notifyListeners() 时
  4. Provider 监听到变化,重建 InheritedWidget
  5. updateShouldNotify 返回 true
  6. 所有依赖者收到通知并重建

xyz追问:Consumer 和 Provider.of 的区别?

本质相同,但 Consumer 把 rebuild 范围限制在 builder 内部。

Provider.of(context) 会让整个 build 方法重建。 Consumer 只重建 builder 返回的部分。

xyz追问:Selector 是怎么优化的?

Selector 增加了一层"选择":

  1. 用 selector 函数从数据中提取需要的部分
  2. 只有提取的部分变化时才重建
  3. 使用 == 比较(或自定义 shouldRebuild)

这避免了"数据的其他字段变化导致我重建"的问题。

串联知识点

这就是为什么状态管理要"细粒度"——把大状态拆成小状态,每个组件只依赖需要的部分,减少不必要的重建。


四、Dart 异步机制

Q11:Dart 的事件循环是怎么工作的?microtask 和 event 的区别?

核心答案:Dart 是单线程模型,通过事件循环处理异步。事件循环维护两个队列:microtask 队列(高优先级)和 event 队列(低优先级)。每次处理完所有 microtask 后才处理一个 event。

执行顺序

同步代码
    ↓
所有 microtask(直到队列空)
    ↓
一个 event
    ↓
所有 microtask(直到队列空)
    ↓
一个 event
    ↓
...循环...

加入队列的方式

方式 加入的队列
Future() event
Future.delayed() event
Timer event
Future.microtask() microtask
scheduleMicrotask() microtask
then/catchError/whenComplete microtask

xyz追问:为什么要有 microtask?

microtask 用于"在当前事件处理完成后、下一个事件开始前"执行的操作。

典型场景:Future.then 的回调需要在 Future 完成后立即执行,而不是等其他事件。

xyz追问:输出顺序题

print('1');
Future(() => print('2'));
Future.microtask(() => print('3'));
scheduleMicrotask(() => print('4'));
print('5');

答案:1, 5, 3, 4, 2

解析:

  • 1, 5:同步代码
  • 3, 4:microtask(按加入顺序)
  • 2:event

串联知识点

这就是为什么 setState 后 UI 不会立即更新——setState 只是把重建任务加入了调度,真正的重建在下一帧的事件中执行。


Q12:Future 和 async/await 的原理是什么?

核心答案:Future 是对异步操作的封装,代表一个未来会完成的值。async/await 是 Future 的语法糖,编译器会将其转换为 then 链。

Future 的三种状态

  • Uncompleted:操作进行中
  • Completed with value:成功完成
  • Completed with error:失败

async/await 转换

// 源代码
Future<int> foo() async {
  var a = await bar();
  var b = await baz(a);
  return a + b;
}

// 等价于
Future<int> foo() {
  return bar().then((a) {
    return baz(a).then((b) {
      return a + b;
    });
  });
}

xyz追问:async 函数一定是异步的吗?

async 函数总是返回 Future,但不一定真的异步执行。

Future<int> foo() async {
  return 42;  // 同步返回
}

这个函数同步执行完,但返回的是 Future<int>,获取值需要 await 或 then。

xyz追问:多个 await 是并行还是串行?

串行!每个 await 都要等上一个完成。

并行需要用 Future.wait

var results = await Future.wait([foo(), bar(), baz()]);

串联知识点

理解 async/await 是语法糖,就能理解很多"诡异"行为:

  • 为什么 async 函数返回的 Future 即使没 await 也能执行——then 的回调会被调度
  • 为什么 catchError 能捕获 async 函数中的异常——编译器转换成了 try-catch

Q13:Isolate 是什么?和 Future 什么区别?

核心答案:Future 是单线程内的异步,用于 I/O 操作;Isolate 是真正的多线程,用于 CPU 密集型计算。Isolate 之间内存隔离,通过消息传递通信。

本质区别

特性 Future Isolate
线程 单线程 多线程
适用场景 I/O 密集 CPU 密集
内存 共享 隔离
通信 直接访问 消息传递

为什么 I/O 用 Future 就够了?

I/O 操作(网络请求、文件读写)是"等待",不占用 CPU。Dart 通过事件循环调度,等待期间可以处理其他事件。

为什么 CPU 密集操作需要 Isolate?

CPU 密集操作(JSON 解析、图片处理)会阻塞事件循环,导致 UI 卡顿。Isolate 在独立线程执行,不阻塞主线程。

Isolate 通信机制

Main Isolate          New Isolate
     |                     |
 SendPort ──────────► ReceivePort
     |                     |
 ReceivePort ◄────────── SendPort
     |                     |
  独立堆内存            独立堆内存

消息是深拷贝的,不共享内存,所以没有锁和竞争条件。

xyz追问:compute 函数是什么?

Flutter 提供的便捷函数,封装了 Isolate 的创建、通信、销毁:

final result = await compute(parseJson, jsonString);

适合一次性计算任务。

串联知识点

这就是为什么 Flutter 有时候会"卡一下"——可能是同步的 CPU 密集操作阻塞了事件循环。解决方案:用 compute 或 Isolate 把计算移到后台。


五、Platform Channel

Q14:Flutter 如何与原生通信?三种 Channel 的区别?

核心答案:通过 Platform Channel 通信,本质是二进制消息传递。MethodChannel 用于方法调用,EventChannel 用于事件流,BasicMessageChannel 用于基础消息。

三种 Channel 对比

Channel 通信模式 使用场景
MethodChannel 请求-响应 获取电量、打开相机
EventChannel 事件流 传感器数据、网络状态变化
BasicMessageChannel 双向消息 自定义协议

通信流程

Dart 调用 invokeMethod
    ↓
参数序列化为二进制
    ↓
通过 C API 传递到原生层
    ↓
原生层反序列化,执行方法
    ↓
结果序列化为二进制
    ↓
传回 Dart 层
    ↓
反序列化,完成 Future

xyz追问:在哪个线程执行?

Dart 侧:UI 线程 原生侧:也应该在主线程调用

如果原生有耗时操作,应该切到后台线程,完成后再切回主线程返回结果。

xyz追问:StandardMessageCodec 支持哪些类型?

null、bool、int、double、String、Uint8List、List、Map

复杂对象需要手动序列化为上述类型。

串联知识点

Platform Channel 只用于"功能调用",不用于"UI 渲染"——因为 Flutter 自己渲染 UI。这是 Flutter 与 React Native 的本质区别。


六、热重载

Q15:热重载的原理是什么?为什么能保持状态?

核心答案:热重载利用 JIT 编译的能力,增量编译变化的代码,注入到运行中的 Dart VM,然后触发 Widget 树重建,但保持 Element 树和 State 不变。

原理详解

文件保存
    ↓
检测变化的 Dart 文件
    ↓
增量编译为 Kernel(.dill)
    ↓
通过 VM Service 发送到设备
    ↓
Dart VM 加载新代码,替换类定义
    ↓
Flutter Framework 调用 reassemble()
    ↓
从根节点开始 rebuild
    ↓
Widget 树重建,Element 树复用
    ↓
State 保持不变

为什么能保持状态?

  • 只是 Widget(配置)变了
  • Element 被复用(类型没变)
  • State 对象没有被销毁

相当于给 State 换了一套新的 Widget 配置,但 State 本身还是那个 State。

xyz追问:什么情况下热重载不生效?

  1. 修改 main() 函数
  2. 修改全局变量/静态变量的初始化
  3. 修改枚举定义
  4. 修改泛型类型参数
  5. 原生代码修改

这些情况需要热重启(Hot Restart)或完全重启。

xyz追问:为什么 Release 模式不支持热重载?

因为 Release 模式使用 AOT 编译,代码已经编译为机器码,无法动态替换。

热重载依赖 JIT 编译器的动态代码注入能力。

串联知识点

这就是 Debug 模式启动慢但支持热重载、Release 模式启动快但不支持热重载的原因——编译方式不同。


七、动画原理

Q16:Flutter 动画的原理是什么?Ticker 是什么?

核心答案:Flutter 动画由 Ticker 驱动,Ticker 与 VSync 同步,每帧回调一次。AnimationController 接收 Ticker 信号,更新动画值,通知监听者重建。

核心组件

  • Ticker:时钟信号源,与 VSync 同步,每帧回调
  • AnimationController:持有 Ticker,管理动画值(0.0-1.0)
  • Tween:值映射,把 0.0-1.0 映射到目标范围
  • Curve:时间曲线,控制动画的速度变化

动画更新流程

VSync 信号
    ↓
SchedulerBinding.handleBeginFrame()
    ↓
Ticker 收到回调
    ↓
AnimationController 更新 value
    ↓
notifyListeners()
    ↓
AnimatedBuilder.setState()
    ↓
rebuild → 新的 Widget 配置
    ↓
RenderObject 更新 → 重绘

xyz追问:为什么要用 TickerProviderStateMixin?

Ticker 需要在页面不可见时暂停,避免浪费资源。

TickerProviderStateMixin 会在 State deactivate 时暂停 Ticker。

xyz追问:隐式动画和显式动画的区别?

特性 隐式动画 显式动画
代表 AnimatedContainer AnimationController
控制 自动检测属性变化 手动控制
灵活性
使用难度 简单 复杂

串联知识点

动画本质是"每帧改变一点点"。Ticker 保证与屏幕刷新同步,AnimationController 计算每帧的值,Widget 根据值重建——这就是 Flutter 动画的完整链路。


八、图片与列表

Q17:ListView 的懒加载原理是什么?Sliver 是什么?

核心答案:ListView 内部使用 Sliver 协议,只构建可视区域及缓存区的子项,滚动时动态创建和回收,实现按需加载。

Sliver 协议 vs Box 协议

协议 约束 适用场景
Box 宽高范围 普通布局
Sliver 滚动信息 + 可视范围 滚动视图

懒加载流程

用户滚动
    ↓
Viewport 计算可视范围
    ↓
SliverList 收到新的 SliverConstraints
    ↓
根据 scrollOffset 计算首个可见项
    ↓
按需调用 builder 创建子项
    ↓
创建直到填满可视区域 + 缓存区
    ↓
回收离开缓存区的子项

xyz追问:itemExtent 为什么能优化性能?

没有 itemExtent:需要逐个布局子项才知道高度,才能计算滚动范围 有 itemExtent:高度固定,直接计算,不需要实际布局

对于 1000 项的列表,跳转到第 800 项:

  • 没有 itemExtent:可能需要布局前 800 项
  • 有 itemExtent:直接计算偏移 = 800 * itemExtent

xyz追问:为什么 ListView 里放 ListView 会报错?

Column 给 ListView 的高度约束是 infinity(无界)。 ListView 需要确定的高度来计算滚动范围。 无界约束 + 需要确定高度 = 冲突。

解决:用 Expanded 包裹,或给 ListView 设置固定高度。

串联知识点

Sliver 的设计思想是"只做需要做的事"——只构建可见的,只布局可见的,只绘制可见的。这是 Flutter 列表高性能的根本。


Q18:图片加载和缓存的原理是什么?

核心答案:Flutter 使用 ImageCache 进行内存缓存,ImageProvider 负责加载逻辑。图片加载是异步的,解码后缓存,下次直接复用。

加载流程

Image Widget 创建 ImageProvider
    ↓
ImageProvider 生成缓存 Key
    ↓
检查 ImageCache
    ↓
命中 → 直接返回 ImageInfo
    ↓
未命中 → 调用 load()
    ↓
下载/读取原始数据
    ↓
解码为 ui.Image
    ↓
缓存到 ImageCache
    ↓
通知 Image Widget 更新

ImageCache 策略

  • 最大数量:默认 1000
  • 最大字节:默认 100MB
  • 淘汰策略:LRU(最近最少使用)

xyz追问:为什么图片会内存溢出?

  1. 图片尺寸过大:4000x4000 的图片解码后占 64MB
  2. 缓存不释放:没有限制缓存大小
  3. 同时加载太多:列表快速滚动

解决:

  • 使用 ResizeImage 限制解码尺寸
  • 调整 ImageCache 大小
  • 使用 cached_network_image 等库

串联知识点

图片缓存是内存缓存,应用重启就没了。如果需要磁盘缓存(跨会话),需要使用专门的库(如 cached_network_image)。


九、内存与性能

Q19:Dart 的垃圾回收机制是什么?

核心答案:Dart 使用分代垃圾回收。年轻代使用复制算法(快速但需要双倍空间),老年代使用标记-清除-整理(节省空间但较慢)。

分代假设

大多数对象很快死亡(临时变量、短期 Widget),少数对象活很久(State、全局对象)。

基于这个假设,年轻代频繁 GC、老年代较少 GC。

年轻代 GC

  • 分为 From 和 To 两个半空间
  • 新对象分配在 From
  • GC 时,存活对象复制到 To,From 一次性清空
  • 交换 From 和 To

优点:速度快,无碎片 缺点:需要双倍空间

老年代 GC

  • 标记:找出所有存活对象
  • 清除:释放死对象的内存
  • 整理:移动对象,消除碎片

Dart 使用并发 GC,大部分工作在后台线程,减少主线程停顿。

xyz追问:Flutter 的 Widget 频繁创建会影响性能吗?

影响很小:

  1. Widget 是小对象,分配快
  2. Widget 存活时间短,年轻代 GC 效率高
  3. 复制算法对短命对象友好

这就是 Flutter "每帧重建 Widget 树" 可行的原因。

串联知识点

理解 GC 机制,就能理解为什么 const 重要——const 对象不参与 GC,直接从常量池读取。


Q20:Flutter 有哪些常见的性能优化手段?

核心答案:减少 Build 范围、减少 Layout 范围、减少 Paint 范围、减少图层、合理使用缓存。

Build 优化

手段 原理
使用 const 编译期常量,直接复用
状态下沉 缩小 setState 影响范围
使用 Builder 隔离 context 依赖
Selector 细粒度订阅

Layout 优化

手段 原理
固定尺寸 自动成为 Relayout Boundary
避免深层嵌套 减少布局计算
使用 itemExtent 跳过高度测量

Paint 优化

手段 原理
RepaintBoundary 隔离重绘区域
避免 saveLayer 减少离屏渲染(Opacity、ClipPath)
图片合适尺寸 减少解码和绘制开销

列表优化

手段 原理
ListView.builder 按需创建
使用 Key 正确复用
分页加载 减少内存占用

xyz追问:如何定位性能问题?

  1. DevTools 的 Performance 面板
  2. 看 Build/Layout/Paint 耗时
  3. 看 GPU 线程是否拥堵
  4. 使用 debugProfileBuildsEnabled 等 flag

串联知识点

所有优化都指向一个核心——"减少不必要的工作"。理解渲染管线每个阶段做什么,就知道如何针对性优化。


十、高频对比题

Q21:StatelessWidget 和 StatefulWidget 的区别?

对比项 StatelessWidget StatefulWidget
状态 无内部状态 有内部状态
生命周期 只有 build 完整生命周期
重建触发 只能由父节点触发 可以 setState 自触发
性能 更轻量 略重(多个对象)
使用场景 纯展示 需要交互

深入理解

StatelessWidget 只是"简化版"——它也有 Element,只是 Element 没有持有 State。

StatefulWidget 拆分成两个对象(Widget + State)是为了分离"配置"和"状态":

  • Widget 可以频繁重建
  • State 跨越 Widget 重建存活

Q22:Widget、Element、RenderObject 的对应关系?

Widget 类型 Element 类型 RenderObject
StatelessWidget StatelessElement
StatefulWidget StatefulElement
SingleChildRenderObjectWidget SingleChildRenderObjectElement
MultiChildRenderObjectWidget MultiChildRenderObjectElement
InheritedWidget InheritedElement

关键理解

并不是每个 Widget 都有 RenderObject!

StatelessWidget、StatefulWidget 只是"组合"其他 Widget,不直接渲染。真正渲染的是 RenderObjectWidget(如 Container 内部的 DecoratedBox、Padding)。


Q23:Hot Reload vs Hot Restart vs 完全重启?

特性 Hot Reload Hot Restart 完全重启
速度 ~1秒 几秒 较慢
State 保留 丢失 丢失
全局变量 保留 重置 重置
main() 不重新执行 重新执行 重新执行
原生代码 不更新 不更新 更新

Q24:JIT vs AOT?

特性 JIT AOT
编译时机 运行时 构建时
启动速度
运行性能 可动态优化 固定
包体积 小(源码/字节码) 大(机器码)
热重载 支持 不支持
使用场景 Debug Release

深入理解

Debug 用 JIT 是为了热重载;Release 用 AOT 是为了性能。


Q25:Future vs Stream?

特性 Future Stream
值的个数 一个 多个
完成性 完成就结束 可以持续发送
使用方式 await / then listen / await for
典型场景 网络请求 传感器数据、WebSocket

十一、总结性问题

Q26:为什么 Flutter 能做到高性能跨平台?

核心答案

  1. 自绘引擎:不依赖原生控件,避免跨语言通信开销
  2. Skia + GPU:直接 GPU 渲染,接近原生性能
  3. AOT 编译:Release 模式直接运行机器码
  4. 高效的 diff:三棵树设计,最小化更新
  5. 智能边界:Relayout/Repaint Boundary 减少计算范围
  6. 懒加载:Sliver 按需构建

Q27:Flutter 的设计哲学是什么?

  1. 一切皆 Widget:统一的组件模型
  2. 组合优于继承:小组件组合成大组件
  3. 声明式 UI:描述目标状态,而非操作步骤
  4. 不可变配置:Widget 不可变,变化时重建
  5. 分层架构:关注点分离
  6. 自绘引擎:完全控制渲染

Q28:如何回答"Flutter 的渲染原理"这种开放题?

答题框架

  1. 先说架构:三层架构,自绘引擎
  2. 再说三棵树:Widget/Element/RenderObject 的分工
  3. 然后说管线:VSync → Build → Layout → Paint → Composite → Rasterize
  4. 最后说优化:边界机制、缓存复用

答题技巧

  • 从宏观到微观
  • 主动引出下一个话题("这里涉及到 xxx")
  • 用对比("和 RN 不同的是...")
  • 说明设计原因("这样设计是为了...")

十二、知识点串联图谱

Flutter 高性能
    │
    ├── 自绘引擎 ────────────────────┐
    │                               │
    ├── 三棵树分离                    │
    │   ├── Widget 轻量 ← const 优化  │
    │   ├── Element 复用 ← Key 机制   │
    │   └── RenderObject 专注渲染     │
    │                               │
    ├── 渲染管线                      │
    │   ├── Build ← setState 批量     │
    │   ├── Layout ← Relayout Boundary│
    │   └── Paint ← Repaint Boundary  │
    │                               │
    ├── 异步机制                      │
    │   ├── Future ← Event Loop       │
    │   └── Isolate ← 多线程          │
    │                               │
    ├── 懒加载                        │
    │   └── Sliver ← ListView.builder │
    │                               │
    └── GC 友好                       │
        └── 分代回收 ← Widget 短命     │

以上就是 Flutter 底层原理的融会贯通版八股文。每个问题都可以层层深入,知识点之间相互串联,形成完整的知识体系。

KVC / KVO 与 ivar / property 的底层关系

KVC / KVO 与 ivar / property 的底层关系

关键词:KVC、KVO、ivar、property、Runtime、isa-swizzling


一、为什么 KVC / KVO 一定要和 ivar / property 一起理解

在 Objective-C 中:

  • ivar 是数据的真实存储

  • property 是访问 ivar 的规则

  • KVC / KVO 本质上都是“访问规则之上的机制”

如果不理解 ivar 和 property,就一定理解不清 KVC / KVO


二、KVC(Key-Value Coding)底层原理

1️⃣ 什么是 KVC

KVC 是一种:

通过字符串 key 间接访问对象属性的机制

[person setValue:@"Hanqiu" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];

2️⃣ KVC 的本质

  • 本质是 一套查找规则

  • 最终结果:

    • 要么调用方法

    • 要么直接访问 ivar

📌 KVC 并不依赖 property 是否存在


3️⃣ KVC 的 setValue:forKey: 查找顺序(重点)

当执行:

[person setValue:value forKey:@"name"];

查找顺序如下:

1. setName:
2. _setName:
3. +accessInstanceVariablesDirectly == YES ?
   3.1 _name
   3.2 _isName
   3.3 name
   3.4 isName
4. 调用 setValue:forUndefinedKey:

⚠️ 关键点

  • 默认 +accessInstanceVariablesDirectly 返回 YES
  • KVC 可以绕过 setter,直接改 ivar

4️⃣ KVC 的 valueForKey: 查找顺序

1. getName
2. name
3. isName
4. _name
5. _isName
6. 调用 valueForUndefinedKey:

5️⃣ KVC 与 ivar / property 的关系总结

场景 是否需要 property 是否访问 ivar
存在 setter
无 setter
无 ivar ❌(崩溃)

KVC 是“方法优先,ivar 兜底”的机制****


三、KVO(Key-Value Observing)底层原理

1️⃣ 什么是 KVO

KVO 是一种:

监听属性变化的观察机制

[person addObserver:self
         forKeyPath:@"name"
            options:NSKeyValueObservingOptionNew
            context:nil];

2️⃣ KVO 的本质(一句话)

KVO 监听的是 setter 的调用,而不是 ivar 的变化


3️⃣ KVO 的底层实现机制(核心)

当第一次添加观察者时,系统会:

  1. 动态生成一个子类(NSKVONotifying_XXX)
  2. 修改对象的 isa 指针(isa-swizzling)
  3. 在子类中重写 setter
Person
  ↑ isa
NSKVONotifying_Person

4️⃣ 重写的 setter 做了什么

伪代码如下:

- (void)setName:(id)value {
    [self willChangeValueForKey:@"name"];
    [super setName:value];
    [self didChangeValueForKey:@"name"];
}

👉 通知发生在 setter 内部


5️⃣ 为什么直接修改 ivar 不触发 KVO

_name = @"A";      // ❌ 不触发 KVO
self.name = @"B"; // ✅ 触发 KVO

原因:

  • ivar 赋值不走 setter
  • KVO 根本无法感知

四、KVO 与 property / ivar 的强关联关系

1️⃣ KVO 是否依赖 property?

情况 是否支持 KVO
有 setter
只有 ivar
Category property + Associated Object ⚠️(可行但危险)

📌 KVO 实际依赖的是 setter,而不是 property 关键字


2️⃣ 手动触发 KVO

如果你必须直接改 ivar:

[self willChangeValueForKey:@"name"];
_name = @"C";
[self didChangeValueForKey:@"name"];

五、KVC + KVO 联合场景分析(高频面试)

场景:用 KVC 修改属性,是否触发 KVO?

[person setValue:@"D" forKey:@"name"];

结论:

  • 如果最终调用 setter → ✅ 触发 KVO

  • 如果直接命中 ivar → ❌ 不触发

是否触发,取决于 KVC 查找路径


六、Runtime 视角看 KVC / KVO

1️⃣ KVC 使用的 Runtime 能力

  • objc_msgSend
  • class_getInstanceVariable
  • object_setIvar

2️⃣ KVO 使用的 Runtime 能力

  • objc_allocateClassPair
  • object_setClass
  • 动态方法重写

七、常见面试陷阱总结

❌ 误区 1:KVO 监听的是 ivar

❌ 错

✔ 监听的是 setter 的调用


❌ 误区 2:没有 property 就不能 KVO

❌ 错

✔ 只要有 setter 方法即可


❌ 误区 3:KVC 一定会触发 KVO

❌ 错

✔ 是否触发取决于是否调用 setter


八、一张关系总图(文字版)

           ┌──────────────┐
           │   property   │
           │ getter/setter│
           └──────┬───────┘
                  │
        KVO 监听   │ setter
                  ▼
               ivar(真实数据)
                  ▲
                  │
            KVC 兜底访问

九、终极总结

KVC 是“方法优先、ivar 兜底”的键值访问机制;KVO 是通过 isa-swizzling 重写 setter 来监听属性变化的机制,本质与 ivar 无关,只与 setter 是否被调用有关。


Objective-C 类结构全景解析

从 isa 到 cache,从方法列表到属性列表

一次把「一个 Class 里到底装了什么」讲清楚

在 Runtime 视角下,Objective-C 的 Class 并不是一个抽象概念

而是一块结构严谨、职责清晰的内存结构

本文将围绕 Class 的真实组成,系统讲解:

  • isa 指针到底指向哪
  • cache 为什么决定性能
  • 方法列表、属性列表、协议列表各自干什么
  • 一个类里,除了方法,还存了哪些东西

一、先给结论:一个 Class 里有什么?

从 Runtime 角度,一个类(Class)至少包含以下几大部分:

Class
 ├─ isa
 ├─ superclass
 ├─ cache
 ├─ method list
 ├─ property list
 ├─ protocol list
 ├─ ivar list
 ├─ class_rw_t / class_ro_t
 └─ 元类(Meta Class

下面我们逐一展开。


二、isa —— 类的“身份指针”

1. isa 是什么

  • isa 是一个指针
  • 对象的 isa → Class
  • 类的 isa → Meta Class
instance ──isa──▶ Class ──isa──▶ Meta Class

在 arm64 以后:

  • isa 是 非纯指针(non-pointer isa)

  • 高位存储了:

    • 引用计数信息

    • weak 标志

    • 是否有关联对象

逻辑语义没有变化


三、cache —— 方法调用的性能核心

1. cache 是什么

  • cache 是一个 SEL → IMP 的映射表
  • 存在于 Class 中
  • 用于加速方法查找
cache
 ├─ bucket[SEL → IMP]
 └─ mask / occupied

2. cache 在方法查找中的位置

objc_msgSend 查找顺序:

1️⃣ cache
2️⃣ method list
3️⃣ superclass → 重复 12

cache 永远是第一站。


3. cache 的填充时机

  • cache 是 懒加载

  • 第一次方法调用:

    • cache 未命中

    • method list 找到 IMP

    • 写入 cache

之后同一个 SEL:

直接命中 cache


4. cache 为什么不区分类?

cache 的 key 是:

SEL

但 cache 属于 某一个 Class

因此:

A.fooA 的 cache
B.fooB 的 cache

即使 SEL 相同,也互不干扰。


四、method list —— 方法的“原始数据源”

1. method list 是什么

  • method list 是一个数组
  • 每一项是一个 method_t
method_t
 ├─ SEL name
 ├─ IMP imp
 └─ const char *types

也就是我们熟悉的三要素:

SEL + IMP + Type Encoding


2. method list 的来源

method list 由以下部分合并而来:

  • 类本身实现的方法

  • Category 中的方法

⚠️ Category 的方法:

  • 后加载、前插入
  • 因此可以覆盖原方法

五、property list —— 属性的声明信息

1. property list 是什么

  • 属性列表存的是 声明信息
  • 不是 ivar
  • 不是 getter / setter 的实现
objc_property_t
 ├─ name
 └─ attributes (copy, nonatomic, strong ...)

2. property list 干什么用

  • Runtime 反射

  • KVC / KVO

  • 自动序列化 / ORM

但注意:

方法调用完全不依赖 property list


六、ivar list —— 实例变量的真实布局

1. ivar list 是什么

  • ivar list 描述的是:

    • 成员变量
    • 内存偏移
    • 类型
ivar_t
 ├─ name
 ├─ type
 └─ offset

2. ivar list 与对象内存

instance memory
 ├─ isa
 ├─ ivar1
 ├─ ivar2
  • ivar list 决定对象内存布局
  • 子类 ivar 会追加在父类之后

七、protocol list —— 协议信息

1. protocol list 是什么

  • 存储类遵循的协议

  • 包含:

    • 必选方法

    • 可选方法

主要用于:

  • conformsToProtocol:
  • Runtime 查询

八、class_rw_t / class_ro_t —— 可变与只读区

1. class_ro_t(只读)

  • 编译期确定

  • 存储:

    • 原始方法列表
    • ivar list
    • property list

2. class_rw_t(可写)

  • 运行时动态生成

  • 存储:

    • Category 方法

    • 动态添加的方法

这也是 Category 能“修改类行为”的根本原因。


九、Meta Class —— 类方法的归宿

1. Meta Class 是什么

  • 类方法不是存在 Class 里
  • 而是存在 Meta Class 的 method list 中
[Class foo]
 → 查找 Meta Class 的 cache / method list

十、一张完整 Runtime 结构图(逻辑)

instance
  └─ isa → Class
              ├─ isa → Meta Class
              ├─ superclass
              ├─ cache
              ├─ method list
              ├─ property list
              ├─ ivar list
              ├─ protocol list
              └─ class_rw_t / class_ro_t

十一、终极理解(非常重要)

  • 方法调用性能 = cache 决定

  • 行为修改能力 = method list + rw 区

  • 内存布局 = ivar list 决定

  • 反射能力 = property / protocol 提供

它们各司其职,互不混乱。


十二、一句话总结

Class 是 Runtime 的作战单元:

cache 决定快慢,method list 决定行为,

ivar 决定内存,property 决定语义,

isa 决定你是谁。

理解这一层结构,

你就真正理解了 Objective-C Runtime 的“骨架”。

iOS 常用调试工具大全-打造你的调试武器库

还记得你第一次使用NSLog(@"Hello, World!")的时刻吗?那是调试的起点。但随着应用复杂度呈指数级增长,我们需要的工具也经历了革命性进化:

  • 第一代:基础输出(NSLogprint
  • 第二代:图形化界面(Xcode调试器、Instruments)
  • 第三代:运行时动态调试(FLEX、Lookin)
  • 第四代:智能化监控(性能追踪、自动化检测)

今天,一个成熟的iOS开发者工具箱中,至少需要掌握3-5种核心调试工具,它们就像外科医生的手术刀——精准、高效、各有所长。

一、运行时调试工具

1. FLEX (Flipboard Explorer)

功能最全的运行时调试套件,集成后可以测试期间随时开启\关闭工具条,比如设置摇一摇后启动。

优点: 功能全面,无需连接电脑
缺点: 内存占用稍大
场景: 日常开发调试UI问题排查
GitHub: https://github.com/FLEXTool/FLEX?tab=readme-ov-file

主要功能:

  • 手机上检查和修改层次结构中的视图。
  • 查看对象内存分配,查看任何对象的属性和ivar,动态修改许多属性和ivar,动态调用实例和类方法。
  • 查看详细的网络请求历史记录,包括时间、标头和完整响应。
  • 查看系统日志消息(例如,来自NSLog)。
  • 查看沙盒中的文件,查看所有的bundle和资源文件,浏览文件系统中的SQLite/Rerm数据库。
  • 动态查看和修改NSUserDefaults值。

2. Lookin - 腾讯出品

3D视图层级工具,类Xcode Inspector和Reveal。相比Xcode中查看图层的优势有两个:

  • 独立于Xcode运行,不会被Xcode阻断,能显示view的被引用的属性名。
  • 集成了'LookinServer'库的APP启动后,在Mac上启动Lookin后即可刷新显示当前图层。(真机需连接电脑后才展示)
// 集成步骤一:官网下载lookin;
-  官网: https://lookin.work
-  GitHub: https://github.com/QMUI/LookinServer

// 集成步骤二:
CocoaPods安装:
// 1.如果是OC工程
pod 'LookinServer', :configurations => ['Debug']
// 2.如果是OC工程
// 在 iOS 项目的 Podfile 中 添加 “Swift” 这个 Subspec
pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']
// 或者添加 “SwiftAndNoHook”这个 Subspec 也行
pod 'LookinServer', :subspecs => ['SwiftAndNoHook'], :configurations => ['Debug']

二、网络调试工具

1. Proxyman - 现代网络调试神器

// 官网: https://proxyman.io
// 特点:
 现代UI,操作流畅
 HTTPS解密(无需安装证书到系统)
 重放修改拦截请求
 Map Local/Map Remote功能
 脚本支持(JavaScript)
 支持Apple Silicon

使用场景:
 API接口调试
 图片/资源请求优化
 模拟慢速网络
 修改响应数据测试

2. Charles - 老牌网络代理

// 官网: https://www.charlesproxy.com
// 特点:
 功能极其全面
 跨平台支持
 脚本功能强大(Charles Proxy Script)
 带宽限制断点调试
 支持HTTP/2HTTP/3

设置步骤:
1. 安装Charles
2. 在iOS设备设置代理
3. 安装Charles根证书
4. 信任证书(设置通用关于证书信任设置)

// 常用功能:
 Breakpoints(请求拦截修改)
 Rewrite(规则重写)
 Map Local(本地文件映射)
 Throttle(网络限速)

3. mitmproxy - 开源命令行工具

# 官网: https://mitmproxy.org
# 特点:
✅ 完全开源免费
✅ 命令行操作,适合自动化
✅ 脚本扩展(Python)
✅ 支持透明代理

# 安装:
brew install mitmproxy

# 使用:
# 启动代理
mitmproxy --mode transparent --showhost

# iOS设置:
# 1. 安装证书: mitm.it
# 2. 配置Wi-Fi代理

三、UI/布局调试工具

1. Reveal - 专业的UI调试工具

// 官网: https://revealapp.com
// 特点:
 实时3D视图层级
 详细的AutoLayout约束查看
 内存图查看器
 支持SwiftUI预览
 强大的筛选和搜索

// 集成:
// 方式1: 通过Reveal Server框架
pod 'Reveal-SDK', :configurations => ['Debug']

// 方式2: LLDB加载(无需集成代码)
(lldb) expr (void)[[NSClassFromString(@"IBARevealLoader") class] revealApplication];

// 价格: 付费(提供免费试用)

2. InjectionIII - 热重载神器

// GitHub: https://github.com/johnno1962/InjectionIII
// 特点:
 代码修改后实时生效
 无需重新编译运行
 支持Swift和Objective-C
 保留应用状态

// 安装:
# App Store搜索 "InjectionIII"

// 配置:
1. 下载安装InjectionIII
2. 在AppDelegate中配置:
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
#endif

3. 项目添加文件监视:
// 在InjectionIII App中添加项目路径

四、性能调试工具

1. Xcode Instruments - 官方性能分析套件

// 核心工具集:
┌─────────────────────────────────────┐
          Xcode Instruments          
├─────────────────────────────────────┤
 Time Profiler    # CPU使用分析       
 Allocations      # 内存分配分析      
 Leaks           # 内存泄漏检测      
 Network         # 网络活动分析      
 Energy Log      # 电量消耗分析      
 Metal System    # GPU性能分析       
 SwiftUI         # SwiftUI性能分析   
└─────────────────────────────────────┘

// 使用技巧:
1. 录制时过滤系统调用:
   Call Tree:  Hide System Libraries
               Invert Call Tree
               Flattern Recursion

2. 内存图调试:
   Debug Memory Graph按钮
   查看循环引用内存泄漏

3. 使用Markers:
   import os
   let log = OSLog(subsystem: "com.app", category: "performance")
   os_signpost(.begin, log: log, name: "Network Request")
   // ... 操作
   os_signpost(.end, log: log, name: "Network Request")

2. MetricKit - 线上性能监控框架

// Apple官方性能数据收集框架
import MetricKit

class MetricKitManager: MXMetricManagerSubscriber {
    static let shared = MetricKitManager()
    
    private init() {
        let manager = MXMetricManager.shared
        manager.add(self)
    }
    
    func didReceive(_ payloads: [MXMetricPayload]) {
        // 接收性能数据
        for payload in payloads {
            print("CPU: \(payload.cpuMetrics)")
            print("内存: \(payload.memoryMetrics)")
            print("启动时间: \(payload.launchMetrics)")
            print("磁盘IO: \(payload.diskIOMetrics)")
        }
    }
    
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        // 接收诊断数据(崩溃、卡顿等)
    }
}

// 需要用户授权,适合生产环境监控

3. Tracy - 腾讯开源的性能监控

// GitHub: https://github.com/Tencent/tracy
// 特点:
 卡顿监控(主线程阻塞检测)
 内存泄漏检测
 大对象分配监控
 网络性能监控
 崩溃收集

// 集成:
pod 'Tracy', :configurations => ['Debug']

// 使用:
Tracy.start()
// 自动监控各种性能指标

五、内存/崩溃调试工具

1. MLeaksFinder - 腾讯出品的内存泄漏检测

// GitHub: https://github.com/Tencent/MLeaksFinder
// 特点:
 自动检测视图控制器内存泄漏
 无需编写任何代码
 支持自定义白名单
 精准定位泄漏对象

// 原理:
// 监听UIViewController的pop/dismiss
// 延迟检查是否仍然存在

// 集成:
pod 'MLeaksFinder'

// 自定义配置:
// 1. 添加白名单
[NSClassFromString(@"WhiteListClass") class]

// 2. 忽略特定泄漏
[MLeaksFinder addIgnoreClass:[IgnoreClass class]]

2. FBRetainCycleDetector - Facebook循环引用检测

// GitHub: https://github.com/facebook/FBRetainCycleDetector
// 特点:
 检测Objective-C对象的循环引用
 支持检测NSTimer的强引用
 可集成到单元测试中
 Facebook内部广泛使用

// 使用:
let detector = FBRetainCycleDetector()
detector.addCandidate(myObject)
let cycles = detector.findRetainCycles()

// 输出格式化的循环引用链
for cycle in cycles {
    print(FBRetainCycleDetectorFormatter.format(cycle))
}

3. KSCrash - 强大的崩溃收集框架

// GitHub: https://github.com/kstenerud/KSCrash
// 特点:
 捕获所有类型崩溃(OC异常C++异常Mach异常等)
 生成完整的崩溃报告
 支持符号化
 可自定义上报服务器

// 集成:
pod 'KSCrash'

// 配置:
import KSCrash

let installation = makeEmailInstallation("crash@company.com")
installation.addConditionalAlert(withTitle: "Crash Detected",
                                message: "The app crashed last time")
KSCrash.shared().install()

// 高级功能:
// 1. 用户数据记录
KSCrash.shared().userInfo = ["user_id": "123"]

// 2. 自定义日志
KSCrash.shared().log.error("Something went wrong")

// 3. 监控卡顿
KSCrash.shared().monitorDeadlock = true

六、日志调试工具

1. CocoaLumberjack - 专业日志框架

// GitHub: https://github.com/CocoaLumberjack/CocoaLumberjack
// 特点:
 高性能日志记录
 多日志级别(Error, Warn, Info, Debug, Verbose)
 多种输出目标(Console, File, Database)
 日志轮转和清理
 支持Swift和Objective-C

// 集成:
pod 'CocoaLumberjack/Swift'

// 配置:
import CocoaLumberjackSwift

// 控制台日志
DDLog.add(DDOSLogger.sharedInstance)

// 文件日志
let fileLogger = DDFileLogger()
fileLogger.rollingFrequency = 60 * 60 * 24 // 24小时
fileLogger.logFileManager.maximumNumberOfLogFiles = 7
DDLog.add(fileLogger)

// 使用:
DDLogError("错误信息")
DDLogWarn("警告信息")
DDLogInfo("普通信息")
DDLogDebug("调试信息")
DDLogVerbose("详细信息")

// 上下文过滤:
let context = 123
DDLogDebug("带上下文的消息", context: context)

2. SwiftyBeaver - Swift专用日志框架

// GitHub: https://github.com/SwiftyBeaver/SwiftyBeaver
// 特点:
Swift实现
 彩色控制台输出
 多种目的地(Console, File, Cloud)
 平台同步(macOS App)
 支持emoji和格式化

// 使用:
import SwiftyBeaver
let log = SwiftyBeaver.self

// 添加控制台目的地
let console = ConsoleDestination()
console.format = "$DHH:mm:ss$d $L $M"
log.addDestination(console)

// 添加文件目的地
let file = FileDestination()
file.logFileURL = URL(fileURLWithPath: "/path/to/file.log")
log.addDestination(file)

// 日志级别:
log.verbose("详细")    // 灰色
log.debug("调试")      // 绿色
log.info("信息")       // 蓝色
log.warning("警告")    // 黄色
log.error("错误")      // 红色

3. XCGLogger - 功能丰富的日志框架

// GitHub: https://github.com/DaveWoodCom/XCGLogger
// 特点:
 高度可配置
 支持日志过滤
 自定义日志目的地
 自动日志轮转
 详细的文档

// 使用:
import XCGLogger

let log = XCGLogger.default

// 配置
log.setup(level: .debug,
          showLogIdentifier: false,
          showFunctionName: true,
          showThreadName: true,
          showLevel: true,
          showFileNames: true,
          showLineNumbers: true,
          showDate: true)

// 自定义过滤器
log.filters = [
    Filter.Level(from: .debug),  // 只显示.debug及以上
    Filter.Path(include: ["ViewController"], exclude: ["ThirdParty"])
]

log.debug("调试信息")
log.error("错误信息")

七、自动化调试工具

1. Fastlane - 自动化工具集

# 官网: https://fastlane.tools
# 特点:
✅ 自动化构建、测试、部署
✅ 丰富的插件生态
✅ 与CI/CD深度集成
✅ 跨平台支持

# 常用命令:
fastlane screenshots    # 自动截图
fastlane beta          # 发布测试版
fastlane release       # 发布正式版
fastlane match         # 证书管理

# 集成调试功能:
lane :debug_build do
  # 1. 设置调试配置
  update_app_identifier(
    app_identifier: "com.company.debug"
  )
  
  # 2. 启用调试功能
  update_info_plist(
    plist_path: "Info.plist",
    block: proc do |plist|
      plist["FLEXEnabled"] = true
      plist["NSAllowsArbitraryLoads"] = true
    end
  )
  
  # 3. 构建
  gym(
    scheme: "Debug",
    export_method: "development"
  )
end

2. slather - 代码覆盖率工具

# GitHub: https://github.com/SlatherOrg/slather
# 特点:
✅ 生成代码覆盖率报告
✅ 支持多种输出格式(html, cobertura, json)
✅ 与CI集成
✅ 过滤第三方库代码

# 安装:
gem install slather

# 使用:
# 1. 运行测试并收集覆盖率
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 14' -enableCodeCoverage YES

# 2. 生成报告
slather coverage --html --show --scheme MyApp MyApp.xcodeproj

# 3. 在Jenkins中集成
slather coverage --input-format profdata --cobertura-xml --output-directory build/reports MyApp.xcodeproj

八、特殊场景调试工具

1. SparkInspector - 实时对象监控

// 官网: https://sparkinspector.com
// 特点:
 实时监控所有对象实例
 查看对象属性变化
 方法调用追踪
 内存泄漏检测

// 集成:
// 1. 下载Spark Inspector应用
// 2. 集成框架到项目
// 3. 通过Spark Inspector连接调试

// 适用场景:
 复杂的对象关系调试
 观察模式数据流
 内存泄漏定位

3. LLDB - 底层调试神器

# Xcode内置,但功能极其强大
# 常用命令:

# 1. 查看变量
(lldb) po variable
(lldb) p variable
(lldb) v variable

# 2. 修改变量
(lldb) expr variable = newValue

# 3. 调用方法
(lldb) expr [self doSomething]
(lldb) expr self.doSomething()

# 4. 断点命令
(lldb) breakpoint set -n "[ClassName methodName]"
(lldb) breakpoint command add 1  # 为断点1添加命令
> po $arg1
> continue
> DONE

# 5. 内存查看
(lldb) memory read 0x12345678
(lldb) memory write 0x12345678 0x42

# 6. 自定义LLDB命令
(lldb) command regex rlook 's/(.+)/image lookup -rn %1/'
(lldb) rlook methodName

# 7. Swift特定命令
(lldb) frame variable -L  # 显示局部变量
(lldb) type lookup String # 查看类型信息

九、工具矩阵

需求场景 推荐工具 理由
日常开发调试 FLEX + Proxyman 功能全面,无需额外环境
UI/布局问题 Lookin + Reveal 3D视图,实时修改
性能优化 Xcode Instruments + Tracy 官方工具+线上监控
内存泄漏 MLeaksFinder + FBRetainCycleDetector 自动检测+深度分析
网络调试 Proxyman/Charles 功能专业,操作友好
日志管理 CocoaLumberjack + SwiftyBeaver 功能强大+美观输出
自动化 Fastlane + slather 流程自动化+质量监控
底层调试 LLDB + InjectionIII 深度控制+热重载

团队规范建议

# iOS团队调试工具规范

## 必装工具(所有开发者)
1. Proxyman/Charles - 网络调试
2. Lookin - UI调试  
3. InjectionIII - 热重载

## 项目集成(Podfile)
```ruby
target 'MyApp' do
  # 调试工具(仅Debug)
  pod 'FLEX', :configurations => ['Debug']
  pod 'CocoaLumberjack', :configurations => ['Debug']
  pod 'MLeaksFinder', :configurations => ['Debug']
end

总结

核心建议:

  1. 不要过度依赖单一工具 - 不同工具有不同适用场景
  2. 掌握核心原理 - 理解工具背后的工作原理比单纯使用更重要
  3. 建立个人调试工具箱 - 根据习惯组合适合自己的工具集
  4. 关注新工具发展 - iOS开发工具生态在持续进化
  5. 重视自动化 - 将重复调试工作自动化,提高效率

终极目标: 快速定位问题 → 深入分析原因 → 有效解决问题

这些工具大多数都有免费版本或开源版本,建议从最常用的几个开始,逐步建立自己的调试能力体系。

掌握这些工具,不是为了炫耀技术,而是为了让你的代码更健壮,让你的用户更满意,让你自己在深夜加班时少掉几根头发。

iOS客户端开发基础知识——写文件避“坑”指南(二)

更多精彩文章,欢迎关注作者微信公众号:码工笔记

一、背景 & 问题

上一篇文章讲过,在iOS、macOS平台上,要保证新写入的文件内容成功落盘,需要调用fcntl(fd, FULL_SYNC)(注:开源chromium里也是这么做的[1]):

FULL_SYNC

Does the same thing as fsync(2) then asks the drive to flush all buffered data to the permanent storage device (arg is ignored). As this drains the entire queue of the device and acts as a barrier, data that had been fsync'd on the same device before is guaranteed to be persisted when this call returns. This is currently implemented on HFS, MS-DOS (FAT), Universal Disk Format (UDF) and APFS file systems. The operation may take quite a while to complete. Certain FireWire drives have also been known to ignore the request to flush their buffered data.

从上面man page的描述可以看出,FULL_SYNC是将设备unified buffer里的数据全部强制落盘,因为buffer中的数据可能不只包含刚刚写入的,可能还包含了之前写入的数据,虽然达到了持久化的目的,但时间不可控,可能会耗时很长,严重影响应用性能。

有没有什么优化方式呢?

二、F_BARRIERFSYNC

从应用开发者的角度,很多场景下并不需要这么强的落盘保证,大多数场景下,如果能保证写入顺序,也即先写入数据A,后写入数据B,如果后续读数据时读到了数据B,则A也一定存在,应用侧就可以自己做数据完整性检查了,从而可以做兜底逻辑。这样一来既能减少强制落盘对性能的影响,又能保证数据的完整性。

fcntlF_BARRIERFSYNC这个选项就是为了解决这个问题的。先看一下man page说明:

F_BARRIERFSYNC

Does the same thing as fsync(2) then issues a barrier command to the drive (arg is ignored). The barrier applies to I/O that have been flushed with fsync(2) on the same device before. These operations are guaranteed to be persisted before any other I/O that would follow the barrier, although no assumption should be made on what has been persisted or not when this call returns. After the barrier has been issued, operations on other FDs that have been fsync'd before can still be re-ordered by the device, but not after the barrier. This is typically useful to guarantee valid state on disk when ordering is a concern but durability is not. A barrier can be used to order two phases of operations on a set of file descriptors and ensure that no file can possibly get persisted with the effect of the second phase without the effect of the first one. To do so, execute operations of phase one, then fsync(2) each FD and issue a single barrier. Finally execute operations of phase two. This is currently implemented on HFS and APFS. It requires hardware support, which Apple SSDs are guaranteed to provide.

调用此方法后,系统虽不能保证数据是否真正落盘成功,但能保证写入的顺序,也即如果后写入的数据成功落盘,则先写入的数据一定已经落盘。

注:Apple的SSD都支持。

Apple的官方建议[2]是:如果有强落盘需求,可以用FULL_SYNC,但这会导致性能下降及设备损耗,如果只需要保证写入顺序,则建议用F_BARRIERFSYNC。

Minimize explicit storage synchronization

Writing data on iOS adds the data to a unified buffer cache that the system then writes to file storage. Forcing iOS to flush pending filesystem changes from the unified buffer can result in unnecessary writes to the disk, degrading performance and increasing wear on the device. When possible, avoid calling fsync(_:), or using the fcntl(_:_:) F_FULLFSYNC operation to force a flush.

Some apps require a write barrier to ensure data persistence before subsequent operations can proceed. Most apps can use the fcntl(_:_:) F_BARRIERFSYNC for this.

Only use F_FULLFSYNC when your app requires a strong expectation of data persistence. Note that F_FULLFSYNC represents a best-effort guarantee that iOS writes data to the disk, but data can still be lost in the case of sudden power loss.

三、例子:SQLite主线的问题和苹果的优化

SQLite是移动端最常用的文件数据库,读写文件是其功能的基石。SQLite是如何实现落盘的呢?看看SQLite仓库主线逻辑[3]:

#elif HAVE_FULLFSYNC
  if( fullSync ){
    rc = osFcntl(fd, F_FULLFSYNC, 0);
  }else{
    rc = 1;
  }
  /* If the FULLFSYNC failed, fall back to attempting an fsync().
  ** It shouldn't be possible for fullfsync to fail on the local
  ** file system (on OSX), so failure indicates that FULLFSYNC
  ** isn't supported for this file system. So, attempt an fsync
  ** and (for now) ignore the overhead of a superfluous fcntl call.
  ** It'd be better to detect fullfsync support once and avoid
  ** the fcntl call every time sync is called.
  */
  if( rc ) rc = fsync(fd);

#elif defined(__APPLE__)
  /* fdatasync() on HFS+ doesn't yet flush the file size if it changed correctly
  ** so currently we default to the macro that redefines fdatasync to fsync
  */
  rc = fsync(fd);

如果开了PRAGMA fullsync = ON,也是使用了F_FULLSYNC来保证写入成功。没开的话是使用fsync,这里应该是有问题的。

那iOS的libsqlite是怎么做的呢?这个库苹果没有开源,只能逆向看一下,搜搜相关的几个方法,应该在这一段汇编这里:

                                    loc_1b0d62f40:
00000001b0d62f40 682240F9               ldr        x8, [x19, #0x40]             ; CODE XREF=sub_1b0d62d34+444
00000001b0d62f44 880000B4               cbz        x8, loc_1b0d62f54

00000001b0d62f48 080140F9               ldr        x8, [x8]
00000001b0d62f4c 08A940B9               ldr        w8, [x8, #0xa8]
00000001b0d62f50 28FDFF35               cbnz       w8, loc_1b0d62ef4

                                    loc_1b0d62f54:
00000001b0d62f54 280C0012               and        w8, w1, #0xf                 ; CODE XREF=sub_1b0d62d34+528
00000001b0d62f58 1F0D0071               cmp        w8, #0x3
00000001b0d62f5c A80A8052               mov        w8, #0x55
00000001b0d62f60 08019F1A               csel       w8, w8, wzr, eq
00000001b0d62f64 69024239               ldrb       w9, [x19, #0x80]
00000001b0d62f68 3F011F72               tst        w9, #0x2
00000001b0d62f6c 69068052               mov        w9, #0x33
00000001b0d62f70 0101891A               csel       w1, w8, w9, eq
00000001b0d62f74 741A40B9               ldr        w20, [x19, #0x18]
00000001b0d62f78 A1000034               cbz        w1, loc_1b0d62f8c

00000001b0d62f7c FF0300F9               str        xzr, [sp, #0x170 + var_170]
00000001b0d62f80 E00314AA               mov        x0, x20
00000001b0d62f84 7B93C794               bl         0x1b3f47d70
00000001b0d62f88 60040034               cbz        w0, loc_1b0d63014

                                    loc_1b0d62f8c:
00000001b0d62f8c E00314AA               mov        x0, x20                      ; argument "fildes" for method imp___auth_stubs__fsync, CODE XREF=sub_1b0d62d34+580
00000001b0d62f90 9C7A0494               bl         imp___auth_stubs__fsync      ; fsync
00000001b0d62f94 00040034               cbz        w0, loc_1b0d63014

翻译成C语言伪代码:

// x19 is the context pointer (self/this)
// w1 is an input argument (flags)

// 1. Pre-check
struct SubObject* obj = self->ptr_40;
if (obj) {
    if (obj->ptr_0->status_a8 != 0) {
        goto loc_1b0d62ef4; // Busy/Error path
    }
}

// 2. Determine Sync Command
int fd = self->file_descriptor; // offset 0x18
int command = 0;

// Check config flag at offset 0x80
if (self->flags_80 & 0x02) {
    command = 0x33; // F_FULLFSYNC (51)
} 
else if ((w1 & 0x0F) == 3) {
    command = 0x55; // F_BARRIERFSYNC (85)
}

// 3. Try Specialized Sync
int result = -1;
if (command != 0) {
    // Likely fcntl(fd, command, 0)
    result = unknown_func_1b3f47d70(fd, command, 0); 
    
    if (result == 0) {
        goto success; // loc_1b0d63014
    }
}

// 4. Fallback to standard fsync
// Reached if command was 0 OR if specialized sync failed
result = fsync(fd);

if (result == 0) {
    goto success;
}

// ... handle error ...

可以看出这个逻辑中既有F_FULLSYNC又有F_BARRIERFSYNC。写了个简单demo验证了一下,PRAGMA fullsync = ON会用F_FULLSYNCPRAGMA fullsync = OFF用的是F_BARRIERFSYNC

所以,如果在苹果系统上使用自己编译的sqlite库时,需要注意把这个逻辑加上。

*总之,在iOS/macOS平台写文件的场景,需要考虑好对性能、稳定性的需求,选用合适的系统机制。

参考资料

__CFRunLoopServiceMachPort函数详解

借助AI辅助。

__CFRunLoopServiceMachPort 函数逐行注释

这是 RunLoop 在 macOS 上休眠和唤醒的核心函数,通过 mach_msg() 系统调用实现线程阻塞。


完整注释代码

static Boolean __CFRunLoopServiceMachPort(
    mach_port_name_t port,              // 要等待的端口(或端口集合)
    mach_msg_header_t **buffer,         // 消息缓冲区指针的地址
    size_t buffer_size,                 // 缓冲区大小
    mach_port_t *livePort,              // [输出] 被唤醒的端口
    mach_msg_timeout_t timeout,         // 超时时间(毫秒,TIMEOUT_INFINITY=无限)
    voucher_mach_msg_state_t *_Nonnull voucherState,  // voucher 状态(追踪用)
    voucher_t *voucherCopy,             // voucher 副本
    CFRunLoopRef rl,                    // RunLoop(用于追踪)
    CFRunLoopModeRef rlm                // Mode(用于追踪)
) {
    // ========================================
    // 函数返回值说明:
    // • true: 收到消息,livePort 指向唤醒的端口
    // • false: 超时或错误
    // ========================================
    
    Boolean originalBuffer = true;
    // 标记是否使用原始缓冲区
    // true: 使用调用者提供的栈上缓冲区
    // false: 使用动态分配的堆缓冲区(消息太大时)
    
    kern_return_t ret = KERN_SUCCESS;
    // Mach 内核调用的返回值
    // 初始化为成功状态
    
    for (;;) {
        // 无限循环,直到:
        // 1. 成功接收消息(return true)
        // 2. 超时(return false)
        // 3. 致命错误(break 后 HALT)
        
        /* In that sleep of death what nightmares may come ... */
        // 莎士比亚《哈姆雷特》引用:"在死亡的睡眠中会有什么噩梦来临..."
        // 暗示线程即将进入"休眠"状态
        
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        // 获取消息指针
        // *buffer 是指向缓冲区的指针
        
        // ========================================
        // 步骤 1: 初始化消息头
        // ========================================
        
        msg->msgh_bits = 0;
        // 消息标志位,初始化为 0
        // mach_msg 会设置适当的接收标志
        
        msg->msgh_local_port = port;
        // 设置本地端口(接收端口)
        // 这是我们要等待的端口(或端口集合)
        
        msg->msgh_remote_port = MACH_PORT_NULL;
        // 远程端口(发送目标)设为空
        // 因为我们只接收,不发送
        
        msg->msgh_size = buffer_size;
        // 设置缓冲区大小
        // 告诉内核我们能接收多大的消息
        
        msg->msgh_id = 0;
        // 消息 ID,初始化为 0
        // 接收后会包含实际的消息 ID
        
        // ========================================
        // 步骤 2: 记录追踪事件(调试用)
        // ========================================
        
        if (TIMEOUT_INFINITY == timeout) {
            // 如果是无限等待
            CFRUNLOOP_SLEEP();
            // 探针宏:记录休眠事件(DTrace)
            cf_trace(KDEBUG_EVENT_CFRL_SLEEP, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 即将休眠
        } else {
            // 如果有超时时间(轮询模式)
            CFRUNLOOP_POLL();
            // 探针宏:记录轮询事件
            cf_trace(KDEBUG_EVENT_CFRL_POLL, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 轮询
        }
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段结束
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:开始等待阶段
        
        // ========================================
        // 步骤 3: 调用 mach_msg 等待 ⭐⭐⭐
        // 【这是整个 RunLoop 最核心的一行代码!】
        // ========================================
        
        ret = mach_msg(
            msg,                    // 消息缓冲区
            // 选项组合:
            MACH_RCV_MSG |          // 接收消息模式
            MACH_RCV_VOUCHER |      // 接收 voucher(追踪信息)
            MACH_RCV_LARGE |        // 支持大消息(自动重新分配缓冲区)
            ((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0) | 
            // 如果有超时,添加 MACH_RCV_TIMEOUT 标志
            MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) |
            // 接收 trailer(消息尾部附加信息)
            MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
            // trailer 包含 audit token 和 voucher
            0,                      // 发送大小(不发送,所以为 0)
            msg->msgh_size,         // 接收缓冲区大小
            port,                   // 接收端口(或端口集合)
            timeout,                // 超时时间(毫秒)
            MACH_PORT_NULL          // 通知端口(不使用)
        );
        // 【线程在这里阻塞】
        // 等待以下情况之一:
        // 1. port 收到消息 → 返回 MACH_MSG_SUCCESS
        // 2. 超时 → 返回 MACH_RCV_TIMED_OUT
        // 3. 消息太大 → 返回 MACH_RCV_TOO_LARGE
        // 4. 其他错误 → 返回错误码
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:等待阶段结束(被唤醒)
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段开始
        
        // ========================================
        // 步骤 4: 处理 voucher(性能追踪)
        // ========================================
        
        // Take care of all voucher-related work right after mach_msg.
        // 在 mach_msg 之后立即处理所有 voucher 相关工作
        // If we don't release the previous voucher we're going to leak it.
        // 如果不释放之前的 voucher,会造成内存泄漏
        
        voucher_mach_msg_revert(*voucherState);
        // 恢复之前的 voucher 状态
        // 释放上次收到的 voucher(如果有)
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        // 调用者负责调用 voucher_mach_msg_revert
        // 这个调用让接收到的 voucher 成为当前的
        
        *voucherState = voucher_mach_msg_adopt(msg);
        // 采用(adopt)消息中的 voucher
        // 返回新的 voucher 状态
        // voucher 用于追踪消息的来源和上下文
        
        if (voucherCopy) {
            // 如果调用者需要 voucher 副本
            *voucherCopy = NULL;
            // 重置为 NULL
            // 调用者可以在需要时拷贝
        }

        CFRUNLOOP_WAKEUP(ret);
        // 探针宏:记录唤醒事件,传入返回值
        
        cf_trace(KDEBUG_EVENT_CFRL_DID_WAKEUP, port, 0, 0, 0);
        // 内核追踪:记录 RunLoop 被唤醒
        
        // ========================================
        // 步骤 5: 处理返回结果
        // ========================================
        
        if (MACH_MSG_SUCCESS == ret) {
            // 情况 1: 成功接收到消息
            
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            // 返回被唤醒的端口
            // 调用者通过这个值判断唤醒源:
            // • _wakeUpPort → 手动唤醒
            // • _timerPort → 定时器
            // • dispatchPort → GCD 主队列
            // • 其他 → Source1
            
            return true;
            // 返回成功,结束函数
        }
        
        if (MACH_RCV_TIMED_OUT == ret) {
            // 情况 2: 接收超时(正常情况)
            
            if (!originalBuffer) free(msg);
            // 如果使用了动态分配的缓冲区,释放它
            
            *buffer = NULL;
            // 将缓冲区指针设为 NULL
            
            *livePort = MACH_PORT_NULL;
            // 没有唤醒端口(超时)
            
            return false;
            // 返回失败(超时)
        }
        
        if (MACH_RCV_TOO_LARGE != ret) {
            // 情况 3: 其他错误(非 "消息太大")
            // 这些是致命错误,需要崩溃
            
            if (((MACH_RCV_HEADER_ERROR & ret) == MACH_RCV_HEADER_ERROR) || 
                (MACH_RCV_BODY_ERROR & ret) == MACH_RCV_BODY_ERROR) {
                // 如果是消息头错误或消息体错误
                
                kern_return_t specialBits = MACH_MSG_MASK & ret;
                // 提取特殊错误位
                
                if (MACH_MSG_IPC_SPACE == specialBits) {
                    // IPC 空间不足
                    CRSetCrashLogMessage("Out of IPC space");
                    // 设置崩溃日志消息
                    // 可能原因:Mach 端口泄漏
                    
                } else if (MACH_MSG_VM_SPACE == specialBits) {
                    // 虚拟内存空间不足
                    CRSetCrashLogMessage("Out of VM address space");
                    // 内存耗尽
                    
                } else if (MACH_MSG_IPC_KERNEL == specialBits) {
                    // 内核 IPC 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling IPC");
                    // 内核资源不足
                    
                } else if (MACH_MSG_VM_KERNEL == specialBits) {
                    // 内核 VM 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling out-of-line memory");
                    // 内核内存不足
                }
            } else {
                // 其他类型的错误
                CRSetCrashLogMessage(mach_error_string(ret));
                // 设置错误字符串为崩溃日志
            }
            break;
            // 跳出循环,准备崩溃
        }
        
        // ========================================
        // 步骤 6: 处理 MACH_RCV_TOO_LARGE(消息太大)
        // ========================================
        // 如果执行到这里,说明 ret == MACH_RCV_TOO_LARGE
        
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        // 计算需要的缓冲区大小
        // round_msg: 向上取整到合适的大小
        // msg->msgh_size: 实际消息大小(已在 msg 中设置)
        // MAX_TRAILER_SIZE: trailer 的最大大小
        
        if (originalBuffer) *buffer = NULL;
        // 如果之前使用的是原始缓冲区(栈上的)
        // 将指针设为 NULL,下面会分配新的
        
        originalBuffer = false;
        // 标记不再使用原始缓冲区
        
        *buffer = __CFSafelyReallocate(*buffer, buffer_size, NULL);
        // 重新分配更大的缓冲区
        // 如果 *buffer 是 NULL,相当于 malloc
        // 否则相当于 realloc
        // 下次循环会使用新缓冲区重新接收

        if (voucherCopy != NULL && *voucherCopy != NULL) {
            // 如果有 voucher 副本
            os_release(*voucherCopy);
            // 释放 voucher(引用计数 -1)
        }
    }
    // 继续循环,使用新缓冲区重新调用 mach_msg
    
    HALT;
    // 如果跳出循环(因为致命错误),停止程序
    // HALT 宏会触发断点或终止进程
    
    return false;
    // 这行代码实际不会执行(HALT 不会返回)
    // 但保留以满足编译器要求
}

函数执行流程图

┌─────────────────────────────────────────────────────────────┐
│  开始 __CFRunLoopServiceMachPort                            │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  初始化消息头                                                 │
│  • msgh_local_port = port (等待的端口)                        │
│  • msgh_size = buffer_size                                  │
│  • 其他字段置 0                                               │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  记录追踪事件                                                 │
│  • SLEEP (无限等待) 或 POLL (有超时)                           │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  ⭐ 调用 mach_msg() - 线程在此阻塞 ⭐                          │
│                                                             │
│  等待事件:                                                  │
│  • Timer 端口有消息                                          │
│  • Source1 端口有消息                                        │
│  • dispatch 端口有消息                                       │
│  • _wakeUpPort 有消息                                        │
│  • 超时                                                      │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  被唤醒,检查返回值                                            │
└─────────────────────────────────────────────────────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │ SUCCESS │    │ TIMED_OUT│    │  TOO_LARGE   │
    └─────────┘    └──────────┘    └──────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │处理voucher│   │ 清理缓冲  │    │ 扩大缓冲区    │
    │返回true  │   │返回false  │     │ 重新接收     │
    └─────────┘    └──────────┘    └──────────────┘
                                          │
                                          ▼
                                    ┌──────────┐
                                    │返回循环开始│
                                    └──────────┘

关键点说明

1. mach_msg 的两种模式

模式 timeout 值 行为
休眠模式 TIMEOUT_INFINITY 永久阻塞,直到收到消息
轮询模式 0 或小值 立即返回或短暂等待

2. 可能的返回值

返回值 说明 处理方式
MACH_MSG_SUCCESS 成功收到消息 返回 true
MACH_RCV_TIMED_OUT 超时 返回 false
MACH_RCV_TOO_LARGE 消息太大 扩大缓冲区重试
其他错误 致命错误 崩溃(HALT)

3. voucher 的作用

voucher 是 macOS 的性能追踪机制:
├── 追踪消息来源
├── 记录 QoS(服务质量)
├── 性能分析(Instruments)
└── 调试辅助

4. 缓冲区管理

初始: 使用栈缓冲区(3KB)
  ↓
mach_msg 返回 TOO_LARGE
  ↓
计算实际大小: msg->msgh_size + MAX_TRAILER_SIZE
  ↓
动态分配堆缓冲区
  ↓
重新调用 mach_msg
  ↓
成功接收大消息

总结

__CFRunLoopServiceMachPort 是 RunLoop 休眠的核心

  1. 准备消息头:设置接收端口和缓冲区
  2. 调用 mach_msg:线程阻塞等待 ⭐
  3. 被唤醒:检查返回值和 livePort
  4. 处理特殊情况:超时、消息过大、错误
  5. 返回结果:告诉调用者是哪个端口唤醒的

这就是 RunLoop "无事件时不消耗 CPU" 的秘密!

mach_msg_header_t详解

借助AI能力分析。

mach_msg_header_t - Mach 消息头

作用

这是 Mach 消息的头部结构,用于在 macOS/iOS 的进程间(或线程间)传递消息。

6个字段详解

typedef struct {
    mach_msg_bits_t      msgh_bits;         // 消息标志位
    mach_msg_size_t      msgh_size;         // 消息总大小(字节)
    mach_port_t          msgh_remote_port;  // 目标端口(收信人)
    mach_port_t          msgh_local_port;   // 本地端口(回信地址)
    mach_port_name_t     msgh_voucher_port; // 追踪端口(调试用)
    mach_msg_id_t        msgh_id;           // 消息ID(自定义)
} mach_msg_header_t;

形象比喻(信封):

字段 对应信封上的 说明
msgh_remote_port 收件人地址 消息发往哪个端口
msgh_local_port 回信地址 如果需要回复,发到这里
msgh_size 信件大小 包括信封和内容
msgh_bits 邮寄方式 挂号信、平信等
msgh_id 信件编号 用于区分不同类型的信
msgh_voucher_port 追踪单号 用于追踪和调试

在 RunLoop 中的使用

1. 发送唤醒消息(CFRunLoopWakeUp)

// 构造消息头
mach_msg_header_t header;
header.msgh_remote_port = rl->_wakeUpPort;  // 发往唤醒端口
header.msgh_local_port = MACH_PORT_NULL;    // 不需要回复
header.msgh_size = sizeof(mach_msg_header_t); // 只有头,无内容
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_id = 0;

// 发送(唤醒 RunLoop)
mach_msg(&header, MACH_SEND_MSG, ...);

2. 接收消息(RunLoop 休眠)

// 准备缓冲区
uint8_t buffer[3 * 1024];
mach_msg_header_t *msg = (mach_msg_header_t *)buffer;

msg->msgh_local_port = waitSet;  // 在哪个端口等待
msg->msgh_size = sizeof(buffer);  // 缓冲区大小

// 阻塞等待(线程休眠)
mach_msg(msg, MACH_RCV_MSG, ...);

// 被唤醒后,检查消息来源
if (msg->msgh_local_port == _wakeUpPort) {
    // 手动唤醒
} else if (msg->msgh_local_port == _timerPort) {
    // 定时器到期
}

关键理解

mach_msg_header_t 是 Mach IPC 的核心

  1. 通信基础:所有 Mach 消息都以这个头开始
  2. 路由信息:指明消息的来源和去向
  3. RunLoop 休眠/唤醒:通过接收/发送消息实现

完整消息结构

┌──────────────────────┐
│ mach_msg_header_t    │ ← 消息头(必需)
├──────────────────────┤
│ 消息体(可选)        │ ← 实际数据
├──────────────────────┤
│ trailer(可选)       │ ← 附加信息
└──────────────────────┘

RunLoop 的简化消息:只有头部,无消息体(称为 "trivial message"),足以唤醒线程。

Flutter 最新xyz

一、Dart 语言基础xyz(15题) 1. Dart 是值传递还是引用传递? 答案: 类型 传递方式 示例 基本类型(int、double、bool、String) 值传递 修改不影响原值 对象和集
❌