最近在做hybrid相關的工作,專案中用到了EasyJsWebView
,程式碼量不大,一直想分析一下它的具體實現,抽空寫了這篇文章。
1.前言
原生程式碼+h5頁面+甚至React Native(或其他) 的方式開發移動客戶端已經成為當前的主流趨勢,因此老生常談的一個問題就是原生程式碼與js的互動。原生程式碼中執行js程式碼,沒什麼可講的直接webView執行js程式碼即可,本文主要由安卓的js呼叫原生的方式切入,分析iOS端是如何實現類似比較方便的呼叫的。
2.安卓端(js -> native interface)
對安卓的開發不是很熟,只是列舉一個簡單的例子講述這樣一種方式。
- native端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void onCreate(Bundle savedInstanceState) { ... // 新增一個物件, 讓JS可以訪問該物件的方法, 該物件中可以呼叫JS中的方法 webView.addJavascriptInterface(new Contact(), "contact"); } private final class Contact { //Html呼叫此方法傳遞資料 public void showcontacts() { String json = "[{"name":"zxx", "amount":"9999999", "phone":"18600012345"}]"; // 呼叫JS中的方法 webView.loadUrl("javascript:show('" + json + "')"); } } |
- h5端
1 2 3 |
姓名 存款 電話 |
當h5頁面載入時,onload
方法執行,對應的native端中的Contact
類中的showcontacts
方法被執行。因此核心思想就是通過webView將native原生的類與自定義的js物件關聯,js就可以直接通過這個js物件呼叫它的例項方法。
3.iOS端(js -> native interface)
上述安卓的js呼叫native的方式是如此簡單明瞭,不禁想如果iOS端也有如此實現的話,這樣同時即保證安卓,iOS,h5的統一性也能讓開發者只用關心互動的介面即可。因此便引出了EasyJSWebView
的第三方的框架(基於說明2設計),下面從該框架的使用出發,分析框架的具體實現。
說明:
- 1.iOS端雖然也可以通過
JSContext
注入全域性的方法但是達不到與安卓端統一- 2.iOS端可以通過攔截h5請求的url,通過url的格式區分類或方法,但是這樣不夠直觀,也達不到與安卓端統一
4.EasyJsWebView
4.1 EasyJsWebView使用
本文直接列舉EasyJsWebView Github README例子
- native端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@interface MyJSInterface : NSObject - (void) test; - (void) testWithParam: (NSString*) param; - (void) testWithTwoParam: (NSString*) param AndParam2: (NSString*) param2; - (NSString*) testWithRet; @end // 注入 MyJSInterface* interface = [MyJSInterface new]; [self.myWebView addJavascriptInterfaces:interface WithName:@"MyJSTest"]; [interface release]; |
- js端
1 2 3 4 5 |
MyJSTest.test(); MyJSTest.testWithParam("ha:ha"); MyJSTest.testWithTwoParamAndParam2("haha1", "haha2"); var str = MyJSTest.testWithRet(); |
4.2 EasyJsWebView具體實現
4.2.1 EasyJsWebView初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (id)init{ self = [super init]; if (self) { [self initEasyJS]; } return self; } - (void) initEasyJS{ self.proxyDelegate = [[EasyJSWebViewProxyDelegate alloc] init]; self.delegate = self.proxyDelegate; } - (void) setDelegate:(id)delegate{ if (delegate != self.proxyDelegate){ self.proxyDelegate.realDelegate = delegate; }else{ [super setDelegate:delegate]; } } |
初始化設定webView的delegate,實際的webView的回撥的在EasyJSWebViewProxyDelegate
中實現,因此我們主要關注EasyJSWebViewProxyDelegate
中的webView的回撥的實現即可。
4.2.2 EasyJSWebViewProxyDelegate webView回撥實現
4.2.2.1 webViewDidStartLoad回撥實現
程式碼片段一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
NSMutableString* injection = [[NSMutableString alloc] init]; //inject the javascript interface for(id key in self.javascriptInterfaces) { NSObject* interface = [self.javascriptInterfaces objectForKey:key]; [injection appendString:@"EasyJS.inject(""]; [injection appendString:key]; [injection appendString:@"", ["]; unsigned int mc = 0; Class cls = object_getClass(interface); Method * mlist = class_copyMethodList(cls, &mc); for (int i = 0; i |
- 遍歷注入的介面的列表key
- 通過key獲取注入類的例項
- 通過類的例項獲取例項方法的列表
- 依次拼接需要執行js函式的程式碼
- EasyJS物件的載入,執行EasyJS.inject方法
例子:參考Demo除錯結果如下
1 2 3 4 5 6 7 8 9 |
EasyJS.inject("MyJSTest", [ "test", "testWithParam:", "testWithTwoParam:AndParam2:", "testWithFuncParam:", "testWithFuncParam2:", "testWithRet" ]); |
4.2.2.2 EasyJS物件
程式碼片段一:
1 2 3 4 5 |
inject: function (obj, methods){ window[obj] = {}; var jsObj = window[obj]; for (var i = 0, l = methods.length; i |
遍歷注入的類的例項方法的列表,通過一個全域性的window[obj]的字典維護對應方法的具體實現。下面我們具體看看EasyJS.call
方法的實現。
程式碼片段二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
call: function (obj, functionName, args){ var formattedArgs = []; for (var i = 0, l = args.length; i 0 ? ":" + encodeURIComponent(formattedArgs.join(":")) : ""); alert(argStr); var iframe = document.createElement("IFRAME"); iframe.setAttribute("src", "easy-js:" + obj + ":" + encodeURIComponent(functionName) + argStr); document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null; var ret = EasyJS.retValue; EasyJS.retValue = undefined; if (ret){ return decodeURIComponent(ret); } }, |
這段程式碼做了三件事:
- 1.分別針對引數function型別與其他型別區分處理
- 2.建立一個
IFRAME
標籤元素,設定src
- 3.將新建的
IFRAME
新增到root元素上
修改IFRAME
的src
預設會觸發webView的回撥的執行,因此便有了下面方法shouldStartLoadWithRequest
的攔截。
4.2.2.3 shouldStartLoadWithRequest回撥實現
程式碼片段一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
NSArray *components = [requestString componentsSeparatedByString:@":"]; //NSLog(@"req: %@", requestString); NSString* obj = (NSString*)[components objectAtIndex:1]; NSString* method = [(NSString*)[components objectAtIndex:2] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSObject* interface = [javascriptInterfaces objectForKey:obj]; // execute the interfacing method SEL selector = NSSelectorFromString(method); NSMethodSignature* sig = [[interface class] instanceMethodSignatureForSelector:selector]; NSInvocation* invoker = [NSInvocation invocationWithMethodSignature:sig]; invoker.selector = selector; invoker.target = interface; NSMutableArray* args = [[NSMutableArray alloc] init]; if ([components count] > 3){ NSString *argsAsString = [(NSString*)[components objectAtIndex:3] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSArray* formattedArgs = [argsAsString componentsSeparatedByString:@":"]; for (int i = 0, j = 0, l = [formattedArgs count]; i |
- 1.拆分攔截到的requestString拆分為
obj
,method
,formattedArgs
三個部分- 2.獲取類例項方法的簽名,新建一個
NSInvocation
例項,指定例項與方法- 3.
invoker
設定引數,然後執行invoke,注意引數中function型別的區分,以下5中會分析回撥function的處理過程。
程式碼片段二:
1 2 3 4 5 6 7 8 9 10 11 |
if ([sig methodReturnLength] > 0){ NSString* retValue; [invoker getReturnValue:&retValue]; if (retValue == NULL || retValue == nil){ [webView stringByEvaluatingJavaScriptFromString:@"EasyJS.retValue=null;"]; }else{ retValue = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL,(CFStringRef) retValue, NULL, (CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8)); [webView stringByEvaluatingJavaScriptFromString:[@"" stringByAppendingFormat:@"EasyJS.retValue="%@";", retValue]]; } } |
獲取invoker執行的結果通過webView執行js程式碼返回結果值。
5.EasyJSDataFunction 與 invokeCallback
以下主要分析EasyJsWebView
是如何處理回撥方法引數的。
程式碼片段一:
1 2 3 4 5 6 |
if (typeof args[i] == "function"){ formattedArgs.push("f"); var cbID = "__cb" + (+new Date); EasyJS.__callbacks[cbID] = args[i]; formattedArgs.push(cbID); } |
js端call方法這樣處理function引數,EasyJS物件一個全域性的__callbacks字典儲存方法實現物件
程式碼片段二:
1 2 3 4 5 6 |
if ([@"f" isEqualToString:type]){ EasyJSDataFunction* func = [[EasyJSDataFunction alloc] initWithWebView:(EasyJSWebView *)webView]; func.funcID = argStr; [args addObject:func]; [invoker setArgument:&func atIndex:(j + 2)]; } |
native端攔截到請求,執行方法
程式碼片段三:
1 2 3 4 5 6 7 |
- (NSString*) executeWithParams: (NSArray*) params{ NSMutableString* injection = [[NSMutableString alloc] init]; [injection appendFormat:@"EasyJS.invokeCallback("%@", %@", self.funcID, self.removeAfterExecute ? @"true" : @"false"]; if (params){ for (int i = 0, l = params.count; i |
回撥方法執行,將回撥方法執行引數解析封裝js函式字串,注意前兩個引數第一個表示js函式的唯一ID方便js端找到該函式物件,第二個表示第一次回撥完成是否移除該回撥執行的函式物件的bool值,然後webView主動執行,這樣就完成個整個的回撥過程。
例子:Demo回撥執行語句除錯
1 |
EasyJS.invokeCallback("__cb1462414605044", true, "blabla%3A%22bla"); |
6.存在問題
見如下程式碼我們分析實現會發現jsObj
全域性字典方法區分的key是方法名的拼接,且去處了連線符號:
,因此產生疑問這樣可能還是會出現同一個key對應不同的方法。
1 2 3 4 5 6 7 |
(function (){ var method = methods[i]; var jsMethod = method.replace(new RegExp(":", "g"), ""); jsObj[jsMethod] = function (){ return EasyJS.call(obj, method, Array.prototype.slice.call(arguments)); }; })(); |
鑑於以上的疑問我改了一下Demo工程,MyJSInterface
增加一個實現的介面
1 2 3 4 |
- (void) testWithTwoParamAndParam2: (NSString*) param { NSLog(@"testWithTwoParamAndParam2 invoked %@",param); } |
這樣就會與以下方法衝突
1 2 3 |
- (void) testWithTwoParam: (NSString*) param AndParam2: (NSString*) param2{ NSLog(@"test with param: %@ and param2: %@", param, param2); } |
Demo改成如下呼叫
1 |
MyJSTest.testWithTwoParamAndParam2("haha1", "haha2"); |
丟擲異常,原因就是js方法全域性字典的keytestWithTwoParamAndParam2
所對應的方法被下一個方法覆蓋。
1 |
*** WebKit discarded an uncaught exception in the webView:decidePolicyForNavigationAction:request:frame:decisionListener: delegate: -[NSInvocation setArgument:atIndex:]: index (3) out of bounds [-1, 2] |
解決:
- 1.可以儘量避免重名問題
- 2.也可以替換分隔符號”:”用其他特殊字元替換
本文結,本人還在不斷學習積累中,如果對文章有疑問或者錯誤的描述歡迎提出。
或者你有hybrid iOS一塊比較好的實現也歡迎分享大家一起學習,謝謝!!!