阅读视图

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

AI编程时代解决bug的新业态

本文是想通过一个例子来讲述,AI在修复Bug方面令人惊艳的能力。

一、传统方式下

先来看一个Crash日志的堆栈信息:

Termination Reason:<RBSTerminateContext| domain:10 code:0x8BADF00D 
explanation:scene-create watchdog transgression: application<com.xxx.aaa>:
34689 exhausted real (wall clock) time allowance of 3.43 seconds

// 
Thread 0 Crashed:
0      libsystem_pthread.dylib       _pthread_mutex_lock$VARIANT$armv81 + 120
1      libc++.1.dylib                std::__1::mutex::lock() + 12
2      libicucore.A.dylib            icu::Locale::getDefault() + 32
3      libicucore.A.dylib            icu::Locale::init(char const*, signed char) + 1400
4      libicucore.A.dylib            _ures_getLocaleByType + 436
5      libicucore.A.dylib            icu::DecimalFormatSymbols::initialize(icu::Locale const&, UErrorCode&, signed char, icu::NumberingSystem const*) + 256
6      libicucore.A.dylib            icu::DecimalFormatSymbols::DecimalFormatSymbols(icu::Locale const&, icu::NumberingSystem const&, UErrorCode&) + 236
7      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 4608
8      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::getDecimalFormatSymbols() const + 1632
9      libicucore.A.dylib            icu::number::LocalizedNumberFormatter::formatImpl(icu::number::impl::UFormattedNumberData*, UErrorCode&) const + 128
10     libicucore.A.dylib            icu::SimpleDateFormat::zeroPaddingNumber(icu::NumberFormat const*, icu::UnicodeString&, int, int, int) const + 524
11     libicucore.A.dylib            icu::SimpleDateFormat::subFormat(icu::UnicodeString&, char16_t, int, UDisplayContext, int, char16_t, icu::FieldPositionHandler&, icu::Calendar&, UErrorCode&) const + 904
12     libicucore.A.dylib            icu::SimpleDateFormat::_format(icu::Calendar&, icu::UnicodeString&, icu::FieldPositionHandler&, UErrorCode&) const + 688
13     libicucore.A.dylib            icu::SimpleDateFormat::format(icu::Calendar&, icu::UnicodeString&, icu::FieldPosition&) const + 80
14     libicucore.A.dylib            icu::DateFormat::format(double, icu::UnicodeString&, icu::FieldPosition&) const + 124
15     libicucore.A.dylib            _udat_format + 356
16     CoreFoundation                ___cficu_udat_format + 64
17     CoreFoundation                _CFDateFormatterCreateStringWithAbsoluteTime + 180
18     Foundation                    -[NSDateFormatter stringForObjectValue:] + 160
19     MyAPP                        -[HAMLaunchMonitor startUUID] + 136
...    MYApp                               其它调用函数

传统解决步骤:

  1. 查看HAMLaunchMonitor中startUUID方法里面内容,看代码上有没明显bug;
  2. 查看调用NSDateFormatter的string...方法的方式是否有问题;
  3. 后面是系统堆栈,所以得出结论是系统库发生了长时间等锁;
  4. 查看崩溃说明是0x8BADF00D,确定是被系统WatchDog检测到杀死了进程导致的崩溃。
  5. 解决方式:少量偶现且逻辑不好修改,出临时性修复方案~
  6. 问题可能没有根本解决,只是概率降低了。

可以看到我们人工查看这个问题有几个瓶颈:

  1. 需要手动找到文件查看分析函数调用链,速度慢。
  2. 对于顶部的系统函数,我们一般是缺乏了解的,不知道里面发生了什么导致出了问题。
  3. 弄清楚了上面内容后,我们的解决方法也是基于自己认知的,存在偏差。

而这几个瓶颈,交给AI后那都不是事儿。

二、AI时代下Bug修复

复制堆栈信息交给AI工具(我目前用的是Qoder),AI直接给出了根本原因及有效的方案:

Crash 类型

这是一个 Watchdog 超时 Crash0x8BADF00D = "ate bad food"),发生在 scene-create(应用启动/场景创建) 阶段。系统允许的时间为 3.43 秒,但应用在该时间内未能完成启动。

根因分析

从调用栈来看,崩溃发生在 [-[HAMLaunchMonitor startUUID]] 方法中:

- (NSString *)startUUID { 
    NSDateFormatter *formatter = [NSDateFormatter new];
    formatter.dateFormat = @"yyMMddHHmmssSSS";
    formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 
    NSString* time = [formatter stringFromDate:[NSDate date]]; 
    // ... 
} 

主线程被阻塞在 pthread_mutex_lock,具体路径是:

  1. startUUID 调用 NSDateFormatter stringForObjectValue: 进行日期格式化
  2. 底层 ICU 库执行 SimpleDateFormat::format 进行实际格式化
  3. ICU 在格式化数字时需要获取 DecimalFormatSymbols
  4. 这触发了 icu::Locale::getDefault() 来获取系统默认 locale
  5. getDefault() 内部需要获取一个 全局互斥锁(std::mutex
  6. 该锁被其他线程持有,导致主线程死等(pthread_mutex_lock 卡住)

核心问题

锁竞争(Lock Contention) —— ICU 库内部有一个全局的 std::mutex 保护 Locale::getDefault()。在应用启动阶段,如果有多个线程同时使用 NSDateFormatterNSNumberFormatter 或其他依赖 ICU 的 API,就会争抢这把锁。主线程被阻塞等锁,累计时间超过 watchdog 限制,系统杀掉了应用。

加剧因素

  1. 启动阶段调用 startUUID:该方法在 [commonParameters] 中被调用,而 commonParameters 在 [appDidFinishLaunch] 流程中执行——正处于 watchdog 监控最严格的时间窗口。
  2. 每次调用都创建新的 NSDateFormatterNSDateFormatter 的初始化本身就是重量级操作,且会触发 ICU locale 的初始化。
  3. ICU 内部的全局锁:这是系统库的已知问题,在 iOS 启动早期阶段锁竞争尤为严重,因为各种系统框架也在并发初始化。

建议修复方案

  1. 缓存 NSDateFormatter(最直接有效):将 NSDateFormatter 创建为静态变量或单例属性,避免每次调用 startUUID 都重新创建:
- (NSString *)startUUID {
 static NSDateFormatter *formatter = nil;
  static dispatch_once_t onceToken; 
  dispatch_once(&onceToken, ^{
   formatter = [NSDateFormatter new]; 
   formatter.dateFormat = @"yyMMddHHmmssSSS"; 
   formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; 
  }); 
   NSString *time = [formatter stringFromDate:[NSDate date]]; 
   NSString *startUuid = [NSString stringWithFormat:@"%@2000000%@", time, [self getRandomNumber]]; 
   return startUuid; 
 } 
  1. 延迟执行:将 startUUID 和上报逻辑延迟到首帧渲染之后,脱离 watchdog 监控的关键时间窗口。
  2. 替代方案:如果仅需要生成唯一标识,可以完全避免 NSDateFormatter,改用时间戳数字拼接: 这完全绕开了 ICU/NSDateFormatter,不存在锁竞争问题。
- (NSString *)startUUID { 
NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; 
long long ms = (long long)(now * 1000); 
NSString *startUuid = [NSString stringWithFormat:@"%lld2000000%@", ms, [self getRandomNumber]]; 
return startUuid;
 } 

推荐方案 3,因为它从根本上消除了 ICU 锁竞争的风险,同时性能开销最小。

三、感想

AI时代,整体APP行业的性能将得到大幅提升

优秀大模型解决Bug的能力很强,且修复速度和质量都发生了颠覆性的变化,意味着如果有规划+APP开发人员有心,大部分问题都能解决;AI编程工具下,目前AI写复杂功能可能还有点问题,但如果是指定AI去写一个特定功能bug率可能会比资深工程师还要好;借助AI来深入了解底层知识也很方便,对于提升工程师认知也有帮助,进一步提升了性能。

AI时代,Bug的解决方式会发生变化

现在的热修复功能集成到APP后,往往需要编写修复后的脚本语言文件,下发到APP,APP动态运行时交换方法实现解决。

AI时代的方式可能是:
-》Crash发生后,自动分析原因,出解决方案,发出通知;
-》人工收到通知后,选择一个方案;
-》自动生成对应的脚本文件,自动下发到对应的APP版本。
-》APP再次打开时,Bug已经自动修复。

Swift 并发编程深度解析:从 async/await 到智能调度

深入理解 Swift 5.5+ 的现代并发模型,掌握如何编写安全高效的多线程代码


引言:为什么需要新的并发模型?

在传统 iOS/macOS 开发中,我们使用 GCD(Grand Central Dispatch)或 OperationQueue 来处理并发任务。然而,这些技术存在一些痛点:

  1. 回调地狱:多层嵌套的回调难以阅读和维护
  2. 手动内存管理:容易忘记 weak self 导致内存泄漏
  3. 线程爆炸:过度创建线程消耗系统资源
  4. 数据竞争:共享状态需要手动加锁,容易出错

Swift 5.5 引入的 async/await 和结构化并发解决了这些问题,提供了更安全、更简洁的并发编程方式,iOS 13以上是支持的。


第一部分:async/await 基础语法

1.1 异步函数声明

// 传统回调方式
func fetchUser(completion: @escaping (Result<User, Error>) -> Void)

// 异步函数方式
func fetchUser() async throws -> User

1.2 异步函数调用

// 使用 await 调用异步函数
do {
    let user = try await fetchUser()
    print("用户: \(user.name)")
} catch {
    print("错误: \(error)")
}

第二部分:async let 与结构化并发

2.1 并发启动多个任务

// 同时启动多个异步任务
func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()          // 立即开始
    async let orders = fetchOrders()      // 立即开始
    async let messages = fetchMessages()  // 立即开始
    
    // 等待所有任务完成
    return try await Dashboard(
        user: user,
        orders: orders,
        messages: messages
    )
}

2.2 与顺序执行的对比

// 并发执行(总耗时 ≈ 最慢的任务)
async let a = taskA()  // 0-1秒
async let b = taskB()  // 0-2秒
let results = await (a, b)  // 总耗时: 2秒

// 顺序执行(总耗时 = 所有任务时间之和)
let a = await taskA()  // 0-1秒
let b = await taskB()  // 1-3秒(等A完成后才开始)
// 总耗时: 3秒

2.3 重要概念澄清

Q: async let user = fetchUser() 立即返回什么? A: 它不立即返回数据,而是返回一个异步任务句柄。实际数据在 await 时获取。

Q: 多个 async let 相当于 GCD 的异步任务吗? A: 相似但有重要区别。async let 是结构化并发的一部分,任务生命周期自动管理,支持取消和错误传播。


第三部分:数据安全与线程调度

3.1 数据竞争的解决方案

方案一:使用 Actor(银行柜台模型)

actor UserCache {
    private var storage: [String: User] = [:]
    
    func getUser(id: String) -> User? {
        return storage[id]
    }
    
    func setUser(_ user: User, for id: String) {
        storage[id] = user
    }
}

// 使用时自动序列化访问
let cache = UserCache()
let user = await cache.getUser(id: "123")  // 自动排队等待

原理:编译器强制同一时间只有一个任务能访问 Actor 内部状态,通过消息传递模型确保安全。

方案二:使用值语义(发复印件模型)

struct UserProfile {
    let user: User
    var settings: Settings
    // 结构体是值类型,复制安全
}

func processProfile(profile: UserProfile) async {
    // 每个任务获取独立的副本
    async let task1 = {
        var copy = profile
        copy.settings.theme = .dark
        return copy
    }()
    
    async let task2 = {
        var copy = profile
        copy.settings.fontSize = 16
        return copy
    }()
    
    let results = await (task1, task2)  // 独立修改,互不影响
}

原理:通过复制而非共享,从根本上消除数据竞争的可能性。

3.2 智能线程调度

Q: async let 任务在哪个线程执行? A: Swift 并发运行时智能决定,基于以下因素:

  1. 当前线程负载 - 太忙就调度到其他线程
  2. 任务类型 - I/O密集型 vs CPU密集型
  3. 优先级 - 高优先级任务可能更快执行
  4. 硬件资源 - CPU核心数、当前负载
  5. 执行器约束 - 如 @MainActor 强制主线程

智能调度的具体表现:

@MainActor
func updateUIWithData() async {
    // 从主线程调用,但会自动优化
    async let data = fetchHeavyData()  // 运行时:这个会阻塞 → 调度到后台线程
    
    let processed = await process(data)  // 可能在后台线程继续处理
    
    // 更新UI时自动回到主线程
    self.label.text = processed.title
}

3.3 什么时候需要显式控制线程?

// 1. UI操作必须主线程
@MainActor
func updateUI() {
    // 编译时确保在主线程
}

// 2. CPU密集型长时间计算
func processImage(_ image: UIImage) async -> UIImage {
    // 明确指定在独立线程执行
    return await Task.detached {
        return image.applyFilters()  // 耗时的图像处理
    }.value
}

// 3. 不应该干预的案例
// ❌ 不要这样:破坏了智能调度
Task {
    DispatchQueue.global().async {
        await someAsyncWork()
    }
}

// ✅ 应该这样:信任运行时
Task {
    await someAsyncWork()  // 让系统决定最佳执行方式
}

第四部分:实际应用模式

4.1 网络请求组合

class UserService {
    func loadFullProfile(userId: String) async throws -> FullProfile {
        // 并发获取所有数据
        async let userInfo = fetchUserInfo(userId)
        async let posts = fetchUserPosts(userId)
        async let friends = fetchUserFriends(userId)
        async let preferences = fetchUserPreferences(userId)
        
        // 等待所有结果
        return try await FullProfile(
            info: userInfo,
            posts: posts,
            friends: friends,
            preferences: preferences
        )
    }
    
    // 对比传统回调方式
    func loadFullProfileOld(userId: String, 
                           completion: @escaping (Result<FullProfile, Error>) -> Void) {
        fetchUserInfo(userId) { result1 in
            switch result1 {
            case .success(let userInfo):
                self.fetchUserPosts(userId) { result2 in
                    switch result2 {
                    case .success(let posts):
                        // 更多嵌套...
                    case .failure(let error):
                        completion(.failure(error))
                    }
                }
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

4.2 限制并发数量

func downloadMultipleFiles(urls: [URL], maxConcurrent: Int = 4) async throws -> [Data] {
    // 使用 TaskGroup 控制并发数
    return try await withThrowingTaskGroup(of: Data.self) { group in
        var results: [Data] = []
        results.reserveCapacity(urls.count)
        
        // 分批处理,限制并发数
        for index in urls.indices {
            if group.taskCount >= maxConcurrent {
                // 等待一个任务完成再添加新的
                if let result = try await group.next() {
                    results.append(result)
                }
            }
            
            group.addTask {
                return try await downloadFile(from: urls[index])
            }
        }
        
        // 收集剩余结果
        for try await result in group {
            results.append(result)
        }
        
        return results
    }
}

第五部分:与系统框架的集成

5.1 iOS 13+ 的系统 API 更新

// iOS 13+ 提供了异步版本的 openURL
func openSettings() async -> Bool {
    guard let url = URL(string: UIApplication.openSettingsURLString) else {
        return false
    }
    
    return await UIApplication.shared.open(url)
}

// 使用示例
Task {
    let success = await openSettings()
    print("设置应用打开\(success ? "成功" : "失败")")
}

// 为什么使用Task?
// ❌ 错误:不能在同步函数中直接使用 await
func buttonTapped() {
    let success = await openSettings()  // 编译错误!
    print("结果: \(success)")
}

// ✅ 正确:需要 Task 包装
func buttonTapped() {
    Task {  // 创建异步执行环境
        let success = await openSettings()
        print("结果: \(success)")
    }
}

5.2 适配旧版本系统

// 为 iOS 13+ 提供兼容方案
func openURL(_ url: URL) async -> Bool {
    if #available(iOS 13.0, *) {
        return await UIApplication.shared.open(url)
    } else {
        // 使用 continuation 桥接到 async/await
        return await withCheckedContinuation { continuation in
            UIApplication.shared.open(url) { success in
                continuation.resume(returning: success)
            }
        }
    }
}

第六部分:最佳实践总结

6.1 代码组织原则

  1. 优先使用 async/await 替代回调
  2. 合理使用 async let 进行并发,但注意数量控制
  3. 使用 Actor 保护共享状态,避免手动锁
  4. 尽量使用值类型,减少共享可变状态

6.2 架构设计建议

// 推荐的层次结构:
// UI层 (@MainActor) - 处理用户交互和界面更新
// 业务层 (混合) - 协调数据流,处理业务逻辑
// 数据层 (async/await) - 网络请求、数据库操作
// 工具层 (值类型) - 纯函数计算、数据处理

@MainActor
class ViewController: UIViewController {
    private let viewModel: UserViewModel
    
    func loadData() async {
        await viewModel.loadUserData()
        updateUI()
    }
}

actor UserViewModel {
    private let repository: UserRepository
    
    func loadUserData() async {
        let user = await repository.fetchUser()
        // 处理业务逻辑
    }
}

class UserRepository {
    func fetchUser() async throws -> User {
        // 数据层操作
        return try await apiClient.fetchUser()
    }
}

第七部分:内部原理机制

Swift的async/await基于协程实现: 技术关系:

// 1个线程上可以运行多个协程
Thread A: [协程1运行] → [协程2运行] → [协程1恢复] → [协程3运行]
                ↑           ↑           ↑           ↑
           遇到await挂起 遇到await挂起 结果返回恢复 遇到await挂起

// 协程在挂起时释放线程,让其他协程使用

// 传统线程 vs 协程

// 线程:操作系统调度,上下文切换成本高
Thread 1: [运行] → [阻塞等待I/O] → [运行]
Thread 2: [等待] → [运行] → [等待]

// 协程:用户态调度,轻量级
协程 A: [运行] → [挂起] → [运行]
协程 B:     [运行] → [挂起]
// 在同一线程上交替执行,没有线程切换开销

结合实际代码说明:

// 规则1:一个协程必须在一个线程上运行
// 规则2:协程只能在特定点挂起(await处)
// 规则3:挂起的协程不占用线程

// 示例:
func fetchMultipleResources() async {
    // 开始:在主线程运行(如果从@MainActor调用)
    
    let data1 = await fetchData()  // 挂起点1
    // 挂起:释放主线程,其他协程可用
    
    // 恢复:可能在任意线程(不一定是主线程)
    process(data1)  // 在某个后台线程执行
    
    let data2 = await fetchData()  // 挂起点2
    // 再次挂起...
    
    // 最后如果需要更新UI,要确保在主线程
    await MainActor.run {
        updateUI(data1, data2)
    }
}

结语

Swift 的现代并发模型代表了并发编程的范式转变:

  1. 从手动调度到智能调度 - 信任运行时做出最优决策
  2. 从回调地狱到线性代码 - 使用 async/await 简化异步流程
  3. 从容易出错到内存安全 - 通过 Actor 和值语义避免数据竞争
  4. 从复杂管理到结构化 - 自动处理任务生命周期和取消

虽然学习曲线比 GCD 更陡峭,但一旦掌握,你将能编写出更安全、更简洁、更高效的并发代码。


进一步学习资源:

❌