dna --- 一个 dart 到 native 的超级通道
作者简介
雍光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 的使用 有以下缺点:
- Channel 的名字、调用的方法名是字符串硬编码的;
- channel 只能单次整体调用字符串匹配的代码块,参数限定是单个对象;不能调用 native 类已存在的方法,更不能组合调用若干个 native 方法.
- 在native 字符串匹配的代码块,仍然需要手动对应取出参数,供真正关键方法调用,再把返回值封装返回给dart.
- 定义一个Channel 调用 native 方法, 需要维护 dart、ObjC、Java 三方代码
- flutter 调试时,native 代码是不支持热加载的,修改 native 代码需要工程重跑;
- channel 调用可能涵盖了诸多细碎的原生能力,native 代码处理的 method 不宜过多,且一般会依赖三方库;多个channel 的维护是分散的;
继续分析,我们得出认知:
- 跨平台,定位一个方法的硬编码是绝对免不了的;
- native 里字符串匹配的代码块里,真正的关键方法调用是不可或缺的;
- 方法调用必须支持可变参数
为此,我们实现了一个 dart 到 native 的超级通道 — dna,试图解决 Channel 的诸多使用和维护上的缺点,主要有以下能力和特性:
- 使用 dart代码 调用 native 任意类的任意方法;意味着channel 的 native代码 可以写在 dart 源文件中;
- 可以组合调用多个 native 方法确定返回值,支持上下文调用,链式调用;
- 调用 native 方法的参数直接顺序放到不定长度数组,native 自动顺序为参数解包调用;
- 支持 native 代码的 热加载,不中断的开发体验.
- 更加简单的代码维护.
dna 的使用
定义了
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.returnVar
是 context
最终执行完毕返回值的标记
- 设定context.returnVar: 返回该NativeObject对应的Native变量
- 不设定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通过参数类型来进行重载功能的实现,因此这个方案不被接受。
我们想要的方案应当具有以下特性:
• 使用简单,避免自定义混淆规则的配置
• 安全,低侵入性
针对上述要求,我们提出了几种方案:
- 通过 mapping 反链接来实现
- 通过将整个调用链封装成协议传到 Native 层,然后通过动态生成代理代码的方式来将调用链封装成方法体
- 通过注解的方式,在编译期生成每个调用方法的代理方法
目前我们使用方案三进行操作,它的颗粒度更细,更利于复用。
混淆的操作是针对.classes文件,它的执行在javac编译之后。因此我们在编译期间,对代码进行扫描,生成方法代理文件,将目标方法的信息存储起来,然后进行输出。在运行时,我们查找到代理文件,通过比对其中的方法信息获取到代理方法,通过代理方法执行我们想要执行的目标方法。具体实现方式,我们需要通过APT(Annotation Processing Tool 注解处理器)进行实现。
方案流程
实现
下面,我们举一个
dna --- 一个 dart 到 native 的超级通道相关推荐
- 开辟 Dart 到 Native 的超级通道,饿了么跨平台的最佳实践
作者 | 雍光Assuner.菜叽.执卿.泽卦 责编 | 屠敏 出品 | CSDN 博客 前言 Flutter 作为当下最火的跨平台技术,提供了媲美原生性能的 App 使用体验.Flutter 相比 ...
- 一个使用react native实现的短视频APP
黄豆仔短视频APP 一个使用react native实现的短视频APP.该项目是我没事搞着玩,用react native 写的.用了很多的库同时也修改了几个库: react-native-card-s ...
- 用一个uchar 类型表示八个通道的状态
//描述:用一个参数uchar 表示八个通道的状态,可以方便传参uint8_t num = 0;//通道的状态 //备注:uint8_t 就是uchar //调用直接输出所有通道的状态 void re ...
- java jni 结构体_JAVA 的JNI,传参为结构体问题: 我在网上找的资料://返回一个结构 public native DiskInfo getStruct();...
Java代码:classDiskInfo{//名字publicStringname;//序列号publicintserial;}//返回一个结构publicnativeDiskInfogetStruc ...
- 开源一个基于cocos2d-x的游戏--超级六边形(SuperSector)
超级六边形(SuperSector)是安卓平台下面一款非常刺激的游戏.我非常喜欢它. 可以在GooglePlay https://play.google.com/store/apps/details? ...
- 一个男孩子写的超级情书!!!
一个18 岁男孩写的超级情书绝对能得100 分! 我对你1 见钟情,绝无2 心,想照顾你3 生3 世,因为我偷偷上你的网站4 次,你那迷人的5 官,总让我6 神无主,一颗心7 上8 下,99 不能平息 ...
- 测试工作——如何区别一个 App 是 Native App, Web App 还是 Hybrid app?
nativeapp是一个原生程序,一般运行在机器操作系统上,有很强的交互,一般静态资源都是在本地的.浏览使用方便,体验度高.在实现上要么使用Objecttive-c和cocoaTouch Framew ...
- 开发Flex for Android第一个ANE(ActionScript Native Extensions)本地扩展
本地扩展就是需要调用原生的东西要开发的插件 首先打开Android Studio,建个空项目, 再建立个名为 FirstANE 的Android Library Module, 然后把C:\Progr ...
- jmeter如何进行一个简单的测试(超级详细,有图有文字,闭着眼都能成功)
大家好,我是雄雄. 内容先知 前言 软件获取 开始测试 1.新建线程组 2.创建一个请求 3.添加HTTP信息头 4.开始测试 5.查看请求情况 前言 上头问题要服务器的配置,基于我们现在做的项目,需 ...
最新文章
- CentOS7升级JDK
- android网络游戏开发实战pdf_Python项目开发实战+第2版PDF高清文档下载
- 有赞搜索引擎实践(算法篇)
- 02-缓存一致性---实现big.LITTLE、GPU 计算和企业应用
- POJ 1611 The Suspects
- Heron 数据模型,API和组件介绍
- 2017.9.1 最小生成树 失败总结
- 其实,我只想安静的写写代码...
- OpenGL与gl glu glut freeglut glew glfw封装库关系(十五)
- 玩转你的AlphaGo(MAC OS)
- 负债均衡(三)下载安装Nginx
- 互联网晚报 | 7月10日 星期天 | 快手官宣:7月18日周杰伦独家直播;​400亿额度,秒光!7月总票房破10亿...
- Jade平台的下载与原装
- html数值计算计算
- 几种前端h264播放器记录
- Apache Web服务器安全配置全攻略
- 推荐系统:石器与青铜时代
- Azure:云平台概述
- mov格式的视频转换成mp4,教你3种快速方法来处理
- winpe修复计算机无法启动,PE修复系统启动故障的详细教程
热门文章
- oracle aix迁移到x86,Oracle采用XTTS从小机迁移X86平台时,system/SYSAUX中的表如何迁移...
- ESP32 擦除flash
- 凯恩帝1000对刀图解_凯恩帝数控机床对刀方法
- Webots2021b和ROS2调试笔记21-07-27
- 达梦同步工具dmhs同步kafka配置
- Linux服务器的配置和数据迁移方案
- uni-app 商城 的sku算法(vue)
- 11、Nepxion Discovery 之全链路界面操作蓝绿灰度发布
- excel的vlookup如果是空白就不显示0,而是显示空白
- 华为虚拟服务器密码忘记怎么办,手机云服务器密码忘记了