阅读 264

Fluttify编译器原理介绍

系列文章:

(一)Flutter插件开发必备 原生SDK->Dart接口生成引擎Fluttify介绍

(二)如何利用Fluttify开发一个新的Flutter插件

(三)Fluttify输出Flutter插件工程详解

(四)Fluttify编译器原理介绍

Fluttify网站:fluttify.com

前言

普通开发Flutter插件的方式既繁琐又容易出错,因为需要在dart和原生之间传递大量的数据,在这个过程中需要手写大量模板代码。前段时间饿了么团队发布了一个插件dna,这个插件提供了一个通用的channel在dart和原生之间传递数据,避免了手写原生代码,过程中使用反射来调用对应原生代码。和dna不同的是Fluttify提供了一个更静态的方案,即从原生出发,生成对应的dart绑定。

正如Fluttify的定位“编译器”所示,Fluttify的整体实现分成前端和后端,连接前端和后端的是一个代表SDK的中间表示。

前端负责借助antlr从jar/aarframework中解析出中间表示(目前使用的是json格式),后端则消费这个中间表示,把其转换成dart/java/objc代码。

解析器ANTLR

引用自Wiki:

ANTLR(全名:ANother Tool for Language Recognition)是基于LL(*)算法实现的语法解析器生成器(parser generator),用Java语言编写,使用自上而下(top-down)的递归下降LL剖析器方法。由旧金山大学的Terence Parr博士等人于1989年开始发展。

ANTLR本身是Java实现的,向ANTLR输入一个语法规则文件,能够生成对应语言源文件的解析代码,目前支持输出Java, C#, Python2|3, JavaScript, Go, C++, Swift代码,也就是说你可以用这些语言的代码解析任何语法规则对应的源代码。

比如说现在有一份A语言的语法文件A.g4,把这个文件作为参数传入ANTLR,ANTLR可以为你生成一份Swift代码,这份Swift代码可以遍历A语言的源代码,你可以解析出A语言代码里任何你感兴趣的部分。

我们这里使用ANTLR默认的输出语言Java。

从远程依赖获取源代码

从maven坐标到jar

Fluttify支持从maven坐标直接生成插件工程,其中的难点便是怎么把maven坐标下载成真实的SDK,我找了很多maven相关的rest api服务,但是要么是少字段,要么是速度很慢,再要么就是商业接口要付费。

幸运的是Fluttify是基于gradle实现的,一顿google后,发现gradle api可以指定maven坐标直接下载artifact,后来才发现其实这跟在build.gradle里添加依赖是一样的。只不过平时都是写在build.gradle里,换成gradle api就懵逼了。

project.repositories.run {
    maven { it.url = URI("http://maven.aliyun.com/nexus/content/groups/public/") }
    jcenter()
    mavenCentral()
}
val config = project.configurations.create("targetJar")
val dep = project.dependencies.create(ext.android.remote.run { "$org:$name:$version" })
config.dependencies.add(dep)
config.files // 调用这句后,如果本地没有缓存,gradle就会去下载
复制代码

从cocoapods到framework

从cocoapods获取到源代码的方法就更trick一点,一开始也是各种找有没有开放的rest api,很多地方说cocoapods官方有开放api,但是试了之后都不能用。后来只能想一些偏门的方法,比如说直接读取cocoapods的本地索引。

cocoapods在用户目录下会有一个~/.cocoapods/repos/master/Specs文件夹,一开始看见这个文件夹下的内容很容易会被劝退,因为它是这样的:

cocoapods

这些16进制数字文件夹会有三层,到第4层就是实际的pod了,每个pod下面会有所有版本的podspec.json,剩下的工作就是解析这个json,获取到里面的下载链接,下载压缩包即可。

编译器前端

生成中间表示第一步需要拿到源代码,android端采用反编译jar的方式获取到源代码,ios端则直接拿到objc的头文件直接解析即可。

反编译使用的是intellij使用的Fernflower反编译器,反编译结果效果不错,目前没有碰到大问题。

第二步就是遍历源代码,这是整个编译器中最困(bu)难(dong)的部分。由于对objc语言的不熟悉,很多objc的语言元素的叫法分不清哪个是哪个,各种specifier,而且很多语法元素可以递归嵌套,很难从语法文件想象出原本的源代码的样貌。

个中细节不再赘述,最终编译器会把SDK分解为7个Java类,分别是SDKTypeConstructorFieldMethodParameterVariable

一个SDK会被一个SDK类表示,然后SDK对象会被序列化,并写入一个文件中,供后端使用。

一个中间表示的部分内容:

{
  "version": "0.0.1",
  "platform": "Android",
  "libs": [
    {
      "name": "com",
      "types": [
        {
          "platform": "Android",
          "name": "com.autonavi.ae.gmap.maploader.Pools$Pool",
          "genericTypes": [
            "com.autonavi.ae.gmap.maploader.T"
          ],
          "typeType": "Interface",
          "isPublic": true,
          "isAbstract": true,
          "isInnerType": true,
          "isStaticType": true,
          "isJsonable": false,
          "superClass": "",
          "interfaces": [],
          "constructors": [],
          "fields": [],
          "methods": [
            {
              "exactName": "acquire",
              "returnType": "com.autonavi.ae.gmap.maploader.T",
              "name": "acquire",
              "formalParams": [],
              "isStatic": false,
              "isAbstract": true,
              "isPublic": true,
              "className": "com.autonavi.ae.gmap.maploader.Pools$Pool",
              "platform": "Android",
              "isDeprecated": false,
              "isFunction": false,
              "isGenericMethod": false
            },
            ...
复制代码

编译器后端

有了中间表示后,其实编译器后端的工作就相对轻松了。精力消耗都在摸索模板内容中。主要的工作就是怎么把(比如)Method对象转换为Dart/Java/Objc对应的代码。

由于Java,Objc和Dart之间的语法并不能一一对应,所以在编写模板的过程中也遇到不少问题。

比如说高德地图的MAMapView设置delegate,由于delegate是弱引用,所以任何新创建的MAMapViewDelegate对象赋值给delegate都会被立即回收,因为引用计数没有增加,所以delegate必须赋值为self,一开始找不到合适的对象来当这个self,整个插件里只有主Plugin类和PlatformViewFactory类两种类型的对象,所以只能让PlatformViewFactory类来承当这个self,也能让delegate和PlatformViewFactory的生命周期保持一致。

后记

所谓编译不过就是把一段文本转化成另一段文本,创造Fluttify的过程中,对于Fluttify到底是一个什么东西的看法也一直在转变,这都源于我的知识匮乏,一开始觉得它是一个生成器,所以定位成一个所谓的引擎,后来xster大佬在Fluttify输出Flutter插件工程详解 下向我推荐了他们Flutter官方搞的一个类似的东西dartle,我才意识到Fluttify其实是一个编译器,和大多数编译器一样,它有前端和后端,只不过它的目标代码不再是二进制而是可读的代码,甚至理论上借助中间表示,也可以为React Native这样的技术生成插件。