系列文章:
(一)Flutter插件开发必备 原生SDK->Dart接口生成引擎Fluttify
介绍
(二)如何利用Fluttify开发一个新的Flutter插件
Fluttify网站:fluttify.com
前言
普通开发Flutter插件的方式既繁琐又容易出错,因为需要在dart和原生之间传递大量的数据,在这个过程中需要手写大量模板代码。前段时间饿了么团队发布了一个插件dna,这个插件提供了一个通用的channel在dart和原生之间传递数据,避免了手写原生代码,过程中使用反射来调用对应原生代码。和dna
不同的是Fluttify提供了一个更静态的方案,即从原生出发,生成对应的dart绑定。
正如Fluttify的定位“编译器”所示,Fluttify的整体实现分成前端和后端,连接前端和后端的是一个代表SDK的中间表示。
前端负责借助antlr从jar/aar和framework中解析出中间表示(目前使用的是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
文件夹,一开始看见这个文件夹下的内容很容易会被劝退,因为它是这样的:
这些16进制数字文件夹会有三层,到第4层就是实际的pod了,每个pod下面会有所有版本的podspec.json,剩下的工作就是解析这个json,获取到里面的下载链接,下载压缩包即可。
编译器前端
生成中间表示第一步需要拿到源代码,android端采用反编译jar的方式获取到源代码,ios端则直接拿到objc的头文件直接解析即可。
反编译使用的是intellij使用的Fernflower反编译器,反编译结果效果不错,目前没有碰到大问题。
第二步就是遍历源代码,这是整个编译器中最困(bu)难(dong)的部分。由于对objc语言的不熟悉,很多objc的语言元素的叫法分不清哪个是哪个,各种specifier,而且很多语法元素可以递归嵌套,很难从语法文件想象出原本的源代码的样貌。
个中细节不再赘述,最终编译器会把SDK分解为7个Java类,分别是SDK
,Type
,Constructor
,Field
,Method
,Parameter
,Variable
。
一个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这样的技术生成插件。