普通视图

发现新文章,点击刷新页面。
昨天以前首页

Android生态震荡:Swift官方支持落地,跨平台开发迎来“原生级”方案

2025年6月30日 11:02

前言

Swift.org 在 2025年6-26日宣布成立 Android Workgroup,目标是:将Android确立为Swift的官方支持平台并持续维护

Establish and maintain Android as an officially supported platform for Swift.


为什么要做出这个决定?

原因1:跨平台需求高

  • 目前开发者的跨平台开发需求很高;
  • iOS 团队希望一套 Swift 业务逻辑可复用到 Android;
  • Server-Side Swift & Embedded 已验证多平台可行性。

原因2:竞品压力

Google的Kotlin Multiplatform 正快速发展,Flutter也逐渐成熟,成为跨端主流开发方案,官方希望能够提升 Swift 的竞争力。

下面将主要讲解:

  1. Swift支持的Android内容
  2. 关键技术实现
  3. 具体使用
  4. 对比主流跨平台方案
  5. 对开发者的影响

1. Swift支持的Android内容

“官方支持”绝非简单的“Swift代码能在Android编译”就算完成,包括:

1.1 编译器与工具链

  • 为Android的AArch64、x86-64和armv7架构生成预编译SDK
  • 与 NDK 链接脚本、libswiftCore、libdispatch 等系统库适配

1.2 持续集成系统

所有Swift的Pull Request将自动运行Android目标测试,防止平台兼容性退化

1.3 核心库适配

Foundation和Concurrency等核心库将针对Android的文件系统、线程模型差异进行专门优化

1.4 互操作桥接

设计Swift与Java/Kotlin之间的双向调用机制,打破语言壁垒

1.5 开发体验提升

  • 将提供Gradle/Bazel/SwiftPM插件支持
  • VS Code扩展也在开发路线图中

2. 关键技术实现:LLVM

Swift对Android的支持并非从零开始构建,Swift编译器从诞生之初就基于LLVM架构,而Android NDK从R13版本开始就完全转向基于LLVM的Clang编译器。

这种同源架构使得Swift编译器能够被“重定向”,为Android支持的CPU架构生成原生机器码。在具体实现上:

  • Swift直接链接Android的native日志系统(logcat),而非创建自己的日志机制
  • 通过Android NDK提供的LLVM工具链完成交叉编译
  • 运行时库适配Android的bionic libc和线程模型

3. 实战体验:Swift on Android初探

虽然完整支持还在路上,但开发者已经可以尝试Swift开发Android应用。以下是一个简单的互操作示例:

// Swift
@_cdecl("sayHello")
public func sayHello() -> UnsafePointer<CChar> {
    return strdup("Hello from Swift 🐦")
}

// kotlin
class HelloBridge {
    companion object {
        init { System.loadLibrary("hello") }
    }
    external fun sayHello(): String
}

通过JNI将Swift函数挂载到Kotlin,编译的.so文件打包进APK即可运行。

社区已有Tokamak UI框架等尝试支持Android,预示着更完整的解决方案即将到来。


4. 对比主流跨平台方案

对比主流的KMP、Flutter、RN方案


5. 对开发者的意义

对于iOS开发者:跨平台新机遇

  • 多端拓展:轻松将iOS应用扩展到Android平台,覆盖更广泛的用户群体
  • 业务逻辑100%复用:Model层、网络模块和核心算法可以直接共享,无需重写

对于Android开发者:新语言选项

  • 人才流动加速:熟悉Swift的iOS开发者将更容易参与Android项目开发
  • 性能敏感模块的新选择:可将Swift作为“更安全的C++”使用于高性能计算模块

社区与生态影响

  • 学习曲线降低:新人学一门Swift即可开发iOS、Android、服务端等多平台应用
  • 跨平台工具链成熟:Swift Package Manager将成为真正的跨平台依赖管理工具
  • 社区协作加强:Android工作组采用开放协作模式,任何开发者都可参与贡献

结语

这次不仅是Swift语言的一次跨界尝试,更是移动开发的一次生态震荡:苹果主导语言首次系统性拥抱 Google 生态,同时也是苹果对于跨端研发模式与 Kotlin / KMP 生态 的全新博弈。

参考链接: github.com/swiftlang/s… github.com/swiftlang/s…

Flutter性能优化Tips

2025年6月30日 09:14

前言

Flutter作为跨平台移动应用开发框架,一直广受欢迎,在开发过程中,确保应用的性能是至关重要的。以下是一些优化Flutter应用性能的工具和方法

一、渲染优化

1. 减少Widget重建范围 使用 const 构造函数

 使用`const`关键字创建不会改变的Widget,这样可以避免不必要的重建。
 使用`const`构造函数创建常量Widget。

flutter 中 widget 都是 inmutable 的,使用const构造函数后,Flutter可以在编译期创建Widget实例,而不是在每次重建时都创建新实例。由于相同的const对象在内存中只存在一份,这减少了内存消耗和对象创建的开销。当父Widget重建时,Flutter会复用这些constWidget而不需要重新创建,从而显著提升渲染性能。

Widget build(BuildContext context) {
  return Container(
    padding: EdgeInsets.all(8.0),
    child: Text('Hello World'),
  );
}
// 优化后
Widget build(BuildContext context) {
  return const Container(
    padding: EdgeInsets.all(8.0),
    child: Text('Hello World'),
  );
}

2. 合理使用StatefulWidget

Flutter的重建机制会从setState()被调用的StatefulWidget开始,重建整个子树。通过将状态隔离在更小的组件中,可以显著减少重建范围。这样当状态变化时,只有包含该状态的小组件会重建,而不是整个页面,从而降低CPU使用率并提高渲染效率。

class MyPage extends StatefulWidget { // 优化前 - 整个页面重建
  @override
  _MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
  bool isLoading = false;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('标题永远不变'),
        if (isLoading) CircularProgressIndicator() else DataList(),
      ],
    );
  }
}
// 优化后 - 只重建变化部分
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('标题永远不变'),
        LoadingStateWidget(),
      ],
    );
  }
}
// 示例代码
class LoadingStateWidget extends StatefulWidget {
  Widget build(BuildContext context) {
    return isLoading ? CircularProgressIndicator() : DataList();
  }
}

3. 使用RepaintBoundary切分重绘区域

当Widget重绘时,Flutter默认会重新绘制该Widget及其所有子Widget。RepaintBoundary会创建一个新的图层,将其子Widget的重绘行为隔离。这意味着当子Widget需要重绘时,Flutter无需重绘边界外的内容。防止不必要的GPU渲染工作,提高复杂界面的性能。可以开启 Inspector 查看重绘

class MyComplexUI extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 频繁更新的区域
        RepaintBoundary(
          child: AnimatedProgressIndicator(),
        ),
        ComplexStaticContent(), // 静态内容
      ],
    );
  }
}

二、列表优化

1. 使用ListView.builder代替Column表优化

Column会一次性构建所有子Widget,无论它们是否在视口内可见。这会导致大量内存使用和初始渲染延迟。相比之下,ListView.builder实现了"视口渲染"技术,只构建和渲染当前可见的项目,其他项会在滚动进入视口时才被构建。这大大减少了内存占用和初始渲染时间,对于长列表尤其重要。

// 优化前 - 使用Column一次性构建所有项
Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Column(
      children: List.generate(1000, (index) => 
        ListTile(title: Text('Item $index'))
      ),
    ),
  );
}
// 优化后 - 使用ListView.builder按需构建
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: 1000,
    itemBuilder: (context, index) => ListTile(
      title: Text('Item $index'),
    ),
  );
}

2. ListView key 的使用

当列表涉及到 增删重排以及列表动效的情况下,需要使用 key,避免重建以后丢失状态,只更新/创建/销毁发生变动的那一项,极大减少无谓的 build、layout、paint 和 State 重建,提升渲染效率。

ListView(
  children: items.map((item) => ListTile(
    key: ValueKey(item.id),
    title: Text(item.title),
  )).toList(),
)

3. ListView itemExtent 使用

  • 不设置 itemExtent 时,ListView 会对子项逐一进行布局测量(layout),每次滚动或构建都需遍历和计算高度,开销大。
  • 设置 itemExtent 后,ListView 可以直接通过数学公式(比如滚动偏移/高度)精确定位和渲染可见Item,避免了对子项的反复测量
  • 这使得列表滚动更加流畅,内存和CPU消耗更低,尤其在长列表、复杂子项情况下优势明显。
  • ListView 的 每个 Title 都固定高度
ListView.builder(
  itemCount: 1000,
  itemExtent: 60.0, // 每项高度固定为 60
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)

三、图片优化

1. 图片缓存和预加载

网络图片加载是耗时操作,如果图片不缓存,同一图片会被重复下载多次,导致网络资源浪费和UI闪烁。使用CachedNetworkImage加载图片,实现了多级缓存机制:内存和持久缓存,可以有效降低内存的占用

import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

2. 图片加载使用合理尺寸

加载远超实际显示尺寸的大图是常见的性能问题。例如,下载一张5MB的4K图片,却只在200x200像素的区域显示,会导致更多的带宽消耗、更大的内存占用,并且图片的解码和缩放会消耗性能,可以使用 Inspector 检测哪些图片过大

// 优化前 - 加载原始大小图片
Image.network('https://example.com/large_image.jpg')
// 优化后 - 指定适当尺寸
Image.network(
  'https://example.com/large_image.jpg?w=300&h=200',  // 服务端支持动态调整图片尺寸
  width: 300,
  height: 200,
  fit: BoxFit.cover,
)

四、代码层面优化

1. 避免不必要的计算

在Flutter中,build方法可能会非常频繁地调用,每次UI状态变化都可能触发重建。如果在build方法中执行复杂计算,会导致: UI卡顿、电池耗电增加、设备发热


  int _calculate(int n) {
    // 检查缓存
    if (_cache.containsKey(n)) {
      return _cache[n]!;
    }
    、、、、计算逻辑
  }
  Widget build(BuildContext context) {
    final result = _calculate(input);
    return Text('Result: $result');
  }

2. Isolate多线程操作

虽然 flutter 有多个线程,但是Dart是单线程执行,dart 一次只能执行一个任务,任务按照顺序一个接一个的执行,具体查看 详解 Flutter engine多线程、Dart isolate和异步普罗哈基米

当执行耗时操作时:UI线程被阻塞 动画会卡顿,帧率下降 可能触发ANR(Android)或页面frezen(iOS)

通过使用compute函数或Isolate,可以将计算密集型任务移至后台线程执行,保持UI线程流畅响应。Flutter的compute函数封装了Isolate创建和通信的复杂性,适合大多数场景。

3. GIF重复解码问题

  • 在Flutter中,直接用Image.assetImage.network加载GIF图片时,如果该GIF在界面上多处被使用,或在滚动列表中多次出现,每次都会重新解码一次。这会导致CPU资源浪费、卡顿、甚至内存暴涨。
  • Flutter的默认图片解码不会自动全局缓存GIF的每一帧,导致每次触达界面都要重新解码(尤其是大GIF或大量列表时),加重主线程负担
  • 使用三方库加载 gif,解析并缓存每一帧图片以及间隔,采用序列帧的方式加载图片

4.减少使用圆角

  • ClipRRectClipOval等圆角裁剪组件,或Containerdecoration: BoxDecoration(borderRadius: ...),在Flutter内部会创建新的图层并触发离屏渲染(offscreen rendering)。
  • 离屏渲染会将裁剪区域单独绘制到一块缓存(内存消耗),然后再合成到主画布。大量使用会极大增加GPU负担,尤其在滑动列表、动画场景中,明显降低性能
  • 能用圆角图片替代裁剪就用图片
  • 只在必要时裁剪(如界面核心部分)
  • 避免嵌套多重ClipRRect。 通过设置checkerboardOffscreenLayers 检测离屏渲染
MaterialApp(
  showPerformanceOverlay: true,
  checkerboardOffscreenLayers: true,
)

5.避免过度使用透明度(Opacity)

  • Opacity widget 会导致其子widget单独绘制到一个新的图层,再整体设置透明度。频繁使用会带来离屏渲染,尤其在动画、列表中影响大。
  • 离屏渲染导致额外的内存消耗和GPU合成压力,严重时会出现掉帧
  • 能用颜色透明值直接设置就不用Opacity(比如Container(color: Colors.black.withOpacity(0.2)))。
  • 动画透明度优先用FadeTransition(配合AnimationController),避免整个子树离屏渲染。
  • 避免在大区域、复杂子树上用Opacity

6. 减少图层嵌套(Widget嵌套过多)

Flutter的Widget嵌套层级过深会导致:

  • 构建(build)树复杂,重建耗时增加
  • Layout、Paint 阶段递归遍历层数增多,性能下降
  • DevTools调试难度增大 每多一层Widget,Flutter的构建、布局、绘制流程都要多一层递归遍历。大量无意义的嵌套严重拖慢渲染效率
  • 合并能合并的Widget(如用Container替代嵌套的Padding+DecoratedBox+Align)。
  • 用自定义Widget封装常用结构,避免重复堆叠
  • 合理拆分大布局,避免过深的子树

7. 动效使用 child 参数,减少重建

动画每一帧都会触发 build。如果子树内容不变却每帧都重建,浪费性能。把不变的内容放到 child 参数,build 只处理“变”的部分,大大减少无意义的构建。

  • AnimatedBuilder
  • AnimatedWidget
  • FadeTransitionScaleTransitionRotationTransition 等 以上组件适用 child 参数
AnimatedBuilder(
  animation: controller,
  child: const Text('静态内容'), // 只 build 一次
  builder: (context, child) {
    return Transform.rotate(
      angle: controller.value * 2 * pi,
      child: child, // 每帧只变 transform
    );
  },
)

8. 优先用 Transform/Opacity 动画,而非重建布局

TransformOpacity 这类属性动画,底层是 GPU 合成变换,不涉及重新布局和绘制,性能极高。相比之下,若动画导致 Widget 结构/布局频繁变动,每帧都要 layout 和 paint,性能很低。

// 推荐
FadeTransition(
  opacity: animation,
  child: Image.asset('xxx.png'),
)

// 不推荐(每帧都重建图片)
AnimatedBuilder(
  animation: controller,
  builder: (context, child) {
    return Opacity(
      opacity: controller.value,
      child: Image.asset('xxx.png'),
    );
  },
)

五、状态管理优化

1. 局部状态更新

  • 全局状态变更导致的过度重建是Flutter性能问题的主要来源之一。当使用setState()或整体状态更新时,会导致整个子树重建,包括许多实际上不依赖变化状态的组件。
  • 使用Provider、GetX、Riverpod和Bloc等细粒度重建机制,都有助于提高应用性能。可以选择性地只通知特定状态变化,而不是刷新整个状态,状态逻辑和UI逻辑分离,提高代码可维护性

2. 内存优化与防止内存泄漏

内存泄漏是Flutter应用中常见的性能问题,特别是在长时间运行的应用中。主要原因包括:

  • 未释放的流订阅(Streams)
  • 动画控制器未dispose
  • 尚未完成的异步任务持有上下文引用
  • 全局单例持有对已销毁组件的引用

六、资源优化

1. 减小应用体积

应用体积直接影响用户下载意愿和存储空间占用。Flutter应用体积较大的主要原因包括:

  • Flutter引擎本身的体积
  • 未优化的资源文件(图片、音视频等)
  • 大量第三方依赖
  • 未配置代码压缩和混淆

通过以下策略可以减小应用体积:

  • 使用--split-per-abi生成特定架构的APK,避免包含所有架构的原生库
  • 启用R8/ProGuard代码压缩和混淆
  • 压缩图片资源,使用适当的格式(WebP优于PNG),也可以使用 tinypng 压缩图片
  • 删除未使用的资源和代码
  • 按需加载功能模块

七、检测工具

1. 使用Flutter DevTools进行性能分析

性能调优的第一步是准确测量和定位问题。Flutter提供了强大的性能分析工具:

  • Inspector 在 flutter 的 debug 和 profile 模式下使用,开启 DevTools 的 Inspector Page
image.png

Highlight Repaint(重绘高亮) Highlight Oversize Images(图片尺寸高亮)

image.pngimage.png

通过 flutter run --profile 开启 profile 模式,使用性能视图(Performance view) 通过火焰图查看哪些方法耗时,修改对应的方法提高性能

  1. Performance Overlay - 显示GPU和UI线程的实时性能图表,帮助识别帧丢失
  2. DevTools Timeline - 详细记录渲染、布局和构建事件,帮助定位性能瓶颈
  3. Widget构建检查器 - 分析Widget树,查找不必要的重建
  4. 内存分析器 - 监控内存使用和潜在泄漏

image.png

总结

Flutter 性能优化的本质是:减少不必要的重建和重绘、减少内存/CPU/GPU压力、让静态内容尽量复用,动态内容最小化刷新,按需加载和渲染,让 UI 始终流畅响应用户。

Flutter AI 工具包:集成 AI 聊天功能到 Flutter App

作者 Bowen_Jin
2025年6月29日 21:00

image.png

主要特点

  1. 多轮聊天:自动维护聊天历史,保持多轮交互的语义连贯性
  2. 流式响应渲染:实时逐字显示 AI 回复,提升交互体验
  3. 富文本显示:支持 Markdown 解析、代码块高亮、链接识别等
  4. 语音输入:使用语音输入prompt。
  5. 多媒体输入:支持发送图片、文件等附件,AI 可识别处理
  6. 自定义样式:提供自定义样式,以匹配App设计。
  7. 聊天序列化/反序列化:存储和检索App会话之间的对话。
  8. 自定义响应Widget:引入专门的UI组件来呈现LLM响应。
  9. 可插拔LLM支持:实现一个简单的界面来插入自定义LLM。
  10. 跨平台支持:兼容Android、iOS、Web和macOS平台。

demo效果

image.png

源代码可在GitHub上找到

安装

依赖项添加到pubspec.yaml文件中

dependencies:
  flutter_ai_toolkit: ^latest_version
  google_generative_ai: ^latest_version # 使用Gemini
  firebase_core: ^latest_version        # 使用Firebase Vertex AI

Gemini AI配置

要使用Google Gemini AI,请从Google Gemini AI Studio获取API密钥

还需要选择一个Gemini model。

import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';

class ChatPage extends StatelessWidget {
  const ChatPage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text(App.title)),
        body: LlmChatView(
          provider: GeminiProvider( // Gemini 服务提供商
            model: GenerativeModel( // Gemini model
              model: 'gemini-2.0-flash',
              apiKey: 'GEMINI-API-KEY', // Gemini API Key
            ),
          ),
        ),
      );
}

GenerativeModel类来自google_generative_ai软件包。GeminiProvider将Gemini AI插入到LlmChatView,LlmChatView是顶级Widget,与您的用户提供基于LLM的聊天对话。

Vertex AI configuration

另外一个AI服务是Firebase的Vertex AI。不需要API密钥,并用更安全的Firebase取代它。要在项目中使用Vertex AI,请按照 Get started with the Gemini API using the Vertex AI in Firebase SDKs 中描述的步骤进行操作。

完成后,使用flutterfire CLI工具将新的Firebase项目集成到您的Flutter App中,如Add Firebase to your Flutter app文档中所述。

按照这些说明操作后,您就可以在Flutter App中使用Firebase Vertex AI了。

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';

// ... other imports

import 'firebase_options.dart'; // from `flutterfire config`

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const App());
}

在Flutter App中正确初始化Firebase后,可以创建Vertex provider的实例了:

class ChatPage extends StatelessWidget {
  const ChatPage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text(App.title)),
        // create the chat view, passing in the Vertex provider
        body: LlmChatView(
          provider: VertexProvider(
            chatModel: FirebaseVertexAI.instance.generativeModel(
              model: 'gemini-2.0-flash',
            ),
          ),
        ),
      );
}

FirebaseVertexAI类来自firebase_vertex ai软件包。构建VertexProvider类,将Vertex AI暴露给LlmChatView。不需要提供API密钥。这些都作为Firebase项目自动处理了。

LlmChatView

LlmChatView Widget 是AI Toolkit提供的互动聊天组件。支持如下功能

  • 多行文本输入:允许用户在输入prompt时粘贴长文本或插入新行。
  • 语音输入:允许用户使用语音输入prompt
  • 多媒体输入:允许用户拍照和发送图像和其他文件类型prompt。
  • 图像缩放:允许用户放大图像缩略图。
  • 复制到剪贴板:允许用户将消息或LLM响应的文本复制到剪贴板。
  • 消息编辑:允许用户编辑最新的消息以重新提交到LLM。
  • 支持Material 和 Cupertino两种设计样式

多行文本输入

语音输入

多媒体输入

图片缩放

点击能缩放图片

复制到剪贴板

文字编辑

长按文字, 弹出编辑菜单

支持Material and Cupertino两种设计样式

额外的功能

  • 欢迎信息:向用户显示初始问候。
  • prompt建议:向用户提供预定义的提建议prompt,以引导互动。
  • 系统指令:让 AI 系统明确 “做什么”“如何做” 以及 “在什么条件下执行”,类似于给 AI 系统下达的 “任务说明书” 或 “操作指南”。
  • 管理历史记录:每个LLM Provider都允许管理聊天记录,用于清除、动态更改和在会话之间存储聊天状态。
  • 聊天序列化/反序列化:存储和检索App会话之间的对话。
  • 自定义响应Widget:引入专门的UI组件来呈现LLM响应。
  • 自定义样式:定义独特的视觉样式,以将聊天外观与整个App相匹配。
  • 自定义LLM Provider:构建自定义LLM Provider,将聊天与您自己的模型后端集成。
  • 重新路由提示:调试、记录或重新路由消息,旨在让Provider动态跟踪问题或路由提示。

欢迎信息

自定义欢迎消息

class ChatPage extends StatelessWidget {
 const ChatPage({super.key});

 @override
 Widget build(BuildContext context) => Scaffold(
       appBar: AppBar(title: const Text(App.title)),
       body: LlmChatView(
         welcomeMessage: 'Hello and welcome to the Flutter AI Toolkit!', //初始化LlmChatView的欢迎消息:
         provider: GeminiProvider(
           model: GenerativeModel(
             model: 'gemini-2.0-flash',
             apiKey: geminiApiKey,
           ),
         ),
       ),
     );
}

prompt建议

没有聊天记录时,提供一组建议的prompt

class ChatPage extends StatelessWidget {
 const ChatPage({super.key});

 @override
 Widget build(BuildContext context) => Scaffold(
       appBar: AppBar(title: const Text(App.title)),
       body: LlmChatView(
         suggestions: [
           'I\'m a Star Wars fan. What should I wear for Halloween?',
           'I\'m allergic to peanuts. What candy should I avoid at Halloween?',
           'What\'s the difference between a pumpkin and a squash?',
         ], /// 建议列表
         provider: GeminiProvider(
           model: GenerativeModel(
             model: 'gemini-2.0-flash',
             apiKey: geminiApiKey,
           ),
         ),
       ),
     );
}

系统指令

让 AI 明确 “做什么”“如何做” 以及 “在什么条件下执行”,类似于给 AI 系统下达的 “任务说明书” 或 “操作指南”。

例如,食谱示例App使用systemInstructions参数来定制LLM,以专注于根据用户的说明提供食谱:

class _HomePageState extends State<HomePage> {
  ...
  // create a new provider with the given history and the current settings
  LlmProvider _createProvider([List<ChatMessage>? history]) => GeminiProvider(
      history: history,
        ...,
        model: GenerativeModel(
          model: 'gemini-2.0-flash',
          apiKey: geminiApiKey,
          ...,
          systemInstruction: Content.system('''
You are a helpful assistant that generates recipes based on the ingredients and 
instructions provided as well as my food preferences, which are as follows:
${Settings.foodPreferences.isEmpty ? 'I don\'t have any food preferences' : Settings.foodPreferences}

You should keep things casual and friendly. You may generate multiple recipes in a single response, but only if asked. ...
''', /// 系统指令
          ),
        ),
      );
  ...
}

历史记录管理

访问history属性查看或设置历史记录:

void _clearHistory() => _provider.history = [];

使用旧的历史来创建新的Provider:

class _HomePageState extends State<HomePage> {
  ...
  void _onSettingsSave() => setState(() {
        // 迁移旧的历史记录到新的供应商
        final history = _provider.history.toList();
        _provider = _createProvider(history);
      });
}

_createProvider方法创建了一个具有上一个Provider历史记录和新用户首选项的新Provider。这对用户来说是无缝的;他们可以继续聊天,但现在LLM会考虑他们的新食物偏好,给他们回复

class _HomePageState extends State<HomePage> {
  ...
  // 根据给定的历史记录和当前设置创建一个新的提供者
  LlmProvider _createProvider([List<ChatMessage>? history]) =>
    GeminiProvider(
      history: history,
      ...
    );
  ...
}

Chat序列化/反序列化

要在App会话之间保存和恢复聊天记录,需要能够对每个用户prompt(包括附件)和每个 LLM 响应进行序列化和反序列化。 两种消息(用户prompt和LLM响应)都暴露在ChatMessage类中。 序列化可以通过使用每个ChatMessage实例的toJson方法来完成。

Future<void> _saveHistory() async {
  // 获取最新的历史
  final history = _provider.history.toList();

  // 保存历史消息
  for (var i = 0; i != history.length; ++i) {
    // 文件存在旧忽略
    final file = await _messageFile(i);
    if (file.existsSync()) continue;

    // 新消息保存到磁盘
    final map = history[i].toJson();
    final json = JsonEncoder.withIndent('  ').convert(map);
    await file.writeAsString(json);
  }
}

同样,要反序列化,使用ChatMessage fromJson方法:

Future<void> _loadHistory() async {
  // 从磁盘读取历史记录
  final history = <ChatMessage>[];
  for (var i = 0;; ++i) {
    final file = await _messageFile(i);
    if (!file.existsSync()) break;

    final map = jsonDecode(await file.readAsString());
    history.add(ChatMessage.fromJson(map));
  }

  /// 设置历史记录
  _provider.history = history;
}

自定义响应Widget

默认聊天视图显示的 LLM 响应格式为 Markdown。可以创建一个自定义Widget来显示您的App风格的样式:

设置LlmChatView的responseBuilder参数:

LlmChatView(
  provider: _provider,
  welcomeMessage: _welcomeMessage,
  responseBuilder: (context, response) => RecipeResponseView(
    response,
  ),
),
class RecipeResponseView extends StatelessWidget {
  const RecipeResponseView(this.response, {super.key});
  final String response;

  @override
  Widget build(BuildContext context) {
    final children = <Widget>[];
    String? finalText;

    // 收到LLM的回复后即时生成内容,因此目前无法得到完整的回复,添加一个按钮以便将食谱添加到列表中
    try {
      final map = jsonDecode(response);
      final recipesWithText = map['recipes'] as List<dynamic>;
      finalText = map['text'] as String?;

      for (final recipeWithText in recipesWithText) {
        // extract the text before the recipe
        final text = recipeWithText['text'] as String?;
        if (text != null && text.isNotEmpty) {
          children.add(MarkdownBody(data: text));
        }

        // 提取食谱
        final json = recipeWithText['recipe'] as Map<String, dynamic>;
        final recipe = Recipe.fromJson(json);
        children.add(const Gap(16));
        children.add(Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(recipe.title, style: Theme.of(context).textTheme.titleLarge),
            Text(recipe.description),
            RecipeContentView(recipe: recipe),
          ],
        ));

        // 添加按钮将食谱添加到列表中。
        children.add(const Gap(16));
        children.add(OutlinedButton(
          onPressed: () => RecipeRepository.addNewRecipe(recipe),
          child: const Text('Add Recipe'),
        ));
        children.add(const Gap(16));
      }
    } catch (e) {
      debugPrint('Error parsing response: $e');
    }

    ...

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: children,
    );
  }
}

自定义样式

使用LlmChatView构造函数的style参数来设置自己的样式,包括背景、文本字段、按钮、图标、建议等默认样式:

LlmChatView(
  provider: GeminiProvider(...),
  style: LlmChatViewStyle(...),
),

万圣节主题演示App

没有UI的聊天

不使用聊天视图也能访问Provider接口。

class _EditRecipePageState extends State<EditRecipePage> {
  ...
  final _provider = GeminiProvider(...);
  ...
  Future<void> _onMagic() async {
    final stream = _provider.sendMessageStream(
      'Generate a modified version of this recipe based on my food preferences: '
      '${_ingredientsController.text}\n\n${_instructionsController.text}',
    ); // 发送用户偏好食谱设置给llm provider
    var response = await stream.join(); // 获取llm推荐的响应
    final json = jsonDecode(response);

    try {
      final modifications = json['modifications'];
      final recipe = Recipe.fromJson(json['recipe']);

      if (!context.mounted) return;
      final accept = await showDialog<bool>( // 只使用了llm服务,没有使用聊天界面
        context: context,
        builder: (context) => AlertDialog(
          title: Text(recipe.title), // 推荐食谱标题
          content: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Modifications:'),
              const Gap(16),
              Text(_wrapText(modifications)), /// 修改的内容
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => context.pop(true),
              child: const Text('Accept'),
            ),
            TextButton(
              onPressed: () => context.pop(false),
              child: const Text('Reject'),
            ),
          ],
        ),
      );
      ...
    } catch (ex) {
      ...
      }
    }
  }
}

重新路由Prompt

设置LlmChatView messageSender来调试、记录或操作聊天视图和底层Provider之间的连接

class ChatPage extends StatelessWidget {
  final _provider = GeminiProvider(...);

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: const Text(App.title)),
      body: LlmChatView(
        provider: _provider,
        messageSender: _logMessage,
      ),
    );

  Stream<String> _logMessage(
    String prompt, {
    required Iterable<Attachment> attachments,
  }) async* {
    // log the message and attachments
    debugPrint('# Sending Message');
    debugPrint('## Prompt\n$prompt');
    debugPrint('## Attachments\n${attachments.map((a) => a.toString())}');

    // 发送消息到provider
    final response = _provider.sendMessageStream(
      prompt,
      attachments: attachments,
    );

    // log response信息
    final text = await response.join();
    debugPrint('## Response\n$text');

    yield text;
  }
}

用于一些高级操作,如动态路由到Provider或检索增强生成(RAG)。

定制LLM Provider

abstract class LlmProvider implements Listenable {
  Stream<String> generateStream(String prompt, {Iterable<Attachment> attachments});
  Stream<String> sendMessageStream(String prompt, {Iterable<Attachment> attachments});
  Iterable<ChatMessage> get history;
  set history(Iterable<ChatMessage> history);
}

任何实现LlmProvider接口的都可以插入聊天视图, 可以是云或本地的

  1. 提供配置支持
  2. 处理历史
  3. 将消息和附件翻译成底层LLM
  4. 调用底层LLM

配置支持

class GeminiProvider extends LlmProvider ... {
  @immutable
  GeminiProvider({
    required GenerativeModel model,
    ...
  })  : _model = model,
        ...

  final GenerativeModel _model;
  ...
}

处理历史

历史记录是Provider的重要组成部分

Provider不仅需要允许直接操作历史记录,而且必须在更改时通知Listener。 为了支持序列化和更改Provider参数,必须支持保存历史记录作为构建过程的一部分。

class GeminiProvider extends LlmProvider with ChangeNotifier {
  @immutable
  GeminiProvider({
    required GenerativeModel model,
    Iterable<ChatMessage>? history,
    ...
  })  : _model = model,
        _history = history?.toList() ?? [],
        ... { ... }

  final GenerativeModel _model;
  final List<ChatMessage> _history;
  ...

  /// 设置对话历史记录并重新初始化聊天会话
  @override
  Stream<String> sendMessageStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) async* {
    final userMessage = ChatMessage.user(prompt, attachments);
    final llmMessage = ChatMessage.llm();
    _history.addAll([userMessage, llmMessage]); /// 添加到历史记录

    final response = _generateStream(
      prompt: prompt,
      attachments: attachments,
      contentStreamGenerator: _chat!.sendMessageStream,
    );

    yield* response.map((chunk) {
      llmMessage.append(chunk);
      return chunk;
    });

    notifyListeners();
  }

  /// 获取当前的对话历史记录
  @override
  Iterable<ChatMessage> get history => _history;

  /// 设置对话历史记录并重新初始化聊天会话
  @override
  set history(Iterable<ChatMessage> history) {
    _history.clear();
    _history.addAll(history);
    _chat = _startChat(history);
    notifyListeners();
  }

  ...
}
import 'package:google_generative_ai/google_generative_ai.dart';
...

class GeminiProvider extends LlmProvider with ChangeNotifier {
  ...
  static Part _partFrom(Attachment attachment) => switch (attachment) {
        (final FileAttachment a) => DataPart(a.mimeType, a.bytes),
        (final LinkAttachment a) => FilePart(a.url),
      };

  static Content _contentFrom(ChatMessage message) => Content(
        message.origin.isUser ? 'user' : 'model',
        [
          TextPart(message.text ?? ''),
          ...message.attachments.map(_partFrom),
        ],
      );
}

调用LLM

class GeminiProvider extends LlmProvider with ChangeNotifier {
  ...

  @override
  Stream<String> generateStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) =>
      _generateStream(
        prompt: prompt,
        attachments: attachments,
        contentStreamGenerator: (c) => _model.generateContentStream([c]),
      );

  @override
  Stream<String> sendMessageStream(
    String prompt, {
    Iterable<Attachment> attachments = const [],
  }) async* {
    final userMessage = ChatMessage.user(prompt, attachments);
    final llmMessage = ChatMessage.llm();
    _history.addAll([userMessage, llmMessage]);

    final response = _generateStream(
      prompt: prompt,
      attachments: attachments,
      contentStreamGenerator: _chat!.sendMessageStream,
    );

    yield* response.map((chunk) {
      llmMessage.append(chunk);
      return chunk;
    });

    notifyListeners();
  }

  Stream<String> _generateStream({
    required String prompt,
    required Iterable<Attachment> attachments,
    required Stream<GenerateContentResponse> Function(Content)
        contentStreamGenerator,
  }) async* {
    final content = Content('user', [
      TextPart(prompt),
      ...attachments.map(_partFrom),
    ]);

    final response = contentStreamGenerator(content);
    yield* response
        .map((chunk) => chunk.text)
        .where((text) => text != null)
        .cast<String>();
  }

  @override
  Iterable<ChatMessage> get history => _history;

  @override
  set history(Iterable<ChatMessage> history) {
    _history.clear();
    _history.addAll(history);
    _chat = _startChat(history);
    notifyListeners();
  }
}

最终的AI 聊天效果

跨端通信终结者|看我是如何保证多端消息一致性的 🔥

作者 园宵
2025年6月27日 20:02

本文当时为了专利过审下架了,现在放回来 ~

本文写作于兔年之前,心境上却是有诸多感叹,大名鼎鼎的网络库 AFNetworking 停止维护了,iOS 开发早已是昨日黄花,移动端也从夕阳走向落幕了 ~

lQLPJxf-k1hlNyzNAaPNA7Gw8JmC9IJ2K5gDwlduWYCXAA_945_419.png

认清现实,摆正心态,拥抱变化,跨端开发确实比原生开发更值得卷 (o_ _)ノ 。作为一个码了10年的老派程序员,卷也要选择优雅的卷。Flutter \ Web \ Rust and so on ... 各种花里胡哨的跨端语言,怎么有机的结合在一起,这确实是一门学问。

起因

言归正传 ~

从小故事开始

我有个薛定谔状态的朋友,他入职了一家公司里做 iOS 开发,一天他接到一个业务需求,需要配合 Web 开发修改一下 windTrack 埋点桥接实现,通过对代码进行字符串搜索,他发现在不同模块里有4个可能是 windTrack 桥接的插件,但好在是3个已经被标记成过时,看起来他只需要处理未过时的那个即可。

故事嘛都有戏剧性,在修改打包后提供给 Web 开发同学测试时发现,修改并没有生效 ... 多方查找问题后发现,windTrack桥接最终会调用到一个标记过时的方法里 ...

绝望难过伤心动态表情包.gif

问题的本质

后续这个朋友如何处理暂且不表,我们来聊聊为什么会出现这样的问题?

本质上是可维护性的缺失,为什么项目里会存留4个具有相同能力的方法呢?因为它经手了 N(N > 4)位开发同学,这在业务快速增长时期,确实是存在的现象,有人在维护、有人在新增、有人在重构...懂得都懂。

思维升华

靠人约束规范是不可控的,加再多的 Code Review 流程也是不可控的,因人而异就会因人而劣化,直至规范形同虚设。这也包括维护文档这件事,理论上每个跨端桥接都要求有完善的使用文档,但现实上,文档的约束比代码更难,毕竟也没有 Docs Review 流程。

分析

我们虽然明了问题的本质,但还是需要具体的分析问题、解决问题。

现有开发方式

以 iOS - Web 通信桥接为例。

例:iOS 实现

底层调用上,基本都是通过 WKWebviewWKScriptMessageHandler 代理来监听 web -> iOS 的调用,通过主动调用 JS callbackId 的方式来回调消息。

部分代码截图:

image.png

image.png

从收到的 message.body 解析出调用的方法名、方法参数以及回调的 callbackId 等字符串。

然后我们都会或多或少的封装下,用各种方式找到具体的实现方法。这里就不展开讲了,基本就是用配置的方式、用代码反射的方式这样来做解耦。

像笔者公司上,就是用的代码反射,如下图:

image.png

实现方法上就可以转变为:

- (void)windTrack:(NSDictionary *)params completeBlock:(void (^)(id _Nullable result))completeBlock {
    ...
}

同理,Flutter 也有一套相同的实现方式,只不过 Flutter Channel 比 CallbackId 更优雅些。

例:Web 调用

在 Web 端使用层面,需要一些代码来隐藏 callbackId 以及抹平 iOS 双端调用差别,这里不做过多说明,还是以 windTrack 为例:

Bridge.call('report/windTrack', { ...params }).then(result => {})

一般来说,Web 开发同学也会把上述调用方法再二次封装一下来使用。

存在问题

可能多数同学或者以前的笔者也会觉得这样实现并没有什么问题,实现方式上大同小异,基本也都是这样来开发的。

但结合我们要求的可维护性来看,是有着以下弊端的:

  • 字符串类型的方法名字段做桥的连接标识位,这个属于人为强行定义,并不是可靠的。虽然可以增加一些运行时校验来检查,但也是环节滞后的测试手段。
  • 参数并没有明确定义,统一都是 Map 对象,虽然也可以增加明确方法注释来缓解这个问题,但需要多端对齐参数及其类型,意外时常发生,最多也是增加运行时校验来断言。
  • Flutter、Web 已定义了大量的桥接方法,也说不上来哪个到底有用,哪个已经没用了,对于原生桥接实现方法来说,就是一个也不敢删,生怕线上出现问题。何况还有命中到作废方法这种奇葩。
  • 最为重要的是,从开发到维护,沟通上的成本居高不下,还会因人而废,一旦出现问题,排查起来十分头疼。

思考

我们总结下上述的问题,来想一下对开发最友好的是什么样的?

不需要写方法匹配

不需要写使用文档

不需要手动解析 Map 入参

不需要手动拼装 Map 回调

这在解决方案上,当然会想到用跨端工具链(说到跨端工具链,我们是在说什么?)的方式来实现。

简单来说:一次定义,多端实现,任意调用。

这也类似笔者前几篇文章中写到的 Flutter 多引擎渲染组件 中跨端处理方式。

举例

我们还是以故事里的windTrack为例:

方法提供者只需要实现 interface windTrack(int eventId, String eventName, Map attributes)

方法使用者直接调用 windTrack(eventId, eventName, attributes)

剩下的过程开发都不需要接入,都有工具链进行生成。

解决方案

实现效果

先看下最终实现的效果。

image.png

如上图,只要定义 YAML,就会根据上述定义,来生成多端的使用代码。

再来看看 iOS 和 Web 端生成的效果

iOS 支撑服务效果

生成的 iOS 组件库

image.png

这里只需开发接口实现层

@interface GNBReportServiceImplement () <GNBReportServiceObserver>

@end

@implementation GNBReportServiceImplement

- (instancetype)init {
    self = [super init];
    if (self) {
        [GNBManager.sharedInstance.reportService addObserver:self];
    }
    return self;
}

// MARK: - GNBReportServiceObserver
- (void)reportProviderDidWindTrack:(nullable NSString *)eventId event:(nullable NSString *)event detailInfo:(nullable NSDictionary *)detailInfo completedBlock:(nonnull void (^)(NSError *))completedBlock {
    ...
}

@end

Web 组件调用效果

生成的 Web 组件

image.png

注册后即可使用

GNB.init(new GNBAppRegister());
...
GNB.report.windTrack(...);

可以看到,流程上就转变为:全局定义 + 各端具体实现 API + 各端调用 API。大大的增强可维护性,间接的降低了人力成本,又能为老板省钱了[手动狗头]。

总体架构

1.png

专有名词解释

[GNB] Gaoding Native Bridge,稿定本地通信方案

上图是从能力视角描述了我们实现了什么,下图是从流程视角,来表明整个过程是如何运转起来的。

image.png

详细设计

定义规范

组件定义采用 YAML 标准化语言定义。

文件命名以用插件模块名称,比如 user.yaml 、 report.yaml 。

APIs 定义
定义 说明
name 名称
note 说明
params 参数 List[name: 名称note: 说明type: 类型default:  默认值required: 是否必填,默认非必填,有默认值也为非必填]
callback 回调 List[name: 名称note: 说明type: 类型required: 是否必填,默认非必填]
Classes 定义
定义 说明
name 名称
note 说明
properties 属性列表[name: 名称note: 说明type: 类型]
TYPE 支持
YAML 定义 Flutter(dart) iOS(objectivec) Android(kt) Web(ts)
String String NSString * String string
int/long/double/bool number NSNumber * Number number
Map Map NSDictionary * Map any
List<String> List<String> NSArray<NSString *> List<String> array<string>
List<int/long/double/bool> List<number> NSArray<NSNumber *> List<Number/Boolean> array<number>
List<Class> List<Class> NSArray<Class > List<Class> array<Interface>
Class Class Class * Class Interface
Any dynamic id Any any

number 类型说明 定义上还是用 int/long/double/bool 来定义,但为了数据传输安全,所以各端用 number 类型来承接,且会在注释上会带上当前的精度说明。至于为什么定义上不直接使用 number,这是为 Rust 扩展考虑,Rust 上数值类型是明确精度(比如 f64),并没有提供 number 泛型。

Class 说明为了不增加拆箱复杂度, List<Class> 只能在 Class 中定义,不能直接在 params / callback 中使用。

Any 说明 Any 类型尽量不要使用,前期为了过渡,后续会禁用掉。

示例
####
classes:
  - name: UserInfo
    note: 用户信息定义
    properties:
      - { name: userId, note: 用户 ID, type: String }
####
apis:
  - name: fetchUserInfo
  - note: 获取当前用户信息
  - callback:
      - name: userInfo
        note: 用户信息
        type: UserInfo

抽象服务

对 iOS / Android 来说,是能力的实现方。

当前并不会改变以前的 channel 或者 bridge 底层实现形式,只会在这个基础上另外封装。

封装上,因为可以自动生成了,所以不再需要主动注册插件,也不需要写动态调用的代码,直接构建 map 对象注册各个方法转发,且去生成相应的 Service。

Service 提供 Observer 作为需实现的 API。

好处是可以在任意模块、代码监听来提供服务实现。

当然,前期我们还是会把实现层都写在一个模块里统一管理。

image.png

调用入口

以 iOS Web 容器为例

image.png

在 WKWebview 的 script 消息接收代理中调用我们生成的 GNB 模块的入口 GNBManager.sharedInstance execute:params:completedBlock: 方法即可。

image.png

因为 GNB 模块的代码是自动生成的,所以可以无视一些复杂度规范,直接用 if else 来进行判断后直接命中方法,不再需要动态反射等不好维护的解耦手段。

接口代理

上图中,执行入口会通过方法名称命中到 XXXService,这里我们来了解下,service 是如何做的,这也是抽象服务的关键设计。

还是以 windTrack 为例:

@implementation GNBReportService

- (void)windTrack:(NSDictionary *)params completedBlock:(void (^)(NSDictionary *result))completedBlock {
    [self notifyObserversWithSelector:@selector(reportProviderDidWindTrack:event:detailInfo:completedBlock:), params[@"eventId"], params[@"event"], params[@"detailInfo"], ^(NSError *error) {
        NSDictionary *_result = [GNBUtils resultWithData:@{
        } error:error]; // 自动装箱
        GDBlockCall(completedBlock, _result);
    }];
}

@end

入口命中后会触发一个观察者通知方法,通知给监听者,这里除了模块解耦外,最主要的是做了装箱拆箱,把入参拆箱,把出参装箱,当然这也是自动生成的,所以可以保证它是可靠的。

// MARK: - Observer
@protocol GNBReportServiceObserver <NSObject>

/// 埋点上报
///
/// - Parameter eventId: <String> 事件 ID
/// - Parameter event: <String> 事件定义
/// - Parameter detailInfo: <Map> 详细内容
/// - Parameter completedBlock: 回调
- (void)reportProviderDidWindTrack:(nullable NSString *)eventId event:(nullable NSString *)event detailInfo:(nullable NSDictionary *)detailInfo completedBlock:(void (^)(NSError *error))completedBlock;

@end

使用上,接口实现者实现上述代理即可。

调用组件

调用组件 Web 的比较简单,因为只需要构造 TS interface 即可。相对的 Flutter 较为麻烦,因为 Class <-> Map 是比较重的。

还是以 Web 为例。

调用入口
image.png

入口可以根据环境注册不同的 Register,以适应不同的宿主环境(Wap / 小程序 / APP),其中 GNBAppRegister 也是自动生成的,Wap / 小程序的实现代码需要手动补充。


export interface GNBRegister {  
  /**
   * 报告相关
   */
  report: GNBReport
  
  ...
}

/**
 * Gaoding Native Bridge
 */
export class GNB {
  private static _register?: GNBRegister

  static get report(): GNBReport {
    assert(GNB._register, 'GNB 必须注册使用')
    assert(GNB._register!.report, 'report 未实现')
    return GNB._register!.report
  }
  
  ...
  
  /**
   * 初始化 GNB
   * @param register 注册者
   */
  static init(register: GNBRegister): void {
    GNB._register = register
  }
}
调用服务

再顺着调用入口看下来,会生成如下的GNBAppRegister

export class GNBAppRegister implements GNBRegister {
  report = {
    windTrack(eventId?: string, event?: string, detailInfo?: any): Promise<GNBReportWindTrackResponse> {
      return bridge.call('GNB_report/windTrack', {
        'eventId': eventId,
        'event': event,
        'detailInfo': detailInfo,
      })
    },
  }
  ...
}

服务定义生成在 bridges 文件夹中

image.png

代码生成

选择 python 作为开发语言,更为通用。

image.png

生成流程上:

  1. 解析 YAML 生成 DSL Model
  2. 拷贝资源文件
  3. 生成 iOS / Android / Web / Flutter 代码
  4. 构建产物包

image.png

具体代码实现不在文章中表述了(掘金不爱大段代码 ~),这里着重讲一下实现难点和思考。

DSL Model

YAML 定义是一种标准的 DSL 语言,但在使用上,用 Map[XXX] 对脚本来言并不好维护,也不够优雅,所以在生成前,我们会做一个 DSL 模型,来承载数据结构。

model.py

class PropertyModel:
    ...
class GNBAPIModel:
    ...
class GNBClassModel:
    ...
class ModuleInfo:
    ...
image.png

简单示意,我们把 YAML 映射到 Model 的过程。

api.py

class API:
    ...
    @staticmethod
    def get_modules() -> list[ModuleInfo]:
        """
        获取模块信息
        """
        modules = []
        for file_name in os.listdir(Define.yaml_dir):
            with open(Define.yaml_dir + '/' + file_name) as f:
                json = yaml.load(f.read(), Loader=yaml.FullLoader)
                info = ModuleInfo(file_name, json.get('note'),
                                  json.get('apis'), json.get('classes'))
                modules.append(info)
        return modules
编码转换

整个生成上,重头戏就是处理各种编码的转换。

首先是类型转换,对基础类型、引用类型、自定义类型进行转换。

这里不同的生成器根据上述 YAML 类型定义,使用不同的类型转换工具方法。

例如 iOS 类型转换工具方法:

image.png

其中较为麻烦的是对自定义模型的处理,在装拆箱中需要有相应的 toJSON / toModel 方法。

在类型处理之外,还提供了如下的工具方法:

def oc_array_class_type(type: str) -> str:
    """
    返回 NSArray<Class> 中的 Class
    """

def oc_to_json(type: str, name: str) -> str:
    """
    获取 oc 的序列化
    """

def oc_assign(type: str) -> str:
    """
    获取 OC 修饰符
    """

def oc_import(name: str, prefix: str = '\n') -> str:
    """
    获取 OC 引用
    """
 
def oc_property(name: str, type: str, note: str = '', **optional) -> str:
    """    
    获取 OC 属性行
    """

def oc_protocol(name: str, note: str = '') -> GenContainer:
    """
    获取 OC 代理块
    """

def oc_interface(
    name: str,
    note: str = '',
    extends: str = 'NSObject',
) -> GenContainer:
    """
    获取 OC interface
    """

def oc_implementation(name: str) -> GenContainer:
    """
    获取 OC implementation
    """
  
def oc_method(name: str,
              note: str = 'no message',
              params: list[PropertyModel] = [],
              callback: list[PropertyModel] = []) -> GenContainer:
    """
    获取 OC 方法
    """

def oc_notification_method(name: str,
                           params: list[PropertyModel] = [],
                           callback: list[PropertyModel] = []) -> GenContainer:
    """
    获取 OC 响应 Observer 方法
    """
  
def oc_block(callback: list[PropertyModel] = []) -> list[str]:
    """
    获取 OC 响应的 Block 值
    """
   
def oc_assert_required(params: list[PropertyModel] = []) -> list[str]:
    """
    获取 OC 必填 Assert
    """
   
def oc_lazy_getter(name: str, type: str) -> str:
    """
    获取 OC 懒加载的 Getter

    Args:
        name (str): 名称
        type (str): 类型
    """
代码格式化

其实有想到用第三方格式化工具,比如 Web 使用 prettier 来格式化生成代码,但现有的就有4种语言,找齐可用的格式化插件有些不现实,特别是 iOS 的格式化。

好在这个项目格式化还不算复杂,提供一些格式化工具方法即可优雅的封装起来。

utils/ios.py

def format_line(line: list[str], prefix='') -> str:
    """
    格式化文本行

    Args:
        line (list[str]): 文本行
        prefix (str, optional): 每行前缀. Defaults to ''.
    """
    text = f'\n{prefix}'.join(line)
    text = text.replace(f'\t', '    ')
    return text

在生成上就优雅的多,比如生成 GNBReportService.h 中的定义头:

image.png

结合编码转换提供的工具类,这样写即可。

def get_header_methods(self, module: ModuleInfo) -> str:
        """
        返回 Service 的方法定义
        """
        line = []
        for api in module.apis:
            line.append(self.get_method_define(api.name) + ';')
        return format_line(line, '\n')
产物包

image.png

对于不同的环境,使用不同的产物包模版。

iOS:cocoapods

Android:gradle

Flutter:FlutterPlugin

Web:npm

其中 Web 比较特别,我们希望直接依赖产物,所以在生成脚本的最后一句构建产物包中,还会执行响应的 Web 构建命令。

main.sh

# Web build

printf "[gnb-codegen]: web building ...\n"

cd ../../components/gaoding_native_bridge/web/gaoding-native-bridge

yarn

yarn build

直接生成产物到 /lib 中,把整个流程自动化起来。

image.png

当然,现在更多的是 monorepo 大仓的形式,所以不会打成远程包,而是采用application-services的方式本地依赖。

后续上也完全可以很简单的指定远端仓库,增加下各个语言的仓库推送命令,生成二方库来使用。

Schema 校验

有心的看官们可能有注意到,如何限定 YAML 的编写呢,这个如果不符合标准,生成出来的东西完全是不可用的。

这里就要介绍下大名鼎鼎的 jsonschema,我们常用的 package.json 也好,pubspec.yaml 也好,都是根据这个规范来检查我们在里面的配置项。

当然,这个不发布也是可以直接使用的。

我们先构建一个 gnb.schema.json 文件

image.png

其中比较有意思的就是自定义类型的判断:

"pattern": "^(String|int|long|double|bool|Map|List|List<(String|int|long|double|bool)>|Any|GNB(?:[A-Z][a-z]+)+)$",

可以看到,是通过正则匹配类型是否正确的,而自定义类型就是GNB开头作为类型名称的才可以,也是一种取巧设计。

然后我们在工程中的 .vscode/settings.json 文件进行配置即可生效:

{
  ...
  "yaml.schemas": {
    "gnb.schema.json": "*.yaml"
  }
}

image.png

题外话:jsonshema 也可以用于后端接口参数校验。

生成在线文档

还有架构图上提到的生成文档能力,这个笔者在 Flutter 多引擎渲染组件 已经用 Ruby 实现过一次,这次是用 python 重写(不为别的,就是折腾)。

套路上也差不多,先看下效果:

image.png

Docs 在线文档用的 VuePress2 编辑,生成相应的 markdown 文件即可。

image.png

生成上比生成代码简单的多,这里不做过多阐述。

总结

这篇文章笔者个人觉得对比前些篇文章会更抽象一些,用的也是 Web 和 iOS 双端举例,限于篇幅,没有 Flutter 和 Android 的代码展示,但原理都是相同的,希望大家能了解到其中的思想 ~

整体方案来说并不只是在通信上的抽象,优势还在于可以很方便的替换底层通信实现。无论是 bridge 还是 channel,甚至可以换成 ffi 或者 protobuf 这样的通信形式,都不会影响上层的服务调用及支撑实现。

后续生成上也会对更多的平台进行支持,比如增加 Rust 的支撑服务,让 Rust 直接与 Web / Flutter 通信,毕竟终端工程师 ~= 全干工程师[手动狗头]。

可能会有同学疑问,这些生成的组件包是怎么通过 monorepo 结合到大仓里的,这里是用了application-services的建设方案,这个后续会另起一篇文章阐述 ~

本方案还在落地过程中,当落地后会把生成代码工具开源共享 ~


感想

本来笔者想靠本文升到创作 Lv4,达成年前定的小目标。但硬靠着前些篇文章的积累就已经达到了 🎉 。

后续写作上,就不不不不参加日更活动了 (o_ _)ノ ,文章上更加精益求精(长篇大论)~ 给自己定的 2023 年目标是 20 篇文章即安好 ~


感谢阅读,如果对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif

Flutter Clean 架构中的 Riverpod 在哪里

作者 tangzzzfan
2025年6月25日 08:49

本篇文章与你深入探讨在 Flutter 中如何运用 Clean Architecture,并结合 Riverpod、Go_Router 和 Retrofit 这套现代化工具栈,打造一个高内聚、低耦合、可扩展、易维护的顶级应用架构。

我们将从“道”的层面(高屋建瓴的架构哲学)入手,再深入到“术”的层面(具体分层、组件和痛点解决方案),并最终通过一个实例来将理论落地。


第一部分:高屋建瓴的架构统领 (The "Why" and "How" at 30,000 Feet)

在引入任何具体技术之前,我们必须先确立架构的核心指导思想。对于 Clean Architecture + Riverpod + Go_Router + Retrofit 这套组合拳,其核心思想是:

“通过依赖倒置(Dependency Inversion),实现业务逻辑与外部世界的彻底解耦,并利用现代化的状态管理和路由框架,优雅地将它们粘合起来。”

这个思想可以拆解为三个关键原则:

  1. 同心圆法则 (The Dependency Rule): 这是 Clean Architecture 的灵魂。内层(Domain)绝对不能依赖外层(Infrastructure, Presentation)。所有依赖关系都指向内部。这意味着,你的核心业务逻辑(比如商品折扣如何计算)不应该知道它是被一个 Flutter App 使用,数据是来自一个 REST API,还是存储在 Hive 数据库里。

  2. 职责分离原则 (Separation of Concerns):

    • Clean Architecture 负责 “结构” 的分离:它定义了代码应该放在哪里(Domain, Application, Presentation, Infrastructure)。
    • Riverpod 负责 “状态与依赖” 的分离:它作为依赖注入(DI)容器和服务定位器(SL),将各层实现“注入”到需要它们的地方;同时,它作为状态管理(SM)框架,将 UI 状态与 UI 渲染分离。
    • Go_Router 负责 “UI 与导航” 的分离:它将页面导航逻辑从 UI 组件的调用中抽离出来,变为中心化的、基于路由地址的声明式导航,极大降低了页面间的耦合。
    • Retrofit/Dio 负责 “业务与数据获取” 的分离:它将 HTTP 请求的构造和解析细节封装起来,让我们的数据层实现更专注于“获取什么数据”,而不是“如何获取”。
  3. 面向接口编程 (Programming to an Interface): 这是实现依赖倒置的具体手段。应用层(Application)和基础设施层(Infrastructure)都依赖于领域层(Domain)中定义的抽象接口(Repository Interfaces),而不是具体的实现。这使得我们可以轻松地替换数据来源(比如从网络 API切换到 Mock 数据或本地数据库)而无需修改任何业务逻辑或 UI 代码。

一言以蔽之:我们的目标是构建一个稳定的“业务核心”(Domain + Application),外部的变化(UI框架升级、数据库更换、API变更)不会轻易撼动这个核心。


第二部分:深入阐述各关键点与痛点分析

现在,我们深入到具体的“术”。

2.1 Clean 架构分层详解

一个典型的 Flutter Clean Architecture 项目结构如下:

  • Domain Layer (领域层):

    • 职责: 包含最核心、最纯粹的业务逻辑和业务实体,完全独立于任何框架。
    • 包含内容:
      • Entities (实体): 代表业务对象的类,如 User, Product, Order。它们可以包含只依赖自身属性的业务逻辑(例如,一个 Order 实体可以有一个 isCompleted() 的 getter 方法)。
      • Repositories (仓库接口): 抽象接口,定义了获取和操作业务实体的数据契约。例如,abstract class ProductRepository { Future<Product> getProductById(String id); }。它只定义“做什么”,不定义“怎么做”。
      • Value Objects (值对象):Email, Password 等,用于封装验证逻辑,保证数据的有效性。
    • 特点: 无任何 Flutter/Dart 外部库依赖(除了可能的 equatable 等辅助库)。这是最稳定、最可移植的一层。
  • Application Layer (应用层 / Use Cases):

    • 职责: 编排和调度 Domain 层和 Infrastructure 层,执行具体的应用功能。它代表了“用户想要做什么”。
    • 包含内容:
      • Use Cases (或称 Interactors): 每个 Use Case 代表一个单一的应用功能点。例如 GetProductDetailsUseCase, AddToCartUseCase。它们会调用一个或多个 Repository 接口来完成任务。
    • 特点: 依赖于 Domain 层。它不知道 UI,也不知道数据来自网络还是本地。它只负责协调。例如,AddToCartUseCaseexecute 方法可能会先调用 ProductRepository 获取商品信息,再调用 UserRepository 检查用户资格,最后调用 CartRepository 将商品加入购物车。
  • Presentation Layer (表现层):

    • 职责: 显示 UI,响应用户交互,并将用户行为传递给 Application 层。
    • 包含内容:
      • Widgets/Pages (UI): Flutter 的 StatelessWidgetConsumerWidget。它们应该尽可能“笨”,只负责根据状态渲染 UI 和转发用户事件。
      • State Management (Providers/ViewModels): Riverpod Providers 就在这里扮演核心角色。通常我们会为每个页面或复杂组件创建一个 StateNotifierProviderAsyncNotifierProvider,它扮演着 ViewModel/Controller 的角色。这个 Provider 会调用相应的 Use Case,处理返回结果(成功、失败、加载中),并管理 UI 所需的状态。
      • Navigation (Go_Router): 路由配置和导航逻辑。
  • Infrastructure Layer (基础设施层):

    • 职责: 实现 Domain 层定义的接口,处理所有与外部世界的交互。
    • 包含内容:
      • Repository Implementations: 对 Domain 层 Repository 接口的具体实现。例如,ProductRepositoryImpl 会实现 ProductRepository 接口,其内部会调用一个或多个数据源。
      • Data Sources:
        • Remote Data Source: 使用 RetrofitDio 来访问网络 API。
        • Local Data Source: 使用 Hive, Isar, shared_preferences 等进行本地数据持久化。
      • Other Services: 封装其他平台相关的功能,如设备信息、权限管理、推送通知、分析服务等。

2.2 Riverpod 的角色与层次归属

这是一个常见困惑点。Riverpod 不属于任何单一层,而是贯穿各层的“粘合剂”和“电力系统”

  • 在 Presentation 层:

    • UI 组件 (ConsumerWidget) 通过 ref.watch 来订阅 Provider 暴露的 UI 状态,实现响应式刷新。
    • 用户事件(如按钮点击)通过 ref.readref.notifier 来调用 Provider 中的方法,从而触发业务逻辑。
  • 作为依赖注入 (DI) 容器:

    • 核心作用:main.dart 或应用启动时,我们通过 ProviderScopeoverride 来“组装”我们的应用。
    • 示例:
      // Domain Layer (product_repository.dart)
      abstract class ProductRepository { ... }
      
      // Infrastructure Layer (product_repository_impl.dart)
      class ProductRepositoryImpl implements ProductRepository { ... }
      
      // Application Layer (get_product_usecase.dart)
      class GetProductDetailsUseCase {
        final ProductRepository _repo;
        GetProductDetailsUseCase(this._repo);
        ...
      }
      
      // Riverpod Providers (providers.dart)
      // 1. 提供基础设施层的具体实现
      final productRepositoryProvider = Provider<ProductRepository>((ref) {
        // 在这里可以根据环境返回 Mock 或真实实现
        return ProductRepositoryImpl(ref.watch(dioProvider));
      });
      
      // 2. 提供应用层的 UseCase,并自动注入依赖
      final getProductDetailsUseCaseProvider = Provider<GetProductDetailsUseCase>((ref) {
        // Riverpod 自动处理依赖关系!
        final repository = ref.watch(productRepositoryProvider);
        return GetProductDetailsUseCase(repository);
      });
      
      // 3. 提供表现层的 StateNotifier/ViewModel
      final productDetailsViewModelProvider = StateNotifierProvider.family<...>((ref, productId) {
        // ViewModel 可以访问 UseCase
        final useCase = ref.watch(getProductDetailsUseCaseProvider);
        return ProductDetailsViewModel(useCase, productId);
      });
      
    • 通过这种方式,ProductDetailsViewModel 依赖 GetProductDetailsUseCase,后者又依赖 ProductRepository 接口。但在运行时,Riverpod 悄悄地将 ProductRepositoryImpl 这个具体实现注入了进来,完美实现了依赖倒置。

2.3 项目组件划分:以“四级存储”为例

在实际项目中,一个功能(比如获取用户信息)可能涉及多级数据源。我们可以设计一个经典的四级存储策略:

  1. 一级缓存 (Memory Cache):

    • 实现: Riverpod Provider 本身。使用 AsyncNotifierProvider.autoDispose 或普通的 AsyncNotifierProvider 配合 keepAlive 就可以实现。数据存在内存中,速度最快,但随 Provider 销毁而失效。
    • 场景: 页面内或短时间内的重复数据请求。
  2. 二级缓存 (Local Persistence Cache):

    • 实现: 在 Repository 实现中引入本地数据源(如 Hive/Isar)。
    • 场景: 跨应用会话的数据缓存,如用户配置、不常变动的列表数据。启动应用时可以先从这里加载,给用户即时反馈。
  3. 三级存储 (Network):

    • 实现: 远程数据源,通过 Retrofit 调用 API。
    • 场景: 数据的最终来源(Source of Truth)。
  4. 四级存储 (Bundled/Pre-populated Data):

    • 实现: 打包在 App assets 里的 JSON 或数据库文件。
    • 场景: 初始配置、地区列表、默认数据等。

RepositoryImpl 中的逻辑流可能是这样的:

Future<User> getUser(String userId) async {
  // 1. 尝试从内存缓存获取 (由 Riverpod 的 Provider 缓存策略管理)
  // Riverpod 自身就处理了这部分,如果 provider 还在,就不会重新执行 fetch 逻辑

  // 2. 尝试从本地数据库获取
  final localUser = await _localDataSource.getUser(userId);
  if (localUser != null && !isStale(localUser.timestamp)) {
    return localUser;
  }

  // 3. 从网络获取
  try {
    final remoteUser = await _remoteDataSource.fetchUser(userId);
    // 成功后,更新本地数据库
    await _localDataSource.saveUser(remoteUser);
    return remoteUser;
  } catch (e) {
    // 网络失败,如果本地有旧数据,也可以考虑返回
    if (localUser != null) return localUser;
    // 实在没有,就抛出异常
    throw e;
  }
}

第三部分:痛点识别与解决方案

这是架构实践中最具挑战性的部分。

痛点1:如何区分业务逻辑?(Domain Logic vs. Application Logic)

  • 困惑: 一段逻辑,是应该放在 Entity 里,还是放在 Use Case 里?
  • 解决方案与心法:
    • 问自己一个问题:“这个逻辑是否具有普适性,并且只依赖于实体自身的数据?”

      • 是 -> 放入 Domain Entity。 例如:Order 实体有一个 totalPrice 属性,一个 calculateTotalPrice() 方法根据其 lineItems 列表计算总价。这个计算逻辑是 Order 固有的,不依赖外部服务。
      • 否 -> 放入 Application Use Case。 例如:ApplyCouponToOrderUseCase。这个逻辑需要:1. 获取 Order;2. 获取 Coupon 信息(可能要调 CouponRepository);3. 验证优惠券是否适用于该订单和用户(可能要调 UserRepository);4. 最后计算出新的价格。这个过程是在协调多个实体和仓库,它是一个应用级别的操作,因此属于 Use Case。
    • 简单法则:

      • Domain Logic: 规则和计算。
      • Application Logic: 流程和编排。

痛点2:如何划分模块?(Monolith vs. Feature-based Modules)

  • 困惑: 项目变大后,lib 文件夹变得臃肿不堪。所有功能的代码混在一起,commonshared 文件夹成为“垃圾场”。
  • 解决方案:功能驱动的垂直切分 (Vertical Slicing by Feature)
    • 放弃按层级划分顶级目录 (data, domain, presentation)。 这种方式在小项目里还行,大项目里会导致你为了修改一个功能,不得不在三个相距甚远的文件夹里跳来跳去。
    • 拥抱按功能划分模块。 这是现代大型应用架构的趋势。

第四部分:以一个“可扩展的电商App”为例进行实战演练

项目背景: 一个电商 App,初期只有商品浏览、购物车、用户中心。未来需要快速迭代,加入直播带货、社区分享等新功能。

目标: 设计一个能够支撑这种演进的架构。

项目结构 (采用功能驱动模块化):

flutter_ecommerce_app/
├── lib/
│   ├── **features/**                   # 核心:按功能划分的模块
│   │   ├── **auth/**                   # 认证模块
│   │   │   ├── presentation/
│   │   │   │   ├── screens/login_screen.dart
│   │   │   │   └── providers/auth_providers.dart
│   │   │   ├── application/
│   │   │   │   └── usecases/login_usecase.dart
│   │   │   ├── domain/
│   │   │   │   ├── entities/auth_token.dart
│   │   │   │   └── repositories/auth_repository.dart (interface)
│   │   │   └── infrastructure/
│   │   │       ├── datasources/auth_remote_datasource.dart
│   │   │       └── repositories/auth_repository_impl.dart
│   │   ├── **products/**               # 商品模块 (列表、详情)
│   │   │   └── ... (同样遵循 presentation/app/domain/infra 结构)
│   │   ├── **cart/**                   # 购物车模块
│   │   │   └── ...
│   │   └── **profile/**                # 用户中心模块
│   │       └── ...
│   │
│   ├── **core/**                     # 跨功能共享的核心代码
│   │   ├── **domain/**                 # 共享的 Domain (e.g., User, AppError)
│   │   │   └── entities/user_entity.dart
│   │   ├── **ui/**                     # 共享的 UI 组件 (e.g., PrimaryButton, LoadingIndicator)
│   │   ├── **network/**                # 共享的网络配置 (Dio instance, interceptors)
│   │   │   └── dio_client.dart
│   │   ├── **navigation/**             # 共享的导航配置
│   │   │   └── app_router.dart (Go_Router 配置)
│   │   ├── **storage/**                # 共享的存储封装 (e.g., Hive helper)
│   │   └── **utils/**                  # 共享的工具类 (e.g., formatters, validators)
│   │
│   ├── **main.dart**                   # 应用入口,组装 ProviderScope 和 GoRouter
│   └── **injection_container.dart**    # (可选) 集中管理所有顶层 Provider 的声明
│
└── pubspec.yaml

这个结构的优势:

  1. 高内聚: auth 相关的所有代码都在 features/auth 目录下,从 UI 到数据源一目了然。
  2. 低耦合:
    • products 模块不直接依赖 cart 模块。如果需要交互(比如“添加到购物车”),products 模块的 AddToCartUseCase 会调用 CartRepository 接口。这个接口的实现在 cart 模块中,但依赖关系是 products -> cart/domain,而不是 products -> cart/infrastructure,耦合度很低。
    • 可移除性: 如果老板说“我们不要购物车功能了”,理论上你可以直接删除 features/cart 文件夹,修复一下路由和调用点,应用主体依然能运行。
  3. 可扩展性:
    • 当需要加入“直播带货”功能时,只需新建一个 features/live_streaming 模块,按照同样的结构进行开发。
    • 新模块可以复用 core 里的所有组件,如 core/network 的 Dio 实例,core/ui 的按钮等。
  4. 团队协作: 不同的团队可以并行开发不同的 feature 模块,冲突仅限于 corepubspec.yaml,大大提高了开发效率。

Go_Router 在此结构中的作用:core/navigation/app_router.dart 中,你会定义所有路由:

final GoRouter router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => ProductsListScreen(), // from products feature
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) => ProductDetailsScreen(id: state.pathParameters['id']!), // from products feature
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => LoginScreen(), // from auth feature
    ),
    GoRoute(
      path: '/cart',
      builder: (context, state) => CartScreen(), // from cart feature
    ),
  ],
);

这样,ProductsListScreen 只需调用 context.go('/product/123'),而无需知道 ProductDetailsScreen 的存在,实现了导航解耦。

总结

作为架构师,我们的工作不是选择最“时髦”的技术,而是构建一个能够应对未来不确定性的、有弹性的系统。

  • Clean Architecture 提供了抵御变化的“防火墙”。
  • 功能模块化 提供了应对业务增长的“扩展坞”。
  • Riverpod 提供了连接一切的、灵活高效的“能源和通信网络”。
  • Go_RouterRetrofit 则是这个网络中负责特定任务(导航、网络)的、可替换的“标准化插件”。

下一篇, 将在此基础上, 讲解如何开发 User 模块.

flutter bloc 使用详细解析

2025年6月24日 17:04

源码地址

flutter_bloc 是基于 BLoC(Business Logic Component)模式的 Flutter 状态管理库,它封装了 bloc package,帮助我们更清晰地组织业务逻辑与 UI 的分离。核心思想是 事件驱动状态响应


🧠 原理简介

1. 核心概念

  • Event(事件):用户的输入或其他外部触发,比如按钮点击。
  • State(状态):界面状态的表现,比如加载中、成功、失败。
  • Bloc(逻辑组件):接收事件 -> 处理逻辑 -> 发出新状态。

流程图如下:

UI → Bloc.add(Event) → Bloc → emit(State) → UI rebuild

🛠️ 如何使用

1. 安装依赖

dependencies:
  flutter_bloc: ^8.1.3 # 检查 pub.dev 上的最新版本

2. 创建 Event & State

// counter_event.dart
abstract class CounterEvent {}

class Increment extends CounterEvent {}

class Decrement extends CounterEvent {}
// counter_state.dart
class CounterState {
  final int count;
  CounterState(this.count);
}

3. 创建 Bloc

// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
    on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
  }
}

4. 在 UI 中使用 Bloc

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(home: CounterPage()),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = context.read<CounterBloc>();

    return Scaffold(
      appBar: AppBar(title: Text("BLoC Counter")),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) => Text('Count: ${state.count}', style: TextStyle(fontSize: 30)),
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(onPressed: () => bloc.add(Increment()), child: Icon(Icons.add)),
          SizedBox(height: 10),
          FloatingActionButton(onPressed: () => bloc.add(Decrement()), child: Icon(Icons.remove)),
        ],
      ),
    );
  }
}

✅ 总结优点

  • UI 和业务逻辑分离,利于测试和维护
  • 明确的事件-状态流转,代码可读性强
  • 强类型 + 自动重建 UI,适用于中大型项目

和 getx 相比有什么优缺点

flutter_blocGetX 是 Flutter 中两种常见的状态管理方案,各有优缺点,适用于不同的场景。下面是二者的详细对比:


🆚 flutter_bloc vs GetX

维度 flutter_bloc GetX
设计思想 响应式 + 明确事件流转(事件 -> 状态) 响应式 + 最小 API(简洁直接)
代码结构 规范、结构清晰(Event / State / Bloc) 极简、灵活(Controller + Observable)
学习曲线 中等偏陡,概念较多 非常简单,上手极快
样板代码(boilerplate) 多,需要定义多个类 极少,一个控制器基本搞定
可维护性(大型项目) 高,适合多人协作和规范化开发 灵活但风险高,依赖命名和使用习惯
社区和文档 强,广泛用于企业项目,如 Google 官方推荐 非官方但很流行,文档充足
依赖注入(DI) 不强制内置,可配合其他包(如 get_it 内置自动依赖注入
导航/路由管理 依赖其他包(如 go_router 自带强大的路由系统
性能 非常高效,基于 Streamemit 极高,Reactive 系统 + 最小重建
测试友好性 强,适合单元测试/集成测试 一般,较多手动控制
异步处理 基于事件流程,清晰且易测试 支持 Future/async,但自由度高可能导致不规范

✅ 何时选 flutter_bloc

适合你如果:

  • 需要严格分离业务逻辑(企业/团队协作项目)
  • 希望状态流转可追踪、可测试
  • 项目规模较大或可预期扩展
  • 你熟悉 Redux / Rx 风格编程

✅ 何时选 GetX

适合你如果:

  • 项目较小或是个人开发
  • 优先快速开发、上手快
  • 喜欢灵活、少写代码
  • 同时希望统一路由/DI/状态/国际化等功能

🏁 举例对比:计数器

Bloc 实现

bloc.add(Increment()); 
// 多个文件:event.dart, state.dart, bloc.dart

GetX 实现

controller.count++;
// 只要一个 Controller 类,UI 使用 Obx 自动监听

📌 总结一句话:

Bloc 注重规范和可维护性,适合大型工程;GetX 注重极简和开发效率,适合快速开发。

BuildContext 上的一些扩展方法

在使用 flutter_bloc(或 provider)时,BuildContext 上的一些扩展方法是关键,它们帮助你从上下文中访问 Bloc、监听状态、或者进行条件性重建。

以下是常见的几种方法的详细解释和对比:


✅ 1. context.read<T>()

功能:

  • 从 widget tree 中 读取 一个 Bloc(或其他 Provider 提供的对象),不会监听其状态变化

用途:

  • 通常用于触发事件,如 .add(Event),因为不需要监听。

示例:

context.read<CounterBloc>().add(Increment());

✅ 2. context.watch<T>()

功能:

  • 读取并订阅监听 Bloc 或状态变化。
  • 状态变化会导致 widget rebuild

用途:

  • 通常用于 UI 需要根据 Bloc 状态实时刷新。

示例:

final state = context.watch<CounterBloc>().state;
return Text('Count: ${state.count}');

✅ 3. context.select<T, R>(R Function(T value))

功能:

  • 读取 Bloc(或 Provider)中某个字段的值,并监听它的变化。
  • 只有当该字段的值发生变化时,才会触发 rebuild

用途:

  • 精细控制重建,避免无谓的 UI 更新

示例:

final count = context.select<CounterBloc, int>((bloc) => bloc.state.count);
return Text('Count: $count');

✅ 4. BlocProvider.of<T>(context)

  • 等价于 context.read<T>()
  • 是旧写法,推荐使用 context.read<T>() 更简洁。

✅ 5. BlocBuilder<T, S>

功能:

  • 监听 Bloc<T> 的状态 S 并根据状态变化 rebuild UI。

示例:

BlocBuilder<CounterBloc, CounterState>(
  builder: (context, state) => Text('Count: ${state.count}'),
);

✅ 6. BlocListener<T, S>

功能:

  • 用于监听状态变化并做一次性副作用操作(如弹窗、跳转)。

示例:

BlocListener<LoginBloc, LoginState>(
  listener: (context, state) {
    if (state is LoginSuccess) {
      Navigator.pushNamed(context, '/home');
    }
  },
  child: ...
);

✅ 7. BlocConsumer<T, S>

  • 相当于 BlocBuilder + BlocListener 的组合。
  • 同时用于 build UI 和执行副作用。

🔁 方法使用场景对比表

方法名 是否 rebuild 是否监听状态变化 用途
read<T>() 获取 Bloc 实例、添加事件
watch<T>() 获取 Bloc 状态,状态变就重建
select<T, R>() ✅ (条件) ✅ (某字段变) 精细控制重建,提高性能
BlocBuilder 渲染 UI
BlocListener 处理一次性副作用
BlocConsumer UI 和副作用一起处理

使用场景举例子

好的,我们用一个异步 API 请求的完整例子来演示 flutter_bloc 中各类常用方法的实际应用,包括:

  • context.read
  • context.watch
  • context.select
  • BlocBuilder
  • BlocListener
  • BlocConsumer

🌐 场景描述:请求用户信息

模拟从网络请求一个用户信息(名字、邮箱),展示加载中、成功、失败三种状态。


📦 第一步:定义状态和事件

// user_event.dart
abstract class UserEvent {}

class FetchUser extends UserEvent {}
// user_state.dart
abstract class UserState {}

class UserInitial extends UserState {}

class UserLoading extends UserState {}

class UserLoaded extends UserState {
  final String name;
  final String email;
  UserLoaded({required this.name, required this.email});
}

class UserError extends UserState {
  final String message;
  UserError(this.message);
}

⚙️ 第二步:创建 UserBloc

// user_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'user_event.dart';
import 'user_state.dart';

class UserBloc extends Bloc<UserEvent, UserState> {
  UserBloc() : super(UserInitial()) {
    on<FetchUser>((event, emit) async {
      emit(UserLoading());
      await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟

      try {
        // 模拟 API 成功返回
        final name = 'Alice';
        final email = 'alice@example.com';
        emit(UserLoaded(name: name, email: email));
      } catch (e) {
        emit(UserError('Failed to fetch user'));
      }
    });
  }
}

🖼️ 第三步:UI 中使用 Bloc

// user_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'user_bloc.dart';
import 'user_event.dart';
import 'user_state.dart';

class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => UserBloc(),
      child: UserView(),
    );
  }
}

class UserView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = context.read<UserBloc>(); // 获取 Bloc 实例

    return Scaffold(
      appBar: AppBar(title: Text('User Info')),
      body: BlocConsumer<UserBloc, UserState>(
        listener: (context, state) {
          if (state is UserError) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          if (state is UserInitial) {
            return Center(
              child: ElevatedButton(
                onPressed: () => bloc.add(FetchUser()),
                child: Text('Load User'),
              ),
            );
          } else if (state is UserLoading) {
            return Center(child: CircularProgressIndicator());
          } else if (state is UserLoaded) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Name: ${state.name}'),
                  Text('Email: ${state.email}'),
                ],
              ),
            );
          } else {
            return Center(child: Text('Unknown state'));
          }
        },
      ),
    );
  }
}

🧠 其他方法用法补充

context.watch 示例(只监听状态)

final userState = context.watch<UserBloc>().state;
if (userState is UserLoaded) {
  print(userState.name);
}

context.select 示例(只监听 name 变化)

final name = context.select<UserBloc, String?>((bloc) {
  final state = bloc.state;
  return state is UserLoaded ? state.name : null;
});

✅ 总结

这个例子完整地展示了:

  • 如何使用 Bloc 组织异步逻辑
  • 如何使用 context.read 触发事件
  • 如何用 BlocConsumer 分离 UI 构建和副作用(如错误提示)
  • 如何用 watch / select 实现更细粒度监听

## 标题RepositoryProvider

RepositoryProviderflutter_bloc 提供的一个工具类,作用是将“非 Bloc 的对象(如 Repository、Service、API 客户端)注入到 widget tree 中”,供 Bloc 或其他组件使用。它本质上是一个语义化的 Provider,目的是让依赖注入更加清晰和语义化。


🧱 1. RepositoryProvider 是什么?

它是 Provider 的语法糖,用于提供数据访问层(Repository),让 Bloc 通过依赖注入获取它。

RepositoryProvider(
  create: (context) => UserRepository(),
  child: BlocProvider(
    create: (context) => UserBloc(context.read<UserRepository>()),
    child: MyApp(),
  ),
)

🧩 2. 为什么需要 RepositoryProvider?

分离关注点 + 提高可测试性

在 BLoC 架构中,Bloc 只处理业务逻辑,而 Repository 专注于数据来源(数据库/API/本地缓存等)。

  • 🧼 清晰的依赖层次结构
  • 🔌 可插拔(方便 mock 测试)
  • ♻️ 依赖共享(避免重复创建对象)

🧪 3. 实战示例

用户仓库

class UserRepository {
  Future<String> fetchUserName() async {
    await Future.delayed(Duration(seconds: 1));
    return 'Alice';
  }
}

注入层级

void main() {
  runApp(
    RepositoryProvider(
      create: (context) => UserRepository(),
      child: BlocProvider(
        create: (context) => UserBloc(context.read<UserRepository>()),
        child: MyApp(),
      ),
    ),
  );
}

在 Bloc 中使用

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository userRepository;

  UserBloc(this.userRepository) : super(UserInitial()) {
    on<FetchUser>((event, emit) async {
      emit(UserLoading());
      final name = await userRepository.fetchUserName();
      emit(UserLoaded(name));
    });
  }
}

✅ 4. 优点总结

优点 说明
👓 语义清晰 明确指出这是 Repository,不是 Bloc
♻️ 对象共享 上层创建、下层 Bloc 可复用
🧪 易测试 Bloc 接收参数,可以注入 mock
🧩 解耦结构 Bloc 不负责创建数据层

⚠️ 5. 注意事项 / 潜在缺点

缺点或注意点 说明
⚠️ 滥用嵌套 BlocProvider 和 RepositoryProvider 层级深容易混乱,建议封装为模块
⚠️ 生命周期问题 如果作用域太小,Bloc 中引用的 Repository 可能会被提前销毁
⚠️ 多仓库依赖复杂度提升 Bloc 依赖多个 Repository 时,构造函数会变长,可考虑封装为 Service 层或使用 DI 工具(如 get_it)

📌 何时使用 RepositoryProvider?

✅ 使用场景:

  • 需要复用或共享 Repository 实例(如网络请求、数据库访问)
  • Bloc 不应该直接创建依赖对象
  • 需要单元测试 Bloc 时方便注入 mock

❌ 不建议使用场景:

  • 简单小项目可以直接在 Bloc 中创建对象(仅限临时用)
  • 对象生命周期不需要跨多个组件

一个多仓库 + 多 Bloc 的组合使用场景实例

好的,我们来构建一个中型项目的多仓库 + 多 Bloc + 多 Provider 示例结构,结合 RepositoryProviderBlocProvider,实现高内聚、低耦合的结构设计。


🧱 示例需求:用户信息 + 设置模块

有两个功能模块:

  1. 用户模块(User):从 API 获取用户信息
  2. 设置模块(Settings):管理本地配置(如暗黑模式)

模块依赖:

模块 依赖内容
UserBloc 依赖 UserRepository
SettingsBloc 依赖 SettingsRepository

📦 项目结构建议

lib/
├── main.dart
├── repositories/
│   ├── user_repository.dart
│   └── settings_repository.dart
├── blocs/
│   ├── user/
│   │   ├── user_bloc.dart
│   │   ├── user_event.dart
│   │   └── user_state.dart
│   └── settings/
│       ├── settings_bloc.dart
│       ├── settings_event.dart
│       └── settings_state.dart
├── pages/
│   ├── user_page.dart
│   └── settings_page.dart
└── app.dart

1️⃣ Repository 实现

user_repository.dart

class UserRepository {
  Future<String> fetchUserName() async {
    await Future.delayed(Duration(seconds: 1));
    return 'Alice';
  }
}

settings_repository.dart

class SettingsRepository {
  bool _darkMode = false;

  bool get isDarkMode => _darkMode;
  void toggleDarkMode() => _darkMode = !_darkMode;
}

2️⃣ Bloc 实现(以 User 为例)

user_bloc.dart

class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository userRepository;

  UserBloc(this.userRepository) : super(UserInitial()) {
    on<FetchUser>((event, emit) async {
      emit(UserLoading());
      final name = await userRepository.fetchUserName();
      emit(UserLoaded(name));
    });
  }
}

3️⃣ 组合使用(main.dart)

void main() {
  runApp(
    MultiRepositoryProvider(
      providers: [
        RepositoryProvider(create: (_) => UserRepository()),
        RepositoryProvider(create: (_) => SettingsRepository()),
      ],
      child: MultiBlocProvider(
        providers: [
          BlocProvider(
            create: (context) => UserBloc(context.read<UserRepository>()),
          ),
          BlocProvider(
            create: (context) => SettingsBloc(context.read<SettingsRepository>()),
          ),
        ],
        child: MyApp(),
      ),
    ),
  );
}

4️⃣ 页面中使用

user_page.dart

class UserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      builder: (context, state) {
        if (state is UserInitial) {
          return ElevatedButton(
            onPressed: () => context.read<UserBloc>().add(FetchUser()),
            child: Text("Load User"),
          );
        } else if (state is UserLoaded) {
          return Text('Hello, ${state.name}');
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  }
}

settings_page.dart

class SettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SettingsBloc, SettingsState>(
      builder: (context, state) {
        return SwitchListTile(
          title: Text('Dark Mode'),
          value: state.isDarkMode,
          onChanged: (_) => context.read<SettingsBloc>().add(ToggleTheme()),
        );
      },
    );
  }
}

✅ 优点总结

  • 清晰的依赖注入结构:Bloc 和 Repository 解耦,易维护
  • 复用性强:多个 Bloc 可共用同一个 Repository 实例
  • 测试友好:可轻松 mock Repository 进行单元测试
  • 清晰语义:RepositoryProvider 表明它不是 Bloc,而是数据提供者

🚀 Bonus:封装更进一步

✅ 已经成功将 MultiRepositoryProviderMultiBlocProvider 封装到了 AppProviderWrapper 组件中,结构清晰、职责分离得非常好!

下面是总结和优化建议:


✅ 目前结构回顾

// main.dart
runApp(AppProviderWrapper(child: MyApp()));
// provider_wrapper.dart
class AppProviderWrapper extends StatelessWidget {
  final Widget child;
  const AppProviderWrapper({required this.child});
  ...
}

这非常适合中大型项目,可以让 main.dart 逻辑更纯粹,同时也方便后续扩展全局错误监听、日志注入等中间件。


💡 可选优化建议

1. 使用 lazy: false 控制立即初始化 Bloc

有些 Bloc(如 SettingsBloc)可能希望 App 启动时就立即初始化:

BlocProvider(
  lazy: false,
  create: (context) => SettingsBloc(context.read<SettingsRepository>()),
),
2. 使用 const 构造节省 rebuild

如果子 Widget 和 Bloc 无关,记得加 const 以避免重建。

3. 模块化进一步优化(可选)

可以把 Bloc、Repository 的创建封装成方法,增强可维护性:

List<RepositoryProvider> buildRepositories() => [
  RepositoryProvider(create: (_) => UserRepository()),
  RepositoryProvider(create: (_) => SettingsRepository()),
];

List<BlocProvider> buildBlocs(BuildContext context) => [
  BlocProvider(create: (_) => UserBloc(context.read())),
  BlocProvider(create: (_) => SettingsBloc(context.read())),
];

这样 AppProviderWrapper 中就可以这么写:

child: MultiBlocProvider(
  providers: buildBlocs(context),
  child: child,
),

✅ 总结

现在的结构已经非常标准、清晰,完全符合企业级 Flutter 项目推荐架构。

源码地址

Flutter Clean 架构下的用户画像系统与各模块的协同工作

作者 tangzzzfan
2025年6月25日 09:14

第一部分:[交互设计] 用户画像系统与各模块的协同工作

用户画像模块不是一个孤岛,它是一个数据枢纽,与其他模块进行着双向的数据交换。其交互的核心原则是:"单向依赖,面向接口"。即,具体功能模块(如 Onboarding)可以依赖 user_profile 模块的 applicationdomain 层,但反之不行。

1. 与 Onboarding 模块的交互

  • 目的: 收集用户初始的、显式的偏好 (UserPreferences)。这是用户画像的冷启动数据。
  • 数据流向: Onboarding (UI) -> UpdateUserPreferencesUseCase (user_profile/application) -> UserProfileRepository (user_profile/domain)。
  • 架构实现:
    1. Onboarding 屏幕是一个 ConsumerWidget,包含一系列让用户选择兴趣标签的 UI。
    2. 当用户点击“完成”按钮时,Onboarding 屏幕的 Provider (或直接在 onPressed 回调中) 会执行以下操作:
      // In Onboarding screen's logic
      void onComplete(WidgetRef ref) {
        // 1. 从 UI 状态中收集用户的选择
        final selectedCategories = ref.read(selectedCategoriesProvider);
        final userPreferences = UserPreferences(likedCategories: selectedCategories, ...);
      
        // 2. 调用 user_profile 模块的 UseCase
        //    注意:Onboarding 模块只知道 UseCase 的存在,不知道其内部实现
        ref.read(updateUserPreferencesUseCaseProvider).execute(userPreferences);
        
        // 3. 导航到主页
        context.go('/home');
      }
      
    3. updateUserPreferencesUseCaseProvider 是在 user_profile 模块中定义的 Provider。Onboarding 模块通过 pubspec.yaml 依赖 user_profile 模块来访问它。

2. 与 Auth 模块的交互

  • 目的: 关联用户身份。UserProfile 必须与一个唯一的 userId 绑定。
  • 数据流向: Auth 模块在登录/注册成功后,会产生一个 userId。其他模块(包括 user_profile)需要能够获取到这个 userId,以便在进行数据操作时进行关联。
  • 架构实现:
    1. Auth 模块的核心产出是一个 AuthProvider,它管理着用户的认证状态(未登录、已登录、加载中)和认证信息(如 AuthToken,其中包含 userId)。
      // In auth/providers
      final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) => ...);
      
      // AuthState could be a sealed class
      // sealed class AuthState {}
      // class Authenticated extends AuthState { final AuthToken token; }
      // class Unauthenticated extends AuthState {}
      
    2. UserProfileRepositoryImpl 在执行任何操作前,都需要获取当前的 userId。它会通过 ref 来读取 authStateProvider
      // In user_profile/infrastructure/repositories/user_profile_repository_impl.dart
      class UserProfileRepositoryImpl implements UserProfileRepository {
        final Ref _ref;
        // ...其他依赖...
      
        UserProfileRepositoryImpl(this._ref, ...);
      
        String _getCurrentUserId() {
          final authState = _ref.read(authStateProvider);
          if (authState is Authenticated) {
            return authState.token.userId;
          }
          throw AuthException('User not authenticated');
        }
      
        @override
        Future<UserProfile> getUserProfile() async {
          final userId = _getCurrentUserId();
          // ...后续所有本地或远程操作都带上 userId
          return _localDataSource.getProfile(userId);
        }
        // ...其他方法同样处理
      }
      
    3. 关键点: user_profile 模块依赖 auth 模块的状态输出 (authStateProvider),而不是其内部实现。这是一种松耦合的依赖关系。

3. 与 个人中心 (Profile Display) 模块的交互

  • 目的: 展示用户画像数据,并提供修改入口。
  • 数据流向: GetUserProfileUseCase (user_profile/application) -> 个人中心 (UI)。同时,用户在个人中心修改偏好时,流向与 Onboarding 类似。
  • 架构实现:
    1. 个人中心 屏幕的 ViewModel (或 StateNotifierProvider) 会调用 GetUserProfileUseCase 来获取 UserProfile 对象。
      // In profile_display/providers
      @riverpod
      class ProfileViewModel extends _$ProfileViewModel {
        @override
        Future<UserProfile> build() {
          // 调用 UseCase 获取数据
          return ref.watch(getUserProfileUseCaseProvider).execute();
        }
      
        // 提供修改入口,例如修改昵称或偏好
        Future<void> updatePreferences(UserPreferences newPrefs) async {
          // ... 更新状态为 loading
          await ref.read(updateUserPreferencesUseCaseProvider).execute(newPrefs);
          // ... 刷新页面数据
          ref.invalidateSelf();
        }
      }
      
    2. UI (ConsumerWidget) watch 这个 ProfileViewModelProvider,并根据其 AsyncValue (loading, data, error) 来构建界面。

第二部分:[分级存储] 基于数据敏感性的安全设计

这是一个至关重要的部分,直接关系到用户隐私和应用安全。我们不能将所有数据都以相同的方式存储在同一个地方。必须设计一个分级的本地存储策略。

核心思想:Infrastructure 层的 DataSource 拆分为多个,每个 DataSource 负责一个安全级别的数据,并使用不同的存储技术。Repository 层作为外观(Façade),负责整合这些 DataSource,对上层(Application 层)屏蔽这些复杂性。

数据敏感性分级

级别 级别名称 数据例子 存储技术建议 特点
L3 (最高) 高度敏感数据 (Highly Sensitive) accessToken, refreshToken, 第三方平台 token flutter_secure_storage (使用 Keychain/Keystore) 系统级加密,App卸载后数据清除。仅在需要时读入内存。
L2 (中等) 个人身份信息 (PII - Personally Identifiable Info) 用户名, 邮箱, 手机号, 用户自己填写的地址 Hive / Isar 加密盒子 (Encrypted Box) App级别加密,性能较高,适合结构化数据。密钥存储在 L3。
L1 (较低) 推断与行为数据 (Inferred & Behavioral) inferredTopCategories, engagementScore, 点击/浏览记录 Hive / Isar 普通盒子 (Regular Box) 无需加密或轻量级加密。追求读写性能,数据量可能很大。
L0 (无敏感) 应用配置 (App Config) isDarkMode, language shared_preferences 简单键值对,性能高,无安全要求。

架构实现

  1. 拆分 DataSource 接口和实现:

    // core/storage/
    // L3
    abstract class SecureAuthDataSource { Future<void> saveToken(AuthToken token); ... }
    class SecureAuthDataSourceImpl implements SecureAuthDataSource { ... } // 使用 flutter_secure_storage
    
    // L2
    abstract class EncryptedPiiDataSource { Future<void> saveUserInfo(UserInfo info); ... }
    class EncryptedPiiDataSourceImpl implements EncryptedPiiDataSource { ... } // 使用加密的 Hive Box
    
    // L1
    abstract class BehavioralDataSource { Future<void> logBehavior(UserBehaviorEvent event); ... }
    class BehavioralDataSourceImpl implements BehavioralDataSource { ... } // 使用普通的 Hive Box
    

    注意:这些 DataSource 可以分散在各自的 feature 模块中,例如 SecureAuthDataSourceauth 模块的 infrastructure 里,而另外两个在 user_profile 模块里。

  2. 重构 UserProfileRepositoryImpl 作为数据整合者:

    // In user_profile/infrastructure/repositories/user_profile_repository_impl.dart
    class UserProfileRepositoryImpl implements UserProfileRepository {
      final Ref _ref;
      // 注入多个不同级别的 DataSource
      final EncryptedPiiDataSource _piiDataSource;
      final BehavioralDataSource _behavioralDataSource;
      final ProfileGeneratorService _profileGenerator;
    
      UserProfileRepositoryImpl(this._ref, this._piiDataSource, this._behavioralDataSource, this._profileGenerator);
      
      String _getCurrentUserId() { /* ... */ }
    
      @override
      Future<UserProfile> getUserProfile() async {
        final userId = _getCurrentUserId();
    
        // 1. 并行从不同安全级别的数据源获取数据
        final results = await Future.wait([
            _piiDataSource.getUserInfo(userId),
            _behavioralDataSource.getRecentBehaviors(userId),
            _piiDataSource.getPreferences(userId) // 假设偏好也存在 L2
        ]);
        
        final userInfo = results[0] as UserInfo;
        final behaviors = results[1] as List<UserBehaviorEvent>;
        final preferences = results[2] as UserPreferences;
    
        // 2. 将原始数据喂给画像生成服务 (ML 或规则引擎)
        final profile = await _profileGenerator.generateProfile(
          preferences: preferences,
          recentBehaviors: behaviors,
          // ...可能还需要 userInfo
        );
        
        // 3. 组合最终返回给 UI 的对象 (可能需要合并 userInfo 和 profile)
        //    对上层屏蔽了数据来自多个源头的复杂性
        return profile.copyWith(
            // 如果 UserProfile 实体也包含 PII 信息
            userName: userInfo.name,
            email: userInfo.email
        );
      }
    
      @override
      Future<void> updateUserPreferences(UserPreferences preferences) async {
        final userId = _getCurrentUserId();
        // 存储到 L2 加密数据源
        await _piiDataSource.savePreferences(userId, preferences);
      }
    
      @override
      Future<void> logBehavior(UserBehaviorEvent event) async {
        final userId = _getCurrentUserId();
        // 存储到 L1 普通数据源
        await _behavioralDataSource.logBehavior(userId, event);
      }
    }
    
  3. 在 Riverpod 中组装依赖:

    // providers.dart
    // L3
    final secureAuthDataSourceProvider = Provider((ref) => SecureAuthDataSourceImpl());
    // L2
    final encryptedPiiDataSourceProvider = Provider((ref) => EncryptedPiiDataSourceImpl(/* hive encryption key */));
    // L1
    final behavioralDataSourceProvider = Provider((ref) => BehavioralDataSourceImpl());
    
    final userProfileRepositoryProvider = Provider<UserProfileRepository>((ref) {
      return UserProfileRepositoryImpl(
        ref,
        ref.watch(encryptedPiiDataSourceProvider),
        ref.watch(behavioralDataSourceProvider),
        ref.watch(profileGeneratorProvider)
      );
    });
    

总结

通过这样的设计,我们构建了一个既健壮又安全的系统:

  1. 交互清晰: 各模块职责单一,通过 UseCase 和 Provider 进行松耦合交互,数据流向明确。
  2. 安全分级: 敏感数据(Token、PII)和非敏感数据(行为、推断)被物理隔离在不同的存储介质和加密等级中,极大地增强了应用的安全性,并为满足 GDPR、数据安全法等隐私法规打下了坚实基础。
  3. 封装良好: UserProfileRepository 完美地扮演了外观模式的角色,它将底层复杂的多源、分级存储细节完全封装起来,对 Application 层和 Presentation 层只暴露一个统一、干净的 UserProfileRepository 接口。这使得上层业务逻辑可以完全不关心数据到底是怎么存的,从而保持了 Clean Architecture 的核心优势。

下一篇文章会给出 Flutter 开发中常见模块划分范例。

Flutter Clean 架构下的用户画像系统的设计与实现

作者 tangzzzfan
2025年6月25日 09:08

第一部分:[高屋建瓴] Mobile User Profile 系统的战略重要性

在现代 App 中,“用户画像”系统不再仅仅是存储用户信息的数据库条目,它是一个动态的、可演进的、智能的资产。其战略重要性体M现于:

  1. 个性化体验的基石: 它是实现“千人千面”的根本。无论是推荐系统(商品、内容)、UI 动态调整(展示用户偏好的模块)、还是营销推送,都依赖于一个准确、实时的用户画像。

  2. 提升用户粘性和留存: 一个“懂你”的 App 能极大地提升用户满意度和使用时长。当用户感觉 App 是为他量身定做时,其迁移成本会显著增高。

  3. 数据驱动决策的引擎: 聚合的用户画像数据可以揭示用户群体的行为模式,为产品迭代、运营策略提供精准的数据支持。

  4. 未来智能化的入口: 它是端侧机器学习的“燃料”。在本地处理用户数据进行模型训练和推理,可以实现:

    • 更高响应速度: 无需网络请求,实时反馈。
    • 更强隐私保护: 敏感数据不出设备,符合越来越严格的隐私法规。
    • 离线可用性: 在无网络环境下依然可以提供部分智能服务。

因此,我们在架构设计之初就必须将其视为一个一级功能模块,而不是“个人中心”或“设置”的附属品。


第二部分:[痛点辨析] 概念边界的划分:Auth vs. Settings vs. Preferences vs. Profile

这是最容易混淆,也是导致后续架构腐化的重灾区。必须在设计之初就用“快刀斩乱麻”的方式清晰界定。

概念 职责 (Responsibility) 数据例子 所属模块 特点
认证信息 (Auth Info) “你是谁?” - 用于验证用户身份和授权访问。 userId, accessToken, refreshToken, loginMethod (手机/微信) features/auth 高敏感性,安全性要求最高,数据量小,相对稳定。
应用设置 (App Settings) “App 该如何为你工作?” - 用户对 App 功能行为的控制。 isDarkMode, notificationEnabled, language, fontSize features/settings (或 core/settings) 功能性,非个人化,与用户兴趣无关,通常是布尔/枚举值。
用户偏好 (User Preferences) “你告诉我们你喜欢什么?” - 用户显式提供的主观兴趣和选择。 喜欢的分类: [科幻, 悬疑],不感兴趣的标签: [体育],价格敏感度: features/user_profile (作为输入) 主观性,是用户画像的原始输入数据之一,用户可直接修改。
用户画像 (User Profile/Portrait) “我们认为你是怎样的人?” - 系统通过分析显式偏好隐式行为后,得出的推断性、结构化的用户模型。 inferredPersona: "价格敏感的科幻迷", engagementScore: 0.85, topCategories: ["科幻", "数码"], nextBestOffer: "XX 耳机" features/user_profile (作为核心产出) 推断性、动态演进,是算法和规则的产物,通常不直接对用户暴露全部内容,是所有个性化服务的直接数据源

核心痛点解决方案:

  • 物理隔离: 在项目结构上,authuser_profile 必须是两个独立的 feature 模块。它们之间可以通过 userId 关联,但数据模型和仓库(Repository)完全分离。
  • 逻辑分离: User PreferencesUser Profile 的一部分,是其输入。UserProfile 实体则包含了 Preferences 以及更多推断出的数据。

第三部分:[架构设计] "用户画像" 模块的落地

基于我们之前的项目结构,我们新增一个 features/user_profile 模块。

flutter_ecommerce_app/
├── lib/
│   ├── features/
│   │   ├── auth/
│   │   ├── products/
│   │   ├── ...
│   │   └── **user_profile/**             # << 新增用户画像模块
│   │       ├── presentation/
│   │       │   ├── screens/onboarding_screen.dart   # 收集偏好
│   │       │   ├── screens/profile_display_screen.dart # “个人中心”的一部分
│   │       │   └── providers/user_profile_providers.dart
│   │       ├── application/
│   │       │   ├── usecases/get_user_profile_usecase.dart
│   │       │   ├── usecases/update_user_preferences_usecase.dart
│   │       │   └── usecases/track_user_behavior_usecase.dart # 用于记录隐式行为
│   │       ├── domain/
│   │       │   ├── entities/user_preferences.dart
│   │       │   ├── entities/user_profile.dart       # << 核心实体
│   │       │   ├── entities/user_behavior_event.dart
│   │       │   └── repositories/user_profile_repository.dart (interface)
│   │       └── infrastructure/
│   │           ├── datasources/profile_local_datasource.dart
│   │           ├── datasources/profile_remote_datasource.dart
│   │           ├── repositories/user_profile_repository_impl.dart
│   │           └── **services/ml_profile_generator.dart** # << 为 Core ML 预留的钩子
│   ├── core/
│   └── main.dart

Domain 层设计 (稳定内核):

// domain/entities/user_preferences.dart
class UserPreferences extends Equatable {
  final List<String> likedCategories;
  final List<String> dislikedTags;
  // ... 其他显式偏好
}

// domain/entities/user_profile.dart
class UserProfile extends Equatable {
  final UserPreferences preferences; // 包含显式偏好
  final List<String> inferredTopCategories; // 推断出的 Top 分类
  final String personaTag; // 推断出的画像标签,如 "TechEnthusiast"
  final double engagementScore; // 用户活跃度得分
  // ... 其他推断性数据
}

// domain/repositories/user_profile_repository.dart
abstract class UserProfileRepository {
  // 获取最终整合后的用户画像
  Future<UserProfile> getUserProfile();
  
  // 更新用户显式偏好
  Future<void> updateUserPreferences(UserPreferences preferences);
  
  // 记录用户隐式行为(如点击、浏览、购买)
  Future<void> logBehavior(UserBehaviorEvent event);
}

Application 层设计 (业务流程):

  • GetUserProfileUseCase: 调用 UserProfileRepository.getUserProfile(),供个人中心页面或推荐系统使用。
  • UpdateUserPreferencesUseCase: 在 Onboarding 或设置页调用,参数是 UserPreferences,内部调用 UserProfileRepository.updateUserPreferences()
  • TrackUserBehaviorUseCase: 这是关键。App 内的各种用户行为(点击商品、观看视频)都会调用这个 UseCase,它会调用 UserProfileRepository.logBehavior(),将行为数据记录下来。

第四部分:[核心难点] 为 Core ML 预留接口

这正是 Clean Architecture 发挥威力的地方。ML 模型是实现细节,属于基础设施层。

设计思路:

  1. 数据收集: UserProfileRepositoryImpl 负责从各个数据源收集原始数据。
  2. 模型调用: 将收集到的原始数据喂给一个“画像生成服务”。
  3. 结果存储: 将服务返回的 UserProfile 缓存并提供给上层。

Infrastructure 层实现:

1. 定义一个抽象的“画像生成服务”接口:

// infrastructure/services/ml_profile_generator.dart

// 这是未来 CoreML 实现需要遵循的契约
abstract class ProfileGeneratorService {
  Future<UserProfile> generateProfile({
    required UserPreferences currentPreferences,
    required List<UserBehaviorEvent> recentBehaviors,
  });
}

2. 实现 Repository,并注入这个服务:

// infrastructure/repositories/user_profile_repository_impl.dart
class UserProfileRepositoryImpl implements UserProfileRepository {
  final ProfileLocalDataSource _localDataSource;
  final ProfileGeneratorService _profileGenerator; // << 注入 ML 服务

  UserProfileRepositoryImpl(this._localDataSource, this._profileGenerator);

  @override
  Future<void> logBehavior(UserBehaviorEvent event) async {
    // 将行为事件存储到本地 (e.g., a separate Hive box or SQLite table)
    await _localDataSource.saveBehavior(event);
  }
  
  @override
  Future<void> updateUserPreferences(UserPreferences preferences) async {
    await _localDataSource.savePreferences(preferences);
  }

  @override
  Future<UserProfile> getUserProfile() async {
    // 1. 检查是否有有效的本地缓存画像
    final cachedProfile = await _localDataSource.getProfile();
    if (cachedProfile != null && !isStale(cachedProfile.timestamp)) {
      return cachedProfile;
    }

    // 2. 如果没有或已过期,则重新生成
    //    a. 从本地数据源获取原始数据
    final preferences = await _localDataSource.getPreferences();
    final behaviors = await _localDataSource.getRecentBehaviors(limit: 100);

    //    b. 调用 ML 服务进行计算和推断
    //       这里就是与 Core ML 交互的“口子”
    final newProfile = await _profileGenerator.generateProfile(
      currentPreferences: preferences,
      recentBehaviors: behaviors,
    );

    //    c. 缓存新生成的画像
    await _localDataSource.saveProfile(newProfile);

    return newProfile;
  }
}

3. 如何为 Core ML 预留口子?

  • 当前阶段: 我们可以提供一个基于规则的 RuleBasedProfileGenerator 实现,它不依赖 ML,只是简单地根据行为计数等来生成画像。
    class RuleBasedProfileGenerator implements ProfileGeneratorService {
      @override
      Future<UserProfile> generateProfile(...) {
        // 简单的逻辑:浏览次数最多的分类就是 Top 分类
        // ...
        return Future.value(UserProfile(...));
      }
    }
    
  • 未来阶段 (引入 Core ML): 你只需要创建一个新的实现 CoreMLProfileGenerator
    class CoreMLProfileGenerator implements ProfileGeneratorService {
      // 内部会通过 platform channels 调用原生的 Core ML SDK
      final MethodChannel _channel = MethodChannel('com.yourapp/coreml');
    
      @override
      Future<UserProfile> generateProfile({ ... }) async {
        // 1. 将 preferences 和 behaviors 序列化为 ML 模型需要的格式 (e.g., JSON or Map)
        final inputData = serializeForML(preferences, behaviors);
        
        // 2. 通过平台通道调用原生 Core ML 模型进行推理
        final resultJson = await _channel.invokeMethod('predictUserProfile', inputData);
        
        // 3. 将返回的 JSON 解析为 UserProfile 实体
        return UserProfile.fromJson(resultJson);
      }
    }
    

最后,在 Riverpod 的 Provider 中切换实现即可,对 App 的其他部分完全透明!

// providers.dart
final profileGeneratorProvider = Provider<ProfileGeneratorService>((ref) {
  // 在这里决定使用哪个实现
  // 可以通过环境变量、A/B Test 等方式动态切换
  if (featureFlags.isCoreMLEnabled) {
    return CoreMLProfileGenerator();
  } else {
    return RuleBasedProfileGenerator();
  }
});

final userProfileRepositoryProvider = Provider<UserProfileRepository>((ref) {
  return UserProfileRepositoryImpl(
    ref.watch(profileLocalDataSourceProvider),
    ref.watch(profileGeneratorProvider), // << 动态注入
  );
});

总结

通过上述设计,我们实现了:

  1. 概念清晰: 明确划分了认证、设置、偏好和画像的边界,避免了职责混乱。
  2. 结构合理: 将用户画像作为一个独立的、高内聚的功能模块,易于维护和扩展。
  3. 拥抱变化: 通过在 Domain 层定义稳定的 UserProfileRepository 接口,将复杂的画像生成逻辑(无论是基于规则还是 ML)封装在 Infrastructure 层。
  4. 无缝升级: 为端侧 ML 预留了清晰的、非侵入式的“挂载点” (ProfileGeneratorService)。未来引入 Core ML 时,只需添加一个新的实现类并修改一行 Provider 的构造代码,而无需触动任何业务逻辑(Use Cases)或 UI 代码,这正是顶级架构弹性的完美体现。

下一篇, 讲述这个用户画像系统如何与其他模块进行交互。

Flutter 又双叒叕可以在 iOS 26 的真机上 hotload 运行了,来看看又是什么黑科技

2025年6月24日 11:05

在之前的 《iOS 26 beta1 重新禁止 JIT 执行》 我们聊过,iOS 18.4 beta1 禁止了 Debug 时 mprotect 的 RX 权限,然后 iOS 18.4 beta2 又放开了,但是在 iOS 26 beta1 又重新禁止了,所以再次导致 Flutter 在 Debug 运行到 iOS 26 真机时又出现 mprotect failed: Permission denied 的问题。

因为 Dart 就算是 Debug 的 JIT 运行,在电脑上还是会编译成二进制 Kernel AST 这种 IR ,而在 iOS 上 Dart 不管是 JIT 运行还是进行 hotload 的时候,都需要涉及代码在内存从 RW 变成 RX 的调整,在此之前是通过 mprotect 完成,而这在 iOS 26 被禁止了。

当然,每次说这个问题都有人问为什么 JIT 需要涉及 RW 变成 RX 的调整,它不是解释执行吗?今天就顺带一篇聊完这个问题。

首先答案肯定不是,JIT 是即时编译,这里举个简单的例子:

  • 正常的解释执行,可以理解为它只翻译不记录,也就是类似你请了一个翻译,解释运行是一直实时翻译但是不记录
  • 而 JIT 虽然也需要翻译,但是它是会编译的成机械码的,对于 JIT 而言,当一个函数被确定为“热点”后,后台的 JIT 编译器会为它生成高度优化的原生机器码,而这段新生成的二进制代码就必须被写入到「可执行」的内存区域中去运行

也就是,解释运行是一直实时翻译但是不记录,而 JIT 是翻译一次之后,内容的重点都转成中文了,再听的时候,你就是直接听中文了,这就是两者的差别。

那有大聪明就要说了,为什么不直接申请 RWX 呢?那肯定是申请不了啊,现代操作系统都有 W^X (Write XOR Execute,即“写入”和“执行”互斥) 特性,基于安全考虑,原则上一块内存区域,不能同时拥有“可写”(W)和“可执行”(X)两种权限。

而在此之前,Flutter 会申请一个 RW 的内存,用于写入翻译好的,或者 hotload 的代码,然后完成写入后,就通过 mprotect 将其修改为 RX,从而让代码可以被执行。

那为什么 iOS 26 之前,Flutter 可以通过 mprotect 实现内存从 RW 到 RX?实际上并不是 Flutter 或者 App 有这个权限,而是在此之前,iOS 为调试构建的应用提供了一个名为 get-task-allow 的特殊授权(entitlement):

这个授权的主要目的是允许调试器(如 LLDB)附加到应用进程并进行控制,而这个能力,可以让带有这个授权的应用,在代码签名验证上会受到较宽松的限制,实际上表现为允许了应用自身修改其可执行内存的支持。

而现在这个能力,在 iOS 26 的真机上被限制了,也就是在 iOS 26 的真机上,就算你是 Debug, App 本身再也不能获得批准修改内存权限的能力。

那为了解决这个问题,Flutter 临时想到了一个 “曲线救国”策略,因为实际上修改内存权限的能力还是在的,只是普通 App 不行而已

在开发过程中,LLDB 还是拥有苹果授予的特殊权限,可以修改应用的内存 ,所以这次的 Flutter 这次实现的临时补丁在于让 Flutter 应用在需要执行新代码时,暂停下来,主动通知旁边的调试器,让调试器利用它的特权来帮忙把代码设置为“可执行”,然后再继续运行

因为 LLDB 在设备上并非直接与应用交互,而是通过一个名为 debugserver 的中间进程,debugserver 是一个由苹果签名、并被授予了特殊私有授权(private entitlements)的系统级程序,这些授权(例如 com.apple.private.memorystatus)赋予了它检查和修改其他进程内存空间的强大能力,其中就包括更改内存页的保护权限。

而具体表现为:

  • 创建了一个专门用于“求助”的 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 函数,主要是为了让 Flutter 在需要执行新代码时,不再调用被禁止的 mprotect,而是转而调用这个新的“求助”函数 NOTIFY_DEBUGGER_ABOUT_RX_PAGES
  • NOTIFY_DEBUGGER_ABOUT_RX_PAGES 里面实际什么都没有,它是一个断点函数,用于“暂停”代码运行,通过“求助” LLDB,让 LLDB 拿到应用传过来的新代码的内存地址,让 LLDB 去修改内存权限

举个例子,在此之前,mprotect 的实现是:

  • 申请一块“毛坯房” : Dart VM 向 iOS 系统申请一块内存,这块内存默认是“可读可写”(RW)的,就像一间空房间,你可以在墙上写字画画
  • 在“毛坯房”里装修: 当你修改了 Dart 代码并触发热重载时,JIT 编译器会快速地将你的新 Dart 代码编译成机器能直接运行的二进制指令,然后把这些指令写入到刚才申请的那块内存(“毛坯房”)里
  • 挂上“办公室”的牌子: “装修”完成后,Dart VM 会调用 mprotect 函数,告诉 iOS 系统:“我已经把这间房装修好了,现在请把它的属性从‘可读可写’(RW)改成‘可读可执行’(RX)”

在 iOS 26 之前,iOS 系统会批准这个请求,于是这块内存就成功变成了可执行的代码区,热重载完成。

而现在iOS 26 的安保升级了,大楼管理员(iOS 系统)发布了新规定: “任何房间(App)都不允许自己给自己挂上‘可执行办公室’的牌子,以防有人把沉重墙砸了。”

所以,之前第三步直接调用 mprotect 的方法行不通了,会被管理员直接拒绝(Permission Denied),所以现在需要走新的“后门”:

  • 申请一块特殊的“双门房间” : Dart VM 现在向系统申请一种特殊的内存,这块内存天生就有两个“入口”(虚拟地址),一个入口的权限是“可读可写”(RW),另一个入口的权限是“可读可执行”(RX),这就是所谓的“双重映射”,你可以想象成虽然只有一间房,从 A 门进去只能写字画画,从 B 门进去只能读取和执行
  • 从“写字”的门进去装修: JIT 编译器像以前一样,通过“可读可写”的 A 门,把新代码编译成机器指令写进去
  • 请“大楼保安”(Debugger)来开门: App 自己没有权限激活那个“可执行”的 B 门,于是它调用一个新增的、特殊的函数 NOTIFY_DEBUGGER_ABOUT_RX_PAGES,这个函数就像是按下一个求助按钮,专门通知正在外面巡逻的“大楼保安”,也就是你连接的调试器(LLDB)。
  • “保安”代为授权: 调试器拥有更高的权限,它收到了求助信号,于是它替 App 向系统管理员打报告:“我确认过了,这间房没问题,可以激活它的‘可执行’入口B门” ,而因为请求来自更高权限的调试器,所以系统管理员就批准了

也就是,在新流程上,虽然还是同一个内存,但是我们用两个“地址”欺骗了保安(LLDB),让保安帮我们激活 RX 地址,这就是这次调整的 “双重映射” 实现。

具体实现

新的流程上,核心关键在于 NOTIFY_DEBUGGER_ABOUT_RX_PAGESNOTIFY_DEBUGGER_ABOUT_RX_PAGES 本身几乎不执行任何操作,因为它的本质是一个“钩子”,这个函数会被配置到 LLDB 的断点里:

也就是,NOTIFY_DEBUGGER_ABOUT_RX_PAGES 是一个会触发 LLDB 断点的函数,它是通过 flutter_tools 下的脚本进行绑定断点,另外这个函数还有对应声明:

  • __attribute__((noinline)): 强制禁止内联优化,它确保了无论编译器优化级别多高,对该函数的调用都会以一个真实且独立的函数调用指令(callbl)存在于最终的二进制文件中,因为它需要为调试器提供了一个稳定且可预测的地址来设置断点
  • __attribute__((visibility("default"))): 函数的符号标记为“公开”,所以在动态链接时对外部可见,这意味着像调试器这样的外部进程可以通过符号名称在应用的可执行文件中找到它
  • extern "C": 声明指示编译器使用 C 语言的链接规范,即不进行 C++ 的名称修饰(name mangling),这保证了函数在符号表中的名称就是简单的 _NOTIFY_DEBUGGER_ABOUT_RX_PAGES,为一个外部工具提供了可预测的、稳定的钩子名称 。

而在适配上,会有一个 ScopedExcBadAccessHandler 对象用于处理执行失败,这个内部类是一个临时的异常处理器,专门用来处理 EXC_BAD_ACCESS 类型的异常,也就是尝试执行「不可执行的内存」或访问「无效内存」引起的问题:

  • 在构造 ScopedExcBadAccessHandler() 时,它会使用 Mach 内核的 thread_swap_exception_ports API,为当前线程安装一个自定义的异常处理器
  • 通过 IgnoreExceptionAndReturnToCaller 让程序会忽略这个异常,并将程序执行点(PC 寄存器)设置到发生异常的函数的返回地址(LR 寄存器),同时在返回值寄存器(X0)中放入一个特殊的错误码 0xDEADDEAD

这个机制被用在 CheckIfRXWorks 函数中用来安全地“试错”,它会尝试执行一块动态生成的代码,如果因为权限问题导致执行失败触发 EXC_BAD_ACCESS,程序不会崩溃,而是会得到那个特殊的返回值 0xDEADDEAD,从而让 VM 知道 JIT 路径走不通。

CheckIfRXWorks 函数是在启动时判断“可行性检查”,这个函数在 VM 初始化时被调用,用来探测当前的运行环境是否真的允许 JIT 编译:

  • 尝试分配一块可执行的内存
  • 检查这块内存的开头是否已经被调试器写入了 "IHELPED!" 这个“暗号”,以确认调试器脚本已经正确加载,这个“暗号”是前面 LLDB 的脚本设置,用于判断 LLDB 和脚本状态是否符合要求
  • 将内存权限改为可写(RW),写入一小段简单的机器码(一个计算 square 的函数)
  • 将内存权限改回只读(R)或可读可执行(RX)
  • 使用上面提到的 ScopedExcBadAccessHandler ,尝试调用刚才写入的 square 函数
  • 检查返回值,如果返回值是正确的结果(例如 11*11=121),则证明 JIT 可行,如果返回值是 0xDEADDEAD,则证明执行失败,JIT 不可行

通过 CheckIfRXWorks 的返回结果会直接决定 can_jit 变量的值,进而影响 VM 的后续行为,如果是不能 JIT,那么在VirtualMemory::AllocateAligned 分配内存时,当 Dart VM 为 JIT 代码分配了一块内存区域后,它不再直接尝试调用 mprotect,而是在在真正使用这块内存之前,插入了对 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 的调用 :

大致核心流程有:

  • 先申请可执行的内存地址 RX
  • 判断当前必须启用“双重映射”,通过 vm_remap 让新的虚拟地址 RW(writable_address)指向与原始 RX address 完全相同的物理内存,此时你的 App 里就有了两个不同的虚拟地址,但它们都通往同一块内存
  • 在映射创建成功后,立刻调用 NOTIFY_DEBUGGER_ABOUT_RX_PAGES,请求调试器介入,完成后续的“激活”工作,通知 lldb 来“激活”原始的 RX 区域,因为一个普通的 App ,无权创造出未经签名和验证的、新的可执行代码页
  • 通过 Protect 函数(它包装了 mprotect)对新的 RW 地址设置为 kReadWrite,因为它是一个全新的地址,所以可以被定义为可读写属性,从而支持被 JIT 编译器写入代码
  • 返回的 VirtualMemory 对象,同时记录了原始的 RX 地址和新建的 RW 别名地址

说了那么多废话,实际上其实就是:通过「双地址映射」让两个地址指向一个内存,一个写入,一个执行,然后利用 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 的断点,让 LLDB 执行授权赋予 RX ,做到在用一块内存上实现 Debug 时具备 RWX 的效果

那为什么说这是一个临时解决方案呢?主要原因有:

  • 首先,这个方案预计会为每个代码空间页的分配增加约 500 毫秒的延迟(adds ~500ms of latency per code space page allocation
  • 其次,操作系统必须挂起应用进程,切换到 debugserver 进程的上下文,完成后再切换回来,这是一个耗时的操作
  • 最后,整个机制的前提是必须有一个调试器被附加到应用进程上,并且该调试器被正确配置了相应的拦截脚本,环境要求高,需要 IDE、Flutter 工具链、LLDB 和 iOS 版本之间脆弱的协同

所以,未来长期考虑,还是需要一个高性能的 Debug 解释器来支持,而目前这个实现,主要还是为了让 Flutter 开发者可以快速在 iOS 26 的真机上进行 Debug 开发的“后门”,但是也可以看得出来,系统安全确实是一个有趣的攻防过程。

那么,你觉得这个后门可以存活多久?

参考资料

❌
❌