阅读视图

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

聊聊组件化案例

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] }
❌