Objective-C與JavaScript交互的那些事
來自: http://ios.jobbole.com/83945/
最近公司的運營瞎搞了個活動,其活動要服務端提供數據支持, web前端 在微信公眾賬號內作為主要的運營陣地,而 iOS 、 Android 要提供相應的入口及頁面進行配合。一個活動,動用了各個端的程序猿。而在這里面技術方面主要就是涉及到 web端 和服務端的交互, web前端 和 iOS 、 Android 的交互。本人作為一個 iOS 開發者,今天就聊聊 web 、 iOS 、 Android 三端的交互,其實在說明白一點就是方法的互相調用而已。這里主要講解 iOS 。 Android 會稍微提一下,僅作參考。
此篇文章的邏輯圖

圖0-0 此篇文章的邏輯圖
概述
iOS原生應用和web頁面的交互大致上有這幾種方法 iOS7之后的JavaScriptCore 、 攔截協議 、 第三方框架WebViewJavaScriptBridge 、 iOS8之后的WKWebView 在這里主要講解 JavaScriptCore 和 攔截協議 這兩種辦法。 WebViewJavaScriptBridge 是基于 攔截協議 進行的封裝。學習成本相對 JavaScriptCore 較高,使用也不如 JavaScriptCore 方便本文不做敘述。 WKWebView 是iOS8之后推出的,還沒有成為主流使用,所以本篇文章也不做詳細敘述。
Objective-C執行JavaScript代碼
相關方法
Objective-C
// UIWebView的方法 - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script; // JavaScriptCore中JSContext的方法 - (JSValue *)evaluateScript:(NSString *)script; - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL
// UIWebView的方法 - (nullableNSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script; // JavaScriptCore中JSContext的方法 - (JSValue *)evaluateScript:(NSString *)script; - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL
相關應用
用這些方法去執行大段的 JavaScript 代碼是沒什么必要的,但是有些小場景用起來還是比較順手和實用的,列舉兩個例子作為參考:
Objective-C
// 獲取當前頁面的title NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"]; // 獲取當前頁面的url NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];
// 獲取當前頁面的title NSString *title = [webviewstringByEvaluatingJavaScriptFromString:@"document.title"]; // 獲取當前頁面的url NSString *url = [webviewstringByEvaluatingJavaScriptFromString:@"document.location.href"];
JavaScriptCore
iOS7 之后蘋果推出了 JavaScriptCore 這個框架,從而讓web頁面和本地原生應用交互起來非常方便,而且使用此框架可以做到 Android 那邊和 iOS 相對統一, web前端 寫一套代碼就可以適配客戶端的兩個平臺,從而減少了web前端的工作量。
web前端
在三端交互中, web前端 要強勢一些,一切傳值、方法命名都按 web前端 開發人員來定義,讓另外兩端去做適配。在這里以調用攝像頭和分享為例來詳細講解,測試網頁代碼取名為 test.html ,其代碼內容如下:
test.html代碼內容
Objective-C
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> </head> <body> <div style="margin-top: 100px"> <h1>Objective-C和JavaScript交互的那些事</h1> <input type="button" value="CallCamera" onclick="Toyun.callCamera()"> </div> <div> <input type="button" value="Share" onclick="callShare()"> </div> <script> var callShare = function() { var shareInfo = JSON.stringify({"title": "標題", "desc": "內容", "shareUrl": "http://www.jianshu.com/p/f896d73c670a", "shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"}); Toyun.share(shareInfo); } var picCallback = function(photos) { alert(photos); } var shareCallback = function(){ alert('success'); } </script> </body> </html>
<!DOCTYPE html> <html> <headlang="en"> <metacharset="UTF-8"> </head> <body> <divstyle="margin-top: 100px"> <h1>Objective-C和JavaScript交互的那些事</h1> <inputtype="button" value="CallCamera" onclick="Toyun.callCamera()"> </div> <div> <inputtype="button" value="Share" onclick="callShare()"> </div> <script> var callShare = function() { var shareInfo = JSON.stringify({"title": "標題", "desc": "內容", "shareUrl": "http://www.jianshu.com/p/f896d73c670a", "shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"}); Toyun.share(shareInfo); } var picCallback = function(photos) { alert(photos); } var shareCallback = function(){ alert('success'); } </script> </body> </html>
test.html代碼解釋
可能有些同學對 web前端 的一些知識不太熟悉,稍微對這段代碼做下解釋,先說 Toyun 是 iOS 和 Android 這兩邊在本地要注入的一個對象【參考下面iOS的代碼更容易明白】,充當原生應用和web頁面之間的一個橋梁。頁面上定義了兩個按鈕名字分別為 CallCamera 和 Share 。點擊 CallCamera 會通過 Toyun 這個橋梁調用本地應用的方法 - (void)callCamera ,沒有傳參;而點擊 Share 會先調用本文件中的 JavaScript 方法 callShare 這里將要分享的內容格式轉成 JSON字符串 格式(這樣做是為了適配 Android , iOS 可以直接接受 JSON對象 )然后再通過 Toyun 這個橋梁去調用原生應用的 - (void)share:(NSString *)shareInfo 方法這個是有傳參的,參數為 shareInfo 。而下面的兩個方法為原生方法調用后的回調方法,其中 picCallback 為獲取圖片成功的回調方法,并且傳回拿到的圖片 photos ; shareCallback 為分享成功的回調方法。
iOS
iOS 這邊根據前端定義的方法名來寫代碼,但是有些時候 web前端 會讓我們定義,但是我們定義好之后他又要修改,這時候就會很煩啊。所以碰到三端交互的時候最好就是讓 web前端 去定義方法名, iOS 和 Android 根據 web前端 定義好的去寫代碼。 JavaScriptCore 中 web頁面 調用原生應用的方法可以用 Delegate 或 Block 兩種方法,此文以按 Delegate 講解。
JavaScriptCore中類及協議:
- JSContext:給 JavaScript 提供運行的上下文環境
- JSValue: JavaScript 和 Objective-C 數據和方法的橋梁
- JSManagedValue:管理數據和方法的類
- JSVirtualMachine:處理線程相關,使用較少
- JSExport:這是一個協議,如果采用協議的方法交互,自己定義的協議必須遵守此協議
ViewController中的代碼
Objective-C
#import "ViewController.h" #import <JavaScriptCore/JavaScriptCore.h> @protocol JSObjcDelegate <JSExport> - (void)callCamera; - (void)share:(NSString *)shareString; @end @interface ViewController () <UIWebViewDelegate, JSObjcDelegate> @property (nonatomic, strong) JSContext *jsContext; @property (weak, nonatomic) IBOutlet UIWebView *webView; @end @implementation ViewController #pragma mark - Life Circle - (void)viewDidLoad { [super viewDidLoad]; NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"]; [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:url]]; } #pragma mark - UIWebViewDelegate - (void)webViewDidFinishLoad:(UIWebView *)webView { self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; self.jsContext[@"Toyun"] = self; self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"異常信息:%@", exceptionValue); }; } #pragma mark - JSObjcDelegate - (void)callCamera { NSLog(@"callCamera"); // 獲取到照片之后在回調js的方法picCallback把圖片傳出去 JSValue *picCallback = self.jsContext[@"picCallback"]; [picCallback callWithArguments:@[@"photos"]]; } - (void)share:(NSString *)shareString { NSLog(@"share:%@", shareString); // 分享成功回調js的方法shareCallback JSValue *shareCallback = self.jsContext[@"shareCallback"]; [shareCallback callWithArguments:nil]; } @end
#import "ViewController.h" #import <JavaScriptCore/JavaScriptCore.h> @protocol JSObjcDelegate <JSExport> - (void)callCamera; - (void)share:(NSString *)shareString; @end @interface ViewController () <UIWebViewDelegate, JSObjcDelegate> @property (nonatomic, strong) JSContext *jsContext; @property (weak, nonatomic) IBOutlet UIWebView *webView; @end @implementation ViewController #pragma mark - Life Circle - (void)viewDidLoad { [super viewDidLoad]; NSURL *url = [[NSBundle mainBundle]URLForResource:@"test"withExtension:@"html"]; [self.webViewloadRequest:[[NSURLRequest alloc]initWithURL:url]]; } #pragma mark - UIWebViewDelegate - (void)webViewDidFinishLoad:(UIWebView *)webView { self.jsContext = [webViewvalueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; self.jsContext[@"Toyun"] = self; self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"異常信息:%@", exceptionValue); }; } #pragma mark - JSObjcDelegate - (void)callCamera { NSLog(@"callCamera"); // 獲取到照片之后在回調js的方法picCallback把圖片傳出去 JSValue *picCallback = self.jsContext[@"picCallback"]; [picCallbackcallWithArguments:@[@"photos"]]; } - (void)share:(NSString *)shareString { NSLog(@"share:%@", shareString); // 分享成功回調js的方法shareCallback JSValue *shareCallback = self.jsContext[@"shareCallback"]; [shareCallbackcallWithArguments:nil]; } @end
ViewController中的代碼解釋
自定義 JSObjcDelegate 協議,而且此協議必須遵守 JSExport 這個協議,自定義協議中的方法就是暴露給 web頁面 的方法。在 webView 加載完畢的時候獲取 JavaScript 運行的上下文環境,然后再注入橋梁對象名為 Toyun ,承載的對象為 self 即為此控制器,控制器遵守此自定義協議實現協議中對應的方法。在 JavaStript 調用完本地應用的方法做完相對應的事情之后,又回調了 JavaStript 中對應的方法,從而實現了 web頁面 和 本地應用 之間的通訊。
JavaScriptCore使用注意
JavaStript 調用本地方法是在 子線程 中執行的,這里要根據實際情況考慮線程之間的切換,而在回調 JavaScript 方法的時候最好是在剛開始調用此方法的線程中去執行那段 JavaStript 方法的代碼,我在實際運用中開始沒注意,就被坑慘了啊。什么,說的太繞,看下面的代碼解釋:
Objective-C
// 假設此方法是在子線程中執行的,線程名sub-thread - (void)callCamera { // 這句假設要在主線程中執行,線程名main-thread NSLog(@"callCamera"); // 下面這兩句代碼最好還是要在子線程sub-thread中執行啊 JSValue *picCallback = self.jsContext[@"picCallback"]; [picCallback callWithArguments:@[@"photos"]]; }
// 假設此方法是在子線程中執行的,線程名sub-thread - (void)callCamera { // 這句假設要在主線程中執行,線程名main-thread NSLog(@"callCamera"); // 下面這兩句代碼最好還是要在子線程sub-thread中執行啊 JSValue *picCallback = self.jsContext[@"picCallback"]; [picCallbackcallWithArguments:@[@"photos"]]; }
運行效果
運行效果如圖3-1所示

攔截協議
攔截協議這個適合一些比較簡單的一些情況,不需要引入什么框架,只需要 web前端 配合一下就好。但是在具體調用哪一個方法上,以及在傳值的時候可能會有些不方便,而且調用完后無法在回調 JavaScript 的方法。
web前端
test.html中的代碼
Objective-C
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> </head> <body> <div> <input type="button" value="CallCamera" onclick="callCamera()"> </div> <script> function callCamera() { window.location.href = 'toyun://callCamera'; } </script> </body> </html>
<!DOCTYPE html> <html> <headlang="en"> <metacharset="UTF-8"> </head> <body> <div> <inputtype="button" value="CallCamera" onclick="callCamera()"> </div> <script> function callCamera() { window.location.href = 'toyun://callCamera'; } </script> </body> </html>
test.html中的代碼解釋
這段代碼相比上面的那段測試代碼是很簡單的,同樣有一個按鈕,名字為 CallCamera 點擊之后調用自己的 callCamera 方法, window.location.href 這里是改變主窗口的指向從而馬上發出一個鏈接為 toyun://callCamera 請求,而想要傳給原生應用的參數也可已包含到此請求中,而在iOS方法中我們要攔截這個請求,根據請求內容去判斷 JavaStript 想要做的事情,從而實現 web頁面 和 本地應用 之間的交互。
iOS
iOS對應的代碼
Objective-C
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSString *url = request.URL.absoluteString; if ([url rangeOfString:@"toyun://"].location != NSNotFound) { // url的協議頭是toyun NSLog(@"callCamera"); return NO; } return YES; }
- (BOOL)webView:(UIWebView *)webViewshouldStartLoadWithRequest:(NSURLRequest *)requestnavigationType:(UIWebViewNavigationType)navigationType { NSString *url = request.URL.absoluteString; if ([urlrangeOfString:@"toyun://"].location != NSNotFound) { // url的協議頭是toyun NSLog(@"callCamera"); return NO; } return YES; }
iOS對應的代碼的解釋
在 webView 的代理方法中去攔截自定義的協議 Toyun:// 如果是此協議則據此判斷 JavaStript 想要做的事情,調用原生應用的方法,這些都是提前約定好的,同時阻止此鏈接的跳轉。
總結
隨著手機硬件的配置越來越強大和 HTML5 的興起,一個 App 完全可以由 web頁面 來寫。現在已經有部分應用這么干了,我是遇見過的,如 古詩文網 。盡管比較少但是 web頁面 和 本地應用 的交互不論是 iOS 還是 Android 都是會有遇到的。 iOS 我還是比較推薦 JavaScriptCore ,這樣三端可以相對統一起來,寫的時候都比較簡單。隨著時間的推移 iOS8 推出的 WKWebView 會逐漸成為主流,這個的功能更強大。 攔截協議 也只能說用到比較簡單的一些情況吧,復雜的情況處理相互之間參數的傳遞還是比較麻煩的,而且這個不能回調 JavaScript 的方法,確實喜歡攔截協議的同學可以研究 WebViewJavaScriptBridge 這個第三方庫。對于 Android 本人也就是略知皮毛而已,就不班門弄斧了,對于一些 Android開發者 來說,可以看地第一段的 test.html 這個頁面的寫法完全是可以適配 Android 的。