阅读视图

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

Fishhook原理深度剖析:从Mach-O到运行时函数Hook

前言

在iOS/macOS开发中,函数Hook是一项强大的技术,它允许我们在运行时修改函数行为,广泛应用于调试、监控、性能分析等领域。Fishhook作为Facebook开源的一个轻量级Hook框架,其设计精巧、实现简洁,是理解macOS/iOS系统动态链接机制的绝佳案例。本文将深入剖析Fishhook的工作原理,解答一系列核心问题。

一、Fishhook是什么?

Fishhook是一个基于动态链接特性的函数Hook库,专门针对macOS和iOS平台设计。它的核心思想是通过修改Mach-O文件中的符号绑定表来实现C函数的Hook。

核心特点:

  • 轻量级,仅几百行代码
  • 专注于C函数Hook
  • 基于合法的dyld API
  • 主要用于调试和监控场景

二、核心问题:Fishhook为什么只能Hook动态链接函数?

1. Mach-O文件结构回顾

要理解Fishhook的限制,首先要了解Mach-O文件结构:

Mach-O Header
Load Commands
    - LC_SEGMENT_64
    - LC_SYMTAB (符号表)
    - LC_DYSYMTAB (动态符号表)
    - LC_DYLD_INFO (绑定信息)
Data
    - __TEXT (代码段,只读)
    - __DATA (数据段,可写)
        - __la_symbol_ptr (惰性绑定指针)
        - __nl_symbol_ptr (非惰性绑定指针)
        - __got (全局偏移表)
    - __LINKEDIT (链接信息)

2. 动态链接 vs 静态链接

// 静态链接函数(无法Hook)
static void internal_function() {
    // 编译时直接嵌入二进制
    // 调用方式:call 0x100000f00 (直接地址)
}

// 动态链接函数(可以Hook)
void printf(const char *format, ...) {
    // 来自libSystem.dylib
    // 调用方式:call [__la_symbol_ptr + offset] (间接寻址)
}

关键区别: 动态链接函数通过符号指针表间接调用,Fishhook正是通过修改这个指针表中的地址来实现Hook。

3. 为什么只能Hook系统库函数?

这是一个常见的误解!Fishhook不仅可以Hook系统库函数,也可以Hook自定义动态库函数。

// 自定义动态库中的函数也可以Hook
__attribute__((visibility("default")))
void my_dynamic_function() {
    // 这个函数可以被Fishhook Hook
}

// 只要满足以下条件:
// 1. 函数来自动态库(不是静态链接)
// 2. 函数具有外部可见性(不是static)
// 3. 函数通过符号表引用

三、符号绑定机制深度解析

1. 惰性绑定(Lazy Binding) vs 非惰性绑定

惰性绑定(__la_symbol_ptr)

; 第一次调用printf时的流程
call printf    ; 实际调用桩代码

; __stubs中的桩代码
___stub_printf:
    jmp *___la_symbol_ptr[0]   ; 第一次跳转到dyld_stub_binder

; 绑定后的状态
___la_symbol_ptr[0] = 0x7fff12345678 (真实printf地址)

非惰性绑定(__nl_symbol_ptr)

// 程序启动时立即绑定的函数
// 现代iOS/macOS中,__nl_symbol_ptr已较少使用
// 取而代之的是__got(全局偏移表)

// 使用__got的示例
void *function_ptr __attribute__((section("__DATA,__got"))) = &some_function;

现代绑定机制变化

在现代 iOS/macOS 中,传统的 __nl_symbol_ptr / __la_symbol_ptr 的角色已被弱化, 编译器和 dyld 更多通过 GOT-like 的间接寻址方式以及 chained fixups 机制来完成符号绑定;因此使用时需要注意是否能正确实现hook.

# 传统方式的缺点
# 1. 重定位表可能很大
# 2. 加载时需要大量随机内存访问
# 3. 不支持新硬件特性
# 4. 安全保护有限

# Chained Fixups 的优势
# 1. 更小的二进制大小(减少30-50%重定位数据)
# 2. 更快的启动速度
# 3. 支持指针认证(PAC)
# 4. 更好的安全性和性能

// 寻址方式对比
; 传统方式(x86_64)
; 通过 __la_symbol_ptr 调用 printf
lea     rdi, [rip + format_string]
call    qword ptr [rip + ___la_symbol_ptr_printf]

; __la_symbol_ptr 节区内容:
___la_symbol_ptr_printf:
    .quad   ___dyld_stub_binder_printf

; 现代方式(arm64)
; 通过 GOT-like 间接寻址
adrp    x0, _printf@GOTPAGE     ; 获取 GOT 页
ldr     x1, [x0, _printf@GOTPAGEOFF] ; 从 GOT 加载地址
blr     x1                      ; 间接跳转

; 实际上,编译器可能生成更优化的代码
; 直接使用 PC 相对寻址,不需要显式的 GOT 节区

2. dyld_stub_binder的工作原理

当第一次调用动态链接函数时,系统如何查找真实地址?

// dyld_stub_binder的简化流程
void* dyld_stub_binder(uint32_t lazy_binding_info_offset) {
    // 1. 从__LINKEDIT获取绑定信息
    LazyBindingInfo *info = get_binding_info(offset);
    
    // 2. 提取符号名和库序号
    const char *symbol_name = info->symbol_name;
    int library_ordinal = info->library_ordinal;
    
    // 3. 在指定动态库中查找符号
    void *address = find_symbol(symbol_name, library_ordinal);
    
    // 4. 修改符号指针表
    void **symbol_ptr = info->symbol_pointer;
    *symbol_ptr = address;  // 关键步骤!
    
    // 5. 跳转到目标函数
    return address;
}

3. ASLR(地址空间布局随机化)的影响

ASLR是现代操作系统的安全特性,每次启动时系统模块的加载地址都随机变化:

// 没有ASLR:
// printf地址固定为0x7fff12345678

// 有ASLR:
// 第一次启动:printf地址为0x7fff12345678
// 第二次启动:printf地址为0x7fff56789abc
// 第三次启动:printf地址为0x7fff9abcdef0

// Mach-O通过位置无关代码支持ASLR:
// 所有外部引用都通过指针表间接访问
// 指针表地址在运行时由dyld填充

四、Fishhook的核心实现原理

1. 核心数据结构:rebinding

struct rebinding {
    const char *name;      // 要Hook的函数名
    void *replacement;     // 替换函数的地址
    void **replaced;       // 保存原始函数指针
};

2. Fishhook的工作流程

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    // 1. 遍历所有镜像(动态库)
    for (int i = 0; i < _dyld_image_count(); i++) {
        const struct mach_header *header = _dyld_get_image_header(i);
        
        // 2. 查找__DATA段中的符号指针表
        struct segment_command *seg = find_segment(header, "__DATA");
        if (!seg) continue;
        
        // 3. 查找__la_symbol_ptr和__got节
        struct section *la_section = find_section(seg, "__la_symbol_ptr");
        struct section *got_section = find_section(seg, "__got");
        
        // 4. 处理每个符号指针
        perform_rebinding_with_section(la_section, rebindings, rebindings_nel);
        perform_rebinding_with_section(got_section, rebindings, rebindings_nel);
    }
    
    return 0;
}

3. 关键函数:查找和替换符号

static void perform_rebinding_with_section(struct section *section,
                                          struct rebinding rebindings[],
                                          size_t rebindings_nel) {
    // 1. 获取间接符号表
    uint32_t *indirect_symbol_indices = indirect_symbol_table + section->reserved1;
    
    // 2. 遍历符号指针表中的每个条目
    for (uint32_t i = 0; i < section->size / sizeof(void*); i++) {
        uint32_t symtab_index = indirect_symbol_indices[i];
        
        // 3. 在动态符号表中查找符号信息
        struct nlist_64 *symbol = &symtab[symtab_index];
        uint32_t strtab_offset = symbol->n_un.n_strx;
        const char *symbol_name = strtab + strtab_offset;
        
        // 4. 检查是否是需要Hook的函数
        for (size_t j = 0; j < rebindings_nel; j++) {
            if (strcmp(symbol_name, rebindings[j].name) == 0) {
                // 5. 找到目标!进行Hook
                void **symbol_ptr = (void**)(section->addr + i * sizeof(void*));
                
                // 保存原始函数指针
                if (rebindings[j].replaced != NULL) {
                    *rebindings[j].replaced = *symbol_ptr;
                }
                
                // 替换为新的函数地址
                *symbol_ptr = rebindings[j].replacement;
                
                break;
            }
        }
    }
}

五、实际应用场景和限制

1. 支持的函数类型

函数类型 是否支持 说明
C函数(动态库) ✅ 完全支持 Fishhook的主要目标
Objective-C方法 ❌ 不支持 使用Method Swizzling
Swift函数(@_cdecl) ✅ 条件支持 需要C接口导出
静态链接函数 ❌ 不支持 编译时直接链接
内联函数 ❌ 不支持 编译时展开

2. iOS上的特殊限制

// 在非越狱iOS设备上:
// ✅ 可以Hook自己的函数和系统公开API
// ❌ 不能Hook系统私有函数(受代码签名保护)

// 在越狱iOS设备上:
// ✅ 可以Hook所有函数

// 验证方法:
#include <fishhook.h>
#include <stdio.h>

// 可以Hook的系统公开函数
static int (*original_printf)(const char *, ...);

int hooked_printf(const char *format, ...) {
    // Hook实现
    return original_printf("[HOOKED] %s", format);
}

__attribute__((constructor))
static void setup_hook() {
    struct rebinding rebind = {"printf", hooked_printf, (void**)&original_printf};
    rebind_symbols(&rebind, 1);
}

3. 自定义动态库的Hook

// MyDynamicFramework.framework
__attribute__((visibility("default")))
void framework_function() {
    NSLog(@"Original framework function");
}

// 主工程中的Hook
static void (*original_framework_func)() = NULL;

void hooked_framework_func() {
    NSLog(@"Before framework function");
    original_framework_func();
    NSLog(@"After framework function");
}

// 安装Hook
struct rebinding rebind = {
    "framework_function",
    hooked_framework_func,
    (void**)&original_framework_func
};
rebind_symbols(&rebind, 1);

六、与Method Swizzling的对比

1. Fishhook(C函数Hook)

// 适用于C函数
void hook_c_function() {
    struct rebinding rebind = {"printf", hooked_printf, &original_printf};
    rebind_symbols(&rebind, 1);
}

2. Method Swizzling(Objective-C方法Hook)

// 适用于Objective-C方法
+ (void)hookObjectiveCMethod {
    Method original = class_getInstanceMethod([UIView class], @selector(setBackgroundColor:));
    Method swizzled = class_getInstanceMethod([self class], @selector(hooked_setBackgroundColor:));
    method_exchangeImplementations(original, swizzled);
}

3. 选择指南

场景 推荐技术
Hook C系统函数 Fishhook
Hook C自定义函数 Fishhook
Hook Objective-C方法 Method Swizzling
Hook Swift方法 Method Swizzling(通过@objc暴露)
底层系统监控 Fishhook

七、实战案例

1. 性能监控

// 监控内存分配
static void *(*original_malloc)(size_t);
static void (*original_free)(void *);

void *hooked_malloc(size_t size) {
    void *ptr = original_malloc(size);
    
    // 记录大内存分配
    if (size > 1024 * 1024) {
        printf("[MEMORY] Large malloc: %zu bytes\n", size);
    }
    
    return ptr;
}

void hooked_free(void *ptr) {
    // 可以在这里添加内存释放监控
    original_free(ptr);
}

八、常见问题解答

Q1: Fishhook能Hook静态库函数吗?

A: 取决于静态库的链接方式:

  • 如果静态库被完全复制到动态库中,可以Hook
  • 如果静态库保持外部引用,需要在链接静态库的地方Hook
  • 静态库的私有函数(static)无法Hook

Q2: 为什么OC和Swift方法不能用Fishhook Hook?

A: Objective-C和Swift的方法调用机制完全不同:

  • Objective-C使用消息发送机制(objc_msgSend)
  • Swift有自己的分发机制(虚函数表、直接调用等)
  • 它们不通过符号绑定表调用,因此Fishhook无法修改

Q3: Hook的时机是什么?

A: Fishhook可以在运行时任何时间点进行Hook,但最佳实践是:

// 1. 程序启动时(推荐)
__attribute__((constructor))
static void setup_hooks() {
    // 在main()函数之前执行
}

// 2. 运行时动态Hook
void dynamic_hook() {
    // 可以在需要时动态安装或移除Hook
}

// 3. 使用dispatch_once保证只Hook一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 安装Hook
});

Q4: 在MachOView中为什么看不到__nl_symbol_ptr?

A: 在现代iOS/macOS系统中:

  • __nl_symbol_ptr已被__got(全局偏移表)替代
  • 所有需要立即绑定的符号现在都放在__got
  • 这是为了更好的位置无关代码支持和性能优化

九、Fishhook的局限性

1. 技术限制

  • 只能Hook通过符号表调用的函数
  • 无法Hook内联函数和静态链接函数
  • 对C++函数支持有限(名称重整问题)
  • 线程安全问题需要开发者自己处理

2. 平台限制

  • iOS非越狱设备受限较多
  • 某些系统函数受代码签名保护
  • 不同iOS版本可能有行为差异

3. 性能考虑

  • 第一次Hook需要遍历符号表,有性能开销
  • 频繁的动态Hook/Unhook可能影响性能
  • 需要考虑多线程环境下的安全性

十、未来展望

1. 替代方案

// 1. Dobby(原DobbyHook)
// 支持inline hook,更强大
#include <dobby.h>
DobbyHook((void *)printf, (void *)hooked_printf, (void **)&original_printf);

// 2. substrate(越狱设备)
// iOS越狱环境下的完整Hook框架
MSHookFunction((void *)printf, (void *)hooked_printf, (void **)&original_printf);

2. 发展趋势

  • Apple正在加强系统安全性
  • 静态分析和运行时保护机制增强
  • 对动态代码修改的限制可能增加
  • 开发者需要更多关注官方提供的调试和监控API

结语

Fishhook通过精巧地利用Mach-O文件的动态链接机制,实现了轻量级的函数Hook功能。虽然它有一定的局限性,但仍然是理解macOS/iOS系统底层机制的绝佳案例,也是许多调试和监控工具的基础。

通过本文的详细解析,我们希望读者能够:

  1. 深入理解Mach-O文件结构和动态链接机制
  2. 掌握Fishhook的工作原理和使用方法
  3. 了解不同Hook技术的适用场景
  4. 在实际开发中正确应用Hook技术

记住,强大的工具也意味着重大的责任。Hook技术应该用于正当的调试、监控和优化目的,而不是破坏系统安全或侵犯用户隐私。


参考资源:

iOS网络层工程范式迁移

引言

生活并非直线前进,而是在一次又一次的循环中向前。随着项目的更迭与技术栈的变化, 又一次站在了这个熟悉的网络层设计问题前——这已经是第几次,我大抵也记不太清了。有趣的是,问题几乎未曾改变,但在不同的技术背景与开发阶段下,循环的终点却彼此并不矛盾。为什么网络层总在被重构? 我反复更换的,究竟是网络请求库,还是对“网络层”这一概念本身的理解?当开发生产力不断提高,网络层的意义,是否也在随之发生变化?

回答这些问题,或许需要暂时抽离当下的技术语境,逆拨时间的指针,回到网络层第一次成为“工程问题”的年代。

网络层的第一次工程化

在 iOS 开发的早期,我们并没有一套足够简洁且稳定的 API 来完成一次 HTTP 请求。开发者需要直接面对 NSURLConnection 及其 delegate 回调,手动处理线程切换、状态维护、错误分支与生命周期管理。网络请求往往散落在各个业务代码中,高度依赖个人经验以及项目本身的成熟程度。

也正是在这一阶段,网络层开始显露出最初的工程问题: 如何将网络请求从零散的实现中抽离出来,使其具备一定的模块化与可复用性。

秩序的建立

AFNetworking 并不是第一个网络封装,但它是第一个被大规模接受为工程基础设施的解决方案。它所做的事情,在今天看来并不复杂:

  • 将零散的 delegate 回调收敛为 block;
  • 将请求调度交由 operation queue 统一管理;
  • 提供一致的序列化、状态与网络可达性支持。

AFNetworking 并没有试图重新定义网络的抽象边界,它解决的是一个更朴素的问题:如何统一 HTTP 请求的工程流程。从这个角度看,它的历史地位并不来自于 API 的优雅,而在于完成了一件更基础的事情——让网络请求第一次脱离“个人技巧”,进入“团队工程”的范畴。

当网络请求不再是一种高风险操作,而成为可以被稳定复用的工程能力之后,新的问题也随之浮现: 在秩序建立之后,网络层是否还需要继续演进?

语言变了,其他什么都没变

随着 Swift 的出现,iOS 开发迎来了第一次明显的语言层级跃迁。更安全的类型系统、更清晰的控制流,以及对错误处理与并发模型的重新思考,都让开发者对“代码结构”本身产生了新的期待。然而,在 iOS 的发展历程中,语言层面的变化往往具有明显的滞后性。

在相当长的一段时间里,Swift 项目仍然沿用着 AFNetworking 所建立的工程范式:请求依然被视作一次次独立的操作,生命周期、错误分支与调度逻辑分散在各处。Alamofire 也正是在这样的背景下出现的——在它的早期阶段,它所承担的角色并不是重新定义网络层,而是提供一个 Swift 版本的 AFNetworking

这并非缺乏野心,而是现实所迫。Swift 在早期版本中经历了频繁而剧烈的 API 变动,从语言特性到标准库,再到与 Foundation 的桥接关系,都在不断重写之中。这种不稳定性,对于试图迁移或学习新语言的开发者而言,无异于一盆又一盆的冷水。

也正因如此,在那个 Swift 生态尚未成熟的阶段,稳定性本身,才是最重要的工程价值

范式二次迁移

随着 Swift 生态逐渐稳定,网络层的关注点也开始发生转移:它不再只是关心“是否能够稳定工作”,而是进一步走向“流程是否足够清晰、是否具备工程可控性”。Alamofire 正是在这一时期不断演进,并主动吸收了来自其他语言与平台的工程经验。它逐渐不再只是一个请求封装库,而是开始将一次网络请求视作一个具有明确生命周期的工程流程。

SessionRequestInterceptorRetrier 等概念的引入,使得请求在进入与离开网络层时,拥有了可插拔、可观察、可扩展的处理节点。网络请求不再是一次孤立的调用,而成为一条可以被组合与调度的处理管线。也正是在这一阶段,Alamofire 从一套工程工具,演变为 iOS 网络开发中的事实标准。当人们谈论网络层时,几乎不需要再解释背景——Alamofire 本身,已经成为了那个时代 iOS 网络工程的默认前提。

再次抽象

随着 RxSwift、Combine 等声明式与函数式思想逐渐在 iOS 生态中流行,开发者开始尝试以更“描述性”的方式来组织代码结构。相比命令式地拼装请求参数,人们更希望先回答一个问题:系统中究竟存在哪些 API,它们的形态是什么?

正是在这样的背景下,Moya 在已经趋于完善的网络层实现之上,引入了一套基于 Swift 类型系统的 DSL(Domain-Specific Language)。通过 enum、case 与 protocol 的组合,Moya 尝试将网络接口本身建模为一种结构化、可推导的描述,而不再仅仅是零散的请求构造逻辑。

这种 DSL 的价值,并不在于提升网络请求的执行能力——这部分早已由 Alamofire 等底层实现所解决——而在于为接口组织、Mock 与测试提供了一种更具约束性的表达方式。网络层之上,第一次出现了“以接口为中心”的组织视角。

然而,抽象的上移并不意味着复杂度的消解。相反,当流程、接口与描述层不断叠加,网络层开始承载越来越多本不属于它的概念与责任。这种堆叠式的演进,也悄然将网络层推向了一个临界点——下一次变化,或许不再来自抽象本身。

控制权回归

真正的分水岭,并不是某一个网络库的发布,而是语言本身发生了改变。

随着 Swift Concurrency 的引入,异步执行、取消、错误传播与生命周期管理被统一纳入语言语义之中。在 Swift Concurrency 之前,即使不依赖任何第三方库,一个最基础的网络请求,也需要显式处理回调、线程、错误、生命周期等问题:

let request = URLRequest(url: url)

URLSession.shared.dataTask(with: request) { data, response, error in
    if let error = error {
        DispatchQueue.main.async {
            self.state = .error(error)
        }
        return
    }

    guard
        let data = data,
        let response = response as? HTTPURLResponse,
        200..<300 ~= response.statusCode
    else {
        DispatchQueue.main.async {
            self.state = .error(NetworkError.invalidResponse)
        }
        return
    }

    do {
        let user = try JSONDecoder().decode(User.self, from: data)
        DispatchQueue.main.async {
            self.state = .loaded(user)
        }
    } catch {
        DispatchQueue.main.async {
            self.state = .error(error)
        }
    }
}.resume()

Swift Concurrency 语境下,同样的请求可以被表达为

let request = URLRequest(url: url)

do {
    let (data, response) = try await URLSession.shared.data(for: request)

    guard
        let response = response as? HTTPURLResponse,
        200..<300 ~= response.statusCode
    else {
        throw NetworkError.invalidResponse
    }

    let user = try JSONDecoder().decode(User.self, from: data)
    state = .loaded(user)
} catch {
    state = .error(error)
}

从表面上看,变化只是几十行代码变成了几行。但真正发生转变的是:

  • 执行顺序由语言控制,而非回调嵌套
  • 错误传播通过 throw 统一语义
  • 取消由 Task 作为语言级能力管理
  • 生命周期首次成为编译器可理解的结构

旧世界的终章

面对这一轮范式转变,Alamofire 并未停滞不前。相反,它持续尝试适配新的语言能力,引入 async/await 接口,与 Swift Concurrency 接轨。然而,作为一个诞生于前并发时代的工程体系,Alamofire 不可避免地背负着历史结构的惯性:其核心模型并非以 Sendable 为前提构建,内部仍存在大量可变状态;部分 API 在新的并发语境下显得不够“语言化”,更像是一层面向过渡期的兼容包装。这并非能力不足,而是时代断层所留下的工程痕迹。值得注意的是,Alamofire 并未因此止步。只要它仍在持续演进,仍在尝试靠近语言本身的表达边界,那么它的故事就尚未结束。甚至可以设想,在未来某一个关键版本节点,当工程结构真正与语言范式完成对齐,也许 6.x 之后的 Alamofire,仍存在“王者归来”的可能。

相比之下,Moya 则呈现出另一种截然不同的轨迹。它的核心价值建立在一套以 Endpoint 为中心的 DSL 之上。然而,随着 Swift 5 之后类型系统、协议与泛型能力的成熟,以及 Swift Concurrency 的到来,这套 DSL 却并未随语言一同演化。长期缺乏对 async/await 的原生支持,使得 Moya 在新的并发模型下逐渐失去立足点;而对既有 DSL 结构的高度依赖,也限制了它吸收语言层新能力的空间。当抽象停止生长,它便不再是助力,而开始成为负担。时至今日,Moya 已逐渐淡出主流讨论视野,这并非因为它曾经的设计不够优秀,而是因为它未能继续回应时代的变化。

这也引出了一个并不新,却常被忽略的事实:

再优秀的库,如果缺乏持续维护与演进,都无法抵御语言与时代本身的变化。

于是,一个新的问题被摆在面前:在语言接管流程之后,网络层还应该承担什么?

重新思考

当网络请求的传输与调度被语言层大幅简化之后,人们开始重新审视网络层本身的价值与边界。关注点不再是如何完整地接管请求生命周期,而是转向更基础的问题:如何让网络能力足够可组装、可测试、可替换,以及可观测。在这一语境下,网络层反而开始变“薄”——不再试图包揽流程,而是清晰地暴露能力边界。

从这个角度看,Moya 所代表的 API 描述 DSL 思路本身并未过时。过时的并不是“用 DSL 描述接口”这件事,而是围绕这一思路所构建的 API 形态,未能随着语言能力的演进而持续调整。如果回到 Moya 的源码与早期讨论中,会发现一个颇具时代感的事实:Moya 从一开始,便将自己定义为 Alamofire 的二次封装。这一定位在当时是合理且务实的——它使得 Moya 能够迅速建立在成熟执行层之上,专注解决接口组织的问题。

但也正是这一自我定位,在无形中为它设定了边界。当语言开始原生承担异步流程与生命周期管理之后,一个以“封装某个执行框架”为前提的 DSL,便很难继续向语言层靠近。这并非设计失误,而是时代变化所带来的结构性结果。许多早期的讨论与 issue,已经敏锐地意识到了这一张力,只是在当时的语境下,尚不足以推动一次彻底的转向。更别说之后的 Swift Concurrency 了。

当网络层变薄之后

当异步流程、错误传播与生命周期管理被语言原生承担之后,网络层本身所剩下的事情,其实变得异常简单。

在今天的语境下,一次网络请求不再需要被层层包装,也不需要被过度建模。它可以被拆解为几个极其清晰、彼此解耦的步骤:

  • 通过某种协议定义,构建一个 URLRequest
  • 通过某种传输机制(Transport)发出请求
  • 接收响应数据或者错误
  • 将结果解码为某种业务所需的模型

这里并不存在一个“中心调度者”,也不存在隐式的流程控制。每一步都只是一个普通的函数调用,每一个内容都一被配置或者可插拔,每一个节点都拥有明确的输入与输出,并且都可以自然地抛出错误。

仅此而已,这就足够了。

但到这里, 许多读者估计会感到非常一位, 聊了这么久, 最后的结论就仅仅是这个? 对的, 结果就是这个, 有些简单、有些朴素。 但这个就是当前语境的最优解, 也是你回望过去得到的最优解。你可以使用任何形式的 API 来构建 URLRequest:无论是链式调用,还是拦截器式的配置;你也可以选择官方的 URLSession 作为传输实现,或者继续使用 Alamofire。这些选择本身并不重要,因为它们都是可替换的实现细节。

真正重要的,并不是你是否使用了某一个具体的网络库,而是你的系统是否允许它被替换。当网络层足够薄、足够中立时,底层实现的变化不再具有破坏性:它不会牵动业务结构,也不会迫使上层逻辑随之重写。在这样的前提下,讨论“是否还需要使用 Alamofire”,就不再是一场立场之争,而只是一次技术选择;系统也不会因为某个 API 的演进或退场,而整体失衡。

After Moya

在这条演进路径上,很难绕开 Moya。对我而言,它并不仅仅是一个网络库,更像是一次提前到来的启蒙。在刚入行的阶段,我第一次从网络层的视角,真正意识到“接口组织”“抽象边界”与“工程结构”这些概念的存在。那些一次又一次出现的架构图与层层抽象,并非晦涩,而是单纯地超出了当时的经验边界。

随着 Swift 语言范式的演变进化,Moya 所依赖的那套抽象语境不再能对的上实际语境。面对上百个的 issue 与尚未完整的Feature,这些内容已并非一两次重构所能解决的问题。也许在这个时代,之后的世界没有承载Moya的船只了。

伤感虽伤感, 但是日子总归都得向前, 项目中依然需要适应全新时代语言的网络层架构, 我开始重新审视那些曾经影响过自己的设计内容, 重新放回到当下的语言与工程语境之中。在这样的背景下,我为新的尝试取了一个名字——Moira。 它并非为了替代什么,而只是希望将那些曾经成立、并仍然有价值的思想,继续传递下去。

又一次的循环

回到最初的问题,为什么网络层总在被重构?

或许所谓的“重构”,从来不是推翻,而是一次次回到原点后的再出发。

在语言不断演进、经验不断累积的过程中,我们反复校准网络层的责任边界,也在反复确认:什么值得被保留,什么应该被交还。

于是循环继续向前——

每一次回望,都让设计变得更简单一些;

每一次重写,都是让设计更贴近当下的语境。

也许我们此刻所做的事情,

与几十年前那些在车库里反复试错的人,并无本质上的不同。

从 Anthropic 封杀与苹果谷歌结盟,看 AI 护城河的构建 -- 肘子的 Swift 周报 #119

issue119.webp

从 Anthropic 封杀与苹果谷歌结盟,看 AI 护城河的构建

2026 年伊始,AI 界便迎来了两记重磅消息。

首先是 Anthropic 痛下杀手,禁止第三方命令行工具(CLI)和“套壳”软件调用 Claude 的个人订阅账号(Pro/Max)。官方意图非常明显:堵住利用低价订阅“薅”高价 API 羊毛的漏洞,并将这种高效的编码体验收归为官方工具 Claude Code 的专属护城河。

紧接着,苹果正式确认与谷歌达成深度合作。Gemini 将成为 Siri 及 Apple Intelligence 后端推理、逻辑处理的主力基石。这意味着,在苹果的 AI 版图中,谷歌凭借其在云计算和 TPU 算力上的深厚积累成为了“基础设施”,而 OpenAI 的角色则被重新定位为可选的“高级插件”。

这两个看似独立的新闻,揭示了 2026 年 AI 行业的一个显著趋势:随着大模型能力的边际效应递减,单纯的“智力”已不再是唯一的壁垒,各家公司正在疯狂构建属于自己的差异化护城河。

Anthropic 选择了“垂直整合”,通过强绑定 Claude Code 与订阅计划,将模型能力固化为不可替代的工作流体验;而谷歌则赢在了“规模效应”,凭借 TPU 集群带来的极致成本控制和稳定性,拿下了苹果这张通往十亿用户的船票。

2025 年,我们还在为“哪个模型更聪明”而频繁切换订阅,各家服务虽各有千秋,但可替代性强。进入 2026 年,这种“同质化竞争”的窗口期已临近关闭。

在烧钱如流水的 AI 赛道,留给“不错但平庸”的中间派的时间已经不多了。无论你是拥有极致的成本优势(如 Google),还是拥有独占的杀手级工作流(如 Anthropic),你必须具备一个竞争对手无法轻易复制的“特质”。

我目前仍有 4 个不同的 AI 服务订阅,我相信不用到年底,留 2 个订阅或许就能满足我的全部需求。

2026 年的主轴将不再是跑分榜上的数字游戏,而是通过价格壁垒或极致体验,提供切实打动 B 端与 C 端的生存理由。

本期内容 | 前一期内容 | 全部周报列表

🚀 《肘子的 Swift 周报》

每周为你精选最值得关注的 Swift、SwiftUI 技术动态

近期推荐

AI 速度快,你的构建也应如此

通过构建缓存、选择性测试以及可操作的洞察,让你的构建、测试运行和 CI 工作流飞速运转,让你的 AI 代理获得更快的反馈和更可靠的结果。

立即体验 →


在 tvOS 上活下來:一個非典型播放器的工程實錄

在苹果庞大的硬件生态中,Apple TV 往往处于一个略显尴尬的位置:它在客厅场景中拥有稳定且忠实的用户群,却始终不是多数开发者的优先目标,投入与回报的不确定性也让不少人望而却步。Ronnie Wong 以一款非典型播放器为切入点,回顾了她在 tvOS 开发过程中的真实经历——从平台限制、交互差异,到工程取舍与妥协。这篇文章这篇文章没有试图粉饰 tvOS 开发的痛点,而是如实呈现了在这一“小众但独特”的平台上,开发者如何在现实条件中做出理性选择并“活下来”。


@_exported import vs. public import

Swift 6 引入了一个并不显眼、却影响深远的新特性:Access-level modifiers on import declarations。该提案要求开发者在使用 import 时显式声明访问级别,以减少依赖无意外泄、避免模块接口被“悄然污染”。在本文中,Alexander Weiß 系统讲解了这一机制的设计初衷与使用注意事项,并重点对比了 public import@_exported import 在模块接口暴露、依赖传递以及 API 设计层面的实际差异,解释了为什么前者往往并不能实现开发者直觉中的“重导出”效果。这对于正在构建 SDK、Swift Package,或希望明确模块边界的开发者来说,具有很强的现实指导意义。

我在读完这篇文章后,已经在当前项目以及部分第三方库中启用了 InternalImportsByDefault,并立刻迎来了一批编译错误,多少重温了刚接触严格并发时的体验。不过,相比并发相关的问题,这类错误通常更直观、也更容易修复。建议你尽早开启这一设置,以避免在未来官方默认启用时,集中面对更大规模的迁移成本。


Universal Links at Scale:那些鲜有人提及的规模化挑战 (Universal Links At Scale: The Challenges Nobody Talks About)

Universal Links 的原理和实现本身并不复杂,也因此常常给人一种“简单而优雅”的错觉。但一旦进入大规模应用、多个域名、复杂路由和长期演进的真实场景,其复杂度会迅速显现。在本文中,Alberto De Bortoli 结合自身在大型项目中的实践经验,系统梳理了 Universal Links 在规模化使用时常被忽视的问题:AASA 文件的维护成本、缓存与生效时机的不确定性、多环境与多团队协作下的风险,以及调试与回滚几乎没有“安全网”的现实困境。Alberto 提醒开发者在设计深度链接方案时,必须提前为“规模效应”做好准备。


当模块化遇到资源重复问题 (Swift Modules and Code/Assets Duplication)

随着 Swift 模块化程度的不断加深,资源(CodeAssets)管理逐渐成为一个容易被忽视、却可能在规模化项目中引发实际问题的领域。Paulo Andrade 通过具体示例分析了 Swift 模块与 CodeAssets 在多模块依赖场景下产生重复打包与体积膨胀的问题,解释了其背后的构建与链接机制,并探讨了当前工具链下可行的规避策略。Paulo 提醒开发者:模块化并不仅仅是 API 与依赖关系的设计,资源的边界与归属同样需要被认真对待。


使用 SwiftUI 构建文档型应用 (Crafting document-based apps in SwiftUI)

文档型应用(Document-Based App)是一个既常见、却常被低估复杂度的应用形态。相比普通应用,其文件生命周期管理、权限与安全作用域,以及与系统文档浏览体验的协同方式,对不少开发者而言都较为陌生。 在本文中,Alfonso Tarallo 以 SwiftUI 为核心,系统梳理了构建文档型应用所需的关键组成部分,包括 FileDocumentDocumentGroup 的职责划分,以及 SwiftUI 如何与系统文件机制协同工作。


Swift 2026 生态现状 (State of Swift 2026)

这是一篇围绕 Swift 生态的深度观察与趋势研判。文章将 2025 年定义为 Swift 的“变革之年”,详细梳理了并发模型从“严格”向“平易近人(Approachable)”的演进路径,以及服务端(如 Apple 密码服务迁移案例)与 Android 开发的实质性突破。作者没有回避社区关于 SwiftUI 与 AppKit 的架构争议,而是客观分析了其中的机遇与挑战。相比于单纯的技术清单,本文更像是一份战略报告,帮助开发者在日常代码之外,看清 Swift 正从单一的“App 语言”向全场景安全语言蜕变的宏观图景。


和 ChaoCode 一起学 CS146S

CS146S 是斯坦福大学开设的一门前沿计算机科学课程,全名为 "CS146S: The Modern Software Developer"。该课程的核心理念在于教导学生利用最新的 AI 工具(如 LLMs、Cursor、Claude 等)将开发效率提升 10 倍,从单纯的“代码编写者”转变为“AI 智能体的指挥官”。

如果你对这门课程感兴趣,但面对大量的英文阅读材料和纯文本 Slides 感到枯燥或畏难,那么 ChaoCode (Jane) 刚刚开启的这个“伴读”系列可能正是你需要的。Jane 以“视觉学习者”的视角,通过视频形式记录并分享了这门课程的学习过程。她不仅将原始 Slides 整理成了繁体中文/英文双语版本,补充了原版缺失的讲者注释,还专门维护了一个 Notion 页面 来整合所有的学习资源与作业。这不是一个课程搬运,而是一个“Learning in Public”的最佳实践。

工具

Axiom:面向 iOS 开发的 Claude Code Agent 集合

Axiom 是由 Charles Wiltgen 维护的一套面向 iOS 开发者的 Claude Code Agents 与 Skills 知识体系。它将大量真实项目中反复出现的开发任务、排障流程与最佳实践,系统整理为可复用的 Skills、Agents 与 Commands,覆盖 Swift 6、SwiftUI、并发、数据持久化、调试、可访问性以及 Apple Intelligence 等多个方向。

Axiom 尝试回答另一个问题:如何把经验丰富的工程判断,系统化地交给 AI 助手执行。作者也鼓励使用者在 Reddit 的 axiomdev 板块 中交流实践经验与反馈。


BibTeXKit:类型安全的 BibTeX 解析库

BibTeX 文件是一种基于文本的文献数据库格式,广泛用于 LaTeX 文档体系中管理参考文献。通过维护独立的 .bib 文件,用户可以集中存储并复用文献的元数据,在排版阶段由工具自动完成引用与格式化。

Ezequiel Santos 开发的 BibTeXKit 是一个使用 Swift 编写的 BibTeX 解析与建模库,目标非常明确:为 Swift 项目提供一套类型安全、可扩展的 BibTeX 处理能力。它将 BibTeX 文件中的条目、字段与引用关系映射为清晰的 Swift 数据结构,避免了停留在字符串层面反复解析与手工处理所带来的复杂性,更适合作为文献管理类工具或研究型应用的底层组件。


MirageKit:Apple 平台高性能投屏与远程控制框架

MirageKit 是一个由 Ethan Lipnik 开发的 Apple 平台专用流媒体框架,旨在实现 macOS 画面向 iOS、visionOS 或另一台 Mac 的低延迟投屏与控制。

该框架充分利用了 Apple 最新的系统特性(需 iOS 26+ / macOS 26+),核心基于 ScreenCaptureKit 与 Metal 构建,利用 UDP 传输视频流并支持 AWDL 点对点连接,在保证画质(支持 1:1 虚拟显示器像素级渲染)的同时实现了极低的延迟。除了单纯的画面传输,MirageKit 还打通了输入反向控制,支持将客户端的键鼠、手势操作甚至原生菜单栏交互回传至主机。对于想要构建高性能远程桌面、副屏应用或 Vision Pro 生产力工具的开发者来说,这是一个非常有潜力的起步方案。

需要注意的是,MirageKit 目前仍处于 早期且活跃的开发阶段,API 可能会出现破坏性变更。推文 中展示的视频,是 Ethan 基于 MirageKit 构建、仍在开发中的应用演示。

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

拒绝“裸奔”!一款基于 SourceKit-LSP 的 Swift 代码混淆工具 (SwiftyShield)

拒绝“裸奔”!一款基于 SourceKit-LSP 的 Swift 代码混淆工具 (SwiftyShield)

前言:由 Swift 安全引发的思考

作为 iOS/macOS 开发者,我们往往专注于功能迭代和 UI 交互,却容易忽视发布的“最后一公里”——源码安全。虽然 Swift 是编译型语言,但其二进制文件中依然保留了大量的符号信息。通过 IDA Pro 或 Hopper 等逆向工具,攻击者可以轻易还原出类名、方法名甚至核心业务逻辑。

为了解决这个问题,我开发了 SwiftyShield —— 一款专为 macOS 和 iOS 开发者打造的专业级 Swift 代码混淆工具。

始于颜值,忠于体验:谁说开发工具必须“丑”?

在使用过市面上大量“灰头土脸”的命令行工具或基于 Java/Electron 简单套壳的混淆器后,我决定用 SwiftUI 为 SwiftyShield 打造一套原生、精致的 macOS 体验。

  • 精致的 UI 设计:完全遵循 Apple Human Interface Guidelines,支持完美的深色模式(Dark Mode),与你的 macOS 系统浑然一体。
  • 流畅的交互动画:从代码分析的进度条流转,到混淆完成后的成功动效,每一个交互细节都经过精心打磨。我们希望你在进行枯燥的“加固”工作时,也能感受到操作的愉悦。

核心原理:基于 Apple 官方工具链的深度解析

SwiftyShield 的核心不仅仅是好看,其技术路线基于 Apple 官方的 SourceKit-LSP 和 Xcode Toolchain 构建

这意味着它不是简单地用正则表达式(Regex)去“猜”代码,而是像 Xcode 一样真正“理解”你的代码语法树。它能智能识别哪些符号是公共 API、哪些是模块依赖,从而进行安全、精准的重命名。

下面展示两个最能体现 SwiftyShield 智能程度的核心场景。

场景 1:智能语义分析 —— 搞定“隐形”继承链

这是 SwiftyShield 最硬核的能力之一。

很多初级混淆工具是“文件隔离”的。如果你的控制器 A 遵循了 UITableViewDelegate 但没实现方法,而控制器 B 继承了 A 并实现了 didSelectRowAt,普通工具往往会误判。

因为在控制器 B 的定义中,看不到 UITableViewDelegate 的影子,普通工具会误以为 didSelectRowAt 是一个自定义函数,从而将其重命名,导致 TableView 点击失效

SwiftyShield 通过全项目 AST(抽象语法树)分析,能精准识别出这种“隔代继承”关系:

混淆前 (Before):

ProductListController 继承自 BaseListController,虽然它自己没写 Delegate 声明,但 SwiftyShield 知道它的父类遵循了协议。

Swift

// 文件 A:BaseListController.swift
class BaseListController: UIViewController, UITableViewDelegate {
    // 这里遵循了协议,但没有实现 didSelectRowAt
}

// 文件 B:ProductListController.swift
class ProductListController: BaseListController {
    // ⚠️ 挑战来了:普通工具只看到这是一个普通的函数,可能会误改名
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Item selected")
    }
}

混淆后 (After):

SwiftyShield 成功识别了继承链,保留了系统回调方法名,确保业务逻辑不崩坏。

Swift

// 文件 A
class InogenicMartyressIntroflexIliocaudal: UIViewController, UITableViewDelegate {
    // 类名已混淆
}

// 文件 B
class StrongylonCircumterraneousSemicolon: InogenicMartyressIntroflexIliocaudal {
    // ✅ 成功识别:
    // SwiftyShield 判定该方法属于 System Protocol Requirement,自动豁免混淆
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Item selected")
    }
}

场景 2:深度逻辑混淆 —— 连参数和局部变量也不放过

除了保护继承结构,SwiftyShield 还能深入函数内部。开启临时声明混淆后,连方法的参数标签、参数名以及函数内部的局部变量都会被重写。

混淆前:

参数名 password 和局部变量 combined 直接暴露了意图。

Swift

func generateHash(password: String, salt: String) -> String {
    let combined = password + salt
    return combined.md5()
}

混淆后:

代码变成“天书”,逆向者难以推断数据流向。

Swift

func anomalismSwornUnguentiferous(_ unpersuadableness: String, _ nonillionth: String) -> String {
    let polymerizeSterno = unpersuadableness + nonillionth
    return polymerizeSterno.md5()
}

SwiftyShield 的独特优势

1. 权益跟号走,拒绝“设备焦虑”

很多传统开发软件采用“一机一码”的绑定策略,换台电脑开发就得重置许可,非常麻烦。

SwiftyShield 采用现代化的账号授权体系。所有权益与您的账号绑定,而非特定设备。 无论您是在公司的 iMac 上工作,还是回家用 MacBook Pro 加班,只需登录账号,即可随时同步并使用您的 Pro 权益。

2. 100% 本地化处理,隐私无忧

我们深知源码是开发者的命脉。SwiftyShield 实现了完全离线运行。所有的混淆分析、数据库存储(基于 Realm)都只发生在您的 Mac 本地。没有任何代码会被上传到服务器

3. 实战验证的稳定性

SwiftyShield 已通过 100+ 个热门开源库(如 Alamofire, RxSwift, Lottie)的兼容性测试。工具会自动识别并跳过 Objective-C、SwiftUI、XIB 等文件,确保混合编译项目无缝衔接。

🎬 实战演示

眼见为实,与其看枯燥的文档,不如直接看看 SwiftyShield 如何为你的项目穿上“防弹衣”。

(注:如果视频无法播放,请访问演示链接:观看演示视频)

适用范围与下载

  • 系统要求:macOS 13 Ventura 或更高版本。
  • 安全性:应用已通过 Apple 公证 (Notarized),请放心使用。

👉 官网下载 & 体验www.swiftyshield.com

👉 GitHub:github.com/SwiftyShiel…

(具体使用规则、详细文档,请参考 GitHub 仓库 README)

如果你也是一名追求极致体验的开发者,欢迎下载试用!有任何建议或 Feature Request,欢迎在评论区或 GitHub Issue 中提出。

HTTPS的双向认证,钥匙串获取不到证书问题

HTTPS配置了双向认证,证书获取方式有三种:

  1. 使用时从服务器下载,安装到沙盒;
  2. 证书直接放到项目中,通过NSBundle获取;
  3. 系统自动从钥匙串获取对应证书;

为了证书的安全性,直接采用的第三种方式,但是一直获取不到,是什么原因呢?

具体步骤如下:

  1. 通过存储证书的服务器,在Safari浏览器访问后直接下载客户端证书pfx,会默认放到设置-通用-VPN与设备管理-描述文件位置,然后安装,在项目里用代码上钥匙串查询,结果查询不到,然后在描述文件查看,证书是尚未认证的。
  2. 然后把根CA crt也安装到了设备上,并且在设置-通用-关于本机-证书信任设置里对根CA开启了完全信任,然后客户端证书的”尚未认证“标识消失,然后在代码里重新查询证书,还是查询不到。

注意:但是把客户端证书放到项目里,通过代码可以手动导入到钥匙串里面,就可以查询到了。

需求肯定是从服务器链接下砸后,通过描述文件自动安装到钥匙串。

查询证书代码:


- (**void**)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(**void** (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * **_Nullable**))completionHandler {



    //第四种

    NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;

    // 1. 处理服务器信任验证(确保你连接的是正确的服务器)

    **if** ([protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

        // 这里应做严格的服务器证书校验,例如证书绑定(Pinning)

        // 为简化示例,我们选择信任一个有效的服务器证书

        SecTrustRef serverTrust = protectionSpace.serverTrust;

        **if** (serverTrust != **NULL**) {

            NSURLCredential *serverCredential = [NSURLCredential credentialForTrust:serverTrust];

            completionHandler(NSURLSessionAuthChallengeUseCredential, serverCredential);

            **return**;

        } **else** {

            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, **nil**);

        }

    }

    // 2. 处理客户端证书验证(向服务器证明你的身份)
    **else** **if** ([protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {

        // 关键:从钥匙串或文件中加载客户端证书和私钥(SecIdentityRef)

        SecIdentityRef clientIdentity = [**self** loadClientIdentityFromKeychain];

        **if** (clientIdentity != **NULL**) {

            // 创建客户端证书凭证

            NSURLCredential *clientCredential = [NSURLCredential credentialWithIdentity:clientIdentity certificates:**nil** persistence:NSURLCredentialPersistenceForSession];

            CFRelease(clientIdentity); // 如果使用Create/Copy规则,需要管理内存

            completionHandler(NSURLSessionAuthChallengeUseCredential, clientCredential);

            //return;

        } **else** {

            // 无法获取客户端证书

            NSLog(@"错误:无法从钥匙串获取客户端证书");

            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, **nil**);

        }

    }

    // 3. 对于其他挑战类型或加载失败,取消挑战
    completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, **nil**);

}


/// 从钥匙串获取客户端身份 (SecIdentityRef)

- (SecIdentityRef)loadClientIdentityFromKeychain {

    SecIdentityRef identity = **NULL**;

    

    // 构建查询字典

    NSDictionary *query = @{

        ( **__bridge** **id**)kSecClass: ( **__bridge** **id**)kSecClassIdentity,

        ( **__bridge** **id**)kSecReturnRef: @YES,

//        (__bridge id)kSecAttrLabel: @"", // 使用描述文件中的名称

        ( **__bridge** **id**)kSecMatchLimit: ( **__bridge** **id**)kSecMatchLimitOne,

        // 可以添加更多过滤条件来精确匹配你的证书

        // kSecAttrLabel: (NSString *) 证书标签

        // kSecAttrSubject: (NSData *) 主题信息

    };

    

    OSStatus status = SecItemCopyMatching(( **__bridge** CFDictionaryRef)query, (CFTypeRef *)&identity);

    

    **if** (status != errSecSuccess) {

        NSLog(@"钥匙串查询失败,错误码: %d", (**int**)status);

        

        // 打印所有可用的证书,帮助调试

        [**self** listAllIdentitiesInKeychain];

        

        **return** **NULL**;

    }

    

    **return** identity;

}

聊聊组件化案例

iOS 组件化详解 - CTMediator 原理与实践

📌 核心概念速记

┌─────────────────────────────────────────────────┐
  CTMediator 组件化方案                           
  ├─ 核心思想:Target-Action 模式                 
  ├─ 实现方式:Runtime 反射调用                   
  ├─ 优势:完全解耦,无需 import  └─ 适用:OCSwift(需桥接)                  
└─────────────────────────────────────────────────┘

一、为什么要做组件化开发?

传统单体架构的问题

随着项目规模扩大,传统单体架构会暴露以下问题:

1. 代码耦合严重
// ❌ 传统方式:直接依赖
#import "PayViewController.h"  // 订单模块直接依赖支付模块
#import "UserViewController.h" // 订单模块直接依赖用户模块

- (void)onPayClick {
    PayViewController *payVC = [[PayViewController alloc] init];
    [self.navigationController pushViewController:payVC animated:YES];
}

问题:

  • 修改支付模块会影响订单模块
  • 删除支付模块会导致订单模块编译失败
  • 无法独立测试订单模块
2. 团队协作低效
问题场景:
- 开发A修改了支付模块
- 开发B正在开发订单模块(依赖支付模块)
- 两人同时提交代码 → 冲突频繁
- 需要频繁合并代码 → 效率低下
3. 编译速度慢
单工程问题:
- 代码量:10万+ 行
- 每次编译:全量编译所有代码
- 编译时间:5-10 分钟
- 开发效率:严重下降
4. 复用性差
场景:公司有多个 APP
- APP A:需要登录模块
- APP B:需要登录模块
- APP C:需要登录模块

传统方式:每个 APP 都复制一份登录代码
组件化:登录模块独立,多个 APP 直接引用
5. 测试成本高
问题:
- 修改支付模块 → 需要测试整个 APP
- 修改订单模块 → 需要测试整个 APP
- 回归测试范围大 → 测试时间长

组件化的解决方案

核心目标:解耦

将项目拆分为独立、可复用的组件,通过"中间件"实现组件间通信。

传统架构:
订单模块 ──直接依赖──> 支付模块
订单模块 ──直接依赖──> 用户模块

组件化架构:
订单模块 ──中间件──> 支付模块
订单模块 ──中间件──> 用户模块
         ↑
      CTMediator

二、iOS 组件化如何分层?

分层架构图

┌─────────────────────────────────────────┐
│  壳工程(Main Project)                   │
│  - 置文件、启动页、根控制器  配           │
│  - 无业务逻辑,只负责组装组件             │
└─────────────────────────────────────────┘
              ↓ 依赖
┌─────────────────────────────────────────┐
│  中间件层(CTMediator)                   │
│  - 组件间通信桥梁                         │
│  - 路由跳转、方法调用                     │
└─────────────────────────────────────────┘
              ↓ 依赖
┌─────────────────────────────────────────┐
│  业务组件层                               │
│  - 首页组件、订单组件、支付组件            │
│  - 购物车组件、个人中心组件                │
└─────────────────────────────────────────┘
              ↓ 依赖
┌─────────────────────────────────────────┐
│  业务基础层                               │
│  - 登录、支付、用户信息管理                │
│  - 埋点统计                               │
└─────────────────────────────────────────┘
              ↓ 依赖
┌─────────────────────────────────────────┐
│  基础层                                   │
│  - 网络(AFNetworking)                   │
│  - 存储(FMDB)                           │
│  - 工具类(Category)                     │
│  - 基础 UI 组件                           │
└─────────────────────────────────────────┘

各层详细说明

层级 职责 示例组件 依赖关系
基础层 提供全局通用能力,不依赖任何上层模块 网络、存储、工具类、基础 UI 无依赖
业务基础层 封装跨业务的通用能力 登录、支付、用户信息、埋点 依赖基础层
业务组件层 拆分独立业务模块 首页、购物车、订单、个人中心 依赖基础层 + 业务基础层
中间件层 负责组件间通信 CTMediator、URLRouter 依赖基础层
壳工程 整合所有组件 Main Project 依赖所有层

依赖原则

关键规则:单向依赖,禁止反向依赖

✅ 正确:
上层 → 依赖 → 下层

❌ 错误:
下层 → 依赖 → 上层(禁止!)

三、CTMediator 核心原理

3.1 什么是 CTMediator?

CTMediator 是基于 Target-Action 模式的组件化中间件。

核心思想:

  • 调用方通过中间件发送"指令"
  • 中间件找到目标组件的 Target 类
  • 执行对应的 Action 方法
  • 全程无需 import,完全解耦

3.2 Target-Action 模式

调用流程:

订单组件
  ↓ 调用
CTMediator.payWithOrderId()
  ↓ 查找
Target_Pay 类(支付组件的 Target)
  ↓ 执行
action_payWithOrderId:callback: 方法(支付组件的 Action)
  ↓ 调用
PayService(支付组件内部逻辑)

3.3 命名约定

CTMediator 通过字符串查找类和方法,必须遵循命名约定:

Target 类命名:Target_ + 组件名
示例:
- 支付组件 → Target_Pay
- 订单组件 → Target_Order
- 用户组件 → Target_User

Action 方法命名:action_ + 方法名
示例:
- payWithOrderId:callback: → action_payWithOrderId:callback:
- showOrderDetail: → action_showOrderDetail:

四、CTMediator OC 实践详解

4.1 完整代码示例

假设场景:"订单组件"调用"支付组件"的支付功能

步骤 1:中间件 CTMediator(基础类,全局唯一)
// CTMediator.h
#import <UIKit/UIKit.h>

@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 支付组件调用方法(中间件声明)
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback;

@end
// CTMediator.m
#import "CTMediator.h"
#import <objc/runtime.h>

@implementation CTMediator

+ (instancetype)sharedInstance {
    static CTMediator *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [CTMediator new];
    });
    return instance;
}

// 调用支付组件的支付方法(通过Target-Action调用)
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback {
    
    // 1. 通过字符串查找 Target 类
    // Target_Pay:支付组件的Target类(约定命名:Target_+组件名)
    Class targetClass = NSClassFromString(@"Target_Pay");
    
    if (!targetClass) {
        NSLog(@"❌ 找不到 Target_Pay 类");
        if (callback) callback(NO);
        return;
    }
    
    // 2. 创建 Target 实例
    id target = [[targetClass alloc] init];
    
    // 3. 构造 Action 方法名
    // action_payWithOrderId:callback::支付组件的Action方法
    SEL action = NSSelectorFromString(@"action_payWithOrderId:callback:");
    
    // 4. 检查 Target 是否实现了 Action 方法
    if (![target respondsToSelector:action]) {
        NSLog(@"❌ Target_Pay 未实现 action_payWithOrderId:callback:");
        if (callback) callback(NO);
        return;
    }
    
    // 5. 通过 performSelector 调用 Action 方法
    // 注意:performSelector 最多支持 2 个参数
    // 如果参数超过 2 个,需要使用 NSInvocation
    [target performSelector:action withObject:orderId withObject:callback];
}

@end

关键点解析:

  1. NSClassFromString(@"Target_Pay")

    • 通过字符串动态查找类
    • 如果类不存在,返回 nil
  2. NSSelectorFromString(@"action_payWithOrderId:callback:")

    • 通过字符串构造方法选择器
    • 方法名必须完全匹配(包括冒号)
  3. performSelector:withObject:withObject:

    • Runtime 动态调用方法
    • 最多支持 2 个参数
    • 超过 2 个参数需要使用 NSInvocation
步骤 2:支付组件(业务组件层)的实现
// Target_Pay.h(仅组件内部可见,不对外暴露)
#import <UIKit/UIKit.h>

@interface Target_Pay : NSObject

// 支付Action(参数与中间件声明一致)
- (void)action_payWithOrderId:(NSString *)orderId 
                     callback:(void(^)(BOOL success))callback;

@end
// Target_Pay.m
#import "Target_Pay.h"
#import "PayService.h" // 组件内部的支付逻辑

@implementation Target_Pay

- (void)action_payWithOrderId:(NSString *)orderId 
                     callback:(void(^)(BOOL success))callback {
    
    NSLog(@"✅ Target_Pay 收到支付请求,订单ID:%@", orderId);
    
    // 调用组件内部的支付逻辑
    [[PayService shared] pay:orderId completion:^(BOOL success) {
        if (callback) {
            callback(success);
        }
    }];
}

@end
// PayService.h(支付组件内部逻辑)
#import <Foundation/Foundation.h>

@interface PayService : NSObject

+ (instancetype)shared;

- (void)pay:(NSString *)orderId 
 completion:(void(^)(BOOL success))completion;

@end
// PayService.m(支付组件内部逻辑)
#import "PayService.h"

@implementation PayService

+ (instancetype)shared {
    static PayService *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [PayService new];
    });
    return instance;
}

- (void)pay:(NSString *)orderId 
 completion:(void(^)(BOOL success))completion {
    
    // 模拟支付请求
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 模拟网络请求延迟
        sleep(1);
        
        // 模拟支付成功
        BOOL success = YES;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion) {
                completion(success);
            }
        });
    });
}

@end
步骤 3:调用方(订单组件)使用
// OrderViewController.h
#import <UIKit/UIKit.h>
#import "CTMediator.h" // 只依赖中间件,不依赖支付组件

@interface OrderViewController : UIViewController

@end
// OrderViewController.m
#import "OrderViewController.h"
// ❌ 不需要 #import "PayService.h"
// ❌ 不需要 #import "Target_Pay.h"
// ✅ 只需要 #import "CTMediator.h"

@implementation OrderViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建支付按钮
    UIButton *payButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [payButton setTitle:@"支付" forState:UIControlStateNormal];
    [payButton addTarget:self 
                  action:@selector(onPayClick) 
        forControlEvents:UIControlEventTouchUpInside];
    payButton.frame = CGRectMake(100, 100, 100, 44);
    [self.view addSubview:payButton];
}

// 点击支付按钮
- (void)onPayClick {
    NSString *orderId = @"ORDER_123";
    
    // 通过中间件调用支付功能,完全解耦
    [[CTMediator sharedInstance] payWithOrderId:orderId callback:^(BOOL success) {
        if (success) {
            NSLog(@"✅ 支付成功");
            // 更新UI
        } else {
            NSLog(@"❌ 支付失败");
            // 显示错误提示
        }
    }];
}

@end

4.2 多参数处理(NSInvocation)

当 Action 方法参数超过 2 个时,需要使用 NSInvocation:

// CTMediator.m 中处理多参数的方法
- (void)performTarget:(NSString *)targetName
                action:(NSString *)actionName
                params:(NSDictionary *)params {
    
    Class targetClass = NSClassFromString(targetName);
    id target = [[targetClass alloc] init];
    SEL action = NSSelectorFromString(actionName);
    
    NSMethodSignature *signature = [target methodSignatureForSelector:action];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    
    invocation.target = target;
    invocation.selector = action;
    
    // 设置参数(跳过 self 和 _cmd)
    NSInteger index = 2;
    for (NSString *key in params.allKeys) {
        id value = params[key];
        [invocation setArgument:&value atIndex:index];
        index++;
    }
    
    [invocation invoke];
    
    // 获取返回值
    __unsafe_unretained id returnValue;
    if (signature.methodReturnLength > 0) {
        [invocation getReturnValue:&returnValue];
    }
}

五、CTMediator Swift 实践详解

5.1 Swift 与 OC 的桥接问题

关键挑战:

  • CTMediator 是 OC 写的
  • Swift 组件需要暴露给 OC 调用
  • 需要使用 @objc 标记

5.2 完整代码示例

步骤 1:中间件扩展(Swift 中调用 CTMediator)
// CTMediator+SwiftExtension.swift
import UIKit

extension CTMediator {
    
    // Swift中声明支付调用方法
    func pay(with orderId: String, callback: @escaping (Bool) -> Void) {
        // 调用OC的Target-Action
        // 注意:Swift 的闭包需要转换为 OC 的 Block
        let ocCallback: (Bool) -> Void = { success in
            callback(success)
        }
        
        // 使用 performSelector(需要桥接)
        // 注意:这里需要将 Swift 闭包转换为 OC Block
        self.perform(NSSelectorFromString("payWithOrderId:callback:"), 
                    with: orderId, 
                    with: ocCallback)
    }
}

更好的方式:直接调用 OC 方法

// CTMediator+SwiftExtension.swift
import UIKit

// 创建 OC 兼容的 Block 类型
typealias PayCallback = @convention(block) (Bool) -> Void

extension CTMediator {
    
    // Swift中声明支付调用方法
    @objc func pay(with orderId: String, callback: @escaping (Bool) -> Void) {
        // 将 Swift 闭包转换为 OC Block
        let ocCallback: PayCallback = callback
        
        // 调用 OC 方法
        self.perform(NSSelectorFromString("payWithOrderId:callback:"), 
                    with: orderId, 
                    with: ocCallback)
    }
}
步骤 2:Swift 支付组件的 Target 实现
// Target_Pay.swift(需@objc暴露给OC)
import UIKit

// ⚠️ 关键:必须指定OC类名,否则CTMediator找不到
@objc(Target_Pay)
class Target_Pay: NSObject {
    
    // Action方法需@objc,参数匹配
    @objc func action_payWithOrderId(_ orderId: String, 
                                    callback: @escaping (Bool) -> Void) {
        print("✅ Target_Pay 收到支付请求,订单ID:\(orderId)")
        
        // 调用Swift支付逻辑
        PayService.shared.pay(orderId: orderId) { success in
            callback(success)
        }
    }
}

// 支付逻辑(组件内部)
class PayService {
    static let shared = PayService()
    
    func pay(orderId: String, completion: @escaping (Bool) -> Void) {
        // 模拟支付请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            // 模拟支付成功
            completion(true)
        }
    }
}

关键点:

  1. @objc(Target_Pay)

    • 必须指定 OC 类名
    • 否则 CTMediator 通过 NSClassFromString(@"Target_Pay") 找不到类
  2. @objc func action_payWithOrderId

    • Action 方法必须用 @objc 标记
    • 参数类型必须与 OC 兼容(String、Int、Bool 等)
  3. 闭包转换

    • Swift 闭包需要转换为 OC Block
    • 使用 @convention(block)@escaping
步骤 3:Swift 调用方(订单组件)
// OrderViewController.swift
import UIKit

class OrderViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 创建支付按钮
        let payButton = UIButton(type: .system)
        payButton.setTitle("支付", for: .normal)
        payButton.addTarget(self, 
                           action: #selector(onPayClick), 
                           for: .touchUpInside)
        payButton.frame = CGRect(x: 100, y: 100, width: 100, height: 44)
        view.addSubview(payButton)
    }
    
    @objc func onPayClick() {
        let orderId = "ORDER_456"
        
        // 通过中间件调用支付功能,完全解耦
        CTMediator.sharedInstance().pay(with: orderId) { success in
            if success {
                print("✅ 支付成功")
                // 更新UI
            } else {
                print("❌ 支付失败")
                // 显示错误提示
            }
        }
    }
}

5.3 Swift 桥接配置

如果 CTMediator 是 OC 写的,需要在桥接头文件中导入:

// YourProject-Bridging-Header.h
#import "CTMediator.h"

六、CTMediator 的优势与劣势

6.1 优势

✅ 完全解耦
订单组件 ←→ CTMediator ←→ 支付组件
         (无需 import
✅ 类型安全(相对 URL 路由)
// CTMediator 提供方法声明,编译期检查
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback;

// URL 路由是字符串,运行时才发现错误
[Router open:@"app://pay?orderId=123"]; // 拼写错误不会编译报错
✅ 支持复杂参数
// 支持对象、字典、数组等复杂参数
- (void)showOrderDetail:(NSDictionary *)params;
✅ 支持返回值
// 可以返回对象
- (UIViewController *)getProfileViewController;

6.2 劣势

❌ 需要维护中间件方法
// 每增加一个组件调用,需要在 CTMediator 中添加方法
- (void)payWithOrderId:callback:;
- (void)showOrderDetail:;
- (void)getUserInfo:;
// ... 方法越来越多
❌ 命名约定严格
必须遵循:
- Target 类:Target_组件名
- Action 方法:action_方法名
- 拼写错误会导致运行时崩溃
❌ Swift 支持不够优雅
需要 @objc 标记
需要桥接头文件
闭包转换复杂

七、组件化 vs 工程化 vs 插件化

7.1 概念对比

概念 核心目标 技术手段 优势 适用场景
组件化 拆分模块,解决代码耦合 静态拆分模块(编译期整合),通过中间件通信 解耦彻底、编译速度快、团队协作高效、模块可复用 大型 APP、多团队协作、业务稳定
工程化 规范开发流程,提升效率 自动化工具(Jenkins、Fastlane)、代码规范、CI/CD 减少人为操作、标准化流程、降低出错率 所有项目(基础保障)
插件化 动态加载模块,解决包体积和动态更新 动态库(.framework)、反射加载、沙盒隔离 按需加载、减小包体积、支持动态更新(无需发版) 模块频繁更新、包体积敏感场景

7.2 详细对比

组件化(Componentization)

特点:

  • ✅ 编译期整合(静态链接)
  • ✅ 完全解耦
  • ✅ 编译速度快(模块独立编译)
  • ✅ 兼容性好(无动态加载风险)

实现方式:

订单组件.framework
支付组件.framework
用户组件.framework
    ↓ 编译期整合
Main Project(壳工程)
工程化(Engineering)

特点:

  • ✅ 流程规范
  • ✅ 自动化工具
  • ✅ CI/CD 流程
  • ✅ 代码规范检查

实现方式:

开发 → Git 提交 → Jenkins 构建 → 自动化测试 → 打包 → 发布

与组件化的关系:

  • 工程化是"流程规范"
  • 组件化是"架构设计"
  • 工程化为组件化提供落地保障
插件化(Pluginization)

特点:

  • ✅ 运行时加载(动态链接)
  • ✅ 按需加载
  • ✅ 减小包体积
  • ⚠️ 实现复杂
  • ⚠️ 受 iOS 审核限制(动态库可能被拒)

实现方式:

主 APP
  ↓ 运行时下载
插件 A.framework
插件 B.framework
  ↓ 动态加载
运行

iOS 限制:

  • App Store 不允许下载可执行代码
  • 动态库需要主 APP 签名
  • 审核可能被拒

7.3 选择建议

项目规模小(< 5人):
→ 不需要组件化,工程化即可

项目规模中(5-20人):
→ 推荐组件化 + 工程化

项目规模大(> 20人):
→ 必须组件化 + 工程化

需要动态更新:
→ 考虑插件化(但要注意审核风险)

八、CTMediator 最佳实践

8.1 错误处理

// CTMediator.m
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback {
    
    Class targetClass = NSClassFromString(@"Target_Pay");
    
    if (!targetClass) {
        NSLog(@"❌ 找不到 Target_Pay 类,请检查组件是否已集成");
        if (callback) callback(NO);
        return;
    }
    
    id target = [[targetClass alloc] init];
    SEL action = NSSelectorFromString(@"action_payWithOrderId:callback:");
    
    if (![target respondsToSelector:action]) {
        NSLog(@"❌ Target_Pay 未实现 action_payWithOrderId:callback:");
        if (callback) callback(NO);
        return;
    }
    
    // 使用 NSInvocation 安全调用
    NSMethodSignature *signature = [target methodSignatureForSelector:action];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = target;
    invocation.selector = action;
    
    [invocation setArgument:&orderId atIndex:2];
    [invocation setArgument:&callback atIndex:3];
    
    [invocation invoke];
}

8.2 参数验证

// CTMediator.m
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback {
    
    // 参数验证
    if (!orderId || orderId.length == 0) {
        NSLog(@"❌ 订单ID不能为空");
        if (callback) callback(NO);
        return;
    }
    
    if (!callback) {
        NSLog(@"⚠️ 回调不能为空");
        return;
    }
    
    // ... 后续逻辑
}

8.3 日志记录

// CTMediator.m
- (void)payWithOrderId:(NSString *)orderId 
              callback:(void(^)(BOOL success))callback {
    
    #ifdef DEBUG
    NSLog(@"🔍 CTMediator: 调用支付功能,订单ID:%@", orderId);
    #endif
    
    // ... 调用逻辑
    
    #ifdef DEBUG
    NSLog(@"✅ CTMediator: 支付功能调用成功");
    #endif
}

8.4 性能优化

// CTMediator.m
@interface CTMediator ()
@property (nonatomic, strong) NSMutableDictionary *targetCache; // Target 缓存
@end

@implementation CTMediator

- (instancetype)init {
    if (self = [super init]) {
        _targetCache = [NSMutableDictionary dictionary];
    }
    return self;
}

- (id)getTarget:(NSString *)targetName {
    // 先从缓存获取
    id target = self.targetCache[targetName];
    if (target) {
        return target;
    }

    // 缓存中没有,创建并缓存
    Class targetClass = NSClassFromString(targetName);
    if (targetClass) {
        target = [[targetClass alloc] init];
        self.targetCache[targetName] = target;
    }

    return target;
}

@end

8.5 扩展性优化:新增支付方式的正确姿势

问题场景

当需要新增支付方式(如Apple Pay)时,常见的错误做法是在PayService中添加分支:

// ❌ 错误做法:修改核心代码
- (void)pay:(NSString *)orderId paymentType:(NSString *)paymentType {
    if ([paymentType isEqualToString:@"alipay"]) {
        // 支付宝逻辑
    } else if ([paymentType isEqualToString:@"applepay"]) { // 新增的
        // Apple Pay 逻辑 ⭐ 需要修改核心代码
    }
}
✅ 推荐方案:注册机制
步骤1:定义支付协议
@protocol PaymentProtocol <NSObject>
@required
- (NSString *)paymentType;
- (void)pay:(NSString *)orderId completion:(void(^)(BOOL success))completion;
@end
步骤2:CTMediator支持注册
@interface CTMediator ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, id<PaymentProtocol>> *paymentHandlers;
@end

- (void)registerPaymentHandler:(id<PaymentProtocol>)handler forPaymentType:(NSString *)paymentType {
    self.paymentHandlers[paymentType] = handler;
}

- (void)payWithOrderId:(NSString *)orderId
           paymentType:(NSString *)paymentType
              callback:(void(^)(BOOL success))callback {

    id<PaymentProtocol> handler = self.paymentHandlers[paymentType];
    if (handler) {
        [handler pay:orderId completion:callback];
    } else {
        // 降级到Target-Action模式
        [self performTargetActionWithOrderId:orderId paymentType:paymentType callback:callback];
    }
}
步骤3:各支付方式实现协议
// ApplePayHandler.h
@interface ApplePayHandler : NSObject <PaymentProtocol>
@end

// ApplePayHandler.m
@implementation ApplePayHandler

- (NSString *)paymentType {
    return @"applepay";
}

- (void)pay:(NSString *)orderId completion:(void(^)(BOOL success))completion {
    // Apple Pay 支付逻辑
    // 完全独立,不需要修改任何现有代码
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        completion(YES);
    });
}

@end
步骤4:在组件初始化时注册
// 在支付组件或App启动时
ApplePayHandler *applePayHandler = [[ApplePayHandler alloc] init];
[[CTMediator sharedInstance] registerPaymentHandler:applePayHandler
                                      forPaymentType:@"applepay"];
优势对比
方式 是否修改核心代码 扩展性 维护性 测试性
分支判断 ✅ 需要 ❌ 差 ❌ 差 ❌ 难
注册机制 ❌ 不需要 ✅ 好 ✅ 好 ✅ 易
存储注册表的建议
// 使用NSMutableDictionary存储映射关系
@property (nonatomic, strong) NSMutableDictionary<NSString *, id<PaymentProtocol>> *paymentHandlers;

// 线程安全考虑
@property (nonatomic, strong) dispatch_queue_t registryQueue;

- (void)registerPaymentHandler:(id<PaymentProtocol>)handler forPaymentType:(NSString *)paymentType {
    dispatch_barrier_async(self.registryQueue, ^{
        self.paymentHandlers[paymentType] = handler;
    });
}

九、常见问题 FAQ

Q1: CTMediator 和 URL 路由有什么区别?

CTMediator(Target-Action):

  • ✅ 类型安全(编译期检查)
  • ✅ 支持复杂参数
  • ✅ 支持返回值
  • ❌ 需要维护中间件方法

URL 路由:

  • ✅ 简单易用
  • ✅ 无需维护方法
  • ❌ 字符串不安全(运行时错误)
  • ❌ 参数传递受限(只能字符串)

Q2: Swift 组件如何暴露给 CTMediator?

// 必须使用 @objc 标记
@objc(Target_Pay)
class Target_Pay: NSObject {
    @objc func action_payWithOrderId(_ orderId: String, 
                                    callback: @escaping (Bool) -> Void) {
        // ...
    }
}

Q3: 如何避免 Target 类找不到?

  1. 检查命名约定

    Target 类名:Target_组件名(首字母大写)
    示例:Target_Pay、Target_Order
    
  2. 检查组件是否已集成

    在 Build Phases → Link Binary With Libraries 中检查
    
  3. 添加日志调试

    Class targetClass = NSClassFromString(@"Target_Pay");
    if (!targetClass) {
        NSLog(@"❌ 找不到 Target_Pay,已加载的类:%@", 
              [self getAllLoadedClasses]);
    }
    

Q4: 如何支持多参数方法?

使用 NSInvocation:

- (void)performTarget:(NSString *)targetName
                action:(NSString *)actionName
                params:(NSArray *)params {

    Class targetClass = NSClassFromString(targetName);
    id target = [[targetClass alloc] init];
    SEL action = NSSelectorFromString(actionName);

    NSMethodSignature *signature = [target methodSignatureForSelector:action];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

    invocation.target = target;
    invocation.selector = action;

    // 设置参数
    for (NSInteger i = 0; i < params.count; i++) {
        id param = params[i];
        [invocation setArgument:&param atIndex:i + 2]; // 跳过 self 和 _cmd
    }

    [invocation invoke];
}

Q5: 新增支付方式为什么要用注册机制而不是直接修改代码?

传统方式的问题
// ❌ 在PayService中添加分支判断
- (void)pay:(NSString *)orderId paymentType:(NSString *)paymentType {
    if ([paymentType isEqualToString:@"alipay"]) {
        // 支付宝逻辑
    } else if ([paymentType isEqualToString:@"applepay"]) { // 新增
        // Apple Pay 逻辑 - 需要修改核心代码
    }
}

问题:

  • 需要修改已有代码
  • 违反开闭原则
  • 难以测试
  • 增加代码复杂度
注册机制的优势
// ✅ 注册新支付方式,无需修改核心代码
ApplePayHandler *handler = [[ApplePayHandler alloc] init];
[[CTMediator sharedInstance] registerPaymentHandler:handler forPaymentType:@"applepay"];

优势:

  • 零侵入:不修改现有代码
  • 可扩展:动态添加新支付方式
  • 易测试:各支付方式独立测试
  • 解耦合:支付逻辑完全隔离
注册机制的本质

注册机制就是从硬编码的字符串映射,转换为手动维护的映射表

// 硬编码方式:NSClassFromString(@"Target_Pay")
// 注册方式:维护一个 @{@"pay": [Target_Pay class]} 的字典

这种转变虽然增加了一层注册,但换来了更好的扩展性和维护性。

Q6: 注册表用什么数据结构存储?

最常用:NSMutableDictionary

@interface CTMediator ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, id<PaymentProtocol>> *paymentHandlers;
@end
存储方式对比
方式 查找性能 内存占用 线程安全 适用场景
NSDictionary O(1) 中等 大多数项目
NSMapTable O(1) ⚠️ 需要弱引用
NSArray O(n) 中等 需要排序
数据库 O(log n) 需要持久化
线程安全实现
@property (nonatomic, strong) dispatch_queue_t registryQueue;

- (void)registerPaymentHandler:(id<PaymentProtocol>)handler forPaymentType:(NSString *)paymentType {
    dispatch_barrier_async(self.registryQueue, ^{
        self.paymentHandlers[paymentType] = handler;
    });
}

- (id<PaymentProtocol>)getHandlerForPaymentType:(NSString *)paymentType {
    __block id handler = nil;
    dispatch_sync(self.registryQueue, ^{
        handler = self.paymentHandlers[paymentType];
    });
    return handler;
}

十、总结

CTMediator 核心要点

  1. Target-Action 模式

    • Target 类:Target_组件名
    • Action 方法:action_方法名
  2. Runtime 反射调用

    • NSClassFromString 查找类
    • NSSelectorFromString 构造方法
    • performSelectorNSInvocation 调用
  3. 完全解耦

    • 调用方无需 import 目标组件
    • 通过中间件通信
  4. 适用场景

    • 大型 APP
    • 多团队协作
    • 业务稳定

与其他方案对比

方案 类型安全 易用性 性能 适用场景
CTMediator ⭐⭐⭐ 大型项目
URL 路由 ⭐⭐⭐⭐⭐ 中小型项目
Protocol ⭐⭐⭐⭐ 中型项目

最后更新:2024年

聊聊看千问AI分析滤镜库Harbeth

Harbeth 是一个基于Apple Metal框架的高性能图像处理和滤镜开发库,采用Swift语言编写,为iOS和macOS平台提供了强大的GPU加速图像处理能力。该项目由开发者yangKJ创建,旨在替代已不再更新维护的GPUImage库,同时继承了其设计理念并进行了全面升级,现已成为移动端图像处理领域的热门开源项目。

一、HarBeth的核心架构与技术特点

1. 模块化设计

HarBeth采用高度模块化的架构设计,主要包括以下几个核心模块:

Core模块:负责配置Metal信息,以及与CoreImage的兼容转换

Extensions模块:处理各类资源与MTLTexture之间的转换方法

Matrix模块:包含矩阵相关操作,提供常用矩阵卷积内核和颜色矩阵

Outputs模块:包含对外转换接口,如BoxxIO快速向源添加过滤器

Setup模块:包含配置信息和小工具

滤镜模块细分

HarBeth的滤镜部分进一步细分为多个子模块,每个子模块专注于特定类型的图像处理:

l Blend:图像融合技术

l Blur:模糊效果

l ColorProcess:图像基本像素颜色处理

l Effect:效果处理

l Lookup:查找表过滤器

l Matrix:矩阵卷积滤波器

l Shape:图像形状大小相关处理

l VisualEffect:视觉动态特效

2. 与CoreImage的兼容性

HarBeth的一个显著特点是其与CoreImage的深度兼容性。开发者通过以下方式实现了这种兼容:

双向转换:提供了CIImage与MTLTexture之间的高效转换方法,通过CIContext的createCGImage方法将CIImage转换为CGImage,再利用MTLDevice创建MTLTexture

共享GPU队列:优化了MTLCommandQueue的使用,减少GPU任务切换开销,提高处理效率

滤镜链整合:支持将CoreImage的CIFilter直接嵌入HarBeth的处理流程,允许开发者利用CoreImage丰富的内置滤镜库

这种兼容性设计使得HarBeth不仅能够独立工作,还能与Apple官方CoreImage框架无缝集成,为开发者提供了更大的灵活性和更丰富的功能选择。

3. 零侵入代码设计

HarBeth采用"零侵入"代码设计理念,使得开发者可以在不修改原有代码结构的情况下轻松添加滤镜功能。例如:

这种设计极大简化了滤镜功能的集成流程,使开发者能够快速地在现有项目中添加高级图像处理能力。

二、性能优化与实现机制

1. Metal加速技术

HarBeth的核心优势在于其出色的GPU加速性能。与传统的CPU处理相比,它充分利用了苹果设备的图形处理器,通过以下技术实现高性能图像处理:

MTLTexture处理:图像数据首先被转换为MTLTexture格式,以便在GPU上进行高效并行处理

MetalPerformanceShaders集成:利用Apple官方提供的高性能计算库加速计算密集型任务,如矩阵卷积

异步处理机制:通过异步回调方式处理图像,避免阻塞主线程,提高应用响应速度

2. 资源管理与性能优化

为确保高效的图像处理性能,HarBeth在资源管理方面做了多项优化:

智能内存管理:优化MTLTexture的创建和释放流程,减少内存占用和分配开销

共享GPU队列:通过共享MTLCommandQueue,使任务在GPU上更高效地执行

异步处理最佳实践:采用异步处理模式,避免CPU/GPU同步带来的性能瓶颈

3. 实时处理能力

HarBeth特别注重实时图像处理能力,主要体现在:

相机采集特效:支持实时相机捕获并应用滤镜,为相机应用提供专业级实时美颜和风格化处理能力

视频滤镜处理:能够在播放过程中实时应用滤镜效果,无需等待视频解码完成

高帧率维持:通过优化的Metal任务调度和计算着色器,确保在图像处理密集场景下维持稳定帧率

三、应用场景分析

1. 社交媒体应用

HarBeth在社交媒体应用中表现出色,特别适合以下场景:

实时美颜滤镜:支持在视频通话和直播中应用实时美颜效果

照片编辑功能:提供丰富的预设滤镜和自定义滤镜选项,满足用户多样化照片编辑需求

动态滤镜效果:如"灵魂出窍"等视觉动态特效,为照片和视频增添艺术感

2. 专业图像/视频编辑

对于专业图像和视频编辑软件,HarBeth提供了以下关键功能:

批量处理能力:支持对大量图像和视频进行高效批处理,显著提升工作效率

视频滤镜导出:能够对已有视频添加滤镜效果并导出,支持多种视频格式

高级风格转换:如矩阵卷积和颜色变换等高级图像处理技术,满足专业图像编辑需求

3. AR/VR应用开发

尽管现有文档未明确提及,但HarBeth的技术特性使其非常适合AR/VR应用开发:

实时图像渲染:强大的GPU加速能力可支持AR应用中实时图像渲染

高精度色彩处理:专业的色彩矩阵和颜色处理模块,适合虚拟现实场景中的视觉效果

低延迟处理:优化的图像处理流水线可降低处理延迟,提升用户体验

四、与其他图像处理库的对比

特性 HarBeth GPUImage CoreImage
技术基础 Metal + CoreImage GLKit + OpenGLES CPU/GPU混合
最新更新 2025-2026年 2015年左右 持续更新
内置滤镜数量 超150种 约60种 约100种
实时处理性能 极高 较高 中等
集成复杂度 低(零侵入设计) 中等 中等
平台支持 iOS/macOS iOS iOS/macOS
开源许可 MIT MIT 闭源

数据来源:

与GPUImage对比:HarBeth继承并扩展了GPUImage的设计理念,但通过采用Metal替代过时的OpenGLES,显著提升了性能。同时,HarBeth提供了更简洁的API和更丰富的滤镜库,且仍在持续更新维护。

与CoreImage对比:HarBeth在保持CoreImage易用性的同时,通过直接利用Metal框架实现了更高的性能。对于简单图像处理任务,CoreImage可能更为便捷;而对于复杂、计算密集型的图像处理,HarBeth通常能提供更好的性能表现。

五、使用建议与最佳实践

1. 安装与集成

HarBeth可以通过多种方式集成到项目中:

CocoaPods:简单一键安装

Swift Package Manager:适用于SwiftUI项目

2. 基础使用示例

HarBeth提供了多种使用方式,包括直接应用单个滤镜、组合多个滤镜,以及函数式编程风格:

3. 性能优化建议

为充分发挥HarBeth的性能优势,建议采用以下最佳实践:

异步处理:对于大型图像或视频处理,优先使用异步处理模式

共享上下文:在同一个视图控制器中复用Metal上下文和CIContext,减少资源创建开销

合理使用缓存:对于频繁应用的滤镜,考虑缓存处理结果

监控性能:使用Xcode Instruments工具监控Metal性能,识别潜在瓶颈

4. 滤镜设计与扩展

HarBeth提供了灵活的滤镜设计和扩展机制:

自定义滤镜:支持基于Metal Shading Language编写自定义滤镜

组合滤镜:通过组合现有滤镜创建新效果,减少代码重复

参数化调优:大多数滤镜支持参数调整,允许动态控制效果强度

六、结论与展望

HarBeth作为一个基于Metal的高性能图像处理框架,凭借其丰富的滤镜库、优秀的性能表现以及与CoreImage的深度兼容性,已成为iOS和macOS平台图像处理领域的重要工具。相比已停止更新的GPUImage,HarBeth不仅保持了API的简洁性,还通过底层技术的全面升级,实现了显著的性能提升。

未来发展趋势

1. 持续功能扩展:随着开发者社区的参与,HarBeth的滤镜库和功能集有望进一步丰富

2. 性能持续优化:随着Metal框架的更新迭代,HarBeth有望进一步优化其处理性能

3. 跨平台支持:虽然目前专注于Apple平台,但未来可能考虑跨平台支持以扩大应用范围

4. AI增强:可能集成机器学习技术,提供基于深度学习的智能图像处理效果

对于需要在Apple平台实现高性能图像处理的应用开发者,HarBeth是一个值得优先考虑的技术选择,它能够以较低的学习成本和集成复杂度,为应用提供强大的视觉效果和流畅的用户体验。

参考来源

[1]悬镜源鉴·Gitee 极速下载/Harbeth-Gitee.com

gitee.com/mirrors/Har…

[2]进阶!展现最优的技术和最好的声音:听评英国Harbeth(雨后初晴)M40.3 XD 音箱_监听_单元_产品

www.sohu.com/a/764150569…

[3]哈勃分析系统_百度百科

baike.baidu.com/item/%E5%93…

[4]探秘Harbeth:如何用Metal技术打造终极图像处理框架-CSDN博客

blog.csdn.net/gitblog_000…

[5]探索深度学习的速度极限:Haste开源库解析与应用-CSDN博客

blog.csdn.net/gitblog_000…

[6]小学生/Harbeth

gitee.com/huansghijie…

[7]突破传统,全新时代—HarbethNLE-1书架式有源音箱-哔哩哔哩

www.bilibili.com/opus/107042…

[8]深入讲解一下 Harbor 的源码_harbor源码-CSDN博客

blog.csdn.net/u011091936/…

[9]Harbeth首页、文档和下载-图形处理和滤镜制作-OSCHINA-中文开源技术交流社区

www.oschina.net/p/harbeth

[10]探秘Harbeth:如何用Metal技术打造终极图像处理框架-CSDN博客

blog.csdn.net/gitblog_000…

[11]Metal(技术)百度百科

baike.baidu.com/item/METAL/…

[12]iOS 利用 Metal 实现滤镜与动效滤镜_ios metal 美颜-CSDN博客

blog.csdn.net/qq_34534179…

[13]Metal-快懂百科

www.baike.com/wiki/Metal/…

[14]MetalFilters 开源项目教程-CSDN博客

blog.csdn.net/gitblog_007…

[15]高性能文本渲染:HarfBuzz与GPU加速技术结合方案-CSDN博客

blog.csdn.net/gitblog_010…

[16]推荐文章:探索高效图像视频处理—MetalImage框架-CSDN博客

blog.csdn.net/gitblog_009…

[17]Metal助力专业 App-WWDC19-视频-Apple Developer

developer.apple.com/cn/videos/p…

[18]CIImage.FromMetalTexture(IMTLTexture,NSDictionaryNSObject>Method(CoreImage)Microsoft Learn

learn.microsoft.com/zh-CN/dotne…

[19]Active Learning Based on Locally Linear Reconstruction

ai.nju.edu.cn/zlj/pdf/TPA…

[20]探秘Harbeth:如何用Metal技术打造终极图像处理框架-CSDN博客

blog.csdn.net/gitblog_000…

[21]MTLTexture_Extensions.GetBufferBytesPerRow(IMTLTexture)方法(Metal)Microsoft Learn

learn.microsoft.com/zh-cn/dotne…

[22]iOS 实时图像处理技术:使用Core Image和Metal进行高效滤镜应用-阿里云开发者社区

developer.aliyun.com/article/147…

[23]教你如何玩转Metal滤镜?Harbeth是一款基于Metal API设计的滤镜框架,主要介绍与设计基于GPU的滤镜,掘金

juejin.im/entry/70669…

[24]深入掌握CoreImage滤镜的使用与实战-CSDN博客

blog.csdn.net/weixin_3343…

[25]探索Core Image内核改进-WWDC21-视频-Apple Developer

developer.apple.com/cn/videos/p…

[26]CIImage.MetalTexture Property(CoreImage)Microsoft Learn

learn.microsoft.com/zh-cn/dotne…

[27]【函数式 Swift】封装Core Image-CSDN博客

blog.csdn.net/weixin_3380…

[28]MMBAT: A MULTI-TASK FRAMEWORK FOR MMWAVEREEDUCATION AND TRANSLATIONS

arxiv.org/abs/2312.10…

[29]探秘Harbeth:如何用Metal技术打造终极图像处理框架-CSDN博客

blog.csdn.net/gitblog_000…

[30]Decoding the Underlying Meaning of Multmodal Hateful MEMes

arxiv.org/abs/2305.17…

[31]Harbeth首页、文档和下载-图形处理和滤镜制作-OSCHINA-中文开源技术交流社区

www.oschina.net/p/harbeth

[32]Single color virtual H&E staining with In-and-Out Net

arxiv.org/abs/2405.13…

[33]悬镜源鉴·Gitee 极速下载/Harbeth-Gitee.com

gitee.com/mirrors/Har…

(AI生成)

使用pymobiledevice3进行iphone应用性能采集

执行步骤

安装依赖(未安装则执行)

pip install pymobiledevice3

启动Tunnel(系统版本>=17需要)

sudo pymobiledevice3 remote tunneld

获取性能数据(打开一个新的终端)

根据PID获取性能数据

# 获取PID
pymobiledevice3 developer dvt process-id-for-bundle-id com.xxx.xxx

# 获取指定PID的性能数据
pymobiledevice3 developer dvt sysmon process single -a pid=xxxx

获取全部进程的性能数据

pymobiledevice3 developer dvt sysmon process monitor --rsd xxxx:xxxx:xxxx::x 58524 0.01

简单方案

上面的多个步骤都是通过pymobiledevice3一个工具来实现的,因此是否可以一步就完成性能的采集?当然可以,通过深扒(并复制+改造)pymobiledevice3的源码,将所有操作封装到了一个脚本中~~~

"""
—————— 使用说明 ——————
usage: python3 ios-monitor.py [-h] [-g GAP] [-b BUNDLE_ID] [-r REPORT_HOST] [--detail] [--csv] [--debug]

ios设备性能收集

options:
  -h, --help            show this help message and exit
  -g, --gap GAP         性能数据获取时间间隔,默认1s
  -b, --bundle_id BUNDLE_ID
                        包名, 默认: com.mi.car.mobile
  -r, --report_host REPORT_HOST
                        性能采集数据上报地址
  --detail              输出详细信息
  --csv                 结果写入到CSV文件
  --debug               打印debug日志

—————— 示例 ——————
# 进行性能采集,ctrl+c 终止后写入csv文件
sudo python3 ios-monitor.py --csv

# 进行性能采集,数据上报到指定服务
sudo python3 ios-monitor.py -r 127.0.0.1:9311

"""
import argparse
import asyncio
import csv
import json
import logging
import os
import signal
import sys
import time
from functools import partial
import multiprocessing
from multiprocessing import Process

from pymobiledevice3.remote.common import TunnelProtocol
from pymobiledevice3.remote.module_imports import verify_tunnel_imports
from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
from pymobiledevice3.services.dvt.dvt_secure_socket_proxy import DvtSecureSocketProxyService
from pymobiledevice3.services.dvt.instruments.device_info import DeviceInfo
from pymobiledevice3.services.dvt.instruments.process_control import ProcessControl
from pymobiledevice3.services.dvt.instruments.sysmontap import Sysmontap
from pymobiledevice3.tunneld.server import TunneldRunner

import requests

TUNNELD_DEFAULT_ADDRESS = ('127.0.0.1', 49151)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

report_error_num = 0

csv_data_list = []
csv_fieldnames=['cpu', 'mem']

def run_tunneld():
    """ Start Tunneld service for remote tunneling
    sudo pymobiledevice3 remote tunneld
    """
    if not verify_tunnel_imports():
        logger.warning("verify_tunnel_imports false")
        return
    host = TUNNELD_DEFAULT_ADDRESS[0]
    port = TUNNELD_DEFAULT_ADDRESS[1]
    protocol = TunnelProtocol(TunnelProtocol.DEFAULT.value)

    tunneld_runner = partial(TunneldRunner.create, host, port, protocol=protocol, usb_monitor=True,
                             wifi_monitor=True, usbmux_monitor=True, mobdev2_monitor=True)

    tunneld_runner()
    return

def process_id_for_bundle_id(lockdown, app_bundle_identifier: str = "com.mi.car.mobile"):
    """ Get PID of a bundle identifier (only returns a valid value if its running). """
    with DvtSecureSocketProxyService(lockdown=lockdown) as dvt:
        return ProcessControl(dvt).process_identifier_for_bundle_identifier(app_bundle_identifier)

def sysmon_process_single(lockdown, pid, detail, report_host='', write_csv=False, tunnel_process=None):
    """ show a single snapshot of currently running processes. """
    count = 0
    result = []

    with DvtSecureSocketProxyService(lockdown=lockdown) as dvt:
        device_info = DeviceInfo(dvt)
        with Sysmontap(dvt) as sysmon:
            for process_snapshot in sysmon.iter_processes():
                count += 1
                if count < 2:
                    # first sample doesn't contain an initialized value for cpuUsage
                    continue
                for process in process_snapshot:
                    # print(process)
                    if str(process["pid"]) != str(pid):
                        continue
                    # adding "artificially" the execName field
                    process['execName'] = device_info.execname_for_pid(process['pid'])
                    result.append(process)
                # exit after single snapshot
                break
    if len(result) == 0:
        logger.info("[]")
        return
    
    cpu_usage = "%.2f" % result[0]['cpuUsage']
    mem = "%.2f" % (result[0]['memResidentSize'] / 1024 / 1024)
    
    if write_csv:
        csv_data = {'cpu': cpu_usage, 'mem': mem}
        csv_data_list.append(csv_data)

    if report_host:
        report(host=report_host, info=result[0], pid=str(pid), tunnel_process=tunnel_process)
        return

    if detail:
        logger.info(json.dumps(result, indent=4, ensure_ascii=False))
    else:
        logger.info("[CPU]{} %  [内存]{} MB".format(cpu_usage, mem))

def report(host: str, info: dict, pid: str, tunnel_process=None):
    global report_error_num
    url = 'http://%s/monitor/collect' % host
    mem = info['memResidentSize'] / 1024 / 1024
    data = {
        'device': 'ios-%s' % pid,
        'list': [0, info['cpuUsage'], 0, mem],
        'app_cpu_rate': info['cpuUsage'],
        'app_mem': mem,
        'timestamp': int(time.time() * 1000)
    }
    report_err = False
    try:
        resp = requests.post(url=url, json=data, timeout=(0.5, 0.5))
    except Exception:
        report_err = True

    if report_err is False and resp.status_code != 200:
        report_err = True

    if report_err:
        report_error_num += 1
        logger.warning("上报失败 %d" % report_error_num)
        if report_error_num > 5:
            logger.info("接收端已关闭, 监控退出")
            if tunnel_process:
                tunnel_process.terminate()
            sys.exit(0)
    else:
        cpu_usage = "%.2f" % info['cpuUsage']
        logger.info("report [CPU]{} %  [内存]{} MB".format(cpu_usage, mem))
        report_error_num = 0

def get_tunnel_addr(attemp_times=30):
    url = 'http://127.0.0.1:%d/' % TUNNELD_DEFAULT_ADDRESS[1]
    try_times = 0
    while try_times < attemp_times:
        try:
            logger.info('--- 获取设备连接信息')
            resp = requests.get(url=url, timeout=(1, 1)).json()
            for v in resp.values():
                if not v:
                    continue
                return v[0]['tunnel-address'], v[0]['tunnel-port']
        except Exception:
            pass
        try_times += 1
        time.sleep(1)
        continue
    logger.warning('--- 未找到ios设备')
    return None, None

def run_in_one(bundle_id: str, gap: int, detail: bool, report_host: str = '', write_csv: bool = False):
    logger.info('--- 连接设备')
    p = Process(target=run_tunneld, args=())
    p.start()

    def sys_exit(status: int = 0):
        p.terminate()
        # 写入csv
        if len(csv_data_list) > 0:
            logger.info('--- 写入CSV')
            filename = 'ios-monitor-result-%d.csv' % int(time.time())
            filepath = os.path.join(os.getcwd(), filename)
            with open(filepath, 'w', encoding='UTF8', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=csv_fieldnames)
                writer.writeheader()
                writer.writerows(csv_data_list)
        
        logger.info(' --- 退出 ---')
        sys.exit(status)

    def signal_handler(*args, **kwargs):
        sys_exit(0)

    time.sleep(3)
    signal.signal(signal.SIGINT, signal_handler)

    addr, port = get_tunnel_addr(attemp_times=30)
    if not addr:
        sys_exit(1)

    logger.info("--- connect device: %s %d" % (addr, port))

    logger.debug('start run')
    rsd = RemoteServiceDiscoveryService(address=(addr, port))
    logger.debug('start rsd connect')
    asyncio.run(rsd.connect(), debug=True)
    time.sleep(1)
    logger.debug('get pid')
    pid = process_id_for_bundle_id(lockdown=rsd, app_bundle_identifier=bundle_id)
    logger.info('获取应用(%s)PID: %d' % (bundle_id, pid))

    while True:
        if not pid:
            pid = process_id_for_bundle_id(lockdown=rsd, app_bundle_identifier=bundle_id)
            logger.info('获取应用(%s)PID: %d' % (bundle_id, pid))
            time.sleep(0.3)
            continue

        try:
            sysmon_process_single(lockdown=rsd, pid=pid, detail=detail, report_host=report_host, write_csv=write_csv, tunnel_process=p)
            time.sleep(gap/1000)
        except Exception as err:
            logger.error('获取性能指标失败: {}'.format(err))
            addr, port = get_tunnel_addr(attemp_times=30)
            if not addr:
                sys_exit(1)

            pid = process_id_for_bundle_id(lockdown=rsd, app_bundle_identifier=bundle_id)
            logger.info('获取应用(%s)PID: %d' % (bundle_id, pid))

if __name__ == '__main__':
    multiprocessing.freeze_support()
    parser = argparse.ArgumentParser(description='ios设备性能收集', add_help=True)
    parser.add_argument('-g', '--gap', type=int, required=False, default=1000,
                        help='性能数据获取时间间隔(ms),默认1000ms')
    parser.add_argument('-b', '--bundle_id', required=False, default='com.mi.car.mobile',
                        help='包名, 默认: com.mi.car.mobile')
    parser.add_argument('-r', '--report_host', required=False, default='',
                        help='性能采集数据上报地址')
    parser.add_argument('--detail', default=False, action='store_true',
                        help='输出详细信息')
    parser.add_argument('--csv', default=False, action='store_true',
                        help='结果写入到CSV文件')
    parser.add_argument('--debug', default=False, action='store_true',
                        help='打印debug日志')

    args = parser.parse_args()

    if args.debug:
        logger.setLevel(logging.DEBUG)

    if not args.bundle_id:
        logger.error('bundle_id invalid')
        sys.exit(1)

    gap_ms = args.gap
    # 最低200ms间隔
    if gap_ms < 200:
        gap_ms = 200

    rpt_host = args.report_host
    # 上报到本机客户端
    if rpt_host in {'local', 'localhost', '*', '-', '9311'}:
        rpt_host = '127.0.0.1:9311'
    run_in_one(bundle_id=args.bundle_id, gap=gap_ms, detail=args.detail, report_host=rpt_host, write_csv=args.csv)

SwiftUI navigation stack 嵌套引起的导航错误

最近我们在推进 ModernNavigation 插件化架构时,遇到了一个 SwiftUI 开发中非常经典、但也极其容易让人抓狂的“幽灵问题”:嵌套 NavigationStack。 作为一名在 iOS 领域摸爬滚打多年的开发者,我深知这种架构层面的“小瑕疵”如果不彻底理清,后续会导致手势失效、双标题、甚至 Path 状态莫名丢失等一系列连锁反应。 今天我把这个问题深度复盘了一下,总结出了一套更符合我们 Redux 思想的解决方案。

案发现场:为什么你的 Navigation 崩了? 我们在做插件化时,为了让模块独立,经常会习惯性地在 AppRoute 的 body 里写下这段代码:

case .settings: NavigationStack { // 罪魁祸首在这里 SettingsView() }

当你从 HomeView(已经在一个 NavigationStack 内部)执行 router.push(.settings) 时,你就亲手制造了一个“栈中栈”。

症状分析:

  • 权限冲突:外层的 NavigationPath 想管进度,内层的 NavigationStack 也想管进度,SwiftUI 直接“摆烂”,导致侧滑返回可能直接回到 App 根部。
  • UI 灾难:你可能会在屏幕顶端看到两个叠加在一起的导航栏,或者是返回按钮莫名其妙消失。
  • Redux 状态断裂:内层栈的操作完全脱离了我们 NavigationStore 的掌控。

处方:区分“动作”而非“视图” 解决这个问题的核心思想只有一句话: Push 是一场“接力”, Sheet 是一场“派对”。

  • Push(推栈):它是当前导航流的延续,绝对不能自带 NavigationStack。
  • Sheet/Cover(弹窗):它开启了一个全新的、独立的导航流,必须自带 NavigationStack 来管理它自己的子页面。
  1. 给视图加点“环境感知” 为了让视图在“被推入”和“被弹出”时表现不同(比如弹窗时需要一个“关闭”按钮),我们引入一个 isModal 标识(不太优雅):

struct SettingsView: View { var isModal: Bool = false @Environment(NavigationStore<AppRoute, AppSheet>.self) private var navStore

var body: some View {
    List { ... }
    .navigationTitle("设置")
    .toolbar {
        if isModal {
            ToolbarItem(placement: .cancellationAction) {
                Button("关闭") { navStore.dispatch(.dismiss) }
            }
        }
    }
}

}

  1. 在路由层实现“插件化”分流 在我们的 RouteViewFactory 实现中,我们需要明确区分这两种场景。不需要写超大的 switch,而是让 Factory 能够根据上下文返回正确的包装:

struct UserRouteFactory: RouteViewFactory { func view(for route: Any) -> AnyView? { // 方案 A:通过路由类型区分 if let userRoute = route as? UserRoute { return AnyView(SettingsView(isModal: false)) // 纯净视图用于 Push }

    // 方案 B:通过特定的 Sheet 路由类型
    if let sheet = route as? UserSheet {
        switch sheet {
        case .settingsModal:
            return AnyView(
                NavigationStack { // 只有 Sheet 才包裹 Stack
                    SettingsView(isModal: true)
                }
            )
        }
    }
    return nil
}

}

深度思考:Redux 架构下的单向流 在 Redux 模式下,我们的 NavigationStore 应该对这种层级关系有清晰的定义:

  • path 数组:管理的是同一个 NavigationStack 内的线性增减。
  • presentedSheet:管理的是一个全新的 UIWindow 级别的层级。 为什么我们要这么做? 这样做最大的好处是导航状态的可预测性。 当你在处理 Deep Linking(深度链接)时,你可以清晰地在代码里写:

“先 Push 到用户中心,再从用户中心 Present 一个修改头像的弹窗。”

如果每个页面都自带 NavigationStack,这种跨层级的逻辑跳转将会是调试噩梦。

总结与更新 我们要对现有的导航包进行以下约定:

  • 所有模块暴露的 Push 视图必须是“裸”的(无 Stack)。
  • 模块可以定义专有的 SheetRoute,由对应的 Factory 负责包裹 NavigationStack。
  • 统一使用 isModal 或 Environment 变量来处理导航栏交互的差异。 这样一来,我们的 ReduxRouterStack 就能保持极其精简,同时具备极强的健壮性。

【swift开发基础】33 | 访问和操作数组 - 遍历和索引

一、访问和操作数组

1. 数组遍历

1)for-in循环

  • 基本用法:通过for num in numbers形式遍历数组元素,是Swift中最基础的遍历方式
  • 控制流支持:完整支持break和continue控制语句,可以灵活控制循环流程

2)forEach方法

  • 语法特点:采用闭包形式numbers.forEach { num in ... }进行遍历

  • 控制限制:

    • break/continue:无法使用这两个控制语句跳出或跳过循环
    • return行为:仅退出当前闭包执行体,不会终止整个遍历过程
  • 示例说明:当尝试在num == 3时执行break会导致编译错误,改为return则只会跳过数字3的输出

3)同时得到索引和值

  • enumerated()方法:

    • 返回(index, value)元组序列
    • 示例:for (index, num) in numbers.enumerated()
  • 替代方案:可通过0..<numbers.count区间遍历索引,再通过下标访问值

  • 推荐实践:相比手动索引访问,更推荐使用enumerated()方法,代码更简洁清晰

4)使用Iterator遍历数组

  • 实现步骤:

    • 通过makeIterator()获取迭代器
    • 使用while let配合next()方法遍历
  • 终止条件:当next()返回nil时循环自动结束

  • 适用场景:适合需要自定义遍历逻辑的情况,但日常开发中使用频率较低

2. 索引

1)startIndex

  • 特性:始终返回0,表示数组第一个元素的位置
  • 空数组情况:当数组为空时,startIndex等于endIndex

2)endIndex

  • 定义:返回最后一个元素索引+1的位置
  • 等价关系:对于数组而言等同于count属性值
  • 特殊说明:与String不同,数组的索引都是Int类型

3)indices

  • 功能:返回数组的有效索引区间(Range)
  • 遍历应用:可通过for i in numbers.indices形式遍历所有有效索引
  • 优势:比手动指定0..<count更安全可靠

3. 代码示例

1)forEach方法应用

  • 基础输出:成功输出数组[2,3,4,5,6,7]所有元素

  • 控制尝试:

    • break/continue会导致编译错误
    • return仅跳过当前元素(如跳过数字3)

2)enumerated方法应用

  • 输出格式:同时打印索引和元素值(如"the index is: 0 2")
  • 数值处理:示例中将元素值乘以10后输出

3)使用iterator遍历数组

  • 迭代过程:通过while let num = it.next()持续获取下一个元素
  • 终止机制:当next()返回nil时自动结束循环

4)使用索引区间遍历

  • 实现方式:for i in numbers.indices配合下标访问
  • 输出效果:与enumerated()方法输出结果相同

4. 最佳实践建议

  • 简单遍历:仅需元素值时优先使用for-in循环
  • 索引需求:需要同时访问索引和值时推荐使用enumerated()方法
  • 性能考虑:避免在循环体内进行不必要的数组操作,保持遍历高效性

二、知识小结

知识点 核心内容 易混淆点/注意事项 代码示例
for-in循环 基础遍历方式,可配合break/continue控制流程 与forEach方法的关键区别在于流程控制 for number in numbers { ... }
forEach方法 闭包式遍历,语法简洁 不支持break/continue,return仅退出当前闭包 numbers.forEach { if$0 == 3 { return } }
enumerated() 同时获取索引(index)和值(value) 等效于for i in 0..<count但更优雅 for (index, num) in numbers.enumerated()
迭代器遍历 通过makeIterator()和while let组合实现 需手动处理迭代终止条件(nil) while let num = numbers.makeIterator().next()
索引属性 startIndex=0,endIndex=count 空数组时startIndex == endIndex numbers.indices返回索引区间
索引区间遍历 使用indices属性获取合法索引范围 与显式写0..<count效果相同 for i in numbers.indices { numbers[i] }
❌