最近跟同事聯(lián)調(diào)一個(gè)iOS與JS交互想接口,折騰了整整一天,被迫重新研究了一下iOS與JS交互的原理,才發(fā)現(xiàn)原來項(xiàng)目中用的方案已經(jīng)是很老很老的方式了,
效率不高,學(xué)習(xí)成本倒不低,但總歸之前用了很久都沒出現(xiàn)過什么問題,所以還是簡單的總結(jié)下吧,然后就準(zhǔn)備把這套東西淘汰掉用新的方式了。
OC調(diào)用JS
OC調(diào)用JS的方法很簡單,就是UIWebView的stringByEvaluatingJavaScriptFromString方法,或者
WKWebView的- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;方法。
JS調(diào)用OC
其實(shí)現(xiàn)在項(xiàng)目中用的方案原理很簡單,就是攔截請(qǐng)求,然后獲取參數(shù)執(zhí)行OC的方法;
不過為了跟安卓統(tǒng)一,并且方便復(fù)用,做了一層封裝,給JS注入方法。
考慮到j(luò)s有很多的全局函數(shù),所以直接給js添加方法是不太合適的,可能會(huì)導(dǎo)致命名沖突,比較理想的做法是模塊化,就是給js注入個(gè)對(duì)象,給這個(gè)對(duì)象再添加方法。
具體來說就是:
客戶端這里有一個(gè)bridge類,里面有跟js端協(xié)商好的方法;
當(dāng)網(wǎng)頁加載結(jié)束后(webViewDidFinishLoading),iOS這端會(huì)執(zhí)行一個(gè)方法,取到bridge里面所有方法列表(用runtime方法獲?。?,
然后拼裝成對(duì)應(yīng)的js方法,用evaluateJavaScript方法注入給js,
js需要做的就是在需要的時(shí)候調(diào)用對(duì)應(yīng)方法即可。
注意,js端除了判斷方法存在不存在和調(diào)用方法,其他什么都沒做,對(duì)象和所有的方法都是客戶端注入的。
所以這個(gè)方案的關(guān)鍵是拼接注入給js的方法,
這里用了一個(gè)巧妙的方法,即利用js可以執(zhí)行字符串中的JavaScript代碼的eval()方法。
iOS端將bridge里獲取的方法列表拼接成字符串,跟js代碼的字符串拼在一起,拼成一段JavaScript字符串,
然后用webView的evaluateJavaScript方法調(diào)用就可以了
而注入給js的方法,就是讓js去加載一個(gè)指定格式的自定義的URL,然后iOS端攔截這個(gè)請(qǐng)求,判斷如果是自定義的URL,則去執(zhí)行自己的方法,
這里一般是提前一定好一個(gè)scheme,然后根據(jù)后面的字段來區(qū)別不同的方法,
這個(gè)方案js傳遞參數(shù)給OC也是很簡單,直接拼在URL后面即可。
理論上OC傳遞參數(shù)給js也是很簡單的,只需js端定義一個(gè)帶返回值的函數(shù),然后iOS端要拼接這個(gè)函數(shù)的調(diào)用的字符串,然后直接用webView的evaluateJavaScript方法調(diào)用它就行了;
但是這跟上面“所有方法都是讓js去load一個(gè)自定義格式的URL”的設(shè)計(jì)是沖突的,
因?yàn)樯厦娴脑O(shè)計(jì)是,所有方法都是iOS端注入的,注入的方法內(nèi)容是讓js去load一個(gè)自定義URL,而這個(gè)傳參的js方法不是去load一個(gè)自定義的URL,而是去取一個(gè)OC執(zhí)行的js的結(jié)果,然后再進(jìn)行其他操作;
如何解決這個(gè)問題呢?
我自己想到的方法是,在注入方法的時(shí)候,將注入的方法分為兩類,一類就是去load自定義URL的方法;另外一種是專門傳遞參數(shù)給js的方法,也就是直接執(zhí)行拼接好的js函數(shù)的方法。
我感覺這個(gè)方法是可行的,如果是一些簡單的傳值的方法的話,所不定還挺高效,但貌似有一個(gè)問題,執(zhí)行結(jié)果貌似是同步傳回給js的,如果OC端執(zhí)行的方法比較耗時(shí),那可能會(huì)卡住js端的進(jìn)程。
前輩們在項(xiàng)目中使用了個(gè)更高級(jí)的辦法,就是調(diào)用js方法時(shí),如果需要返回值的話,傳一個(gè)回調(diào)函數(shù)到OC。回調(diào)函數(shù)固定以一個(gè)json字符串作為輸入?yún)?shù)。
為什么說這個(gè)方法高級(jí)呢,一是統(tǒng)一了注入給js的方法格式,而是統(tǒng)一了js注入方法的返回值的格式,三是統(tǒng)一了用異步回調(diào)。
需要注意的是:OC最終調(diào)用的方法只有一個(gè)參數(shù),這個(gè)參數(shù)是一個(gè)js傳過來的參數(shù)數(shù)組,具體參數(shù)數(shù)組里面都是什么內(nèi)容,要在定義注入函數(shù)的時(shí)候就跟js端商量好!
(這里還有一個(gè)小細(xì)節(jié),如果沒有參數(shù),OC端的selector是沒有“:”的,如果有參數(shù),一定記得要拼接“:”,否則OC端會(huì)識(shí)別成不同的函數(shù)名)
回調(diào)函數(shù)還有一個(gè)需要注意的地方,因?yàn)镺C回調(diào)的是js函數(shù),所以js端要在某個(gè)地方有保存這個(gè)函數(shù),直接在調(diào)用注入方法時(shí)實(shí)現(xiàn)這個(gè)函數(shù)貌似是不行的,必須要建立其調(diào)用js注入函數(shù)與callback函數(shù)之間的對(duì)應(yīng)關(guān)系才行,否則oc調(diào)用js回調(diào)是不知道對(duì)應(yīng)的回調(diào)函數(shù)是誰的。。。。。這個(gè)比較繞
我又看了下js端同學(xué)些的代碼,js端是在傳入callback的同時(shí),將callback函數(shù)保存到一個(gè)數(shù)組里面,然后傳數(shù)組名稱和callback的index給OC,所以O(shè)C端實(shí)際調(diào)用的是,拿到數(shù)組里保存的函數(shù)然后傳參數(shù)調(diào)用。
也就是說,如果是需要OC傳遞參數(shù)給JS的方法,實(shí)際上不完全是OC這邊注入的,還是需要JS端同學(xué)配合下的。
這樣看來,跟直接讓JS端同學(xué)實(shí)現(xiàn)幾個(gè)專門的傳參數(shù)方法代價(jià)也差不多。。。
這個(gè)方案是UIWebView和WKWebView都可以使用的。
新的方式也就是iOS7之后的推出的JavaScriptCore,這個(gè)下一篇再寫。