iOS 中的web開發(2)--- JavaScriptCore

JavaScriptCore-feature.png

JavaScriptCore其實也是一個很老的API了,之前都是在mac應用中使用,是純c的代碼,在iOS7中,用Objective-C進行了封裝。JavaScriptCore允許我們在不使用UIWebViewWKWebView的情況下去執行JavaScript代碼。而JavaScript作為web開發的一門腳本語言,在跨平臺的應用上,有著很重要的作用。許多跨平臺方案,比如JSPatch,React-Native都是基于JavaScriptCore作為iOS 端的實現方式。

JavaScriptCore

首先我們需要認識一下JavaScriptCore這個庫里有什么東西。其實這個頭文件只是導入了5個頭文件

#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"

對于這個五個類,簡單總結如下

  1. JSContext是JavaScript的運行上下文,是執行JavaScript代碼和注冊native方法接口的執行環境。
  2. JSValueJSContext執行后的返回結果,可以是任何JavaScript類型,JSValue提供很多的方法進行JavaScript和Objective-C類型的轉換。但是需要注意的是JSValue是一個強引用類型,JavaScript內部通過垃圾回收機制進行內存管理。JavaScript和Objective-C類型的轉換可以參考JSValue提供的方法。
  3. JSManagedValueJSValue的封裝,用它可以解決JavaScript和native代碼之間循環引用的問題。
  4. JSVirtualMachine管理JavaScript運行時和管理JavaScript暴露的native對象的內存。一般情況下是不需要直接和這個類交流的。但是如果你需要并行執行JavaScript代碼,JSContext就需要運行在不同的JSVirtualMachine之上。每一個JSVirtualMachine有著自己的
  5. JSExport是一個協議,通過實現它可以完成把一個native對象暴漏給JavaScript。

引用一張圖來說明上述類型的一些關系


javascriptcore-700x310.png

Objective-C 使用JavaScript

JavaScript沒有類的概念,可以通過原型繼承的方式實現繼承。Objective-C可以調用JavaScript的函數和JavaScript屬性。這些函數和屬性需要先以NSString的形式被加載到JSContext環境上去,執行成功后,如果有返回值的話,返回一個JSValue對象。一般情況下,JavaScript文件都會存在本地,打包進你的程序中去,當然也可以從網絡中獲取JavaScript文件,加載到context中去,這就是我們講的熱跟新。

函數和屬性

現有如下JavaScript代碼

//jscore.js
var jsGlobleVar = "JS Demo";
function min(a,b){
    return a-b;
};

那么這個min方法就可以在Objective-C中以類似key-value的形式被檢索到,這個value就是一個JSValue,然后調用JSValue實例的callWithArguments傳入Objective-C的類型的參數,在JavaScript環境中會進行相應的類型轉換

//JSCoreViewController.m
  NSString *jsCode = [self readFile]; // jscore.js
    [self.context evaluateScript:jsCode]; //將上述min函數加載到context環境中去 
    JSValue *min = [self.context[@"min"] callWithArguments:@[@2,@4]]; // self.context[@"min"] 對應的就是js中的 min函數或者min屬性
    JSValue *jsVar = self.context[@"jsGlobleVar"];
     NSLog(@"jsGlobleVar-----%@",[jsVar toString]);// "JS Demo"
    NSLog(@"min+++++%d",[min toInt32]); //-2

JavaScript 調用 Objective-C

JavaScript調用Objective-C的block

如下代碼,向context注冊了key 為multi的 JSValue對象

  self.context[@"multi"]  = ^(NSInteger a,NSInteger b){
        return a*b;
    };

相當在JavaScript中定義一個如下的函數

        function multi (a,b){
             return a*b;
        }

那么在context調用這個block的方式和Objective-C中調用JavaScript的形式是一致的即

  JSValue *multi  = [self.context[@"multi"] callWithArguments:@[@3,@4]];
    //    或者
    multi = [self.context evaluateScript:@"multi(10,2)"];

自定義類型和方法

除了使用JSContext下標方法暴露JavaScript對象以外,還可以使用JSExprot協議把Objective-C中的自定義類轉換為JSValue,并暴露給JavaScript對象,在JavaScript的環境中操作這個Objective-C對象。
首先需要定義個遵循JSExport的子協議。在子協議中規定了哪些屬性和方法可以在JavaScript環境中可用。由于JavaScript中函數參數沒有類型,所以所有的Objective-C方法都會以駝峰命名的方式被調用,比如-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;將以minuseBC(a,b,c)的方式被調用。也可以通過JSExportAs宏重自定義在JavaScript中被調用的方法名//JSExportAs(add, - (NSInteger)add:(NSInteger)a b:(NSInteger)b)。總結一下JSExport的主要功能有

  1. 將Objective-C的函數和屬性傳給JavaScript
  2. @property---> JavaScript的getter/setter
  3. Objective-C 實例方法 ---> JavaScript 函數Objective-C
  4. Objective-C類方法----> 在全局類對象上的JavaScript函數

Objective-C對象

先定義如下的Objective-C協議,并寫個類遵循這個協議(這里使用JSExportModel來定義)

@protocol JSModelProtocol <JSExport>
-(void)minuse:(NSInteger)a b:(NSInteger)b c:(NSInteger)c;
@property (assign,nonatomic)NSInteger sum;
@end

接下來就可以將遵循了這個協議的類JSExportModel的實例對象加到執行環境中去了,并調用在JavaScript中已經被加載到context中的useOCObject方法。

-(void)excuteOCCdoeInJS{
    JSExportModel *model = [JSExportModel new];
    self.context[@"model"] = model;
    [self.context[@"useOCObject"] callWithArguments:nil];
    NSLog(@"%ld",(long)model.intV);//14
}

useOCObject的JavaScript代碼如下

function useOCObject(){
    model.minuseBC(100,12,12);
    model.intV = 14;
};

這里調用了Objective-C的對象的方法,并對傳入的對象的屬性進行了修改。

傳入一個類對象

因為JavaScript沒有類的概念,所有傳入到JavaScript環境中的Objective-C類就變為了一個Constructor對象,如果你需要在JavaScript環境中生成一個對象,你需要使這個Objective-C類有個類方法來生成實例對象,即在之前的協議中,我們先定義一個類方法用來生成實例對象

//在協議中聲明,來暴露給JavaScript
+(instancetype)createWithIntV:(NSInteger)value;

然后在Objective-C中進行如下調用

-(void)excuteOCClassInJS{
    self.context[@"Model"] = [JSExportModel class];
   JSValue *returned = [self.context[@"useOCClass"] callWithArguments:nil];
   JSExportModel *m= [returned toObjectOfClass:[JSExportModel class]];
    NSLog(@"%ld",(long)m.intV); //12
  
}

JavaScript代碼如下所示

function useOCClass(){
    var m =  Model.createWithIntV(12);
    m.minuseBC(10,1,1); // 調用Objective-C方法
    return m
}

可以看到我們,上述Objective-C代碼打印輸出了12。

返回一個JavaScript函數

在JavaScript中函數也是變量,是一等公民,也可以被返回。在JavaScript文件中有個JavaScriptFunc函數,這個函數如下所示

function callback (){
    // 這里打印的東西可以在 safari瀏覽器上的開發選項中打開
console.log("method----");
};
function jsFunc(){
    Obj.jsValue = callback // 直接對變量賦值
    return callback;  //將function 以callback的形式返回
}

這個函數就是將callback函數返回給Objective-C對象。上述第一種是將
函數對象直接賦值給Objective-C對象,第二種直接返回函數對象。可以在Objective-C中用如下方式調用函數。

-(void)jsReturnBlock{
     self.obj.jsValue = [self.context[@"jsFunc"] callWithArguments:nil];
    [ self.obj.jsValue callWithArguments:nil];
    
    self.context[@"Obj"] = self.obj;
    [self.context[@"jsFunc"] callWithArguments:nil];
    [ self.obj.jsValue callWithArguments:nil];
}

內存管理

我們都知道Objective-C使用ARC進行內存管理,JavaScriptCore(virtualMechine)內部通過垃圾回收機制來進行內存管理,所有的引用都是強引用。JavaScriptCore內部則保證了大多數的內存管理都是自動進行的,不需要我們進行額外的內存管理。但是還是得注意兩種情況下的內存管理

  1. 在Objective-C對象中存儲JavaScript的值
  2. 將JavaScript區域(主要是函數)加到Objective-C對象上中去
self.jsValue = [JSValue new];
    self.context[@"block"] = ^(){
        JSValue *value = self.jsValue;
        NSLog(@"%@",value);
    };

這里和普通block對象造成循環引用的類似,self.context 引用self,而本身self保有context。這種情況下,其實編譯器也會告訴你這里產生了循環引用。 最好的方法就是將這個jsValue作為block的參數傳到JavaScript環境中去。

還有一種情況就是在block中需要使用JS中其他的對象或者函數,需要從當前的JSContext獲取,這時候就造成了循環引用,推薦的方式則是通過+[JSContext currentContext]來獲取當前的context,即

self.jsValue = [JSValue new];
    self.context[@"block"] = ^(){
     // 循環引用
     // context = self.context;
        JSContext *context = [JSContext currentContext];
        NSLog(@"%@",value);
    };

還有一種情況是你必須要用一個Objective-C對象去保存一個JavaScript的值或者函數,你必須使用JSManagedValue去弱引用JavaScript對象。JSManagedValue本身就是一個弱引用JavaScript對象的對象。-addManagedReference:withOwner:JSManagedValue對象加入到了垃圾回收的引用。這個函數的意思就是JavaScript的垃圾回收機制觀察引用Objective-C對象ower,如果存在,那么它就不回收這個對象,否則就回收這個對象。

線程

JSVirtualMachine保證了JavaScript代碼執行的線程安全,鎖都是JSVirtualMachine來控制。使用不同的JSVirtualMachine來實現串行或并發。

JavaScriptCore 和UIWebView

UIWebView中能夠調用JavaScript代碼,其本質上還是UIWebView將JavaScript代碼在其內部的JSContext上執行。可以在UIWebView加載完后調用,獲取到這個_context。

-(void)webViewDidFinishLoad:(UIWebView *)webView{
     _context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    [_context evaluateScript:@"showWebViewAlert()"];
    
}

網上有說documentView.webView.mainFrame.javaScriptContext是私有API,會有被拒的風險,需謹慎。

JavaScriptCore 和WKWebView

WKWebViewJavaScriptCore之間的通信,可以通過WKWebView的代理實現。在js代碼中 通過發送如下 消息
window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
WKWebView只需要在啟動的時候綁定了AppModel就可以接收到JS發過來的消息了。具體如下

WKUserContentController *contentVC = [WKUserContentController new];
    //然后就可以通過代理取到js發來的消息了
    [contentVC addScriptMessageHandler:self name:@"AppModel"];
    config.userContentController = contentVC;

這樣就可以WKScriptMessageHandler受到消息了

// MARK: - WKScriptMessageHandler
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    // 收到消息
     // window.webkit.messageHandlers.AppModel.postMessage({body: 'call js alert in js'});
    NSLog(@"%@",message.body);
}

demo

demo下載地址iOS 中使用JS的demo ,喜歡的就star一下吧。。哈哈

參考資料

Java?Script?Core
JavaScriptCore by Example
JavaScriptCore and iOS 7
JavaScriptCore Tutorial for iOS: Getting Started
Integrating JavaScript into Native Apps

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容