做者簡介前端
雍光Assuner、菜嘰、執卿、澤卦;蜂鳥大前端java
Flutter 做爲當下最火的跨平臺技術,提供了媲美原生性能的 app 使用體驗。Flutter 相比 RN 還自建了本身的 RenderObject 層和 Rendering 實現,「幾乎」 完全解決了多端一致性問題,讓 dart 代碼真正有效的落實 「一處編寫,到處運行」,接近雙倍的提高了開發者們的搬磚效率。前面爲何說 "幾乎",雖然 Flutter 爲咱們提供了一種快捷構建用戶界面和交互的開發方案,但涉及到平臺 native 能力的使用,如推送、定位、藍牙等,也只能 "曲線救國",藉助 Channel 實現, 這就免不了咱們要分別寫一部分 native 代碼 和 dart 代碼作 「技術對接」,略略破壞了這 「完美」 的跨平臺一致性。另外,大部分公司的 app 都不是徹底從新創建起來的 Flutter app,更多狀況下,Flutter 開發的頁面及業務最終會以編譯產物做爲一個模塊集成到主工程。主工程原先已經有了大量優秀的工具或業務相關庫,如多是功能強大、作了大量優化的網絡庫,也多是一個處處使用的本地緩存庫,那麼無疑,須要使用的 native 能力範圍相比平臺自身的能力範圍擴大了很多,channel 的定義和使用變得更加高頻。android
不少開發者都使用過 channel, 尤爲是 dart 調用 native 代碼的 Method Channel。 在 dart 側,咱們能夠實例化一個 Channel 對象:git
static const MethodChannel examleChannel = const MethodChannel('ExamplePlugin');
複製代碼
使用該 Channel 調用原生方法 :github
final String version = await examleChannel.invokeMethod('nativeMethodA', {"a":1, "b": "abc"});
複製代碼
在 iOS 平臺,須要編寫 ObjC 代碼:json
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());
}
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("nativeMethodA")) {
// ...
}
}
}
複製代碼
由上咱們能夠發現,Channel 的使用 有如下缺點:緩存
繼續分析,咱們得出認知:安全
爲此,咱們實現了一個 dart 到 native 的超級通道 --- dna,試圖解決 Channel 的諸多使用和維護上的缺點,主要有如下能力和特性:bash
dna
在Dart代碼
中:
定義了 NativeContext 類
,以執行 Dart 代碼
的方式,描述 Native 代碼
調用上下文(調用棧);最後調用 context.execute()
執行對應平臺的 Native 代碼
並返回結果。
定義了 NativeObject 類
,用於標識 Native 變量
. 調用者 NativeObject 對象
可藉助 所在NativeContext上下文
調用 invoke方法
傳入 方法名 method
和 參數數組 args list
,獲得 返回值NativeObject對象
。
NativeContext 子類
的API是一致的. 下面先詳細介紹經過 ObjCContext
調用 ObjC
,再區別介紹 JAVAContext
調用 JAVA
.
ObjCContext
僅在iOS平臺會實際執行.
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();
複製代碼
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();
複製代碼
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.returnVar
是 context
最終執行完畢返回值的標記
ObjCContext context = ObjCContext();
context.classFromString('UIDevice').invoke(method: 'currentDevice').invoke(method: 'systemVersion');
// 直接得到原生執行結果
var versionString = await context.execute();
複製代碼
或許有些時候,咱們須要用 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');
複製代碼
JAVAContext
僅在安卓系統中會被實際執行. JAVAContext
擁有上述 ObjCContext
Dart調ObjC
的所有特性.
另外,額外支持了從構造器中實例化一個對象
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
並不涉及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實現主要是基於反射,經過 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經過參數類型來進行重載功能的實現,所以這個方案不被接受。 咱們想要的方案應當具備如下特性: • 使用簡單,避免自定義混淆規則的配置 • 安全,低侵入性 針對上述要求,咱們提出了幾種方案:
目前咱們使用方案三進行操做,它的顆粒度更細,更利於複用。 混淆的操做是針對.classes文件,它的執行在javac編譯以後。所以咱們在編譯期間,對代碼進行掃描,生成方法代理文件,將目標方法的信息存儲起來,而後進行輸出。在運行時,咱們查找到代理文件,經過比對其中的方法信息獲取到代理方法,經過代理方法執行咱們想要執行的目標方法。具體實現方式,咱們須要經過APT(Annotation Processing Tool 註解處理器)進行實現。
方案流程下面,咱們舉一個🌰,來講明具體的實現。 咱們想要調用DnaVersion類中的getVersion方法,首先咱們爲它加上註解。
@DnaMethod
public String getVersion() {
return android.os.Build.VERSION.RELEASE;
}
複製代碼
接下來,在DnaProcessor中,Dna經過繼承AbstractProcessor方法,對代碼進行掃描,讀取DnaMethod所註解的方法:getVersion(),並獲取它的方法信息,生成代理方法。 編譯期間,Dna會在DnaVersion類同包名下生成一個Dna_Class_Proxy的代理類,並在其中生成getVersion的代理方法,代理方法名是類名_方法的格式。這裏代碼生成是經過開源庫JavaPoet實現的。
@DnaParamFieldList(
params = {},
owner = "me.ele.dna_example.DnaVersion",
returnType = "java.lang.String"
)
public static Object DnaVersion_getVersion(DnaVersion owner) {
return owner.getVersion();
}
複製代碼
自動生成的 getVersion 的代理方法 從代理方法中能夠看出,它會傳入調用主體,來進行實際的方法調用。代理方法經過DnaParamFieldList註解配置了三個參數。params用於存儲參數的相關信息,owner 用於存儲類名,returnType 用於存儲返回的對象信息。 在運行時,Dna會經過反射找到 Dna_Class_Proxy 文件中的 DnaVersion_getVersion 方法,經過DnaParamFieldList中的參數配置來肯定這是不是目標方法,而後經過執行代理方法來達到 getVersion 方法的實現。 咱們會對配置自定義混淆規則來避免代理類的混淆:
-keep class **.Dna_Class_Proxy { *; }
複製代碼
混淆後的代理文件:
public class Dna_Class_Proxy {
@a(a = {}, b = "me.ele.dna_example.DnaVersion")
public static b Dna_Constructor_ProxyDnaVersion() {
return new b();
}
}
複製代碼
能夠看到,Dna不會影響到原有代碼的混淆,而是經過代理類以及註解儲存的信息,定位到咱們的目標方法。從而達到了在release 混淆包中,經過方法名調用目標方法的功能。 若是想要使用Dna,那麼須要在原生代碼上註解DnaMethod,而在Android Framework下的代碼是默認不混淆的,同時也沒法進行註解。Dna會對Framework下的代碼進行反射調用,而不是走代理方法調用,從而達到了對於Framework代碼的適配。
iOS 中不須要代碼混淆,可經過豐富的 runtime 接口調用任意類的方法:
使用 NSClassFromString 動態得到類對象;
使用 NSSelectorFromString 得到要調用方法的 selector;
使用 NSInvocation 動態爲某個對象調用特定方法,參數的不定數組會根據 selector 的type encoding 爲對象依次嘗試解包,轉爲非對象類型;也會爲返回值嘗試裝包 轉爲對象類型。
上下文調用細節
創建 objectsInContextMap,存放 context json 中 全部 object_id 和 native 實際對象的映射關係;
順序解析context json 中 invocationNodes 數組中的 invocationNode 爲 NSInvocation 對象,並調用; 單個 NSInvocation 對象調用產生的返回值,將以 invocationNode 中約定的 object_id 放到 objectsInContextMap 中,下一個 invocation 的調用者或者參數,可能會從以前方法調用產生的對象以Object_id爲鍵在 objectsInContextMap 中取出來。
爲 dna channel 返回最終的返回值.
github.com/Assuner-Lee… 後續會遷移到 eleme 帳號下