iOS下JS與OC互相呼叫(四)--JavaScriptCore

哈雷哈雷_Wong發表於2018-02-27

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

NSHipster中文版的Java​Script​Core

iOS7 新JavaScriptCore框架入門介紹

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

iOS下JS與OC互相呼叫(四)--JavaScriptCore

1、簡要介紹JavaScriptCore

JavaScriptCore是一個iOS 7 新新增的框架,使用前需要先匯入JavaScriptCore.framework。 然後我們在JavaScriptCore.h中可以看到,該框架主要的類就只有五個:

JavaScriptCore.h

1.1 JSVirtualMachine

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

iOS下JS與OC互相呼叫(四)--JavaScriptCore

翻譯這段描述:一個JSVirtualMachine例項,代表一個獨立的JavaScript物件空間,併為其執行提供資源。它通過加鎖虛擬機器,保證JSVirtualMachine是執行緒安全的,如果要併發執行JavaScript,那我們必須建立多個獨立的JSVirtualMachine例項,在不同的例項中執行JavaScript

通過alloc/init就可以建立一個新的JSVirtualMachine物件。但是我們一般不用新建JSVirtualMachine物件,因為建立JSContext時,如果我們不提供一個特性的JSVirtualMachine,內部會自動建立一個JSVirtualMachine物件。

1.2 JSContext

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

// 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的型別對應關係表。

iOS下JS與OC互相呼叫(四)--JavaScriptCore

1.4 JSManagedValue

JSManagedValue主要用途是解決JSValue物件在Objective-C 堆上的安全引用問題。把JSValue 儲存進Objective-C 堆物件中是不正確的,這很容易引發迴圈引用,而導致JSContext不能釋放。 這個類主要是將JSValue物件轉換為JSManagedValue的API,而且也不常用,就不做具體介紹了。以後遇到使用場景再補充。

1.5 JSExport

JSExport是一個協議類,但是該協議並沒有任何屬性和方法。 怎麼使用呢? 我們可以自定義一個協議類,繼承自JSExport。無論我們在JSExport裡宣告的屬性,例項方法還是類方法,繼承的協議都會自動的提供給任何 JavaScript 程式碼。 So,我們只需要在自定義的協議類中,新增上屬性和方法就可以了。

2、程式碼操作展示

因為該系列主要是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];
複製程式碼

3、補充介紹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,結果是這樣的:

iOS下JS與OC互相呼叫(四)--JavaScriptCore

如果我們在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!

相關文章