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

WebView與JS交互

iOS中調(diào)用HTML

 
 1. 加載網(wǎng)頁
    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. 拿出所有的網(wǎng)頁內(nèi)容
    NSString *str3 = @"document.body.outerHTML";
    NSString *html = [webView stringByEvaluatingJavaScriptFromString:str3];
    NSLog(@"%@", html);

在HTML中調(diào)用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進行解析和提供執(zhí)行環(huán)境。iOS7后蘋果在iPhone平臺推出,極大的方便了我們對js的操作。我們可以脫離webview直接運行我們的js。iOS7以前我們對JS的操作只有webview里面一個函數(shù)stringByEvaluatingJavaScriptFromString,JS對OC的回調(diào)都是基于URL的攔截進行的操作。大家用得比較多的是WebViewJavascriptBridge和EasyJSWebView這兩個開源庫,很多混合都采用的這種方式。
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
  • JSContext:JS執(zhí)行的環(huán)境,同時也通過JSVirtualMachine管理著所有對象的生命周期,每個JSValue都和JSContext相關(guān)聯(lián)并且強引用context。

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

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對象的內(nèi)存管理輔助對象。由于JS內(nèi)存管理是垃圾回收,并且JS中的對象都是強引用,而OC是引用計數(shù)。如果雙方相互引用,勢必會造成循環(huán)引用,而導致內(nèi)存泄露。我們可以用JSManagedValue保存JSValue來避免。

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

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

  • 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調(diào)用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里面,其實就是轉(zhuǎn)換成了JS的function。然后我們直接執(zhí)行這個function,調(diào)用的就是我們的block里面的內(nèi)容了。
    
  • JSExport protocol:
//定義一個JSExport protocol
@protocol JSExportTest <JSExport>

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

@property (nonatomic, assign) NSInteger sum;

@end

//建一個對象去實現(xiàn)這個協(xié)議:

@interface JSProtocolObj : NSObject<JSExportTest>
@end

@implementation JSProtocolObj
@synthesize sum = _sum;
//實現(xiàn)協(xié)議方法
- (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];
    //創(chuàng)建context
    self.context = [[JSContext alloc] init];
    //設(shè)置異常處理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
    //將obj添加到context中
    self.context[@"OCObj"] = self.obj;
    //JS里面調(diào)用Obj方法,并將結(jié)果賦值給Obj的sum屬性
    [self.context evaluateScript:@"OCObj.sum = OCObj.addB(2,3)"];

}

demo很簡單,還是定義了一個兩個數(shù)相加的方法,還有一個保存結(jié)果的變量。在JS中進行調(diào)用這個對象的方法,并將結(jié)果賦值sum。唯一要注意的是OC的函數(shù)命名和JS函數(shù)命名規(guī)則問題。協(xié)議中定義的是add: b:,但是JS里面方法名字是addB(a,b)。可以通過JSExportAs這個宏轉(zhuǎn)換成JS的函數(shù)名字。
  • 內(nèi)存管理:現(xiàn)在來說說內(nèi)存管理的注意點,OC使用的ARC,JS使用的是垃圾回收機制,并且所有的引用是都強引用,不過JS的循環(huán)引用,垃圾回收會幫它們打破。JavaScriptCore里面提供的API,正常情況下,OC和JS對象之間內(nèi)存管理都無需我們?nèi)リP(guān)心。不過還是有幾個注意點需要我們?nèi)チ粢庀隆?/li>
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所捕獲,這邊同樣也會內(nèi)存泄露,context是銷毀不掉的。

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

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

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

@end

//建一個對象去實現(xiàn)這個協(xié)議:

@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];
    //創(chuàng)建context
    self.context = [[JSContext alloc] init];
    //設(shè)置異常處理
    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;
}"];
   //調(diào)用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

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

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

JSManagedValue 本身就是我們需要的弱引用。用官方的話來說叫g(shù)arbage 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創(chuàng)建JSManagedValue對象。通過JSVirtualMachine的方法- (void)addManagedReference:(id)object withOwner:(id)owner來建立這個弱引用關(guān)系。通過- (void)removeManagedReference:(id)object withOwner:(id)owner``` 來手動移除他們之間的聯(lián)系。

把剛剛的代碼改下:

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

@end

//建一個對象去實現(xiàn)這個協(xié)議:

@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];
    //創(chuàng)建context
    self.context = [[JSContext alloc] init];
    //設(shè)置異常處理
    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;
   }"];
   //調(diào)用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

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

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

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

  • 線程:JavaScriptCore 線程是安全的,每個context運行的時候通過lock關(guān)聯(lián)的JSVirtualMachine。如果要進行并發(fā)操作,可以創(chuàng)建多個JSVirtualMachine實例進行操作。

  • 與UIWebView的操作

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

在UIWebView的finish的回調(diào)中進行獲取

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

}

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容