作者简介

雍光Assuner、菜叽、执卿、泽卦;蜂鸟大前端

前言

    Flutter 作为当下最火的跨平台技术,提供了媲美原生性能的 app 使用体验。Flutter 相比 RN 还自建了自己的 RenderObject 层和 Rendering 实现,“几乎” 彻底解决了多端一致性问题,让 dart 代码真正有效的落实 “一处编写,处处运行”,接近双倍的提升了开发者们的搬砖效率。前面为什么说 “几乎”,虽然 Flutter 为我们提供了一种快捷构建用户界面和交互的开发方案,但涉及到平台 native 能力的使用,如推送、定位、蓝牙等,也只能 “曲线救国”,借助 Channel 实现, 这就免不了我们要分别写一部分 native 代码 和 dart 代码做 “技术对接”,略略破坏了这 “完美” 的跨平台一致性。另外,大部分公司的 app 都不是完全重新建立起来的 Flutter app,更多情况下,Flutter 开发的页面及业务最终会以编译产物作为一个模块集成到主工程。主工程原先已经有了大量优秀的工具或业务相关库,如可能是功能强大、做了大量优化的网络库,也可能是一个到处使用的本地缓存库,那么无疑,需要使用的 native 能力范围相比平台自身的能力范围扩大了不少,channel 的定义和使用变得更加高频。

    很多开发者都使用过 channel, 尤其是 dart 调用 native 代码的 Method Channel。 在 dart 侧,我们可以实例化一个 Channel 对象:

static const MethodChannel examleChannel = const MethodChannel('ExamplePlugin');

使用该 Channel 调用原生方法 :

final String version = await examleChannel.invokeMethod('nativeMethodA', {"a":1, "b": "abc"});

在 iOS 平台,需要编写 ObjC 代码:

FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"ExamplePlugin" binaryMessenger:[registrar messenger]];
[channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {if ([call.method isEqualToString:@"nativeMethodA"]) {NSDictionary *params = call.arguments;NSInteger a = [params[@"a"] integerValue];NSString *b = params[@"b"];// ...}}];

在 Android 平台,需要编写 Java 代码:

 public class ExamplePlugin implements MethodCallHandler {/** Plugin registration. */public static void registerWith(Registrar registrar) {final MethodChannel channel = new MethodChannel(registrar.messenger(), "ExamplePlugin");channel.setMethodCallHandler(new ExamplePlugin());}@Overridepublic void onMethodCall(MethodCall call, Result result) {if (call.method.equals("nativeMethodA")) {// ...}}
}

由上我们可以发现,Channel 的使用 有以下缺点:

  1. Channel 的名字、调用的方法名是字符串硬编码的;
  2. channel 只能单次整体调用字符串匹配的代码块,参数限定是单个对象;不能调用 native 类已存在的方法,更不能组合调用若干个 native 方法.
  3. 在native 字符串匹配的代码块,仍然需要手动对应取出参数,供真正关键方法调用,再把返回值封装返回给dart.
  4. 定义一个Channel 调用 native 方法, 需要维护 dart、ObjC、Java 三方代码
  5. flutter 调试时,native 代码是不支持热加载的,修改 native 代码需要工程重跑;
  6. channel 调用可能涵盖了诸多细碎的原生能力,native 代码处理的 method 不宜过多,且一般会依赖三方库;多个channel 的维护是分散的;

继续分析,我们得出认知:

  1. 跨平台,定位一个方法的硬编码是绝对免不了的;
  2. native 里字符串匹配的代码块里,真正的关键方法调用是不可或缺的;
  3. 方法调用必须支持可变参数

为此,我们实现了一个 dart 到 native 的超级通道 — dna,试图解决 Channel 的诸多使用和维护上的缺点,主要有以下能力和特性:

  1. 使用 dart代码 调用 native 任意类的任意方法;意味着channel 的 native代码 可以写在 dart 源文件中;
  2. 可以组合调用多个 native 方法确定返回值,支持上下文调用,链式调用;
  3. 调用 native 方法的参数直接顺序放到不定长度数组,native 自动顺序为参数解包调用;
  4. 支持 native 代码的 热加载,不中断的开发体验.
  5. 更加简单的代码维护.

dna 的使用

dnaDart代码中:

  • 定义了 NativeContext 类 ,以执行 Dart 代码 的方式,描述 Native 代码 调用上下文(调用栈);最后调用 context.execute() 执行对应平台的 Native 代码 并返回结果。

  • 定义了 NativeObject 类 ,用于标识 Native 变量. 调用者 NativeObject 对象 可借助 所在NativeContext上下文 调用 invoke方法 传入 方法名 method参数数组 args list ,得到 返回值NativeObject对象

NativeContext 子类 的API是一致的. 下面先详细介绍通过 ObjCContext 调用 ObjC ,再区别介绍 JAVAContext 调用 JAVA.

Dart 调用 ObjC

ObjCContext 仅在iOS平台会实际执行.

1. 支持上下文调用

(1) 返回值作为调用者

ObjC代码

NSString *versionString = [[UIDevice currentDevice] systemVersion];
// 通过channel返回versionString

Dart 代码

ObjCContext context = ObjCContext();
NativeObject UIDevice = context.classFromString('UIDevice');
NativeObject device = UIDevice.invoke(method: 'currentDevice');
NativeObject version = device.invoke(method: 'systemVersion');context.returnVar = version; // 可省略设定最终返回值, 参考3// 直接获得原生执行结果
var versionString = await context.execute();
(2) 返回值作为参数

ObjC代码

NSString *versionString = [[UIDevice currentDevice] systemVersion];
NSString *platform = @"iOS-";
versionString = [platform stringByAppendingString: versionString];// 通过channel返回versionString

Dart 代码

ObjCContext context = ObjCContext();
NativeClass UIDevice = context.classFromString('UIDevice');
NativeObject device = UIDevice.invoke(method: 'currentDevice');
NativeObject version = device.invoke(method: 'systemVersion');
NativeObject platform = context.classFromString("NSString").invoke(method: 'stringWithString:', args: ['iOS-']);
version = platform.invoke(method: 'stringByAppendingString:', args: [version]);context.returnVar = version; // 可省略设定最终返回值, 参考3// 直接获得原生执行结果
var versionString = await context.execute();

2. 支持链式调用

ObjC代码

NSString *versionString = [[UIDevice currentDevice] systemVersion];
versionString = [@"iOS-" stringByAppendingString: versionString];// 通过channel返回versionString

Dart 代码

ObjCContext context = ObjCContext();
NativeObject version = context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');
version = context.classFromString("NSString").invoke(method: 'stringWithString:', args: ['iOS-']).invoke(method: 'stringByAppendingString:', args: [version]);context.returnVar = version; // 可省略设定最终返回值, 参考3// 直接获得原生执行结果
var versionString = await context.execute();

*关于Context的最终返回值

context.returnVarcontext 最终执行完毕返回值的标记

  1. 设定context.returnVar: 返回该NativeObject对应的Native变量
  2. 不设定context.returnVar: 执行到最后一个invoke,如果有返回值,作为context的最终返回值; 无返回值则返回空值;
ObjCContext context = ObjCContext();
context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');// 直接获得原生执行结果
var versionString = await context.execute();

3.支持快捷使用JSON中实例化对象

或许有些时候,我们需要用 JSON 直接实例化一个对象.

ObjC代码

ClassA *objectA = [ClassA new];
objectA.a = 1;
objectA.b = @"sss";

一般时候,这样写
Dart 代码

ObjCContext context = ObjCContext();
NativeObject objectA = context.classFromString('ClassA').invoke(method: 'new');
objectA.invoke(method: 'setA:', args: [1]);
objectA.invoke(method: 'setB:', args: ['sss']);

也可以从JSON中生成

ObjCContext context = ObjCContext();
NativeObject objectA = context.newNativeObjectFromJSON({'a':1,'b':'sss'}, 'ClassA');

Dart 调用 Java

JAVAContext 仅在安卓系统中会被实际执行. JAVAContext 拥有上述 ObjCContext Dart调ObjC 的全部特性.

  • 支持上下文调用
  • 支持链式调用
  • 支持用JSON中实例化对象

另外,额外支持了从构造器中实例化一个对象

4. 支持快捷使用构造器实例化对象

Java代码

String platform = new String("android");

Dart 代码

NativeObject version = context.newJavaObjectFromConstructor('java.lang.String', ["android "])

快捷组织双端代码

提供了一个快捷的方法来 初始化和执行 context.

static Future<Object> traversingNative(ObjCContextBuilder(ObjCContext objcContext), JAVAContextBuilder(JAVAContext javaContext)) async {NativeContext nativeContext;if (Platform.isIOS) {nativeContext = ObjCContext();ObjCContextBuilder(nativeContext);} else if (Platform.isAndroid) {nativeContext = JAVAContext();JAVAContextBuilder(nativeContext);}return executeNativeContext(nativeContext);
}

可以快速书写两端的原生调用

platformVersion = await Dna.traversingNative((ObjCContext context) {NativeObject version = context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');version = context.classFromString("NSString").invoke(method: 'stringWithString:', args: ['iOS-']).invoke(method: 'stringByAppendingString:', args: [version]);context.returnVar = version; // 该句可省略
}, (JAVAContext context) {NativeObject versionId = context.newJavaObjectFromConstructor('com.example.dna_example.DnaTest', null).invoke(method: 'getDnaVersion').invoke(method: 'getVersion');NativeObject version = context.newJavaObjectFromConstructor('java.lang.String', ["android "]).invoke(method: "concat", args: [versionId]);context.returnVar = version; // 该句可省略
});

dna 原理简介

核心实现

dna 并不涉及dart对象到Native对象的转换 ,也不关心 Native对象的生命周期,而是着重与描述原生方法调用的上下文,在 context execute 时通过 channel 调用一次原生方法,把调用栈以 JSON 的形式传过去供原生动态解析调用。

如前文的中 dart 代码

ObjCContext context = ObjCContext();
NativeObject version = context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');
version = context.classFromString("NSString").invoke(method: 'stringWithString:', args: ['iOS-']).invoke(method: 'stringByAppendingString:', args: [version]);context.returnVar = version; // 可省略设定最终返回值, 参考3// 直接获得原生执行结果
var versionString = await context.execute();

NativeContext的execute() 方法,实际调用了

static Future<Object> executeNativeContext(NativeContext context) async {return await _channel.invokeMethod('executeNativeContext', context.toJSON());
}

原生的 executeNativeContext 对应执行的方法中,接收到的 JSON 是这样的

{"_objectJSONWrappers": [],"returnVar": {"_objectId": "_objectId_WyWRIsLl"},"_invocationNodes": [{"returnVar": {"_objectId": "_objectId_KNWtiPuM"},"object": {"_objectId": "_objectId_qyfACNGb","clsName": "UIDevice"},"method": "currentDevice"}, {"returnVar": {"_objectId": "_objectId_haPktBlL"},"object": {"_objectId": "_objectId_KNWtiPuM"},"method": "systemVersion"}, {"object": {"_objectId": "_objectId_UAUcgnOD","clsName": "NSString"},"method": "stringWithString:","args": ["iOS-"],"returnVar": {"_objectId": "_objectId_UiCMaHAN"}}, {"object": {"_objectId": "_objectId_UiCMaHAN"},"method": "stringByAppendingString:","args": [{"_objectId": "_objectId_haPktBlL"}],"returnVar": {"_objectId": "_objectId_WyWRIsLl"}}]
}

我们在 Native 维护了一个 objectsInContextMap , 以objectId 为键,以 Native对象 为值。

_invocationNodes 便是方法的调用上下文, 看单个

这里会动态调用 [UIDevice currentDevice], 返回对象以 returnVar中存储的"_objectId_KNWtiPuM" 为键放到 objectsInContextMap

{"returnVar": {"_objectId": "_objectId_KNWtiPuM"},"object": {"_objectId": "_objectId_qyfACNGb","clsName": "UIDevice"},"method": "currentDevice"},

这里 调用方法的对象的objectId"_objectId_KNWtiPuM" ,是上一个方法的返回值,从objectsInContextMap 中取出,继续动态调用,以 returnVar的object_id为键 存储新的返回值。

{"returnVar": {"_objectId": "_objectId_haPktBlL"},"object": {"_objectId": "_objectId_KNWtiPuM" // 会在objectsInContextMap找到中真正的对象},"method": "systemVersion"
}

方法有参数时,支持自动装包和解包的,如 int<->NSNumber.., 如果参数是非 channel 规定的15种基本类型,是NativeObject, 我们会把对象从 objectsInContextMap 中找出,放到实际的参数列表里

{"object": {"_objectId": "_objectId_UiCMaHAN"},"method": "stringByAppendingString:","args": [{"_objectId": "_objectId_haPktBlL" // 会在objectsInContextMap找到中真正的对象}],"returnVar": {"_objectId": "_objectId_WyWRIsLl"
}

如果设置了最终的returnVar, 将把该 returnVar objectId 对应的对象从 objectsInContextMap 中找出来,作为 channel的返回值 回调回去。如果没有设置,取最后一个 invocation 的返回值(如果有)。

* Android 实现细节

动态调用

Android实现主要是基于反射,通过 dna 传递过来的节点信息调用相关方法。
Android流程图

大致流程如上图, 在 flutter 侧通过链式调用生成对应的 “Invoke Nodes“, 通过对 ”Invoke Nodes“ 的解析,会生成相应的反射事件。

例如,当flutter端进行方法调用时:

NativeObject versionId = context.newJavaObjectFromConstructor('me.ele.dna_example.DnaTest', null).invoke(method: 'getDnaVersion');

我们在内部会将这些链路生成相应的结构体通过统一 channel 的方式传入原生端, 之后根据节点信息进行原生端的反射调用。
在节点中存储有方法所在类的类名,方法名,以及参数类型等相关信息。我们可以基于此通过反射,获取该类名中所有相同方法名的方法,然后比对参数类型,获取到目标方法,从而达到重载的实现。
方法调用获取到的结果会回传回去,作为链式调用下一个节点的调用者进行使用,最后获取到的结果,会回传给 flutter 端。

绕过混淆

难点

Dna做到这里还有一个难点需要攻克,就是如何绕过混淆。Release版本都会对代码进行混淆,原有的类,方法,变量都会被重新命名。上文中,Dna实现原理就是从flutter端传递类名和方法信息到Android native端,通过反射进行方法调用,Release版本在编译中,类名和方法名会被混淆,那么方法就会无法找到。
如果无法解决混淆这个问题,那么Dna就只能停留在debug阶段,无法真正上线使用。

方案

我们通常会通过自定义混淆规则,去指定一些必要的方法不被混淆,但是在这里是不适用的。原因如下:
1.我们不能让用户通过自定义混淆规则,来指定本地方法不被混淆。这个会损害代码的安全性,而且操作过于复杂。
2.自定义混淆规则通常只能避免方法名不被混淆,却无法影响到参数,除非将参数的类也进行反混淆。Dna通过参数类型来进行重载功能的实现,因此这个方案不被接受。
我们想要的方案应当具有以下特性:
• 使用简单,避免自定义混淆规则的配置
• 安全,低侵入性
针对上述要求,我们提出了几种方案:

  1. 通过 mapping 反链接来实现
  2. 通过将整个调用链封装成协议传到 Native 层,然后通过动态生成代理代码的方式来将调用链封装成方法体
  3. 通过注解的方式,在编译期生成每个调用方法的代理方法

目前我们使用方案三进行操作,它的颗粒度更细,更利于复用。
混淆的操作是针对.classes文件,它的执行在javac编译之后。因此我们在编译期间,对代码进行扫描,生成方法代理文件,将目标方法的信息存储起来,然后进行输出。在运行时,我们查找到代理文件,通过比对其中的方法信息获取到代理方法,通过代理方法执行我们想要执行的目标方法。具体实现方式,我们需要通过APT(Annotation Processing Tool 注解处理器)进行实现。


方案流程

实现

下面,我们举一个

dna --- 一个 dart 到 native 的超级通道相关推荐

  1. 开辟 Dart 到 Native 的超级通道,饿了么跨平台的最佳实践

    作者 | 雍光Assuner.菜叽.执卿.泽卦 责编 | 屠敏 出品 | CSDN 博客 前言 Flutter 作为当下最火的跨平台技术,提供了媲美原生性能的 App 使用体验.Flutter 相比 ...

  2. 一个使用react native实现的短视频APP

    黄豆仔短视频APP 一个使用react native实现的短视频APP.该项目是我没事搞着玩,用react native 写的.用了很多的库同时也修改了几个库: react-native-card-s ...

  3. 用一个uchar 类型表示八个通道的状态

    //描述:用一个参数uchar 表示八个通道的状态,可以方便传参uint8_t num = 0;//通道的状态 //备注:uint8_t 就是uchar //调用直接输出所有通道的状态 void re ...

  4. java jni 结构体_JAVA 的JNI,传参为结构体问题: 我在网上找的资料://返回一个结构 public native DiskInfo getStruct();...

    Java代码:classDiskInfo{//名字publicStringname;//序列号publicintserial;}//返回一个结构publicnativeDiskInfogetStruc ...

  5. 开源一个基于cocos2d-x的游戏--超级六边形(SuperSector)

    超级六边形(SuperSector)是安卓平台下面一款非常刺激的游戏.我非常喜欢它. 可以在GooglePlay https://play.google.com/store/apps/details? ...

  6. 一个男孩子写的超级情书!!!

    一个18 岁男孩写的超级情书绝对能得100 分! 我对你1 见钟情,绝无2 心,想照顾你3 生3 世,因为我偷偷上你的网站4 次,你那迷人的5 官,总让我6 神无主,一颗心7 上8 下,99 不能平息 ...

  7. 测试工作——如何区别一个 App 是 Native App, Web App 还是 Hybrid app?

    nativeapp是一个原生程序,一般运行在机器操作系统上,有很强的交互,一般静态资源都是在本地的.浏览使用方便,体验度高.在实现上要么使用Objecttive-c和cocoaTouch Framew ...

  8. 开发Flex for Android第一个ANE(ActionScript Native Extensions)本地扩展

    本地扩展就是需要调用原生的东西要开发的插件 首先打开Android Studio,建个空项目, 再建立个名为 FirstANE 的Android Library Module, 然后把C:\Progr ...

  9. jmeter如何进行一个简单的测试(超级详细,有图有文字,闭着眼都能成功)

    大家好,我是雄雄. 内容先知 前言 软件获取 开始测试 1.新建线程组 2.创建一个请求 3.添加HTTP信息头 4.开始测试 5.查看请求情况 前言 上头问题要服务器的配置,基于我们现在做的项目,需 ...

最新文章

  1. CentOS7升级JDK
  2. android网络游戏开发实战pdf_Python项目开发实战+第2版PDF高清文档下载
  3. 有赞搜索引擎实践(算法篇)
  4. 02-缓存一致性---实现big.LITTLE、GPU 计算和企业应用
  5. POJ 1611 The Suspects
  6. Heron 数据模型,API和组件介绍
  7. 2017.9.1 最小生成树 失败总结
  8. 其实,我只想安静的写写代码...
  9. OpenGL与gl glu glut freeglut glew glfw封装库关系(十五)
  10. 玩转你的AlphaGo(MAC OS)
  11. 负债均衡(三)下载安装Nginx
  12. 互联网晚报 | 7月10日 星期天 | 快手官宣:7月18日周杰伦独家直播;​400亿额度,秒光!7月总票房破10亿...
  13. Jade平台的下载与原装
  14. html数值计算计算
  15. 几种前端h264播放器记录
  16. Apache Web服务器安全配置全攻略
  17. 推荐系统:石器与青铜时代
  18. Azure:云平台概述
  19. mov格式的视频转换成mp4,教你3种快速方法来处理
  20. winpe修复计算机无法启动,PE修复系统启动故障的详细教程

热门文章

  1. oracle aix迁移到x86,Oracle采用XTTS从小机迁移X86平台时,system/SYSAUX中的表如何迁移...
  2. ESP32 擦除flash
  3. 凯恩帝1000对刀图解_凯恩帝数控机床对刀方法
  4. Webots2021b和ROS2调试笔记21-07-27
  5. 达梦同步工具dmhs同步kafka配置
  6. Linux服务器的配置和数据迁移方案
  7. uni-app 商城 的sku算法(vue)
  8. 11、Nepxion Discovery 之全链路界面操作蓝绿灰度发布
  9. excel的vlookup如果是空白就不显示0,而是显示空白
  10. 华为虚拟服务器密码忘记怎么办,手机云服务器密码忘记了