这两年做移动端AI,绕不开一个东西:ML Kit。它是目前Android和iOS上把设备端AI能力封装得最顺手的SDK之一,扫码、OCR文本识别、人脸检测、对象检测、自拍分割——几乎你能想到的常见视觉需求,它都提供了开箱即用的API。比起自己搭一套云端模型服务,用ML Kit最大的价值在于:模型推理在手机本地完成,不依赖实时网络,响应快、隐私好、成本几乎为零。这篇文章我打算从一个接过几个移动端AI项目的开发者角度,聊聊ML Kit的选型逻辑、接入步骤、常见坑,以及它和TFLite、MediaPipe这类方案的分工边界,给准备上设备端AI的团队一条可以直接参考的落地路线。
如果你只是想给自己的App快速加一个扫码能力,或者想把文本识别做进离线场景,那ML Kit基本是今天所有主流方案里门槛最低的一个。但门槛低不等于没有坑。我见过不少项目用了半年ML Kit,最后因为模型分发方式选错、生命周期没管好,性能翻车或者包体积暴涨。所以这篇文章不只是讲API,更多是想把"为什么这么用"讲清楚。
1. 设备端AI的取舍逻辑:ML Kit解决的是哪一类问题
1.1 从服务器搬到手机:成本、延迟和隐私的三重账
先算一笔账。假设要做一个扫码识别功能,云端方案是:客户端把图片传上去,后端调用识别接口,再把结果返回。单次调用的成本可能不高,但日活10万、每人每天扫3次,一天就是30万次请求,一个月下来账单并不好看。更麻烦的是用户网络一波动,识别就卡住,弱网环境下体验非常糟糕。还有一个容易被忽略的点:扫码图片里往往包含地址、手机号、支付二维码这些敏感信息,传到服务器会带来额外的合规压力。
设备端AI把这些全部绕开了。模型打包在App里,或者通过一次下载预置在设备上,识别过程完全在手机本地完成。单次推理通常只有几十毫秒,成本几乎为零,没有网络延迟,也没有图片上传的隐私问题。代价是模型体积会占存储空间,移动设备的算力也做不了云端那种千亿参数大模型,所以设备端AI更适合解决的是"轻量、高频、实时"的问题,而不是"复杂、通用、一次性"的问题。想明白这个边界,后面选型才不会跑偏。
1.2 ML Kit在移动AI生态里的真实定位
把设备端AI的工具链铺开看,大致有三层。最底层是推理引擎,比如TFLite,它负责把模型文件加载进来,在CPU、GPU或者NPU上执行张量运算;中间是流水线框架,比如MediaPipe,它帮你编排整个视频流的处理链路;最上层才是面向业务的应用SDK,ML Kit正好站在这个位置。
ML Kit并没有发明新的模型,它做的是把一系列成熟模型、前后处理逻辑、硬件加速调度都封装成一份简单到不能再简单的API。开发者不用关心模型是用什么框架训练的、要不要量化、怎么转换、底层跑在哪个加速器上,只需要拿到一个client对象,喂一张图片进去,等回调结果就行。这正是它和TFLite的核心区别:TFLite给你的是"一条可以跑的管道",你需要自己去铺管;ML Kit给你的是"装好水龙头的成品",拧开就能用。
提示:ML Kit不是替代TFLite,而是基于TFLite的上层封装。它省掉的是重复的样板代码,不是底层的能力。真到了需要自定义模型的时候,ML Kit也提供自定义模型接入通道,底层走的仍然是TFLite,只是外壳统一了。看清楚这一点,后面你就会明白为什么TFLite和ML Kit不是二选一的关系,而是可以互相配合的两层工具。
这个定位决定了它的优劣势。优势在于上手极快,两周能做出一套能演示的AI功能;劣势在于你能调整的空间有限,内置模型是"大众款",碰到特别冷门的业务场景,只能选择往下一层做定制。
2. ML Kit核心能力拆解:按使用频率排序
2.1 天天会碰到的视觉API:条码扫描与文本识别
条码扫描应该算ML Kit世界里使用率最高的API。它的输入是一帧带条码的图片,输出是识别到的条码内容以及位置信息:rawValue保存原始数据,format告诉你这是什么类型的码,boundingBox给出条码外框,cornerPoints给出四个角点。支持QR码、Code 128、EAN-13、UPC-A这些常见格式,覆盖面很广。
这里有一个经验:构造BarcodeScannerOptions时,尽量指定你要的条码格式,不要用默认的全格式扫描。全格式意味着引擎会尝试匹配所有格式,识别速度会明显下降。之前排查一次线上扫码慢的问题,最后发现就是没限制格式导致的,改成只保留QR_CODE之后,延迟体感直接降了一半以上。
文本识别(OCR)是另一个高频需求,识别结果是一个三级结构:TextBlock、Line、Element。它不只是做"把文字抓出来"这一个动作,连每一行文字在图片里的坐标信息都会一起返回,这对后续要做版面分析、裁剪、框选的操作很有价值。注意它有两种语言包,纯英文场合用Latin,中文场合用Chinese,别拿一个只装了Latin的模型去识别中文,效果会差到怀疑人生。
2.2 更进阶的视觉能力:人脸检测、对象检测与图像分割
人脸检测API做的不是人脸比对,而是从图中找出人脸位置、检测关键点和一些基础属性:是否微笑、眼睛是否睁开、头部姿态、左右偏转角度。这个能力经常被做成美颜相机里"找脸"的底层,也可以用来做驾驶场景的注意力检测。关键点列表里有眼睛、鼻子、嘴巴等位置的坐标,组合起来能算很多有趣的特征量。
对象检测API就泛化一些,它可以在同一张图里框出多个物体,同时给出分类标签和置信度,底层是一个轻量级的目标检测模型。像翻拍识别、货架商品识别这类场景,直接用内置对象检测API跑一轮,基本能出个能用的原型。图像分割API则把人像从背景里抠出来,输出一个像素级的mask,自拍背景替换、会议虚拟背景都是这个能力。
还有一个容易被忽略的是姿态估计API,它返回一个人体骨架的关键点坐标,包括手腕、脚踝、肩膀这些位置,适合做健身计数、动作对比。这类API的模型在普通手机上运行都没有明显压力,帧率取决于输入分辨率,我一般会把输入图片缩到640x640以内,在精度和速度之间取一个平衡。
2.3 自定义模型通道:从"用别人的模型"过渡到"跑自己的模型"
ML Kit也有自定义模型API,它允许你加载一个自己训练并转换成TFLite格式的模型,然后用和内置API差不多的工作方式去推理。这意味着团队里有算法同学的话,可以先在服务端把模型训练好,再通过工具链量化、转换,最终交给ML Kit统一调度。
这个通道的价值不在于性能,而在于架构的一致性。模型分发、加载、生命周期管理这些脏活都是同一套代码,你自己只需要聚焦在模型的输入输出张量上。实际操作中,自定义模型通道适合两种场景:要么是内置模型不满足业务指标,要么是确实有垂直领域的数据需要训练一个专属模型。前者是能力不足,后者是业务独有,这两类需求只要沾上其中一条,自定义模型通道就是比内置API更合适的答案。
注意:虽然ML Kit提供自定义模型通道,但不要一上来就自训模型。先用内置API把流程跑通,验证业务真的有这个需求,再考虑定制,这是最经济的顺序。很多团队跳过了这一步,直接从一个模型Demo开始做,最后产品方向变了,模型要重做,时间全丢掉。先把业务风险降下来,再动算法。
至于翻译、实体提取这类文本能力,新版ML Kit也有覆盖,只是移动端项目里实际用得不如视觉API多,而且这部分API迭代较快、不同版本差异很大。我的建议是:要用之前一定去官方最新文档把能力清单和支持区域看一遍,不要拿着旧博客的经验直接套。而且这类API在不同平台的命名和覆盖范围也有差异,某些能力在Android上稳定,在iOS上却是另一套实现,跨端项目必须提前确认,否则做到一半发现某一端缺失,返工成本就大了。把确认能力清单当成需求评审的一环,而不是写代码时才去看,能省很多事。
3. 集成前的关键选型:两种模型分发方式怎么定
3.1 本地捆绑与动态下载的机制区别
ML Kit的依赖包其实提供了两种模型分发机制。一种叫本地捆绑,模型文件会在编译期直接打进APK或IPA里,安装App之后模型就已经躺在设备上了,之后无论有没有网络都能正常推理;另一种叫动态下载,App本身不带模型,首次调用API时才从远程把它拉取下来,模型实际存放在设备的一个共享模块里。
这两种方式肉眼可见的差别主要在四方面。第一是包体积:捆绑模式会让APK涨几MB到几十MB不等,动态下载则对安装包大小几乎没影响。第二是首次使用体验:捆绑模式装完即用,动态下载第一次调用可能要看网络脸色,运气不好会等上好几秒。第三是离线可用性:捆绑模式理所当然离线可跑,动态下载则要求模型已经预下载完成,否则断网状态直接不可用。第四是设备兼容性:动态下载方案要求设备有可用的Play服务环境用来下载和托管模型,没有这个环境时,这条路基本走不通。
我把两种方式的关键差异整理成对照表:
| 对比维度 | 本地捆绑(bundled) | 动态下载(unbundled) |
|---|---|---|
| 安装包体积 | 增加几MB到几十MB | 几乎无影响 |
| 首次调用速度 | 装完即用,无下载等待 | 首次使用需先拉取模型 |
| 离线使用 | 完整支持 | 需要先完成模型下载 |
| 外部环境依赖 | 无 | 依赖Play服务与网络 |
| 适合场景 | 对离线/稳定性要求高的业务 | 包体积敏感且环境受控的场景 |
3.2 我的选型建议:默认本地捆绑,再谈优化
按我实际做项目的习惯,新功能起步阶段一律选本地捆绑。原因很简单:模型只有真正跑起来,你才知道业务能不能转起来,这阶段最怕的就是另一个环节出问题干扰判断。本地捆绑模式让模型的可用性变成一个确定因素,排错会轻松很多。
等到功能稳定了,再去考虑包体积和用户下载成本这类事情。如果目标设备都能正常使用Play服务,且团队能接受"部分用户首次打开AI功能时有一个模型加载过程",再切换到动态下载版本。这里还可以做一个过渡策略:用动态下载模式,但在应用启动后台做一次静默预下载,把首次触达的卡顿前移到启动阶段绕过去。
万一遇到一个两难情况:既对包体积敏感,又有相当比例的设备不能用Play服务,那我会选择保留本地捆绑,同时在构建时按渠道拆开APK,用App Bundle技术让不同设备只下载必要的模型,这是一种常见的折中方案。这个方案能让主流渠道用户获得更小的下载体积,又不牺牲老设备的可用性。核心原则一直是:模型加载失败绝不能成为一个高频错误,它一旦高频发生,再好的识别效果也救不了这个功能的口碑。
4. Android集成实操:从依赖配置到一条实时扫码链路
4.1 环境准备与依赖配置
先说Android端,ML Kit的依赖通常长这样:
dependencies { // 本地捆绑模式的条码扫描 implementation("com.google.mlkit:barcode-scanning:17.3.0") // 本地捆绑模式的文本识别(中文) implementation("com.google.mlkit:text-recognition-chinese:16.0.1") }如果切到动态下载模式,把依赖坐标换成另一组以play-services-mlkit开头的包,并且确保设备环境支持Play服务。具体版本号请直接看官方最新Release页,我这里只展示配置方式,实际用的版本以稳定版为准。
iOS这边则是CocoaPods:
pod 'GoogleMLKit/BarcodeScanning' pod 'GoogleMLKit/TextRecognitionChinese'环境配置这一步容易踩的坑是只加依赖不处理权限。Android上要动态申请相机权限,Android 13以后还细分了CAMERA与READ_MEDIA_IMAGES,扫码Demo通常只需要CAMERA。另外,如果项目开启了资源缩减,要确认没有把模型资源误删,这类问题表现起来很隐蔽,运行时总是报模型加载失败,但日志里看不出跟资源缩减有什么关系。
4.2 用CameraX做实时视频流分析
实时扫码最成熟的组合是CameraX的ImageAnalysis加ML Kit。CameraX负责接管相机预览和帧回调,ML Kit负责对每一帧做条码检测。一个完整的分析器可以这样写:
class BarcodeAnalyzer( private val onResult: (String) -> Unit ) : ImageAnalysis.Analyzer { private val options = BarcodeScannerOptions.Builder() .setBarcodeFormats(Barcode.FORMAT_QR_CODE) .build() private val scanner = BarcodeScanning.getClient(options) override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image val rotation = imageProxy.imageInfo.rotationDegrees if (mediaImage != null) { val inputImage = InputImage.fromMediaImage(mediaImage, rotation) scanner.process(inputImage) .addOnSuccessListener { barcodes -> for (barcode in barcodes) { barcode.rawValue?.let { content -> onResult(content) } } } .addOnCompleteListener { imageProxy.close() } } else { imageProxy.close() } } fun release() { scanner.close() } }然后在Activity里把它绑定到生命周期上:
val analysis = ImageAnalysis.Builder() .setTargetResolution(Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(executor, BarcodeAnalyzer { content -> runOnUiThread { resultText.text = "识别结果:$content" } }) val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val preview = Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis ) }, ContextCompat.getMainExecutor(this))这段代码里有几个细节很重要。第一,InputImage.fromMediaImage的第二个参数rotation一定不要写死,要用imageProxy.imageInfo.rotationDegrees。不同机型的传感器方向不一样,写死90度的话在横屏设备或者前置摄像头上,坐标全对不上,画出来的框会歪到屏幕外面去。第二,imageProxy.close()一定要保证执行,我把它放在addOnCompleteListener里,就是为了无论识别成败都能释放帧,否则长期运行会迅速吃满内存。第三,setTargetResolution(Size(1280, 720))不是随便设的,识别不需要4K分辨率,720p已经足够,分辨率越高帧处理越慢,发热和掉帧都跟着来。
4.3 结果解析与检测效果调优
识别回调返回的是一个Barcode列表,每条包含四个常用字段。rawValue是条码里的原始字符串,比如二维码里的链接或一串JSON;format表示条码类型,用来判断这条数据可不可信;boundingBox是外框;cornerPoints则是更精确的四个角点,适合做绘制和对齐。实际项目里最常见的需求是拿到rawValue后塞进业务流程,其余字段更多用在上层UI绘制上。
检测效果调优我建议按优先级做三件事。第一,限制条码格式范围,至少能换回一半以上的性能提升;第二,减小输入帧分辨率,控制在720p级别就够,别让模型去处理多余像素;第三,复用Scanner实例,把它做成分析器类的成员变量,而不是每次回调都重新构建。每次构建BarcodeScanning的代价比大多数人想象的高,频繁构建会触发不必要的模型加载和初始化。
还有一类容易被忽视的问题是生命周期。Scanner保留到页面销毁时一定要调用close(),事件结束后也要cameraProvider.unbindAll(),否则相机资源和模型资源都不会自动释放。下一次再进页面的体验会明显变卡,时间长了还会伴随各种诡异的回调报错,像是"Camera is closed"这种。这些都不是AI算法问题,是资源管理没做到位。
5. 常见问题与排查技巧:一份直接抄的速查表
5.1 高频问题定位方法
这里把我踩过和身边同事踩过的高频问题整理成一张表,按"症状、原因、解决"的顺序写,方便遇到问题直接对照:
| 症状 | 常见原因 | 解决思路 |
|---|---|---|
| 首次调用process()等待很久 | 动态下载模式下模型还没落盘 | 换成捆绑模式,或启动时后台预下载 |
| 扫描识别速度很慢 | 条码格式没有做限制 | 只保留业务需要的格式 |
| 相机预览卡顿明显 | 输入分辨率设得太高 | 降到720p,配合KEEP_ONLY_LATEST |
| 识别框位置和画面偏移 | rotationDegrees传错或写死 | 改用imageInfo.rotationDegrees动态传 |
| 运行久了内存上涨甚至闪退 | mediaImage未close | 把close写进complete回调 |
| 中文OCR效果差 | 装的是Latin模型 | 换成中文识别依赖 |
| 部分设备初始化失败 | 动态下载但无Play服务环境 | 切换本地捆绑依赖,剔除运行时下载 |
这张表看起来简单,但每一条背后都对应着一次真实的排查过程。单独把它们整理成表,不是为了凑字数,而是想提醒一件事:ML Kit本身出bug的概率真不高,绝大多数问题都出在集成方式和使用姿势上。排查的时候,先怀疑自己,再怀疑SDK,通常比乱翻源码更高效。之前我在项目里也干过"一出问题就怀疑是ML Kit模型没装好"的傻事,后来冷静下来逐项排掉自己的嫌疑,才把问题定位到资源没释放上。
5.2 两个让我印象最深的翻车现场
第一个是扫码框偏移问题。那阵子给一个横屏扫商品条码的功能做联调,明明模型识别成功了,结果框却画在右上角,怎么都对不上。查了半天才发现,InputImage构造时传的rotation是写死的90,但测试平板是横屏模式,传感器方向和UI方向差了180度,角度一错,整个坐标全乱了。后来把rotation统一改成从imageInfo动态取值,在旋转屏和刘海屏上做了好几轮验证才彻底解决。
第二个是个更隐蔽的问题。项目里同时接入了条码扫描和文本识别两个API,都用的动态下载模式,某次灰度测试时发现部分用户第一次打开扫码页面就白屏几十秒。日志里能看到模型下载的超时异常,但只有一小部分机型必现。最后定位到是这些设备存储空间不足,模型下载了好几次都失败,而代码里没做任何失败后的本地回退,所以应用流程就卡死了。这件事之后,我们所有AI功能都定了一个规矩:动态下载失败时必须有超时保护,超时后要么提示重试,要么回退到捆绑模式,绝不能让用户一直看着空白页。
6. 选型边界:ML Kit、TFLite与MediaPipe的分工
6.1 三种工具不是竞争关系
很多人把ML Kit、TFLite、MediaPipe放在一起比较,觉得是同一个问题的三个选项。实际不是这样的,它们的层级不同,解决的问题角度也不同。我用一个表格把差异说明白:
| 维度 | ML Kit | TFLite | MediaPipe |
|---|---|---|---|
| 定位 | 应用层SDK | 推理引擎 | 流水线框架 |
| 上手成本 | 最低,API开箱即用 | 中等,需自己处理后处理 | 稍高,需理解图管线 |
| 定制能力 | 内置模型为主,可带自定义TFLite模型 | 高,随意加载任一转换模型 | 高,可自定义处理节点 |
| 跨平台能力 | Android/iOS | Android/iOS/嵌入式 | Android/iOS/桌面/Web |
| 典型场景 | 扫码、OCR、人脸检测 | 自定义模型在移动端推理 | 实时视频流的复杂pipeline |
选ML Kit,你是在选"最快交付",它把你80%的移动端AI需求都包掉了;选TFLite,你是在选"最多控制",模型、算子、量化精度全部握在自己手里,但工程量也是成倍上涨;选MediaPipe,你是在选"最灵活的管道",它更适合手部关键点、人脸网格这种需要多模型串联、多步骤后处理的实时任务,前端接入体验反而不是它的重点。
6.2 给团队的一条落地路线
我的习惯是把ML Kit当成"验证业务价值的加速器"。第一版功能,永远先考虑用ML Kit内置API实现,目标是尽快让产品经理看到效果,让用户用上功能,这个阶段最重要的不是算法指标,而是流程能不能跑通。等业务数据出来,确认识别率、召回率确实是瓶颈,再考虑进入自定义模型阶段,用TFLite转换自己的模型,接回ML Kit的自定义模型通道继续复用原来的API结构。真要到多路视频实时分析这种级别,再考虑引入MediaPipe或者自建流水线。
这条路线看着保守,但踩坑率最低。它避免了两个极端:一个是技术至上,一上来就训练自定义模型,结果业务方向还没验证就烧掉大量时间;另一个是方案中毒,明明只需要一个扫码,却搭了一套复杂的模型服务加流水线,维护成本远超收益。做移动端AI,先活下来,再谈效果。这也是我每次拿到新需求时,给自己定的第一原则。