从Swift到AI:一个老iOSer的综合踩坑实录

MySQL修理工
2026-05-29 09:37
阅读 470

去年双11前夜,我们组还在疯狂压测新版本的购物车模块。突然监控报警——iOS端崩溃率飙升到3.2%。我当时正一边啃着便利店饭团,一边刷LeetCode准备跳槽面试,看到消息差点把筷子吞下去。

六年了,从Objective-C转战Swift,我几乎见证了这门语言从“玩具”变成生产力工具的全过程。但这次的问题,还真不是Swift本身的锅——而是我们在项目里强行塞进了一个“高大上”的AI图像分类组件,结果翻车翻得妈都不认。

起因:领导说要“智能化”

事情得从三个月前说起。产品老大在季度会上激情演讲:“竞品都上AI推荐了,我们APP还停留在‘猜你喜欢’?太土了!”于是,技术总监一拍大腿:“给商品详情页加个AI识图功能,用户拍张照,自动匹配相似商品。”

听起来很酷,对吧?但问题是——我们是个纯客户端团队,没人搞过模型部署、推理优化这些玩意儿。更惨的是,deadline就在双11前两周。

没办法,硬着头皮上。我作为组里资历最老的iOS开发(其实就是年纪最大),被推出来牵头这个“创新项目”。那会儿我白天写业务代码,晚上刷《动手学深度学习》,感觉自己像个缝合怪。

第一坑:模型太大,装不下

我们拿到的模型是PyTorch训练好的ResNet-50,导出成Core ML格式后整整280MB。你没看错,一个模型比很多轻量APP还大。

产品经理一脸天真地问:“能不能压缩一下?”
我说:“能啊,把准确率干到60%以下就轻松瘦身。”
他沉默了。

最后我们妥协:用知识蒸馏搞了个轻量版MobileNetV2,模型降到42MB。虽然准确率从92%掉到78%,但好歹能塞进包里了。这里有个小技巧:别直接用coremltools.convert(),手动指定minimum_deployment_target=ios15,能省下不少冗余算子。

// 别这么干!默认会包含大量低版本兼容代码
let model = try VNCoreMLModel(for: MyModel().model)

// 推荐:显式指定目标版本
let config = MLModelConfiguration()
config.computeUnits = .cpuAndGPU // 根据场景选,图像任务开GPU
let compiledURL = try MLModel.compileModel(at: modelURL, configuration: config)
let model = try MLModel(contentsOf: compiledURL)

第二坑:内存爆炸,杀后台

上线灰度后,测试同学反馈:“iPhone XR用户切后台回来,APP直接没了。”
我心想:不至于吧?查了下内存报告,好家伙——推理时峰值内存飙到1.4GB

原因很简单:我们一股脑把整张图喂给模型,而用户手机随便一拍就是4032×3024像素。Core ML内部会先把图片转成CVPixelBuffer,这个过程会吃掉巨量内存。

解决方案分两步:

  1. 预处理降分辨率:限制输入不超过1500px(实测对准确率影响<2%)
  2. 复用缓冲区:别每次推理都新建VNImageRequestHandler
class ImageClassifier {
    private var pixelBufferPool: CVPixelBufferPool?
    
    func classify(_ image: UIImage) -> Result<String, Error> {
        // 关键:重用pixel buffer,避免频繁alloc
        guard let pixelBuffer = createPixelBuffer(from: image, 
                                                 pool: &pixelBufferPool) else { 
            return .failure(MyError.bufferFailed) 
        }
        
        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        // ...后续推理
    }
}

第三坑:异步地狱,回调嵌套

为了不卡主线程,我把推理扔到了DispatchQueue.global(qos: .userInitiated)。结果发现——Vision框架本身已经是异步的!双重异步导致回调嵌套三层,代码丑得像意大利面条。

更坑的是,Vision的completion handler不在主线程,直接更新UI会Crash。当时我写了段自以为聪明的代码:

// 反面教材!别学我
DispatchQueue.global().async {
    let request = VNCoreMLRequest(...) { [weak self] req, err in
        DispatchQueue.main.async {
            self?.updateUI(...)
        }
    }
    // 执行请求...
}

后来重构时直接拥抱Combine(感谢公司终于升到iOS 13+):

func classifyPublisher(_ image: UIImage) -> AnyPublisher<ClassificationResult, Error> {
    Future { promise in
        let request = VNCoreMLRequest(model: self.mlModel) { req, err in
            if let err = err {
                promise(.failure(err))
                return
            }
            let result = req.results?.first as? VNClassificationObservation
            promise(.success(result.map(ClassificationResult.init)!))
        }
        // 同步执行Vision请求(内部已异步)
        try? VNImageRequestHandler(cgImage: image.cgImage!).perform([request])
    }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
}

综合权衡:业务价值 vs 技术炫技

折腾一个月后,数据来了:AI识图功能的日活使用率0.7%。而为此多花的开发成本、包体积增量、崩溃率上升……简直血亏。

但也不是全无收获。这次踩坑让我意识到:技术探索必须和业务场景深度结合。后来我们调整策略:

  • 把AI能力下沉到服务端,客户端只传图
  • 针对高价值场景(比如奢侈品鉴定)才启用本地模型
  • 加入“智能兜底”:当本地模型置信度<85%时,自动走云端API
方案 包体积增量 内存峰值 准确率 用户使用率
纯客户端 +42MB 1.4GB 78% 0.7%
纯服务端 +0MB 0.3GB 91% 2.1%
混合方案 +8MB 0.6GB 89% 3.5%

现在回头看,这次项目虽然开局像灾难片,但逼我补齐了AI工程化的短板。最近面试时聊到这个经历,反而成了加分项——毕竟会调API的人很多,但懂端侧部署痛点的iOS开发者不多。

写在最后

前几天和猎头聊天,他说现在大厂招iOS都要求“了解端侧AI”。我苦笑:谁不是被需求逼着成长呢?

技术人的宿命大概就是——永远在填昨天的坑,同时挖今天的坑。但每次从崩溃日志里爬出来时,那种“老子又活下来了”的快感,大概就是支撑我们熬夜debug的燃料吧。

PS:如果你也在搞iOS+AI,记住我的血泪教训——先跑通MVP,再谈优化。别一上来就追求SOTA模型,用户要的是能用的功能,不是论文指标。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝