JavaScriptCore和Objective-C
144 作者 BobooO
2016.05.09 20:20 字?jǐn)?shù) 2140 閱讀 1700評論 8喜歡 54
在iOS開發(fā)中,因?yàn)镠5頁面的一些先天優(yōu)勢拆檬,原生界面里面摻雜著H5頁面是一種很常見的方案沟优。公司應(yīng)用最近因?yàn)闃I(yè)務(wù)需要一下子接入了大量H5界面慨菱,另外還要求:原生界面使用的是友盟統(tǒng)計分析,為了統(tǒng)計數(shù)據(jù)能在平臺連續(xù)、集中的展示出來,希望H5頁面的統(tǒng)計事件和原生界面的統(tǒng)計事件都上報到同一個后臺。為了滿足這個要求葵陵,就需要H5頁面使用友盟統(tǒng)計的iOS SDK來上報用戶事件,也就是說瞻佛,H5頁面需要與原生應(yīng)用進(jìn)行交互脱篙。
本來想從頭到尾把了解的方方面面都寫一下,但是后來在網(wǎng)上發(fā)現(xiàn)有很多優(yōu)秀的博客伤柄,所以就沒必要了绊困,這里簡單做一個歸納。
但是這些博客也存在一個共同的問題适刀,就是幾乎對交互過程中存在的問題和限制鮮有描述秤朗,所以我想本文略有價值的地方在于第二部分。
一笔喉、JavaScript和Objective-C的交互
交互實(shí)際上就是方法的互相調(diào)用取视,所以分兩部分。
(一)常挚、JS調(diào)用OC代碼
1作谭、攔截協(xié)議
JS調(diào)用OC代碼可以通過攔截NSRequest請求來調(diào)用原生方法進(jìn)行交互。
UIWebView的代理方法
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
UIWebView每次加載請求內(nèi)容之前待侵,都會調(diào)用這個方法丢早,該方法返回YES/NO來決定UIWebView是否加載request請求姨裸。所以我們可以通過URL的協(xié)議頭甚至URL字符串來區(qū)別正常的URL請求和本地方法的調(diào)用請求秧倾。JS傳遞給OC的參數(shù)可以通過URL帶過來,如果參數(shù)內(nèi)容過長可以通過post請求來傳遞傀缩,本地在攔截request后那先,可以將HTTPBody中的請求內(nèi)容解析出來。
iOS6及以前赡艰,攔截協(xié)議是JS調(diào)用OC方法唯一的出路售淡,即使出現(xiàn)了一些第三方框架(比如WebViewJavaScriptBridge),也是基于攔截協(xié)議進(jìn)行的封裝慷垮。
iOS7及之后揖闸,攔截協(xié)議的方法仍然可用,但是蘋果給我提供了更友好料身、完善的方案汤纸。
2、使用JavaScriptCore
JavaScriptSore是蘋果在iOS7之后提供的一套框架芹血,它讓JS與OC的交互更加簡單方便贮泞。
要使用JavaScriptCore首先我們需要引入它的頭文件#import <JavaScriptCore/JavaScriptCore.h>
重要對象:
import "JSContext.h"
import "JSValue.h"
import "JSManagedValue.h"
import "JSVirtualMachine.h"
import "JSExport.h"
JSContext是JavaScript的運(yùn)行環(huán)境楞慈,他主要作用是執(zhí)行JS代碼和注冊O(shè)C方法接口,相當(dāng)于HTML中< JavaScript ></JavaScript >之間的內(nèi)容啃擦。
JSValue是JSContext的返回結(jié)果囊蓝,他對數(shù)據(jù)類型進(jìn)行了封裝,并且為JS和OC的數(shù)據(jù)類型之間的轉(zhuǎn)換提供了方法令蛉。
JSManagedValue是JSValue的封裝聚霜,用它可以解決JS和原生代碼之間循環(huán)引用的問題。
JSVirtualMachine 管理JS運(yùn)行時和管理JS暴露的OC對象的內(nèi)存言询。
JSExport是一個協(xié)議俯萎,通過實(shí)現(xiàn)它可以把一個OC對象暴漏給JS,這樣JS就可以調(diào)用這個對象暴露的方法运杭。
發(fā)現(xiàn)一個寫得很好的博客夫啊,做一次大自然的搬運(yùn)工,更詳細(xì)的內(nèi)容請參考 [iOS JavaScriptCore使用]
(二)辆憔、OC調(diào)用JS代碼
1撇眯、使用UIWebView的方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script
2、 使用JavaScriptCore中JSContext的方法
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL
具體使用可參考 [iOS JavaScriptCore使用]
二虱咧、使用JavaScriptCore遇到的坑
1熊榛、內(nèi)存泄漏問題
當(dāng)使用JSExport協(xié)議的方式來實(shí)現(xiàn)交互時,我們可能會在我們的交互對象中聲明了一個JSContext屬性用來保存JS上下文腕巡,代碼可能通常這樣
//聲明屬性
@property (nonatomic,strong) JSContext * context;
//使用
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//或
self.context = [[JSContext alloc] init];
在注入JS的時候當(dāng)前JSContext上下文引用了當(dāng)前交互對象self玄坦,從而造成循環(huán)引用。
解決方法
使用Block進(jìn)行绘沉,不使用JSExport協(xié)議煎楣。
在交互對象與JSContext之間加一層代理。(處理過NSTimer循環(huán)引用問題的同學(xué)應(yīng)該熟悉這個方案)
2车伞、UIWebView加載第一個頁面JS調(diào)用本地方法正常择懂,但是頁面發(fā)生了跳轉(zhuǎn)后,JS調(diào)用本地方法就失效了
我們在代碼中注入JS代碼可能像這樣
//在- (void)viewDidLoad中注入
-
(void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];//1另玖、使用block注入
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"stat"] = ^(NSString *event){};
//2困曙、使用JSExport協(xié)議的方式注入一個對象
Myobj *obj = [[Myobj alloc] init];
self.jsContext[@"obj"] = obj;
}
或者這樣
//在- (void)webViewDidFinishLoad:(UIWebView *)webView中注入
-
(void)webViewDidFinishLoad:(UIWebView *)webView
{
//1、使用block注入
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"stat"] = ^(NSString *event){};
//2谦去、使用JSExport協(xié)議的方式注入一個對象
Myobj *obj = [[Myobj alloc] init];
self.jsContext[@"obj"] = obj;
這里我們要討論的是注入時機(jī)的問題慷丽。
當(dāng)我們在- (void)viewDidLoad中注入JS代碼之后,如果頁面發(fā)生了重定向鳄哭,此時web頁面的JS已經(jīng)發(fā)生了變化要糊,而- (void)viewDidLoad方法只會執(zhí)行一次,所以不再是之前我們注入過的那些JS了窃诉,此時再調(diào)用本地方法自然就失效了杨耙。
如果我們在- (void)webViewDidFinishLoad:(UIWebView *)webView方法中注入JS赤套,看起來貌似可以解決重定向之后調(diào)用失效的問題,因?yàn)閣ebView每次加載完成后都會回調(diào)- (void)webViewDidFinishLoad:(UIWebView *)webView珊膜,也就是說每次重定向之后容握,只要頁面加載完成,JS代碼就會重新被注入车柠。如果JS調(diào)用OC方法的時機(jī)是在頁面加載完成之后剔氏,比如點(diǎn)擊web界面上的按鈕或者由用戶手動觸發(fā)一個事件調(diào)用OC代碼,這種情況一定是web頁面加載完成之后才會發(fā)生的竹祷,而此時我們已經(jīng)重新注入了JS谈跛,這樣一點(diǎn)問題都沒有。但是塑陵,如果JS調(diào)用OC方法的時機(jī)剛好發(fā)生在頁面加載過程中呢尘分?比如web界面加載過程中自動執(zhí)行一些操作需要調(diào)用OC代碼师幕,而此時- (void)webViewDidFinishLoad:(UIWebView *)webView還沒有回調(diào)植酥,所以我們的JS代碼并沒有重新注入俭识,這里仍然會造成失效的問題。至于解決方案兼都,可以看這里 Why use JavaScriptCore in iOS7 if it can't access a UIWebView's runtime?嫂沉,貌似使用了私有API,有被拒的風(fēng)險啊~!
我們的應(yīng)用在統(tǒng)計H5頁面路徑的時候就是屬于需要JS自動調(diào)用OC方法的情況扮碧,當(dāng)用戶進(jìn)入頁面后需要讓JS調(diào)用OC方法上報一個統(tǒng)計事件趟章,上報這個事件時,僅僅是表示用戶進(jìn)入了這個界面慎王,并不跟用戶產(chǎn)生其他任何交互蚓土,所以明顯不能通過點(diǎn)擊一個按鈕來觸發(fā)。為了避開被拒的風(fēng)險柬祠,我是這樣做的
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"stat"] = ^(NSString *event){
[MobClick event:event];
DDLogInfo(@"UMAnalytics...%@",event);
};
//頁面加載完成后北戏,手動觸發(fā)頁面跟蹤的統(tǒng)計事件
[context evaluateScript:[NSString stringWithFormat:@"ios.start()"]];
};
我將注入時機(jī)放在了- (void)webViewDidFinishLoad:(UIWebView *)webView中负芋,并與前端約定好上報H5頁面路徑的統(tǒng)計事件不再讓JS主動調(diào)用OC方法漫蛔,而是改為由我在頁面加載完成后被動觸發(fā),見上面最后一行代碼旧蛾。之所以這樣莽龟,一是避免web頁面重定向?qū)е路椒ㄊУ膯栴},二是頁面路徑的統(tǒng)計事件本來就應(yīng)該在界面顯示完成后再上報锨天,三是只需要知道狀態(tài)毯盈,不需要與用戶交互。這里我在本地觸發(fā)JS調(diào)用之后病袄,最終JS還是會調(diào)用由我注入的stat()方法搂赋,雖然饒了一個彎赘阀,但是H5頁面統(tǒng)計事件的埋點(diǎn)及其他邏輯就不再在OC中實(shí)現(xiàn)了,而是由H5自己去處理脑奠,做到讓H5像原生界面一樣上報統(tǒng)計事件基公。
iOS8引入了WKWebView代替了UIKit中的UIWebView,至于WKWebView與JavaScript的交互宋欺,玩法有比較大的變化轰豆,本文就先這樣。