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工程
基于Debug与Release环境配置

Generated.xcconfig是通用环境配置,我们在这个文件中进行配置

通用配置
  • flutter_engine_demo工程在模拟器上面运行起来,关闭Android Studio,接下来我们在Runner工程中调试
  • 查看工程执行的脚本
查看脚本
脚本目录
  • xcode_backend.sh脚本执行完成之后,还会执行xcode_backend.dart脚本文件
执行xcode_backend.dart脚本
  • Generated.xcconfig文件中配置的变量,会在xcode_backend.dart文件中使用,比如FLUTTER_APPLICATION_PATH环境变量
用AS查看xcode_backend.dart脚本文件

这就是Android Studio执行了Flutter工程会调用Xcode,而Xcode又关联到Android Studio的过程;关联的过程都在脚本文件中处理好了,后面如果想配置持续集成进行打包,也需要配置相应的脚本文件。

  • Generated.xcconfig文件中进行配置,使其加载Flutter自定义引擎
关联自定义引擎相关配置
  • Runner工程添加断点调试
断点调试

通过断点调试,我们发现了touchesBegan的源码实现,这里的源码在FlutterViewController.mm文件中,即编译好的引擎中目录:/src/out/ios_debug_sim_unopt

下面验证FlutterViewController.mm就在关联的引擎工程中

Runner看到的源码中添加注释
引擎工程

我们在Runner工程中进行调试,添加了注释;然后打开引擎工程发现注释存在,就证明了Runner自定义引擎存在了关联。

  • 检查flutter_engine,进行编译
编译Flutter引擎
  • 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.framework
  • 查看哈希值,用于检测Flutter引擎的二进制文件是否发生变化
查看哈希值
// 查看哈希值命令
$ md5 Flutter
// Flutter.framework/Flutter 的哈希值
4a794a8d53a0fadebc0453fa16e56518
// Runner/Flutter.framework/Flutter 的哈希值
4a794a8d53a0fadebc0453fa16e56518

通过对比,哈希值相同,说明是一个产物。

  • 现在我们把Generated.xcconfig文件中加载自定义引擎的配置注释掉,再次编译工程,查看哈希值
查看发布版本的Flutter引擎哈希值

通过对比我们发现自定义引擎发布版本的引擎哈希值不相同。

  • Generated.xcconfig文件中再次加载自定义引擎,编译工程查看哈希值
哈希值对比

我们的工程在每次编译生成Flutter.framework的时候,可能会添加一些其它内容,我们不能单纯的比对Flutter.framwork的哈希值来判断Framework是否发生更新。

疑问?
Runnder项目实际上获取到的是编译完成的Flutter.framework,而Flutter.framework/Flutter是如何定位到源代码的路径呢?跨工程是如何定位到的?
Flutter二进制文件中包含一些调试信息,使其进行关联。

检查二进制文件中是否包含调试信息

查看发布版本的二进制文件调试信息

  • Generated.xcconfig文件中加载自定义引擎的配置注释掉
  • Products/Runner.app -> Show in Finder
Flutter二进制文件
查看Flutter中是否包含调试信息
查看发布版本的调试信息

由打印信息可知,发布版本会把调试信息隐藏掉

查看自定义引擎的二进制文件调试信息
  • Generated.xcconfig文件中的自定义引擎配置打开
  • Products/Runner.app -> Show in Finder
查看Flutter中是否包含调试信息
查看自定义版本的调试信息

通过终端成功查看到自定义引擎的调试信息;说明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
  • 添加断点进行调试
image.png
image.png
image.png

通过methodChannelWithName源码我们发现,如果不传解码器会有默认的解码器,而且是一个单例eventChannelWithName方法也是一样的,会默认传一个单例解码器

image.png

保存namemessenger解码器

  • 断点调试methodChannel接收Flutter发送过来的消息
image.png
image.png

如果有连接connection先清空,否则设置消息回调

image.png
image.png
image.png
image.png

用字典把namehandler进行保存

image.png

self.flutterVc.binaryMessengersetMethodCallHandler方法中创建的messageHandler是同一个对象。

疑问?其实通讯的是数据,那么传递的数据底层是怎么交互的?推荐跟踪invokeMapMethod底层源码......

codec编解码器

数据是怎么解析最终变成二进制的?下面探索编解码器的底层实现...

前面我们学习的任何一种channel,内部都有一个编解码器编解码器其实是一种通讯协议

  • flutter_engine源码中搜索MessageCodec,发现是一种协议,而且编解码都是对二进制进行
image.png
  • 搜索MethodCodec进行查看
image.png

FlutterEventChannel就是通过MethodCodec编解码的

  • 查看FlutterMethodCall
image.png
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源码

image.png
image.png

查看FlutterStandardMethodCodec源码

image.png
image.png

编解码器底层都使用的FlutterStandardReaderWriter实现的,下面我们来分析FlutterStandardReaderWriter的源码

image.png
image.png
写数据
读数据
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容