開闢 Dart 到 Native 的超級通道,餓了麼跨平臺的最佳實踐

開闢 Dart 到 Native 的超級通道,餓了麼跨平臺的最佳實踐

作者 | 雍光Assuner、菜嘰、執卿、澤卦

出品 | CSDN 博客

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

前言

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 對象:

<code>static const MethodChannel examleChannel = const MethodChannel('ExamplePlugin');/<code>

使用該 Channel 調用原生方法 :

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

在 iOS 平臺,需要編寫 ObjC 代碼:

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

在 Android 平臺,需要編寫 Java 代碼:

<code> 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);
}

@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("nativeMethodA")) {
// ...
}
}
}/<code>

由上我們可以發現,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. 更加簡單的代碼維護。

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

dna 的使用

dna 在 Dart 代碼中:

  • 定義了 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代碼

<code>NSString *versionString = [[UIDevice currentDevice] systemVersion];
// 通過channel返回versionString
/<code>

Dart 代碼

<code>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;/<code>

(2) 返回值作為參數

ObjC代碼

<code>NSString *versionString = [[UIDevice currentDevice] systemVersion];
NSString *platform = @"iOS-";
versionString = [platform stringByAppendingString: versionString];

// 通過channel返回versionString/<code>

Dart 代碼

<code>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;
/<code>

2. 支持鏈式調用

ObjC代碼

<code>NSString *versionString = [[UIDevice currentDevice] systemVersion];
NSString *platform = @"iOS-";
versionString = [platform stringByAppendingString: versionString];

// 通過channel返回versionString
/<code>

Dart 代碼

<code>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;
/<code>

關於Context的最終返回值

context.returnVar 是 context 最終執行完畢返回值的標記:

  1. 設定context.returnVar: 返回該NativeObject對應的Native變量

  2. 不設定context.returnVar: 執行到最後一個invoke,如果有返回值,作為context的最終返回值; 無返回值則返回空值;

<code>ObjCContext context = ObjCContext;
context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');

// 直接獲得原生執行結果
var versionString = await context.execute;
/<code>

3.支持快捷使用JSON中實例化對象

或許有些時候,我們需要用 JSON 直接實例化一個對象。

ObjC代碼

<code>ClassA *objectA = [ClassA new]; 
objectA.a = 1;
objectA.b = @"sss";/<code>

一般時候,這樣寫。

Dart 代碼

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

也可以從JSON中生成

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

▐ Dart 調用 Java

JAVAContext 僅在安卓系統中會被實際執行。JAVAContext 擁有上述 ObjCContext Dart調ObjC 的全部特性。

  • 支持上下文調用

  • 支持鏈式調用

  • 支持用JSON中實例化對象

另外,額外支持了從構造器中實例化一個對象

4. 支持快捷使用構造器實例化對象

Java代碼

<code>String platform = new String("android");/<code>

Dart 代碼

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

▐ 快捷組織雙端代碼

提供了一個快捷的方法來 初始化和執行 context。

<code>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);
}
/<object>/<code>

可以快速書寫兩端的原生調用

<code>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', ).invoke(method: 'getDnaVersion').invoke(method: 'getVersion');
NativeObject version = context.newJavaObjectFromConstructor('java.lang.String', ["android "]).invoke(method: "concat", args: [versionId]);

context.returnVar = version; // 該句可省略
});
/<code>
开辟 Dart 到 Native 的超级通道,饿了么跨平台的最佳实践

dna 原理簡介

▐ 核心實現

dna 並不涉及dart對象到Native對象的轉換 ,也不關心 Native對象的生命週期,而是著重與描述原生方法調用的上下文,在 context execute 時通過 channel 調用一次原生方法,把調用棧以 JSON 的形式傳過去供原生動態解析調用。

如前文的中 dart 代碼:

<code>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;/<code>

NativeContext的execute 方法,實際調用了:

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

在原生的 executeNativeContext 對應執行的方法中,接收到的 JSON 是這樣的:

<code>{
"_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"
}
}]
}
/<code>

我們在 Native 維護了一個 objectsInContextMap , 以objectId 為鍵,以 Native對象為值。

_invocationNodes 便是方法的調用上下文, 看單個。

這裡會動態調用 [UIDevice currentDevice], 返回對象以 returnVar中存儲的"_objectId_KNWtiPuM" 為鍵放到 objectsInContextMap 裡:

<code>{
"returnVar": {
"_objectId": "_objectId_KNWtiPuM"
},
"object": {
"_objectId": "_objectId_qyfACNGb",
"clsName": "UIDevice"
},
"method": "currentDevice"
},
/<code>

這裡 調用方法的對象的objectId 是 "_objectId_KNWtiPuM" ,是上一個方法的返回值,從objectsInContextMap 中取出,繼續動態調用,以 returnVar的object_id為鍵 存儲新的返回值。

<code>{
"returnVar": {
"_objectId": "_objectId_haPktBlL"
},
"object": {
"_objectId": "_objectId_KNWtiPuM" // 會在objectsInContextMap找到中真正的對象
},
"method": "systemVersion"
}
/<code>

方法有參數時,支持自動裝包和解包的,如 intNSNumber.., 如果參數是非 channel 規定的15種基本類型,是NativeObject, 我們會把對象從 objectsInContextMap 中找出,放到實際的參數列表裡:

<code>{
"object": {
"_objectId": "_objectId_UiCMaHAN"
},
"method": "stringByAppendingString:",
"args": [{
"_objectId": "_objectId_haPktBlL" // 會在objectsInContextMap找到中真正的對象
}],
"returnVar": {
"_objectId": "_objectId_WyWRIsLl"

}/<code>

如果設置了最終的returnVar, 將把該 returnVar objectId 對應的對象從 objectsInContextMap 中找出來,作為 channel的返回值 回調回去。如果沒有設置,取最後一個 invocation 的返回值(如果有)。

▐ Android 實現細節

動態調用

Android實現主要是基於反射,通過 dna 傳遞過來的節點信息調用相關方法。

Android流程圖

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

大致流程如上圖, 在 flutter 側通過鏈式調用生成對應的 “Invoke Nodes“, 通過對 ”Invoke Nodes“ 的解析,會生成相應的反射事件。

例如,當Flutter端進行方法調用時:

<code>NativeObject versionId = context
.newJavaObjectFromConstructor('me.ele.dna_example.DnaTest', )
.invoke(method: 'getDnaVersion');
/<code>

我們在內部會將這些鏈路生成相應的結構體通過統一 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 註解處理器)進行實現。

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

方案流程

  • 實現

下面,我們舉一個,來說明具體的實現。

我們想要調用DnaVersion類中的getVersion方法,首先我們為它加上註解。

<code>@DnaMethod
public String getVersion {
return android.os.Build.VERSION.RELEASE;

}/<code>

接下來,在DnaProcessor中,Dna通過繼承AbstractProcessor方法,對代碼進行掃描,讀取DnaMethod所註解的方法:getVersion,並獲取它的方法信息,生成代理方法。

編譯期間,Dna會在DnaVersion類同包名下生成一個Dna_Class_Proxy的代理類,並在其中生成getVersion的代理方法,代理方法名是類名_方法的格式。這裡代碼生成是通過開源庫JavaPoet實現的。

<code> @DnaParamFieldList(
params = {},
owner = "me.ele.dna_example.DnaVersion",
returnType = "java.lang.String"
)
public static Object DnaVersion_getVersion(DnaVersion owner) {
return owner.getVersion;
}
/<code>

自動生成的 getVersion 的代理方法

從代理方法中可以看出,它會傳入調用主體,來進行實際的方法調用。代理方法通過DnaParamFieldList註解配置了三個參數。params用於存儲參數的相關信息,owner 用於存儲類名,returnType 用於存儲返回的對象信息。

在運行時,Dna會通過反射找到 Dna_Class_Proxy 文件中的 DnaVersion_getVersion 方法,通過DnaParamFieldList中的參數配置來確定這是否是目標方法,然後通過執行代理方法來達到 getVersion 方法的實現。

我們會對配置自定義混淆規則來避免代理類的混淆:

<code>-keep class **.Dna_Class_Proxy { *; }
/<code>

混淆後的代理文件:

<code>public class Dna_Class_Proxy {
@a(a = {}, b = "me.ele.dna_example.DnaVersion")
public static b Dna_Constructor_ProxyDnaVersion {
return new b;
}
}/<code>

可以看到,Dna不會影響到原有代碼的混淆,而是通過代理類以及註解儲存的信息,定位到我們的目標方法。從而達到了在release 混淆包中,通過方法名調用目標方法的功能。

如果想要使用Dna,那麼需要在原生代碼上註解DnaMethod,而在Android Framework下的代碼是默認不混淆的,同時也無法進行註解。Dna會對Framework下的代碼進行反射調用,而不是走代理方法調用,從而達到了對於Framework代碼的適配。

▐ iOS 實現細節

iOS 中不需要代碼混淆,可通過豐富的 runtime 接口調用任意類的方法:

  1. 使用 NSClassFromString 動態獲得類對象;

  2. 使用 NSSelectorFromString 獲得要調用方法的 selector;

  3. 使用 NSInvocation 動態為某個對象調用特定方法,參數的不定數組會根據 selector 的type encoding 為對象依次嘗試解包,轉為非對象類型;也會為返回值嘗試裝包 轉為對象類型。

上下文調用細節

  1. 建立 objectsInContextMap,存放 context json 中 所有 object_id 和 native 實際對象的映射關係。

  2. 順序解析context json 中 invocationNodes 數組中的 invocationNode 為 NSInvocation 對象,並調用; 單個 NSInvocation 對象調用產生的返回值,將以 invocationNode 中約定的 object_id 放到 objectsInContextMap 中,下一個 invocation 的調用者或者參數,可能會從之前方法調用產生的對象以Object_id為鍵在 objectsInContextMap 中取出來。

  3. 為 dna channel 返回最終的返回值。

謝謝觀看!如有錯誤,請指出!另外,歡迎吐槽!

dna GitHub 地址:https://github.com/Assuner-Lee/dna 後續會遷移到 eleme 賬號下。

原文鏈接:

https://lpdtech.blog.csdn.net/article/details/104651365


分享到:


相關文章: