flutter 调用原生ios oc方法 flutter调用原生sdk_在sdk中添加源文件


Fluttify是什么?

Fluttify是一个可以为原生SDK生成Dart接口的一个工具。github地址:https://github.com/fluttify-project

Fluttify解决了什么问题?

这里要先说明一下插件开发的几种方式:

  • 常规插件开发方式(Native厚,Dart薄)

常规的开发方式是把功能实现下沉到原生端,然后再在Dart端对封装好的原生方法进行一层薄的抽象。这种开发方式在两端SDK接口设计一致时,碰到的阻力会比较小,比如Google Map的官方插件就是这样开发的。
去年(2018)年底我开发 amap_base的时候,一开始也是按照Google Map插件的套路来开发的,Android端非常顺利,因为高德的Android的SDK接口大部分都是抄的Google Map。但是到iOS端时,就头大了,因为iOS端没有抄Google Map的接口设计(甚至同一个功能,需要调用的方法个数也不一样!),导致想要把功能实现下沉到两端再在Dart做一层薄的抽象变成异常困难,且大量的字段需要在两个端之间对比,开发 amap_base的过程让我心力憔悴。

  • Fluttify插件开发方式(Native薄,Dart厚)

amap_base的经验让我开始思考如何才能让插件开发的过程能够最小化的机械劳动。在一段时间的精神编程后,我认为为两端原生接口做抽象的工作一定要放到Dart端这边来做,而 不是在原生端实现好接口,然后在Dart端做一层薄的抽象,应该是 Dart端一定要厚,原生端一定要薄
那么原生端要薄到什么程度呢?编码初期一直不停在调整这个度。一开始我只想为非model类的公开接口生成代码,model类逐句翻译到Dart。写着写着发现 逐句翻译这件事情并不容易,而且如何判断一个类是model类还是非model类呢?这是无法实现的。所以在这之后我觉得为所有的公开类都生成对应的Dart类,并且只区分View类和非View类,因为View类需要生成对应的 PlatformView,然后使用 MethodChannel将Dart/Native连接在一起。

Fluttify的目标是解决开发者只懂一个端或者两端都不懂时,可以借助Fluttify生成的Dart接口进行开发,从而屏蔽了原生代码。

原理介绍

反射

在有了Fluttify的概念之后,我最开始想到的方法自然是把SDK依赖到一个模板工程,然后通过反射把原生SDK里的所有接口反射出来,然后进行生成代码的拼接。这个方案有几个瓶颈:

  1. Android端,在模板工程(普通gradle工程)的gradle中依赖目标jar,可以实现代码的反射,但是如果是aar怎么办?
  2. iOS要依赖framework就必须是一个xcode工程,那么就必须要macos环境,如果想要把Fluttify做成一个服务部署到服务器上还得部署到macos系统的服务器上?
  3. 后续实现Flutter for webFlutter Desktop完全变成了未知的情况,而且还要去学对应平台的工程结构以及对应语言的反射机制。

在短暂试验了反射方案后,我认为反射不是答案。

代码解析

在反射方案失败后,我开始想能不能直接解析源文件生成AST,这样的话限制会小很多,而且后续再有平台接入,也不会产生额外的学习成本。但是如何解析呢?不懂编译原理的我搜索了几天后就快要放弃,就在一天在github上搜索了language parse关键字之后,我找到了答案,那就是antlr

antlr提供了很好的抽象层去遍历源文件,解析代码不再是难题。而且antlr提供了非常多的语法文件,这其中包括了javaobjective-c,以及后续Flutter for webFlutter Desktop需要的语法文件,这为Fluttify的后续发展铺平了道路。

Fluttify最核心的原理就是经过antlr解析之后,产生一个结构化的SDK表示,再根据这个SDK表示生成Flutter插件工程。甚至再扩展一下,利用这个结构化的SDK表示,也可以为React Native以及其他的框架生成插件。

案例分析

这里提供一个高德地图SDK中比较典型的案例做一个分析。高德地图中,提供了在地图上显示标记的能力,在Android端这个标记叫Marker,在iOS端叫Annotation。这个添加标记的接口坑爹的地方在哪里呢?

在Android端,只需要一步,调用AMap::addMarker(MarkerOption)即可。所有的配置项都在MarkerOption中,并且添加完成后会返回对应的Marker对象供你操作。

在iOS端,需要三步:

  1. 调用MAMapView::addAnnotation(MAAnnotation)
  2. 调用MAMapView::delegate配置回调,一般都配置成self,因为delegate是弱引用;
  3. 实现MAMapViewDelegate::mapView:viewForAnnotation,根据第一步中的MAAnnotation配置MAPinAnnotationView并返回MAPinAnnotationView的实例,在Android中一次性配置的标记参数被分散在了MAAnnotation中和MAPinAnnotationView中;

你说这样如何才能在Native端统一好抽象供Dart端调用?不是说做不到,而是需要额外的复杂度,一旦代码上了规模,就必须小心翼翼地处理这些复杂度。另外iOS端这种设计,如何才能跟Android端一样构造出一个标记对象返回给Dart端?我有时候觉得高德地图的iOS SDK作者的脑回路也真的是清奇,怎么会想出这种设计的,跟高德Android SDK一样抄Google Map不香吗?最滑稽的是什么,百度地图的接口设计跟高德的一毛一样,我也不知道到底是谁抄的谁。

开发amap_base的时候我引入了一些多余的复杂度去努力统一Android和iOS的接口,但是效果仍然是不理想,比如原生接口返回类型是SDK内的非model类型时(addMarker方法),我无法把它(Marker对象)返回给Dart端,也就无法在Dart端去控制单个的Marker,除非再在Native端引入一些复杂度,比如把Marker对象放到一个全局的列表或者Map中去。

在有了Fluttify之后,所有的这些不一致都可以放到Dart端这边来调度,Native代码只负责输出。

已知的限制以及临时解决方案

目前的Fluttify也不是什么接口都能生成的,一些是因为Flutter本身的限制,另一些是面向对象的特性导致的。

Native调用Dart方法同步返回

当Native端调用Dart并且需要同步返回Dart端构造的对象时,理想的处理方式是通过MethodChannel调用Dart,在Dart端构造出对应的对象,然后返回给回调方法,但是因为Native调用Dart时,都是异步的,所以没办法同步返回给回调方法,具体案例就是MAMapViewDelegate::mapView:viewForAnnotation这个方法。解决方案是只能手写原生代码。github上有关于同步MethodChannel的issue,感兴趣的可以关注一下 https://github.com/flutter/flutter/issues/28310

Dart调用Native返回接口类型

由于在面向对象中,子类实例可以赋值给父类变量,当Native端返回一个接口父类给Dart端时,Dart端无法构造这个接口类。目前的解决方案是找出SDK中实现该接口的第一个类进行实例化。具体案例是MAMapViewDelegate::mapView:DidAnnotationViewTapped方法。

如何使用Fluttify

请邮件联系我(382146139@qq.com), 并说明来源。

路线图

  • (进行中)在高德地图SDK上实现大部分功能(选择高德只是因为用例比较全);
  • 建立一个网站(如果可以的话使用Flutter构建)开始进行内测;
  • 开放使用;