可能碰到的iOS筆試面試題(17)--WebView與JS交互

WebView與JS交互

iOS中調用HTML

 
 1. 加載網頁
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
    
 2. 刪除
    NSString *str1 = @"var word = document.getElementById('word');";
    NSString *str2 = @"word.remove();";
    
    [webView  stringByEvaluatingJavaScriptFromString:str1];
    [webView  stringByEvaluatingJavaScriptFromString:str2];

3. 更改
    NSString *str3 = @"var change = document.getElementsByClassName('change')[0];"
                       "change.innerHTML = '好你的哦!';";
    [webView stringByEvaluatingJavaScriptFromString:str3];
    
    
    
4.  插入
    NSString *str4 =@"var img = document.createElement('img');"
                     "img.src = 'img_01.jpg';"
                     "img.width = '160';"
                     "img.height = '80';"
                     "document.body.appendChild(img);";
    [webView stringByEvaluatingJavaScriptFromString:str4];
    
 5. 改變標題
    NSString *str1 = @"var h1 = document.getElementsByTagName('h1')[0];"
                      "h1.innerHTML='簡書啊啊啊啊';";
    [webView stringByEvaluatingJavaScriptFromString:str1];   
    
    
6. 刪除尾部
    NSString *str2 =@"document.getElementById('footer').remove();";
    [webView stringByEvaluatingJavaScriptFromString:str2];
    
7. 拿出所有的網頁內容
    NSString *str3 = @"document.body.outerHTML";
    NSString *html = [webView stringByEvaluatingJavaScriptFromString:str3];
    NSLog(@"%@", html);

在HTML中調用OC

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    NSString *str = request.URL.absoluteString;
    NSRange range = [str rangeOfString:@"ZJY://"];
    if (range.location != NSNotFound) {
        NSString *method = [str substringFromIndex:range.location + range.length];
        SEL sel = NSSelectorFromString(method);
        [self performSelector:sel];
    }
    return YES;
}

// 訪問相冊
- (void)getImage{
    UIImagePickerController *pickerImg = [[UIImagePickerController alloc]init];
    pickerImg.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    
    [self presentViewController:pickerImg animated:YES completion:nil];
}

JavaScriptCore 使用

  • JavaScriptCore是webkit的一個重要組成部分,主要是對JS進行解析和提供執行環境。iOS7后蘋果在iPhone平臺推出,極大的方便了我們對js的操作。我們可以脫離webview直接運行我們的js。iOS7以前我們對JS的操作只有webview里面一個函數stringByEvaluatingJavaScriptFromString,JS對OC的回調都是基于URL的攔截進行的操作。大家用得比較多的是WebViewJavascriptBridge和EasyJSWebView這兩個開源庫,很多混合都采用的這種方式。
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
  • JSContext:JS執行的環境,同時也通過JSVirtualMachine管理著所有對象的生命周期,每個JSValue都和JSContext相關聯并且強引用context。

  • JSValue:JS對象在JSVirtualMachine中的一個強引用,其實就是Hybird對象。我們對JS的操作都是通過它。并且每個JSValue都是強引用一個context。同時,OC和JS對象之間的轉換也是通過它,相應的類型轉換如下:

Objective-C type JavaScript type
nil undefined
NSNull null
NSString string
NSNumber number, boolean
NSDictionary Object object
NSArray Array object
NSDate Date object
NSBlock (1) Function object (1)
id (2) Wrapper object (2)
Class (3) Constructor object (3)
  • JSManagedValue:JS和OC對象的內存管理輔助對象。由于JS內存管理是垃圾回收,并且JS中的對象都是強引用,而OC是引用計數。如果雙方相互引用,勢必會造成循環引用,而導致內存泄露。我們可以用JSManagedValue保存JSValue來避免。

  • JSVirtualMachine:JS運行的虛擬機,有獨立的堆空間和垃圾回收機制。

  • JSExport:一個協議,如果JS對象想直接調用OC對象里面的方法和屬性,那么這個OC對象只要實現這個JSExport協議就可以了。

  • Objective-C -> JavaScript

    self.context = [[JSContext alloc] init];

    NSString *js = @"function add(a,b) {return a+b}";

    [self.context evaluateScript:js];

    JSValue *n = [self.context[@"add"] callWithArguments:@[@2, @3]];

    NSLog(@"---%@", @([n toInt32]));//---5
  • JavaScript -> Objective-C.JS調用OC有兩個方法:block和JSExport protocol。
  • block(JS function):
    self.context = [[JSContext alloc] init];

    self.context[@"add"] = ^(NSInteger a, NSInteger b) {
        NSLog(@"---%@", @(a + b));
    };

    [self.context evaluateScript:@"add(2,3)"];
    我們定義一個block,然后保存到context里面,其實就是轉換成了JS的function。然后我們直接執行這個function,調用的就是我們的block里面的內容了。
    
  • JSExport protocol:
//定義一個JSExport protocol
@protocol JSExportTest <JSExport>

- (NSInteger)add:(NSInteger)a b:(NSInteger)b;

@property (nonatomic, assign) NSInteger sum;

@end

//建一個對象去實現這個協議:

@interface JSProtocolObj : NSObject<JSExportTest>
@end

@implementation JSProtocolObj
@synthesize sum = _sum;
//實現協議方法
- (NSInteger)add:(NSInteger)a b:(NSInteger)b
{
    return a+b;
}
//重寫setter方法方便打印信息,
- (void)setSum:(NSInteger)sum
{
    NSLog(@"--%@", @(sum));
    _sum = sum;
}

@end

//在VC中進行測試
@interface ViewController () <JSExportTest>

@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //創建context
    self.context = [[JSContext alloc] init];
    //設置異常處理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
    //將obj添加到context中
    self.context[@"OCObj"] = self.obj;
    //JS里面調用Obj方法,并將結果賦值給Obj的sum屬性
    [self.context evaluateScript:@"OCObj.sum = OCObj.addB(2,3)"];

}

demo很簡單,還是定義了一個兩個數相加的方法,還有一個保存結果的變量。在JS中進行調用這個對象的方法,并將結果賦值sum。唯一要注意的是OC的函數命名和JS函數命名規則問題。協議中定義的是add: b:,但是JS里面方法名字是addB(a,b)。可以通過JSExportAs這個宏轉換成JS的函數名字。
  • 內存管理:現在來說說內存管理的注意點,OC使用的ARC,JS使用的是垃圾回收機制,并且所有的引用是都強引用,不過JS的循環引用,垃圾回收會幫它們打破。JavaScriptCore里面提供的API,正常情況下,OC和JS對象之間內存管理都無需我們去關心。不過還是有幾個注意點需要我們去留意下。
1、不要在block里面直接使用context,或者使用外部的JSValue對象。

//錯誤代碼:
self.context[@"block"] = ^(){
     JSValue *value = [JSValue valueWithObject:@"aaa" inContext:self.context];
};
這個代碼,不用自己看了,編譯器都會提示你的。這個block里面使用self,很容易就看出來了。

//一個比較隱蔽的
     JSValue *value = [JSValue valueWithObject:@"ssss" inContext:self.context];

    self.context[@"log"] = ^(){
        NSLog(@"%@",value);
    };
這個是block里面使用了外部的value,value對context和它管理的JS對象都是強引用。這個value被block所捕獲,這邊同樣也會內存泄露,context是銷毀不掉的。

//正確的做法,str對象是JS那邊傳遞過來。
self.context[@"log"] = ^(NSString *str){
        NSLog(@"%@",str);
    };
2、OC對象不要用屬性直接保存JSValue對象,因為這樣太容易循環引用了。

看個demo,把上面的示例改下:

//定義一個JSExport protocol
@protocol JSExportTest <JSExport>
//用來保存JS的對象
@property (nonatomic, strong) JSvalue *jsValue;

@end

//建一個對象去實現這個協議:

@interface JSProtocolObj : NSObject<JSExportTest>
@end

@implementation JSProtocolObj

@synthesize jsValue = _jsValue;

@end

//在VC中進行測試
@interface ViewController () <JSExportTest>

@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //創建context
    self.context = [[JSContext alloc] init];
    //設置異常處理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
   //加載JS代碼到context中
   [self.context evaluateScript:
   @"function callback (){};

    function setObj(obj) {
    this.obj = obj;
    obj.jsValue=callback;
}"];
   //調用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

上面的例子很簡單,調用JS方法,進行賦值,JS對象保留了傳進來的obj,最后,JS將自己的回調callback賦值給了obj,方便obj下次回調給JS;由于JS那邊保存了obj,而且obj這邊也保留了JS的回調。這樣就形成了循環引用。

怎么解決這個問題?我們只需要打破obj對JSValue對象的引用即可。當然,不是我們OC中的weak。而是之前說的內存管理輔助對象JSManagedValue。

JSManagedValue 本身就是我們需要的弱引用。用官方的話來說叫garbage collection weak reference。但是它幫助我們持有JSValue對象必須同時滿足一下兩個條件(不翻譯了,翻譯了怪怪的!):

The JSManagedValue's JavaScript value is reachable from JavaScript

The owner of the managed reference is reachable in Objective-C. Manually adding or removing the managed reference in the JSVirtualMachine determines reachability.

意思很簡單,JSManagedValue 幫助我們保存JSValue,那里面保存的JS對象必須在JS中存在,同時 JSManagedValue 的owner在OC中也存在。我們可以通過它提供的兩個方法``` + (JSManagedValue )managedValueWithValue:(JSValue )value;

(JSManagedValue )managedValueWithValue:(JSValue )value andOwner:(id)owner創建JSManagedValue對象。通過JSVirtualMachine的方法- (void)addManagedReference:(id)object withOwner:(id)owner來建立這個弱引用關系。通過- (void)removeManagedReference:(id)object withOwner:(id)owner``` 來手動移除他們之間的聯系。

把剛剛的代碼改下:

//定義一個JSExport protocol
@protocol JSExportTest <JSExport>
//用來保存JS的對象
@property (nonatomic, strong) JSValue *jsValue;

@end

//建一個對象去實現這個協議:

@interface JSProtocolObj : NSObject<JSExportTest>
//添加一個JSManagedValue用來保存JSValue
@property (nonatomic, strong) JSManagedValue *managedValue;

@end

@implementation JSProtocolObj

@synthesize jsValue = _jsValue;
//重寫setter方法
- (void)setJsValue:(JSValue *)jsValue
{
    _managedValue = [JSManagedValue managedValueWithValue:jsValue];

    [[[JSContext currentContext] virtualMachine] addManagedReference:_managedValue 
    withOwner:self];
}

@end

//在VC中進行測試
@interface ViewController () <JSExportTest>

@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //創建context
    self.context = [[JSContext alloc] init];
    //設置異常處理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
   //加載JS代碼到context中
   [self.context evaluateScript:
   @"function callback (){}; 

   function setObj(obj) {
   this.obj = obj;
   obj.jsValue=callback;
   }"];
   //調用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

注:以上代碼只是為了突出用 JSManagedValue來保存 JSValue,所以重寫了 setter 方法。實際不會寫這么搓的姿勢。。。應該根據回調方法傳進來參數,進行保存 JSValue。

3、不要在不同的 JSVirtualMachine 之間進行傳遞JS對象。

一個 JSVirtualMachine可以運行多個context,由于都是在同一個堆內存和同一個垃圾回收下,所以相互之間傳值是沒問題的。但是如果在不同的 JSVirtualMachine傳值,垃圾回收就不知道他們之間的關系了,可能會引起異常。

  • 線程:JavaScriptCore 線程是安全的,每個context運行的時候通過lock關聯的JSVirtualMachine。如果要進行并發操作,可以創建多個JSVirtualMachine實例進行操作。

  • 與UIWebView的操作

通過上面的demo,應該差不多了解OC如何和JS進行通信。下面我們看看如何對 UIWebView 進行操作,我們不再通過URL攔截,我們直接取 UIWebView 的 context,然后進行對JS操作。

在UIWebView的finish的回調中進行獲取

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

}

上面用了私有屬性,可能會被蘋果給拒了。這邊要注意的是每個頁面加載完都是一個新的context,但是都是同一個JSVirtualMachine。如果JS調用OC方法進行操作UI的時候,請注意線程是不是主線程。

文章如有問題,請留言,我將及時更正。

滿地打滾賣萌求贊,如果本文幫助到你,輕點下方的紅心,給作者君增加更新的動力。

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

推薦閱讀更多精彩內容

  • JavaScriptCore框架主要是用來實現iOS與H5的交互。由于現在混合編程越來越多,H5的相對講多,所以研...
    水靈芳蕥閱讀 1,431評論 1 8
  • 本文由我們團隊的 糾結倫 童鞋撰寫。 寫在前面 本篇文章是對我一次組內分享的整理,大部分圖片都是直接從keynot...
    知識小集閱讀 15,282評論 11 172
  • 寫在前面 本篇文章是對我一次組內分享的整理,大部分圖片都是直接從keynote上截圖下來的,本來有很多炫酷動效的,...
    等開會閱讀 14,504評論 6 69
  • 注:本文copy自http://www.lxweimin.com/p/ac534f508fb0,純屬當筆記使用。 概...
    BookKeeping閱讀 742評論 1 3
  • 繼續《大秦帝國》,秦國滅趙,完成歷史性的轉變。 國家強盛之起源 戰國時代,國家強盛首先在人,而人關鍵在三杰:明王,...
    自在牛閱讀 706評論 0 0