Showing Posts From

Android

Stripe在线支付功能的工作流程和安全机制解析

在 Web 世界,在线上支付流程中处理用户的信用卡号就是极高风险的操作。对于要在自己的应用程序中实现海外业务场景的全栈开发者而言,要处理跨国信用卡网络通信、3D Secure 动态认证以及严苛的 PCI-DSS 安全合规审查等。针对这个问题, Stripe 应运而生,其伟大之处在于,它通过极致优雅的 RESTful API 和基于 Webhook 的异步事件驱动模型,将极其混乱的现实资金流转,封装成了高度可预测的、开发者友好的接口。在这种情况下,开发者自己的服务器就可以永远不需要触碰、传递或保存用户的真实信用卡号(明文),只需要向 Stripe 发送指令,Stripe 处理完后返回一个 Token 告知支付业务指令的执行结果,极大的简化了这个支付业务的复杂程度。 Stripe在线支付业务的执行流程解释 下图通过在一个Android APP上利用Stripe完成在线支付的流程图来解释Stripe在线支付的完整流程。整个支付流程可以分为四个阶段: - 阶段1:建立支付会话 - 首先是在Android APP上,由用户点击支付购买商品/服务的按钮启动整个支付流程,此时APP首先向开发者自己的后台Spring Boot服务器发起一个支付购买的请求。 - 后台服务器收到请求后,在自己的数据库PostgreSQL上创建一个新的购买订单,因为此时支付还尚未启动,所以这个订单的状态是Pending。 - 然后后台服务器向Stripe Cloud发起请求,创建一个支付会话请求。 - Stripe服务器收到以后,会给后台服务器返回这个会话的URL和token ID。 - 然后后台服务器再把以上的URL和token ID原样通过与APP之间的长连接转发给APP。 - 阶段2:客户端与Stripe服务器之间的支付环节 - 经过第一阶段后,APP就收到了Stripe服务器的支付会话URL和token,此时APP就访问以上URL直接与Stripe的服务器之间会话完成真正的支付流程,这一步对于开发者的APP以及后台服务器而言都是完全透明的。 - 阶段3:支付完成后给后台服务器的通知 - 当用户在APP中完成与Stripe的支付流程后,APP UI处于等待支持确认的页面,此时Stripe服务器向开发者的后台服务器发送支付确认信息。 - 后台服务器收到支付确认后,修改PostgreSQL数据库中的购买订单状态为PAID,在数据库中修改用户的权益信息。 - 最后后台服务器再通过websocket或者向APP的轮询请求发回购买完成的确认信息,APP刷新UI显示,给用户展示出来已经购买的商品或者服务信息。 !stripe-architecture-flow.png Stripe服务器通知支付事件的Webhook机制 在以上的流程中,开发者的后台服务器向Stripe服务器发起请求创建支付会话请求(以上流程图的第3步)后,在后续的支付会话中,Stripe 服务器并不会与后台服务器之间开启和维持一个长连接会话(这样太浪费Stripe服务器的内存了),而是通过一个Webhook的机制在用户完成支付后,由Stripe服务器主动发起请求向开发者后台服务器注册的WebHook URL发出通知,告知支付完成的状态。 因此,为了支持这个Webhook机制,在开发者注册自己的Stripe账号时,需要在Stripe的开发者后台控制台里,把自己的服务器公网地址提前注册进去,这样后续在用户完成支付以后,Stripe服务器才能够知道WebHook的URL,并向其发出支付完成通知。 所以,开发者后台服务器与Stripe服务器之间的通信始终都是标准的RestFul无状态通信模式,每次开发者的后台服务器向Stripe服务器发起请求创建一个支付会话请求后,Stripe服务器返回一个支付会话的URL和Token ID以后,这个连接就关闭了。后续等待支付完成后,Stripe 服务器再通过这个Webhook的机制向开发者后台服务器发回事件完成通知。 以下是用户完成付款后,Stripe服务器向 WebHook URL 主动发起支付事件完成的流程图,其中包含了加密认证信息来确保通信的安全性,同时针对可能发生的 WebHook 通信失败的情况,Stripe 服务器也会在一段时间内重发事件告知的请求,以确保整个支付流程和通知的机制得到完整闭环处理: !stripe-server-webhook.png 前端与Stripe服务器之间的通信模式:Checkout vs Payment Element 对于前端(APP,Web)而言,在开启支付会话请求并且从后台服务器得到Stripe服务器返回的会话URL和Token ID以后,真正的支付流程是在前端和Stripe服务器之间的通信来完成的。此时前端与Stripe服务器之间的通信模式主要可以分为两种:Checkout和Payment Element。 Checkout模式 Checkout模式的运行机制为,当后台服务器调用 Stripe API 创建支付会话后,Stripe 服务器会生成一个逻辑订单,并动态渲染一个专属于这笔订单的 SPA(单页面应用)网页,这个网页会部署在 Stripe 的域名下(通常是 checkout.stripe.com)。然后把这个Checkout URL返回给开发者的后台服务器并转发给前端。 在这种情况下,开发者只需要在自己的 App 或前端网页中,通过重定向 (Redirect) 或打开内置浏览器来访问这个 Checkout URL即可。 Stripe 在这个页面里注入了非常强大的功能。如果它探测到用户用的是 Safari,它会自动展示 Apple Pay;如果是 Chrome,展示 Google Pay;它甚至会自动根据用户的 IP 切换多国语言和本地货币(比如在欧洲自动显示 iDEAL 支付)。而这些复杂的探测逻辑,不需要开发者写一行代码,全部由 Stripe服务器自动处理。 Payment Element 原生模式 该模式的运行机制为,用户在支付页面中不离开开发者的域名或 App,需要开发者自己在 Web 页面或者 APP 的前端用 HTML/CSS 或者 Android 的原生组件画一个美观的结账页面(包含购物车、收货地址等),也就是说支付页面是由开发者自己设计并呈现给用户的。 当然,为了确保支付流程是安全隔离的,在这个前端支付页面需要用户输入信用卡号的那部分区域,必须通过集成 Stripe 的前端 SDK,向 Stripe 申请渲染一个 Payment Element 组件。这个组件在 Web 上本质上是一个跨域的 (在 App 里则是一个安全视图)。 在这种设计的情况下,即使支付页面的输入框由开发者自行设计,用户敲击键盘输入银行卡号,是直接输入到了 Stripe 的 iframe/安全视图里,并直接发送给 Stripe 的金库,开发者自己实现的外层 JavaScript 或 Android 代码无法读取这些明文密码,这样就可以完美绕过 PCI 合规审查,并确保整个支付环节的流程及其安全性完全由Stripe负责。 以上两种模式的处理流程图如下图所示: !stripe-checkout-vs-element.png 那么对于Stripe来讲,它是如何区分这两种模式呢? 答案是:Stripe 根本不关注前端究竟使用了哪种模式,它只关心在支付请求发起的时候,开发者的后台服务器向 Stripe 服务器发起请求的时候采用的究竟是Checkout 模式的 API 还是 Payment Element的API。 - 使用 Checkout 模式时,后台服务器向 Stripe 发起的 API 请求对应的API是POST /v1/checkout/sessions,参考https://docs.stripe.com/api/checkout/sessions。此时 Stripe server 会生成一个带支付界面的网页,并且把这个网页的URL返回给后台服务器。 - 使用 Payment Element 模式时,后台服务器向 Stripe 发起的 API 请求对应的API是POST /v1/payment_intents,参考https://docs.stripe.com/api/payment_intents。此时 Stripe Server 会在内存里创建一个订单记录,并生成一个一次性密钥返回给后台服务器,此后前端会基于这个密钥与 Stripe Server 通信完成支付。 Stripe的安全通信机制及其密钥 如以上流程图所示,在整个支付业务的通信过程中,涉及到了前端、后端以及Stripe服务器三方,为了确保整个支付业务在通信过程中的安全性,Stripe 提供了三组密钥用于确保通信过程中的身份认证和签名确认。这三组密钥在Stripe 的开发者控制台中生成: - Publishable Key (公钥,以 pk_ 开头):硬编码在前端(App/网页)的代码中,是作用是让前端在与 Stripe 服务器通信的过程中,向 Stripe Server 证明“我是谁家的前端”。 - Secret Key (私钥,以 sk_ 开头):必须且只能安全地存储在后端服务器的环境变量中,绝对不能泄露给前端或提交到 Git 仓库。其作用是在后端调用 Stripe API(如创建订单、退款、查询账单)时,将其放在 HTTP 的 Authorization: Bearer 头部,Stripe Server 据此可以得知这个支付请求来自哪个商户。 - 实际上除了 Secret Key以外,还有一个Restricted Key,功能类似于Secret Key,只不过是Secret key的功能子集,只具有Secret Key的部分权限,如果只需要创建支付会话的功能,就可以使用Restricted Key,避免更高权限的Secret Key泄露。 - Webhook Secret (签名密钥,以 whsec_ 开头):同样只能保存在后端服务器的环境变量中。其作用是专用于验证从 Stripe 发到后端服务器的 Webhook 请求是否被黑客中途篡改。 需要注意的是,整个 Stripe 支付流程业务的所有通信都是在 HTTPS/TLS 链接的基础上进行的,以上的三组密钥都是在 HTTPS 的加密信道中以明文传递,用于进行身份的认证,不会参与数据的加解密运算。 如下图所在,整个支付业务的流程中,分别使用了三组密钥来进行通信角色的身份认证和鉴权: - 环节一:在后台服务器向 Stripe server 发起支付会话请求的通信中,后台服务器需要把SK/RK放在其HTTP消息中,这样Stripe Server收到后,通过查询这个SK/RK的身份归属以及确认其是否有创建会话的权限。 - 环节二:在前端与Stripe Server之间进行的实际支付业务的交互中,前端需要在其HTTP消息中把PK传递给Stripe Server,Stripe Server根据这个PK得到与其通信的前端的身份归属。 - 环节三:前端与 Stripe Server之间的支付流程结束后,Stripe Server要通过Webhook机制向后台服务器发出事件通知,后台服务器收到这个消息以后需要通过Webhook Secret密钥对消息的内容进行验签,确保这个消息是由Stripe Server发过来的。 !68951394-a233-4ed6-9d9b-76fd3a268714.png

总结Android Flutter项目中YOLO模型的执行流程

之前基于 Flutter 实现了一个在Android APP中调用YOLO模型来实现图像目标检测功能,本文的 YOLO 模型的 TFLite 导出以及在 Android Flutter 项目中进行调用的完整执行流程进行总结。 在Android上跑AI模型,标准的做法是导出模型文件为 TFLite 格式,然后在 Android 的 TFLite+Interpreter+NCAPI 框架下执行AI模型的推理流程,与该框架相关的知识可参考 如何理解TFLite/LiteRT的核心概念和框架流程?一文。 YOLO模型的TFLite导出 因为 Ultralytics 项目中的模型导出的实现已经非常完备,所以可以直接在ultralytics项目中通过其export接口导出 tflite 模型文件即可,此处选择导出为half即float16的格式: 此外,后续在Flutter项目中调用以上模型文件进行推理,还需要创建一个与模型对应的label.txt文件,其中包含有模型检出类别对应的类别名称字符串,按顺序每行一个。因为我测试中直接使用的是YOLO的预训练pt文件,预训练模型文件对应的是COCO数据集,所以这里的label.txt中包含的类别名称字符串大致就是如下80个类别: 依赖管理 因为要在 Android Flutter APP 增加跑AI模型的的能力,首先需要在项目根目录下的pubspec.yaml和 android/app 目录下的build.gradle.kts增加相关的依赖项。 flutter项目中的pubspec.yaml是该项目的元数据中心(Metadata Center)和依赖管理器(Dependency Manager)。其中包含的主要内容是:项目的基本配置信息,生产环境的依赖库,开发/测试阶段的依赖库,资源与资产等。对于以上提到的依赖项,每次在构建项目时,都会自动调用 flutter pub get 从pub.dev下载指定依赖库的源代码到本地。 该项目中需要对手机camera和本地图片的图像调用YOLO模型进行检测,所以pubspec.yaml中相关的设置项包括: 因此,对于在 Android 项目中跑YOLO模型而言,以上配置文件最关键的就是其中导入了 flutter_vision 这个插件。该插件内置了 TensorFlow Lite C SDK,并针对 YOLO 这种特定模型,包含了输入输出的 ByteBuffer、维度转换(Tensor Shape)和 NMS(非极大值抑制)等的相关处理,对于应用层程序而言,只需要调用 vision.loadYoloModel() 接口,它就可以在 Android 原生层创建 TFLite 的 Interpreter并执行推理任务。 模型在Flutter中的完整推理流程 与其他板端模型的完整推理流程类似,在Android Flutter项目中调用YOLO模型执行推理任务同样包含模型加载、前向推理以及后处理这三个环节。 模型加载 基于flutter_vision插件对AI模型进行加载的整体流程如下: 整体的模型加载过程很简单,调用flutter_vision的loadYoloModel接口创建一个模型加载的异步任务,传递模型文件和label文件等参数,然后等待这个异步任务执行完成即可。 需要注意的是,flutter_vision 插件只能够支持ultralytics项目export接口直接以tflite格式导出的YOLOv5,YOLOv8,YOLOv11这几个模型,而且参数中指定的模型文件与modelVersion的参数必须设置正确,否则后续的后处理流程会出错: !074e2e0e-d2b9-48e6-b0b2-4a1f06769c23.png 那么如果我们要在 Android APP 中跑其他模型呢?那就只能使用 tflite_v2 插件、flutter_tflite插件或者底层 TensorFlow Lite API。 前向推理和后处理 flutter_vision插件的前向推理过程主要就是调用其yoloOnFrame(针对摄像头的图像)和yoloOnImage接口(针对本地图片)。 以上的推理接口执行中本身已经包含了YOLO不同版本的后处理流程,所以其输出结果是一个的数组。这个数组中的每一项都对应于一个最终检测框的类比和坐标+置信度,如下所示: 应用层拿到以上的这个数组以后,就可以对其中的内容进行解析并在图像上画框标注了。因为 yoloOnFrame 和 yoloOnImage 接口中已经包含了后处理的NMS等环节,所以不需要在应用层再执行后处理了。 参考资料 - flutter_vision | Flutter package

如何理解TFLite/LiteRT的核心概念和框架流程?

本文整理了TFLite的核心概念,与ONNX框架的对比,及其内部各个组件的关键工作流程,尝试建立对这个Android系统下的标准AI推理框架的整体概念和工作流程建立完整的理解。 TFLite是什么?以及与ONNX的比较 Google推出的 TensorFlow Lite 是一个专为移动、嵌入式和边缘设备设计的开源深度学习框架。它能够支持开发者在手机(Android/iOS)、微控制器(MCU)及各类物联网设备上,以极低的延迟和较小的二进制体积运行高性能的推理任务。 !image.png 众所周知,在边缘侧的板端设备上运行各种 AI 模型,已经有一个非常流行的 ONNX 框架,那为什么不在 Android 上直接使用 ONNX?原因主要还是 Facebook的 Pytorch/ONNX 与Google的 TensorFlow 之间生态竞争的问题。 虽然 ONNX Runtime 现在也能够支持在 Android上运行,但在 Android APP 中跑AI Model还是应该首选使用 TFLite,原因如下: - 兼容性的问题:在 Android 系统的实现中,Google 提供了一层 NNAPI (Neural Networks API) 的封装。TFLite 是 Google 主推的端侧机器学习框架,它对 NNAPI 的支持是第一优先级的。如果用 ONNX 的话,在调用不同 SOC 厂家的 NPU 时,可能会遇到各种驱动不兼容或性能折损的情况,兼容性的处理会非常头疼。 - 内存与包体积的问题:要在 Linux 或者 Android 系统中使用ONNX来运行模型,就需要把 ONNX Runtime 内置进去,相关的库文件通常很大(几十 MB),这样就会直接增加 App 的安装包大小和内存占用情况。而如果使用TFLite的话就没有这个问题,TFLite 的核心解释器非常小(只有几百 KB),而且 Android 系统中已经内置了相关的运行时环境。 TFLite模型部署的整体工作流程 将一个复杂的深度学习模型部署到手机上,通常需要经历从训练、精简/格式转换/量化再到板端执行的完整的过程。这个流程对于所有的设备端 AI 框架的工作流程而言都是大致相同的。 例如对于瑞芯微平台上的模型适配而言,其整体的适配流程如下: - 首先在PC/AI服务器端完成模型训练,导出为ONNX格式的模型; - 把以上导出的ONNX模型再利用瑞芯微的RKNN-Toolkit工具转换为RKNN格式的板端模型; - 在瑞芯微的处理器中,板端模型与其运行时库librknnrt.so、应用程序相互配合完成板端的推理任务。 以上的这个流程,对于在手机的的 Android 系统中使用 TFLite 框架来运行 AI 推理的流程是完全相同的,只不过整个过程稍微简化了一些而已: - 模型转换部分:首先把原始的训练好的 PyTorch 模型(通常是 .pt 或 .pth)使用 TFLite Converter 之类的工具转换为扁平缓冲区格式的 .tflite 文件。与其他板端模型转换工具的工作流程类似,在这个转换过程中,它会进行算子融合(将多个连续的操作合并为一个,以减少内存访问次数和量化(将原始模型文件中的 32 位浮点数权重转换为 16 位浮点数 FP16 或者 8 位整数 INT8,在微弱损失精度的前提下可换取数倍的速度提升同时降低对模型大小和显存的需求)等操作。 - 板端推理执行部分:在 Android App 运行时,内置在Android 系统中的 TFLite 解释器(Interpreter)会负责加载模型、管理内存缓存并调度计算算子。 > 💡 注:在模型转换部分,Pytorch 框架下训练出来的模型,一般需要先转换为 ONNX 模型,再利用类似 onnx2tensorflow 之类的工具转换为 tflite 格式。不过 Goggle 在2024年也出了一个 AI edge torch的工具可以实现直接的转换。 !image.png TFLite模型在Android框架下是如何运行的? 从以上的框架图中可以看到,模型转换为 TFLite 格式后再Android APP中的运行,需要依赖于两个关键的组件:Interpreter 和 Android Neural Networks API(NNPI)。 其中的Interpreter (解释器)是 TFLite 模型在系统中运行的核心控制中枢。它负责加载模型,解析模型的计算图(Graph),并管理内存缓冲区。在整个模型的工作流程中,Interpreter 并不直接做复杂的浮点和算子运算,而是负责将模型中定义的计算任务分发出去。 - 其实这个Interpreter有些类似于瑞芯微的librknnrt.so,负责辅助应用程序加载模型,解析模型的计算图和管理内存,然后在接收到输入数据的时候,对模型每一层的计算任务进行调度管理,分配到合适的处理器(CPU/GPU/NPU)上,并根据具体的计算调度情况不同的处理器上在处理器之间管理好内存的拷贝。所以其任务就是:加载并解析模型,分发计算任务,管理不同计算任务和处理器上的内存。 而 NNAPI 则是 Android 系统提供的一个 C API。它的存在是为了屏蔽底层 NPU、GPU、DSP 硬件的碎片化。Google 在 Android 系统中所提供的 NNAPI 只是一个与 Interpreter 相互配合的API调用接口的框架,而对于瑞芯微、高通、联发科等 SoC 厂商,要让 Android 手机能流畅地使用 TFLite 执行模型的推理任务,他们的核心工作就在其SOC的设计中进行 NNAPI 接口的具体实现。 具体来讲,这些 SOC 厂商需要根据 Android NNAPI 模块所提供的 NeuralNetworks.h 标准,编写对应的 NPU 驱动指令。这些驱动指令的关键任务就是实现模型的算子映射(将 Android 定义的 ANEURALNETWORKS_CONV_2D 等标准算子,翻译成自己的 NPU 能理解和执行的具体指令)和内存管理(实现不同类型和区域的内存共享控制,尽量避免模型在执行推理过程中数据在 CPU 和 NPU 之间来回拷贝)。 总结起来,就形成了这样的调用关系和流程: !5a379fcc-72eb-471f-9001-bf892b8a6120.png - 首先是APP的应用程序逻辑代码调用 Interpreter 加载和解析模型文件; - 在 Interpreter 对输入数据执行推理流程的过程中,会通过 Delegate 机制调用 Android系统中的 NNAPI 框架中所定义的API。NNAPI中定义了不同的标准算子,Interpreter 在推理中针对具体不同的算子调用 NNAPI 中与之对应的API接口。 - NNAPI之下则是不同 SOC 针对算子的具体实现,这部分由各个 SOC 厂家根据自己SOC中 CPU/GPU/NPU 的能力来进行实现。相当于 NNAPI 定义了一层标准的算子接口,屏蔽了不同 SOC 厂商在其硬件上实现这些算子的差异性,这样的话,对于应用程序和Interpreter而言,看到的就只是 NNAPI 所定义的标准接口。 Interpreter 调用 NNAPI框架的Delegate机制 以上所描述的 Interpreter 和 NNAPI 框架的调用流程只是一个简化的解释,实际上Interpreter并不是直接调用NNAPI所定义的API接口。如果 Interpreter 直接以硬编码的方式去调用 NNAPI的话,会产生两个问题: - 某些算子可能不支持 (Fallback):NNAPI 并不一定能够支持 TFLite 模型文件中的所有算子。如果直接调用的话,遇到不支持的算子程序就崩溃了。 - 跨平台的兼容性问题:TFLite 所谋甚大,不仅要运行在 Android 上,还能够支持在 Windows、Linux、iOS 上跑,而这些系统中根本就没有NNAPI(例如上面架构图中的iOS APP部分根本就没有 NNAPI)。 所以 Interpreter 在 Android 中对于 NNAPI 框架的调用,采用了一种 delegate 的机制。 本质上,Delegate 就是 Interpreter 为了实现跨平台兼容性而设计的一个抽象插件接口。 - 对上(与Interpreter之间),它通过TfLiteDelegate 定义了一个标准的 C API,通过这个C API屏蔽了不同硬件平台、SOC、NPU的差异性。Interpreter始终只需要调用这个C API接口来实现对下层不同算力的调用,实现完整的推理过程。 - 而对下(各种平台不同的NPU实现),它作为客户端,负责调用各个不同系统和硬件平台上特有的深度学习算子运算的加速库(如 Android 的 libneuralnetworks.so 或 iOS 的 Metal.framework)。 至此,TFLite 框架得到完整结构及其调用流程就比较清楚了: !%E6%97%A0%E6%A0%87%E9%A2%98-2026-03-26-1350.png 参考资料 - Google Play 服务 C 和 C++ API 中的 LiteRT  |  Google AI Edge  |  Google AI for Developers - TFLite基础知识 - VitoYeah - 博客

在Android Flutter项目中flutter_litert插件运行TFLite模型

在之前的一篇笔记(总结Android Flutter项目中YOLO模型的执行流程)中,我详细总结了基于flutter_vision插件在一个Android Flutter项目中把Ultralytics项目中导出的YOLOv8模型运行起来的完整流程。 但是Flutter_Vision插件的使用有诸多限制,例如只能运行YOLO的几个模型,并且只能配合Ultralytics的export接口导出的TFLite模型文件来使用。那么如果想要在Android上跑YOLO之外的模型,就没有办法是用Flutter_Vision插件了。 因此,要把一个自定义的模型转换为TFLite格式并且能够在Android APP中跑起来,就需要一个更灵活的应对方式。而这个过程分为两个步骤: - 把自定义模型的pt文件转换为TFLite格式。这部分在如何把PT格式的模型转换为TFLite/LiteRT模型?已有详细总结。 - 把以上转换的TFLite格式的模型文件在Android Flutter项目中跑起来,避免受到FlutterVision插件所带来的限制。这一点就是本文要解决的问题。 flutter_litert vs tflite_flutter 在Android Flutter项目中跑 TFLite模型,目前比较常用的是flutter_litert和tflite_flutter这两个插件。 - tflite_flutter:这个插件是 Flutter 社区中最经典、最老牌的 TFLite 插件,目前搜到的信息大部分都是使用这个插件在Android上跑AI模型。它通过 Dart FFI 直接封装了 C 语言的 TFLite 解释器。目前仍有社区维护版(如tflite_flutter_plus),但配置分散,社区维护分散,在新的项目中不建议使用。 - flutter_litert:这个插件是为了响应 Google 对 TensorFlow Lite 品牌的正式更名和升级 (LiteRT) 而推出的现代继任者。它也是目前推荐的、更符合 Google 最新人工智能路线图的插件,其本质上是 tflite_flutter 的演进版本,旨在解决旧插件的维护痛点。因此强烈建议在新的项目中使用flutter_litert这个插件。 !image.png flutter_litert 这个插件本质上就是一个工作在 Flutter(Dart 语言)层级的硬件抽象层 (HAL) Wrapper。这个插件总的特点是: - 底层机制: 该插件并不是用 Dart 重写模型的推理逻辑,而是通过 Dart FFI (Foreign Function Interface) 接口极其轻量地绑定了 Google 已经预编译好的 C++ LiteRT Native 二进制库(.so 文件),这样的好处当然是可以利用底层的C/C++语言的执行达到最高的效率。 - 零拷贝理念: 其核心思想是在内存里开辟一段连续的 Buffer(如 Float32List),使用Dart 语言往里填数据,C++ 引擎则会直接读取这段物理内存进行模型的矩阵运算,避免了跨语言传值所带来的性能损耗。 flutter_litert 插件运行的整体架构与数据流程 下图是在Android Flutter项目中使用flutter_litert 插件运行AI模型的整体架构与数据流程图: !litert_architecture_high_res.jpg 从以上的整体流程图中,模型的前向推理过程中的数据流动可以大体分为四个层次和阶段。 1.采集与调度层:Flutter Dart Zone (User Space) 这部分运行在 Flutter 的 Dart 虚拟机中,相当于系统的用户态应用程序。它的首要任务是协调前端 UI 显示与模型检测结果的时序、图像预处理这两部分工作。 当然在模型能够对图像进行检测和识别之前,首先要把模型加载到内存中,模型及其label文件一般整合在app的assets中,因此采用assets对应的API加载模型: 对于手机的摄像头而言,在打开的情况下,其以 30/60fps 的帧率向主线程推送高分辨率的 YUV420 裸流数据并显示在屏幕上。而一般情况下,手机所具备的算力无法支持以这么高的算力实现对每一帧图像的检测,可能1秒钟只能完成10帧图像的处理,这样的情况下,Camera采集图像与Model处理图像这两部分的工作就不是同步的。因此需要分开两个线程同时进行处理,主线程按照Camera的采集帧率刷新显示实时图像,而针对模型对图像的处理而言,就必须开辟一个独立的 Isolate(类似后台高优先级线程),执行 YUV 转 RGB、图像缩放等预处理后送入litert_flutter插件进行识别和检测 。 众所周知,图像在送入YOLO等模型中进行前向推理之前,还必须要进行一系列的预处理流程,以符合模型对输入图像数据在各个维度上的要求。因此,对于每一帧从摄像头中取出的YUV420数据,其分辨率可能是1280x720,那么要进行的预处理过程就至少要包括:分辨率缩放到640x640、YUV转RGB、图像归一化到0-1.0之间、图像数据排列格式调整为NCHW(Pytorch模型的数据排列格式默认为NHWC)等。 以上经过预处理以后,输入图像就准备好了,下一步就是调用flutter_litert插件的API接口进行处理了: 2.跨语言函数接口与内存零拷贝机制 在 Interpreter 对输入图像数据所进行的前向推理的执行流程中,图像数据要从APP应用层的Dart语言传递到模型推理代码的C++语言,而且为了在两种语言之间高效的传递大片的图像内存数据,最好能够直接通过类似内存指针的方式来实现零拷贝的图像内存传递机制,这就是 Flutter 框架为了提升应用程序性能的 Dart FFI (Foreign Function Interface)的用武之地了,它可以允许 Dart 直接与底层 C/C++ 共享同一块物理内存的指针,从而避免在调用者和被调用者之间反复拷贝大片的内存。 如上图所示,无论是在 Dart 语言所写的 interpreter 的函数调用,还是最终 interpreter 把模型的运行结果从底层的C++语言返回给用户态的Dart语言层,都采用了 Dart FFI所提供的内存零拷贝机制。 3.底层算力的执行:Native C++ Zone 这部分是模型使用C/C++语言真正执行前向推理的部分。 在如何理解TFLite/LiteRT的核心概念和框架流程? 一文中,已经详细总结了在Android的TFLite/LiteRT框架下运行AI模型的关键概念和执行流程,Google 使用 C++ 语言写了一个 LiteRT Runtime运行时环境(实际上也就是所谓的interpreter),负责执行和调用 TFLite\LiteRT 格式模型在Android系统中的运行和算子的调度。 具体的算子调度执行过程中,Interpreter 进一步 通过Delegates 来决定把具体的运算算子派发给谁: - GpuDelegate (Adreno GPU): 适合处理模型中大规模并行的浮点矩阵乘法。 - NnApiDelegate (Hexagon NPU): 针对 Android Neural Networks API 设计的专用 NPU。如果模型完全量化为 INT8,交给 NPU 跑不仅极快,而且功耗极低。 - XNNPACK (CPU SIMD): 最底层的兜底方案。如果某个算子 GPU/NPU 不支持,就会退回到 CPU 用向量指令硬算,当然这通常是模型运算速度慢导致卡顿的罪魁祸首。 具体的执行和调度逻辑可以参考如何理解TFLite/LiteRT的核心概念和框架流程?以及TFLite如何处理算子版本和兼容性的问题?这两篇文章。 4.模型返回与渲染 通过以上 LiteRT Runtime 所调度的模型前向推理的执行后,该部分的 C++ 引擎更新内存状态,通过Dart FFI机制,应用层的 Dart 语言层捕获到模型执行完成的信号。此时用户层应用程序就可以按照模型输出的数据结构和类型,从连续的内存块中拆解出 YOLO/RT-DETRv2等模型输出的张量信息(例如类别、边框、置信度等),对这些张量信息进行后处理(如YOLO还需要进行一步NMS处理),然后交回给 Flutter 的 GPU 渲染管道,根据屏幕比例映射坐标,在画布上轻量级地画出最终的目标检测框。 模型推理的GPU/NPU加速设置 如上所述,在LiteRT Runtime运行时环境对模型所进行的前向推理的计算过程中,interpreter 会根据具体算子的版本情况选择对应的delegate执行选项,主要是GPU、NPU和CPU SIMD等选项。那么在flutter框架下使用litert_flutter插件运行AI模型的时候,如何设置使用GPU/NPU来实现这些算子的硬件加速呢? 需要通过 InterpreterOptions 来进行设置,具体代码如下: 以上代码的配置逻辑是,在Interpreter进行算子调度的过程中,优先使用GpuDelegateV2,如果算子在GPU上不支持的话,则会回退到CPU上用4线程来进行计算。 在当前(2026年4月)最新的2.0.11版本的 flutter_litert 插件中,InterpreterOptions 能够支持的加速模式包括: - GPU Delegates (图形处理器):适用于大多数模型,特别是 CNN 类网络。优点是速度快,功耗相对较低。缺点是部分算子不支持,可能回退到 CPU。 - GpuDelegateV2():针对android平台,其底层API是OpenGL ES 3.1 / Vulkan。 - GpuDelegate():针对iOS平台,其底层API是iOS的Metal。 - Core ML Delegate (iOS):针对iOS设备,iOS 11+,A12 芯片以上的设备。优点是在Apple 设备上性能最佳;缺点是仅支持 iOS。 - iOS 上应优先尝试 CoreMLDelegate() ,如果不支持再回退到 GpuDelegate() 。 - XNNPACK Delegate (CPU 优化):无 GPU 或 GPU 不支持时。优点是兼容性比较好,速度快于普通的CPU;缺点是仍比 GPU 慢很多。 需要注意的是:通过options.addDelegate只能设置一个加速选项,如果多次调用addDelegate接口,只有最后一个会生效。因此实践中应使用try…catch的方式选择最合适的加速选项:

Android Studio+Flutter全栈开发环境的搭建及其注意事项

本文完整的记录了在Windows环境中基于Android Studio+Flutter的全栈开发环境的搭建过程,整个过程其实并不复杂,但因为概念众多,再加上中国国内网络访问Google服务器不方便等问题,在首次搭建并测试环境中存在不少值得注意和记录的问题,在此做完整的记录,方便后续开发过程中参考。 Flutter SDK的下载和环境变量设置 从中国国内的镜像源下载Flutter SDK,参考Flutter SDK 归档列表,选择Windows系统的Stable Channel选择下载即可,下载速度要比官方默认源下载快很多: !d99ea2d7-2389-4625-b608-a1796c881a53.png 下载后无需安装,只需要把 Flutter SDK 压缩包解压缩到一个固定的路径中,并把该SDK包中Bin目录的路径设置到Path环境变量中即可。 此外针对国内的网络环境,还需要通过增加环境变量的方式修改 Flutter 社区的地址,在环境变量的用户环境变量中增加两个环境变量:PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL。 !7024f394-d4b1-4a8b-8e8e-9fee7ad6de79.png 此时,在Windows终端中应该可以通过flutter --version查看当前flutter sdk版本: Android SDK的安装和配置 从Android Studio中文官网下载最新版本的Android Studio并安装:下载 Android Studio 和应用工具 - Android 开发者  |  Android Developers。 Android Studio安装完成以后,首次打开还需要在线安装 SDK和NDK等工具,这些工具包也动辄数GB的大小,因此在首次运行Android Studio安装SDK的过程中选择Cutom方式,首先设置HTTP Proxy后再继续后面的SDK安装环节,否则SDK的下载环节会很让人崩溃,可设置的常用Proxy列表如下: !b5eda9af-5302-4c0f-bcb3-c7c5fa3727d0.png 默认情况下 Android Studio 在安装SDK的过程中不会安装命令行工具(Android SDK Command-line Tools),但 Flutter 必须通过它来操作镜像,所以还需要额外安装命令行工具。此外,因为后续我计划要基于Flutter来开发需要调用C/C++语言的音视频和AI模型方面的包和库,还需要安装Android NDK包。 因此,在SDK安装完成后还需要进入 Settings 页面额外安装以下SDK包: !766a7d54-6774-4cef-a456-16a5faf546bf.png 此外,还要在Android Studio中安装下一步使用Flutter进行开发的两个插件:Flutter和Dart。 !4d4dfa01-4df5-4623-b865-6f1a1174c997.png 至此,Flutter SDK、Android Studio及其相关的SDK、Flutter 插件等均安装完毕。 环境测试与验证 下一步在基于Android Studio中使用 Flutter 进行开发之前,可以在终端中使用 flutter doctor 命令检查当前环境的正确性。而在此之前首先通过 flutter doctor --android-licenses 命令并通过按下多次 y 按键来确认一些 Android 的license。最终的 flutter doctor命令的输入大致类似于:因为我不关注web开发,所以不关注此处的Chrome浏览器的报错信息,如果需要web开发的话,额外安装一下Chrome浏览器就可以了。 Gradle 换源 到此还没有结束,在Android Studio中编译和打包Android APK的时候还需要用Gradle,这同样牵扯到一大堆工具的下载。 在 Android APP的构建过程中,Android 的构建脚本 gradle 默认会去从 Google 和 MavenCentral 下载各种依赖包,中国国内网络很容易在这个包下载的过程中卡住,所以需要更改依赖包下载服务器的源地址。 在网上大概搜了一下,大部分都提到通过手动修改 gradle/wrapper/gradle-wrapper.properties 文件修改默认的gradle.org镜像,以及在 android/build.gradle文件中修改其中的 allprojects 等解决方案。这种方式很直观简单,但问题是:以后每次新建一个项目,都需要自行手动去修改这两个文件才行,如果忘记的话就会到外网去下载依赖文件,耗时会非常长。 而我希望做到的是直接把这些修改放到整个环境中,这样每次新建的flutter项目在 build android app的时候自动把下载的服务器地址链接到国内的镜像,而不是访问外网的中心服务器。具体的方式就是在 Gradle 的安装目录(例如我的gradle的安装路径是D:\Android\.gradle,默认的安装路径应该是C盘用户目录下的.gradle目录)下创建一个init.d目录,在其中放一个mirror.init.gradle.kts文件,内容为: 这样在 Gradle 每次加载的时候,都会自动替换与其相关的下载服务器的URL,替换为国内的镜像服务器地址,这样下载速度会快很多。 调试 至此,以上的完整过程就总算是完成了。其实整个过程并不复杂,关键是国内网络访问外网不方便,从外网服务器上的下载速度会非常让人抓狂,而Android Studio/SDK/NDK/Flutter/Gradle等多个环节又需要下载很多包才能完成环境的搭建。好在有国内众多的镜像服务器可以中转,让这个过程和环境一旦完成搭建之后,后续的开发就比较顺畅了。 接下来就是经典的调试过程了,直接在终端中使用Gemini Cli这类终端开发工具或者使用Trae这类 AI IDE 进行代码的修改,然后通过flutter devices + flutter run + r这样的黄金组合进行代码的调试和运行了。正常开发的过程中不需要再打开Android Studio了,只需要使用Android Studio管理和更新自己的SDK就好了。 参考资料 - 在中国网络环境下使用 Flutter - Android Studio快速配置国内镜像源和HTTP代理_android studio 国内代理-CSDN博客