Flutter引擎源码调试与Channel底层原理探索
配置项目代码关联引擎源码
通过下载引擎源码可以进行分析
以及动态调试
-
Flutter引擎
编译成功之后,我们获取到模拟器x86
架构下的Xcode工程
(目录:/src/out/ios_debug_sim_unopt
); - 在
/ios_debug_sim_unopt
目录下会有一个Flutter.framework/Flutter
引擎库,然后把我们的Flutter
项目配置成这个framework
,即配置自定义引擎
;
下面新建工程flutter_engine_demo
(注意iOS与Android平台的语言选择OC以及Java),让其加载上面编译好的自定义引擎
-
Flutter引擎
源码最终编译成了Xcode
工程,我们是基于Xcode
进行动态调试的,因此这里要先在Xcode
中进行配置,打开flutter_engine_demo
工程中ios
目录下的Runner
工程
Generated.xcconfig
是通用环境配置,我们在这个文件中进行配置
-
flutter_engine_demo
工程在模拟器上面运行起来,关闭Android Studio
,接下来我们在Runner
工程中调试 查看工程执行的脚本
-
xcode_backend.sh
脚本执行完成之后,还会执行xcode_backend.dart
脚本文件
-
Generated.xcconfig
文件中配置的变量,会在xcode_backend.dart
文件中使用,比如FLUTTER_APPLICATION_PATH
环境变量
这就是Android Studio
执行了Flutter
工程会调用Xcode
,而Xcode
又关联到Android Studio
的过程;关联的过程都在脚本文件中处理好了,后面如果想配置持续集成
进行打包,也需要配置相应的脚本文件。
-
Generated.xcconfig
文件中进行配置,使其加载Flutter自定义引擎
-
Runner
工程添加断点调试
通过断点调试,我们发现了touchesBegan
的源码实现,这里的源码在FlutterViewController.mm
文件中,即编译好的引擎中目录:/src/out/ios_debug_sim_unopt
;
下面验证FlutterViewController.mm
就在关联的引擎工程中
我们在Runner
工程中进行调试,添加了注释;然后打开引擎工程
发现注释存在,就证明了Runner
与自定义引擎
存在了关联。
- 检查
flutter_engine
,进行编译
-
Runner
工程添加断点,调试到引擎中的源码,在源码中添加日志打印
- 在源码中添加了
日志打印
,需要重新编译Flutter引擎
才会执行
重新运行Runner
工程,点击屏幕查看日志打印
Runner
成功与Flutter引擎
进行关联,而且还可以修改引擎源码进行调试
(每次修改源码都需要重新编译引擎)。
查看引擎源码
目录
flutter_engine
引擎源码中查看FlutterViewController.mm
文件的目录,发现目录结构为/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
我们发现引擎源码
在flutter
目录下,并不在out
目录,说明源码只有一份,根据真机、模拟器不同平台,编译出不同的Flutter.framework
库。
查看工程Flutter.framework/Flutter
的哈希值是否相同?
-
Products/Runner.app
->Show in Finder
- 查看哈希值,用于检测
Flutter引擎
的二进制文件是否发生变化
// 查看哈希值命令
$ md5 Flutter
// Flutter.framework/Flutter 的哈希值
4a794a8d53a0fadebc0453fa16e56518
// Runner/Flutter.framework/Flutter 的哈希值
4a794a8d53a0fadebc0453fa16e56518
通过对比,哈希值相同,说明是一个产物。
- 现在我们把
Generated.xcconfig
文件中加载自定义引擎
的配置注释掉,再次编译工程,查看哈希值
通过对比我们发现自定义引擎
与发布版本的引擎
哈希值不相同。
-
Generated.xcconfig
文件中再次加载自定义引擎
,编译工程查看哈希值
我们的工程在每次编译生成Flutter.framework
的时候,可能会添加一些其它内容,我们不能单纯的比对Flutter.framwork
的哈希值来判断Framework
是否发生更新。
疑问?
Runnder
项目实际上获取到的是编译完成的Flutter.framework
,而Flutter.framework/Flutter
是如何定位到源代码的路径呢?跨工程是如何定位到的?
Flutter
二进制文件中包含一些调试信息,使其进行关联。
检查二进制文件中是否包含调试信息
查看发布版本
的二进制文件调试信息
-
Generated.xcconfig
文件中加载自定义引擎的配置注释掉 Products/Runner.app -> Show in Finder
由打印信息可知,发布版本会把调试信息隐藏掉
。
查看自定义引擎
的二进制文件调试信息
-
Generated.xcconfig
文件中的自定义引擎配置打开 Products/Runner.app -> Show in Finder
通过终端成功查看到自定义引擎
的调试信息;说明Runner
工程成功与自定义引擎
关联到了一块,就可以直接调试源码
。
检查⼆进制是否含有调试信息
- lipo命令
#可以查看包含的架构
$ lipo -info xxx
#拆分架构
$ lipo xxx -thin armv7 -output armv7_xxx
#合并多架构
$ lipo -create xxx.a xxx.a -output xxx.a
- LLDB检查是否含有调试信息
$ lldb --file Flutter_arm64
(lldb) target create "Flutter_arm64"
Current executable set to 'Flutter_arm64' (arm64)
// 查看有多少个编译单元,即.o文件
.(lldb) script lldb.target.module['Flutter_arm64'] .GetNumCompileUnits()
1
(lldb)
- 使⽤python列出模块的所有编译单元的完整路径
(lldb) target create "Flutter_arm64"
Current executable set to 'Flutter_arm64' (arm64).
(lldb) script
Python Interactive Interpreter. To exit, type 'quit()', 'exit()' or Ctrl-D.
// 获取到模型
>>> m = lldb.target.module['Flutter_arm64']
>>> for i in range(m.GetNumCompileUnits()):
... cu = m.GetCompileUnitAtIndex(i).file.fullpath
... print(cu)
...
None
>>>
调试引擎源码Channel底层实现
下面我们就通过Runner
工程来调试Flutter引擎源码
-
flutter_engine_demo
工程中添加给原生发送消息的代码
<!-- main.dart文件 -->
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const EnginePage(),
);
}
}
class EnginePage extends StatefulWidget {
const EnginePage({Key? key}) : super(key: key);
@override
_EnginePageState createState() => _EnginePageState();
}
class _EnginePageState extends State<EnginePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('EnginePage'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
// 给原生发送消息
const MethodChannel('engine_page')
.invokeMapMethod('method_channel');
},
child: const Icon(Icons.add),
),
),
);
}
}
-
Generated.xcconfig
文件中进行自定义引擎
相关配置(注意:如果引擎路径发生了变化,需要gn构建
、ninja编译⼯程
) -
Runner
工程中AppDelegate.m
添加如下代码
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@interface AppDelegate ()
@property(nonatomic, strong) FlutterEngine* flutterEngine;
@property(nonatomic, strong) FlutterViewController* flutterVc;
@property(nonatomic, strong) FlutterMethodChannel* methodChannel;
@end
@implementation AppDelegate
// 懒加载引擎,通过引擎获取VC
- (FlutterEngine *)flutterEngine {
if (!_flutterEngine) {
FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"hk"];
if (engine.run) {
_flutterEngine = engine;
}
}
return _flutterEngine;
}
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
self.flutterVc = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
// 通过VC获取channel
// MethodChannel:传递方法的调用
self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"engine_page" binaryMessenger:self.flutterVc.binaryMessenger];
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
NSLog(@"收到了:%@",call.method);
}];
// BasicMessageChannel:传递字符串和半结构化信息
[FlutterBasicMessageChannel messageChannelWithName:@"123" binaryMessenger:self.flutterVc.binaryMessenger];
// EventChannel:传递数据流
[FlutterEventChannel eventChannelWithName:@"123" binaryMessenger:self.flutterVc.binaryMessenger];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
- 添加断点进行调试
通过methodChannelWithName
源码我们发现,如果不传解码器
会有默认的解码器,而且是一个单例
;eventChannelWithName
方法也是一样的,会默认传一个单例解码器
。
保存name
、messenger
、解码器
。
- 断点调试
methodChannel
接收Flutter
发送过来的消息
如果有连接connection
先清空,否则设置消息回调
用字典把name
与handler
进行保存
self.flutterVc.binaryMessenger
与setMethodCallHandler
方法中创建的messageHandler
是同一个对象。
疑问?其实通讯的是数据,那么传递的数据底层是怎么交互的?推荐跟踪invokeMapMethod
底层源码......
codec编解码器
数据是怎么解析最终变成二进制的?下面探索编解码器
的底层实现...
前面我们学习的任何一种channel
,内部都有一个编解码器
,编解码器
其实是一种通讯协议
-
flutter_engine
源码中搜索MessageCodec
,发现是一种协议,而且编解码都是对二进制
进行
- 搜索
MethodCodec
进行查看
FlutterEventChannel
就是通过MethodCodec
编解码的
- 查看
FlutterMethodCall
类
在Flutter
中,MessageCodec
有多种实现:
-
FlutterStandardMessageCodec
:是FlutterBasicMessageChannel
中默认使用的编解码器。(底层使用FlutterStandardReaderWriter
实现的)。用于数据类型和二进制数据之间的编解码。支持基础数据类型包(bool 、char 、double 、float 、int 、long 、short 、String 、Array 、Dictionary)以及二进制数据。 -
FlutterBinaryCodec
:用于二进制数据和二进制数据之间的编解码,在实现上只是原封不动的将接收到的二进制数据返回。 -
FlutterStringCodec
:用于字符串与二进制数据之间的编解码,对于字符串采用UTF-8
编码格式。 -
FlutterJSONMessageCodec
:用于数据类型与二进制数据之间的编解码,支持基础数据类型(bool 、char 、double 、float 、int 、long 、short 、String 、Array 、Dictionary)。在iOS
端使用NSJSONSerialization
作为序列化的工具。
查看FlutterStandardMessageCodec
源码
查看FlutterStandardMethodCodec
源码
编解码器底层都使用的FlutterStandardReaderWriter
实现的,下面我们来分析FlutterStandardReaderWriter
的源码