普通视图

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

零一开源|前沿技术周报 #5

作者 kymjs张涛
2025年6月30日 10:49

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:[零一开源] | [掘金] | [RSS]

码圈新闻

  1. 2025 Google I/O:Android这次真的AI化了
  2. 华为云发布盘古大模型5.5,现场揭秘底层技术
  3. 小米多模态大模型MiMo-VL开源,官方称多方面领先 Qwen2.5-VL-7B
  4. 代码革命的先锋:aiXcoder-7B模型
  5. React Native 0.80 开始支持 iOS 预构建

新技术介绍

  1. MNN LLM:让DeepSeek-R1 跑在手机上,还能支持多模态
  2. 苹果向开发者开放本地AI能力,推出全新Foundation Models框架

大厂在做什么

  1. 节省前端1000+pd人力成本!快手快聘「伏羲工作台」技术实践全解析
  2. B站在KMP跨平台的业务实践之路

深度技术

  1. 万字长文总结多模态大模型最新进展(Modality Bridging篇)
  2. 全网最强ViT (Vision Transformer)原理及代码解析
  3. 与微软“代码女王”关于GitHub、智能编程的一小时对谈
  4. 零基础解码Transformer与大模型核心原理
  5. 万字长文深入浅出教你优雅开发复杂AI Agent

博客推荐

  1. Android: 2025年,Android 16 都快来了,你知道各个版本 Android的特点吗?
    随着 Android 的发展,每个新版本的 Android 都带来了新的 API 和 改进。这篇文章将介绍从 Android 5 开始到最新版 Android 的API及其行为的变更。
  2. 后端: 转转上门履约服务拆分库表迁移实践
    随着业务不断发展,一个服务中部分功能模块适合沉淀下来作为通用的基础能力。作为通用的基础能力,对提供的服务可用性和稳定性有较高的要求,因此把该部分功能模块拆分出来,单独一个服务是比较好的选择。为了更好的与业务服务物理隔离,不仅需要从代码层面拆分,数据库层面也需要拆分。在做技术方案设计时面临着以下几个问题:
  3. 其他: 纪念陈皓(左耳朵耗子)老师
    突然才意识到,原来陈皓老师(网名:左耳朵耗子)已经离开两整年了。 两年前的5月份,这位年仅47岁的技术前辈因病离开,这也让世间从此少了一位倔强又浪漫的技术人。 相信不少同学了解陈皓老师都是从他的个人博客酷壳CoolShell开始的。
  4. iOS: iOS开发:关于路由
    在iOS开发中引入路由框架一直是一个有争议的话题。 因为即使不使用路由框架,似乎也不会有太大的影响。那么我们先来回顾一下几个典型的跳转场景:
  5. iOS: iOS 实现自定义对象深拷贝(OC/Swift)
    在 OC 中,对象的赋值操作是浅拷贝(指针复制),创建一个指针,旧对象引用对象计数加 1。在 Swift 中,类是引用类型,引用类型在赋值和传参时都是操作的引用,这些引用指向同一个对象。如下,对 classA 的修改也会影响到 classB:
  6. Android: 一句话说透Android里面的Activity、Window、DecorView、View之间的关系
    一句话总结: Activity 是老板,Window 是装修队长,DecorView 是毛坯房,View 是你买的家具。老板喊队长来装修毛坯房,最后把你的家具摆进去!

关于我们

零一开源 是我自己做的一个文章和开源项目的分享站,有写博客开源项目的也欢迎来提供投递。
每周会搜集、整理当前的新技术、新文章,欢迎大家订阅

[奸笑]

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 始终流畅响应用户。

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

作者 园宵
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 又双叒叕可以在 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 开发的“后门”,但是也可以看得出来,系统安全确实是一个有趣的攻防过程。

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

参考资料

❌
❌