iOS下JS與OC互相調用(四)--JavaScriptCore

前面講完攔截URL的方式實現JS與OC互相調用,終於到JavaScriptCore了。它是從iOS7開始加入的,用 Objective-C 把 WebKit 的 JavaScript 引擎封裝了一下,提供了簡單快捷的方式與JavaScript交互。 關於JavaScriptCore的使用有兩篇很好的文章:javascript

NSHipster中文版的Java​Script​Corehtml

iOS7 新JavaScriptCore框架入門介紹java

看了上述兩篇文章,對JavaScriptCore應該已經基本瞭解了。我就簡要介紹一下,而後用代碼來實際操做了。先上最終實現的效果:ios

一、簡要介紹JavaScriptCore

JavaScriptCore是一個iOS 7 新添加的框架,使用前須要先導入JavaScriptCore.framework。 而後咱們在JavaScriptCore.h中能夠看到,該框架主要的類就只有五個:git

JavaScriptCore.h

1.1 JSVirtualMachine

JSVirtualMachine看名字直譯是JS 虛擬機,也就是說JavaScript是在一個虛擬的環境中執行,而JSVirtualMachine爲其執行提供底層資源。github

翻譯這段描述:一個JSVirtualMachine實例,表明一個獨立的JavaScript對象空間,併爲其執行提供資源。它經過加鎖虛擬機,保證JSVirtualMachine是線程安全的,若是要併發執行JavaScript,那咱們必須建立多個獨立的JSVirtualMachine實例,在不一樣的實例中執行JavaScriptweb

經過alloc/init就能夠建立一個新的JSVirtualMachine對象。可是咱們通常不用新建JSVirtualMachine對象,由於建立JSContext時,若是咱們不提供一個特性的JSVirtualMachine,內部會自動建立一個JSVirtualMachine對象。安全

1.2 JSContext

JSContext是爲JavaScript的執行提供運行環境,全部的JavaScript的執行都必須在JSContext環境中。JSContext也管理JSVirtualMachine中對象的生命週期。每個JSValue對象都要強引用關聯一個JSContext。當與某JSContext對象關聯的全部JSValue釋放後,JSContext也會被釋放。 建立一個JSContext對象的方式有:bash

// 1.這種方式須要傳入一個JSVirtualMachine對象,若是傳nil,會致使應用崩潰的。
JSVirtualMachine *JSVM = [[JSVirtualMachine alloc] init];
JSContext *JSCtx = [[JSContext alloc] initWithVirtualMachine:JSVM];

// 2.這種方式,內部會自動建立一個JSVirtualMachine對象,能夠經過JSCtx.virtualMachine
// 看其是否建立了一個JSVirtualMachine對象。
JSContext *JSCtx = [[JSContext alloc] init];

// 3. 經過webView的獲取JSContext。
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
複製代碼

上面推薦的兩篇文章以及網上介紹JavaScriptCore的文章可能是經過1和2這兩種方式建立JSContext,而後執行JavaScript,演示JavaScriptCore。我一直有疑問,若是不是HTML結合OC,纔會使用到JavaScript,那在一個虛擬的環境裏運行JS有什麼意義。 因此,後面我是用方式3來建立JSContext。併發

1.3 JSValue

JSValue都是經過JSContext返回或者建立的,並無構造方法。JSValue包含了每個JavaScript類型的值,經過JSValue能夠將Objective-C中的類型轉換爲JavaScript中的類型,也能夠將JavaScript中的類型轉換爲Objective-C中的類型。 上述兩篇文章中均有OC、JSValue、JavaScript的類型對應關係表。

1.4 JSManagedValue

JSManagedValue主要用途是解決JSValue對象在Objective-C 堆上的安全引用問題。把JSValue 保存進Objective-C 堆對象中是不正確的,這很容易引起循環引用,而致使JSContext不能釋放。 這個類主要是將JSValue對象轉換爲JSManagedValue的API,並且也不經常使用,就不作具體介紹了。之後遇到使用場景再補充。

1.5 JSExport

JSExport是一個協議類,可是該協議並無任何屬性和方法。 怎麼使用呢? 咱們能夠自定義一個協議類,繼承自JSExport。不管咱們在JSExport裏聲明的屬性,實例方法仍是類方法,繼承的協議都會自動的提供給任何 JavaScript 代碼。 So,咱們只須要在自定義的協議類中,添加上屬性和方法就能夠了。

二、代碼操做展現

由於該系列主要是JS與OC互調,因此主要介紹如何用JavaScriptCore實現JS與OC互調。

2.1 建立UIWebView,並加載本地HTML。

這步跟 文章(一)中的步驟一是同樣的。

    self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
    self.webView.delegate = self;
    NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
//    NSURL *htmlURL = [NSURL URLWithString:@"http://www.baidu.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:htmlURL];
   
    // 若是不想要webView 的回彈效果
    self.webView.scrollView.bounces = NO;
    // UIWebView 滾動的比較慢,這裏設置爲正常速度
    self.webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
    [self.webView loadRequest:request];
    [self.view addSubview:self.webView];
複製代碼

HTML的內容也大體同樣,不過JS的調用有些區別,更簡單了。

function shareClick() {
    share('測試分享的標題','測試分享的內容','url=http://www.baidu.com');
}

function shareResult(channel_id,share_channel,share_url) {
    var content = channel_id+","+share_channel+","+share_url;
    asyncAlert(content);
    document.getElementById("returnValue").value = content;
}

function locationClick() {
    getLocation();
}

function setLocation(location) {
    asyncAlert(location);
    document.getElementById("returnValue").value = location;
}
複製代碼

更詳細的能夠看demo中的HTML源碼,demo地址在文章末。

2.2 添加JS要調用的原生OC方法。

在HMTL加載成功的回調方法- (void)webViewDidFinishLoad:(UIWebView *)webView中添加要調用的原生OC方法。

#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    NSLog(@"webViewDidFinishLoad");
    
    [self addCustomActions];
}
複製代碼

將全部要添加的功能方法,集中到一個方法addCustomActions中,便於維護。

#pragma mark - private method
- (void)addCustomActions
{
    JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    
    [self addScanWithContext:context];
    
    [self addLocationWithContext:context];
    
    [self addSetBGColorWithContext:context];
    
    [self addShareWithContext:context];
    
    [self addPayActionWithContext:context];
    
    [self addShakeActionWithContext:context];
    
    [self addGoBackWithContext:context];
}
複製代碼

而後每個小功能獨立開來,這樣修改和解決Bug的時候可以快速定位到某個功能。

- (void)addShareWithContext:(JSContext *)context
{
    __weak typeof(self) weakSelf = self;
    context[@"share"] = ^() {
        NSArray *args = [JSContext currentArguments];
        
        if (args.count < 3) {
            return ;
        }
        
        NSString *title = [args[0] toString];
        NSString *content = [args[1] toString];
        NSString *url = [args[2] toString];
        // 在這裏執行分享的操做...
        
        // 將分享結果返回給js
        NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
        [[JSContext currentContext] evaluateScript:jsStr];
    };
}
複製代碼

注意:

  • 1.JS要調用的原生OC方法,能夠在viewDidLoad webView被建立後就添加好,但最好是在網址加載成功後再添加,以免沒法預料的亂入Bug。
  • 2.block 中的執行環境是在子線程中。奇怪的是居然能夠更新部分UI,例如給view設置背景色,調用webView執行js等,可是彈出原生alertView就會在控制檯報子線程操做UI的錯誤信息。
  • 3.避免循環引用,由於block 會持有外部變量,而JSContext也會強引用它全部的變量,所以在block中調用self時,要用__weak 轉一下。並且在block內不要使用外部的context 以及JSValue,都會致使循環引用。若是要使用context 可使用[JSContext currentContext]。固然咱們能夠將JSContext 和JSValue當作block的參數傳進去,這樣就可使用啦。
2.3 OC調用JS方法

OC調用JS方法就有多種方式了。首先介紹使用JavaScriptCore框架的方式。 ** 方式1 ** 使用JSContext的方法-evaluateScript,能夠實現OC調用JS方法。 下面是一個調用JS中payResult方法的示例代碼:

NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[[JSContext currentContext] evaluateScript:jsStr];
複製代碼

** 方式2 ** 使用JSValue的方法-callWithArguments,也能夠實現OC調用JS方法。 下面這個示例代碼依然是調用JS中的payResult:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    
[context[@"payResult"] callWithArguments:@[@"支付彈窗"]];
複製代碼

固然,若是是在執行原生OC方法以後,想要在OC執行完操做後,將結果回調給JS時,能夠這樣寫:

- (void)addPayActionWithContext:(JSContext *)context
{
    context[@"payAction"] = ^() {
        NSArray *args = [JSContext currentArguments];
        
        if (args.count < 4) {
            return ;
        }
        
        NSString *orderNo = [args[0] toString];
        NSString *channel = [args[1] toString];
        long long amount = [[args[2] toNumber] longLongValue];
        NSString *subject = [args[3] toString];
        
        // 支付操做
        NSLog(@"orderNo:%@---channel:%@---amount:%lld---subject:%@",orderNo,channel,amount,subject);
        // 將支付結果返回給js
        [[JSContext currentContext][@"payResult"] callWithArguments:@[@"支付成功"]];
    };
}
複製代碼
方式3

之前介紹過的,利用UIWebView的API。

NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];
[weakSelf.webView stringByEvaluatingJavaScriptFromString:jsStr];
複製代碼

三、補充介紹JavaScriptCore

好處:使用JavaScriptCore,JS調用Native方法時,參數的傳遞更方便,不用擔憂特殊符號的轉換問題。 很差的地方:只能使用在iOS 7以上。這點我相信如今基本沒有多少應用還兼容iOS 6了吧,我去年在作這個功能的時候,還要兼容iOS 6 😭 😭 。

先把JS與OC互調部分的介紹完了,這裏再補充一些關於JavaScriptCore的相關知識。 在OC中如何往JS環境中添加一個變量,便於後續在JS中使用呢?

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];
複製代碼

而用到實際的UIWebView上,能夠這樣:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];
複製代碼

當上面這兩行代碼執行完後,我點擊HTML中的按鈕

<input type="button" value="輸出arr" onclick="showArr()" />
function showArr(){
     asyncAlert(arr);
}
            
function asyncAlert(content) {
     setTimeout(function(){
               alert(content);
         },1);
}
複製代碼

直接輸出arr,結果是這樣的:

若是咱們在OC中想要取出arr,只須要這樣:

JSValue *value = context[@"arr"];
複製代碼

OC中的block能夠傳入到JavaScript中,這樣就建立了一個新的JS方法。咱們上面的JS調用OC方法,就是利用的這個實現的。

關於JSExport如何使用? JSExport 主要是用於將OC中定義的Model類等引入到JavaScript中,便於在JS中使用這種對象和對象的屬性、方法。

JSExport的大體使用流程是:

1.建立一個自定義協議XXXExport 繼承自JSExport

2.在自定義的XXXExport中添加JS裏須要調用的屬性和方法。

3.在自定義的Model類中實現XXXExport中的屬性的get/set方法以及定義的方法。

4.經過JSContext將Model類或者Model類的實例插入到JavaScript中。

固然,咱們也能夠給已經存在的類動態添加協議,來使其能夠供JS 使用。這些示例和示例代碼,在文章NSHipster中文版的Java​Script​CoreJavaScriptCore框架在iOS7中的對象交互和管理中有很詳細的介紹和使用展現。

WKWebView 與JavaScriptCore

關於WKWebView 與JavaScriptCore,因爲WKWebView 不支持經過以下的KVC的方式建立JSContext:

JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
複製代碼

那麼就不能在WKWebView中使用JavaScriptCore了。 並且,WKWebView中有OC 和JS交互的方式,更easy 、更簡潔,所以也用不着使用JavaScriptCore。 WKWebView中如何實現OC與JS交互能夠看前面這篇文章:iOS下JS與OC互相調用(三)--MessageHandler

UIWebView利用JavaScriptCore來實現交互的示例工程:JS_OC_JavaScriptCore

Have Fun!

相關文章
相關標籤/搜索