iOS原生OC與JS互調(diào)
本文demo:git地址
? ? ? ? 由于前段時間剛剛完成一個比較復(fù)雜的本地與原生的交互應(yīng)用,需求要做一個類似有道云筆記或簡書這樣的文本編輯器牺弄,在本地網(wǎng)頁編輯各種復(fù)雜的文章然后保存到后臺,需要隨時從后臺提取展現(xiàn)。現(xiàn)對這種混合應(yīng)用如何實現(xiàn)缀辩,以及開發(fā)過程中遇到的坑進(jìn)行一個總結(jié)。基于JavaScriptCore封裝了一個新的sdk雌澄,文章開頭給出了解決oc與js混合調(diào)用的demo斋泄,如果你覺得有所幫助,請給個星星镐牺。如果覺得代碼有問題或者有疑惑地方請在評論區(qū)留言炫掐,我會第一時間回復(fù)。
一睬涧、理論基礎(chǔ)
1. iOS跟JavaScript中的對象募胃、方法
1.1? ? ?對象:OC我們已經(jīng)非常熟悉了,這里稍微啰嗦下畦浓,OC是一門面向?qū)ο蟮膭討B(tài)語言痹束,基于c語言增加一層面向?qū)ο蟮恼Z法,讓oc能滿足所有面向?qū)ο蟮乃刑攸c(封裝(成員變量)讶请、繼承和多態(tài))祷嘶。oc中除了對象外還有一些基本數(shù)據(jù)類型,通過一些轉(zhuǎn)換方法也可以將基本數(shù)據(jù)類型轉(zhuǎn)換成對象類型夺溢,例如:NSNumber *number = [NSNumber numberWithInt:1]论巍。
JavaScript :JS中事物都是對象:字符串、數(shù)值风响、數(shù)組嘉汰、函數(shù)等等,JSt的對象是一種無序的集合數(shù)據(jù)類型状勤,它由若干鍵值對組成鞋怀。詳細(xì)可參考:JS對象
1.2? ? ?方法:oc中的方法分為類方法跟對象方法,方法本身并不屬于對象持搜,而且無法當(dāng)作參數(shù)傳遞密似,但是oc中可以用block當(dāng)參數(shù)來傳遞代碼塊,實現(xiàn)回調(diào)葫盼。相對于oc辛友,js方便很多,js中方法直接可以當(dāng)作參數(shù)進(jìn)行傳遞實現(xiàn)代碼塊的傳遞剪返。
2.本地跟JS交互手段
2.1? ?iOS7前:OC調(diào)用JS只能通過函數(shù)stringByEvaluatingJavaScriptFromString废累,往JS里面注入一段代碼,這里只能注入字符串脱盲,一般我們會把調(diào)用的方法寫在里面邑滨,例如:
NSString *jsString = [[NSString alloc]initWithString:@"initStyle()"];[(UIWebView*)self.realWebView stringByEvaluatingJavaScriptFromString:jsString];
JS又是如何實現(xiàn)調(diào)用OC方法呢?其實iOS SDK 并沒有原生提供 js 調(diào)用 native 代碼的 API钱反,但是 UIWebView 的一個 delegate 方法使我們可以做到讓 js 需要調(diào)用時掖看,通知 native匣距。
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request? navigationType:(UIWebViewNavigationType)navigationType
這個方法可以攔截網(wǎng)頁端發(fā)出的網(wǎng)絡(luò)請求。具體做法就是讓?js 通知 native 的方法是讓 js 發(fā)起一次特殊的網(wǎng)絡(luò)請求哎壳,PhoneGap 之前是使用加載一個隱藏的 iframe 來實現(xiàn)的毅待,通過將 iframe 的 src 指定為一個特殊的 URL,實現(xiàn)在 delegate 方法中截獲這次請求归榕。通過分析截獲的URL我們可以知道JS想干些什么基于URL的攔截進(jìn)行的操作尸红。大家現(xiàn)在用得比較多的是WebViewJavascriptBridge和EasyJSWebView這兩個開源庫,很多混合都采用的這種方式刹泄。這種方式調(diào)用OC方法外里,有篇文章有過介紹:關(guān)于UIWebView的總結(jié)
2.1? ?iOS7后:蘋果開放了一個新的框架:JavaScriptCore框架
關(guān)于這類的文章網(wǎng)上寫的很多,也很不錯特石,這里就不重復(fù)介紹盅蝗。具體可以參考:iOS7新JavaScriptCore框架介紹、JavaScriptCore使用姆蘸。這里就不過多講述了墩莫。本文主要目的是介紹基于JavaScriptCore封裝新的第三方開源庫:XHWebViewBridge
二、開源庫XHWebViewBridge使用及介紹
1.OC調(diào)用JS
????1.1)實現(xiàn)方法
? ? ? ? ? ?框架初始化針對網(wǎng)頁類型進(jìn)行處理逞敷,通過初始化參數(shù)usingUIWebView:來確認(rèn)初始化的網(wǎng)頁初始化的是屬于WKWebView還是UIWebView贼穆。不管是哪種類型框架對js調(diào)用只提供一個方法:
- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void(^)(id,NSError*))completionHandler;
javaScriptString:需要注入的js代碼? ? ?completionHandler:注入完成的回調(diào)
????1.2 ) 使用場景----oc觸發(fā)狀態(tài)js調(diào)用方法作出響應(yīng)
????????????一般來講我們會有這些場景:1、頁面剛加載完成后傳一些參數(shù)到j(luò)s兰粉,網(wǎng)頁拿到參數(shù)可以自己去請求數(shù)據(jù)或者做其他展示幫組js完成自己的初始化。2顶瞳、點擊本地的按鈕需要網(wǎng)頁作出相應(yīng)的反應(yīng)即調(diào)用js的方法玖姑。3、本地網(wǎng)絡(luò)請求成功后慨菱,得到一些參數(shù)數(shù)據(jù)需要給網(wǎng)頁讓網(wǎng)頁能夠作出相應(yīng)的回應(yīng)焰络。
1.3 )evaluateJavaScript方法解析
????????????一般在方法里面注入一段js為一個js方法的調(diào)用(因為方法可以帶參數(shù),這樣就能把oc的一下數(shù)據(jù)傳遞到j(luò)s里面)符喝。這里我們可能需要轉(zhuǎn)變一下闪彼,一般我們oc調(diào)用某個方法必須是直接由一個類或者一個對象去調(diào)用相應(yīng)的類方法或者對象方法,但js不僅可以這樣還可以像c語言里面的內(nèi)聯(lián)函數(shù)一樣直接調(diào)用(前提是這個js方法是全局方法)例如案例二直接調(diào)用js的“ocCalljsfun1”方法协饲,
? ? ? ? ? ? 很多時候由于頁面剛加載完成很多對象都還沒有初始化需要oc傳遞過去的參數(shù)完成初始化畏腕,所以會讓JS把傳遞初始化參數(shù)的方法寫成全局的,這樣就可以像案例二所示直接調(diào)用茉稠,然后帶上oc需要給js的參數(shù)描馅。
1.4 )? 傳遞參數(shù)注意事項?
? ? ? ? ? ? a、要注意雙引號沖突而线,可以用單引號(JS中單引號跟雙引號效果是一樣的)@"initStyle('done')"或者對雙引號轉(zhuǎn)義一下@"initStyle(\"done\")"铭污。
? ? ? ? ? ? b恋日、多個參數(shù): 用逗號隔開例如:evaluateJavaScript:@"ocCalljsfun2('OC調(diào)用網(wǎng)頁打印','傳遞一個參數(shù)')"
? ? ? ? ? ? c、傳遞一個或者多個字典(字典在js里面也是對象):由于evaluateJavaScript只能注入字符串嘹狞,所以我們需要辦字典轉(zhuǎn)變成字符串岂膳,最好的方法就是序列化,將字典專成json串然后js拿到j(luò)s串后對json進(jìn)行解析就能拿到準(zhǔn)確的數(shù)據(jù)磅网√附兀可能是由于解析的原因字典轉(zhuǎn)的json傳遞到j(luò)s解析不錯來,所以統(tǒng)一處理了下知市,不管是一個字典還是多個字典都添加進(jìn)NSMutableArray里面傻盟,然后對數(shù)組進(jìn)行轉(zhuǎn)json這樣處理后的的json是沒問題的(已驗證過)
? ? ? ? ?array轉(zhuǎn)json字符串:
//數(shù)組轉(zhuǎn)json
+ (NSString*)arrayToJSONString:(NSMutableArray*)array
{
? ? NSError*error =nil;
? ? NSData *jsonData = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:&error];
? ? NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
? ? returnjsonString;
}
轉(zhuǎn)換完成后json會出現(xiàn)與html沖突的一些標(biāo)簽或特殊字符,所以我們對轉(zhuǎn)換完成后的json需要再次處理嫂丙,調(diào)用下面方法
+ (NSString*)removeQuotesFromHTML:(NSString*)html {
? ? html = [htmlstringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
? ? html = [htmlstringByReplacingOccurrencesOfString:@"“" withString:@"""];
? ? html = [htmlstringByReplacingOccurrencesOfString:@"”" withString:@"""];
? ? html = [htmlstringByReplacingOccurrencesOfString:@"\r"? withString:@"\\r"];
? ? html = [htmlstringByReplacingOccurrencesOfString:@"\n"? withString:@"\\n"];
? ? returnhtml;
}
這樣處理后的json就可以直接傳到j(luò)s里面而且能保證數(shù)據(jù)的完整瞻鹏,js拿到后只需解析json就能得到一個數(shù)組對象,數(shù)組對象里面裝一個或者多個字典對象诡蜓。
? ? ? ? 至此oc調(diào)用js就沒有什么問題了起趾,不管是什么參數(shù)都能處理。
2.JS調(diào)用OC
? ? ? ? 2.1)? ? 實現(xiàn)方法?
? ? ? ? ? ? ? ? ? ? 由于iOS7以前的攔截URL方法過于麻煩隅肥,所以框架采用的是JavaScriptCore框架來實現(xiàn)js調(diào)用oc竿奏。
? ? ? ? ? ? ? ? ? ? 新建一個繼承XHFunctionModule管理類,假設(shè)取名XHWebViewBridgeManager腥放,這個新的類需要有一個繼承自<JSExport>的代理泛啸,然后你可以寫自己的代理方法(方法需要在.m文件里面實現(xiàn)),這個代理里面的方法將會自動變成你注入對象的對象方法秃症,有同學(xué)會問那我從哪里注入對象呢候址?新建的類必須實現(xiàn)+(NSString*)moduleName這個類方法,框架會把返回的對象注入到網(wǎng)頁种柑。例如:
+(NSString*)moduleName{
? ? return @"XHWebViewBridgeManager";
}
這樣你就往網(wǎng)頁注入了XHWebViewBridgeManager對象岗仑,然后你在代理里面寫的方法就是這個對象的方法例如你的.h文件是這樣的
@protocolXHWebManagerProtocol
- (void)jsCallOC;
@end
@interfaceXHWebViewBridgeManager : XHFunctionModule
@end
在js里面就可以這樣使用?XHWebViewBridgeManager.jsCallOC();這樣就完成了js調(diào)用oc代碼。如果是要傳參數(shù)或者回調(diào)怎么辦聚请?請見本文開頭demo有示例荠雕;
? ? ? ? 2.2 ) 使用場景
? ? ? ? ? ? ? ? ? ? ? ?a.點擊頁面里面的按鈕然后讓本地處理事件,例如驶赏,點擊按鈕需要插一張本地圖片或者需要調(diào)起本地相機(jī)等
? ? ? ? ? ? ? ? ? ? ? ? b.網(wǎng)頁經(jīng)過某些操作獲取的數(shù)據(jù)需要給本地進(jìn)行后續(xù)的處理炸卑。
? ? ? ? 2.3)使用說明及注意事項
? ? ? ? ? ? ? ? ? ? a.數(shù)據(jù)傳遞區(qū)別
上門就是類型對照,所以oc方法參數(shù)里面只能是上圖左邊類型煤傍,js調(diào)用代理里面的方法時傳遞上圖右邊相應(yīng)的數(shù)據(jù)類型矾兜,大多數(shù)情況下js傳給本地的字符或者對象,字符oc用NSString患久,對象用NSDictionary椅寺,由于js可以將方法作為參數(shù)傳遞浑槽,如果是js傳遞的是function OC里面需要用JSValue類型,例如:
上面為JS里面我們之前注入的XHWebViewBridgeManager對象調(diào)用OC代理里面的一個方法
??OC在.m里面的實現(xiàn)用JSValue接收function類型參數(shù)返帕,由于function里面還有參數(shù)桐玻,那么我們該如何回傳這個參數(shù)?JavaScriptCore框架提供了callWithArguments這個方法荆萤,方法只能傳NSArray類型镊靴,所以在js里面接收到的參數(shù)也是一個array或者叫對象(js除了基本類型都可以看著是對象)。
三链韭、坑處理
1偏竟、在js調(diào)oc方法時,如果js傳function作為參數(shù)有時候會卡屏敞峭。原因是在oc是多線程踊谋,但是刷新UI一定是在主線程里面,而js正常情況下一般只有一個線程旋讹,一旦出現(xiàn)js在回調(diào)里面的方法實現(xiàn)出現(xiàn)延遲(例如上傳下載)殖蚕,而oc需要js立即返回數(shù)據(jù)否則就卡自己主線程了。解決方法:js實現(xiàn)里面加setTimeout方法沉迹,例如:
2.如果想注入多個對象怎么辦睦疫?類似XHWebViewBridgeManager再新建新的類然后實現(xiàn)moduleName方法,然后返回新的對象名即可鞭呕,框架會幫你注入進(jìn)js的content上下文蛤育。
3.如果是頁面想把所有編輯好的樣式保存下來給后臺,然后下次本地網(wǎng)頁直接加載后臺給的數(shù)據(jù)就能實現(xiàn)草稿續(xù)編功能葫松。然而之前編輯的中文或者一些特殊符號在json序列化后會與html一些標(biāo)簽重合導(dǎo)致本地重新加載解析后的數(shù)據(jù)出現(xiàn)截斷瓦糕。解決方法:網(wǎng)頁先保存html所有的樣式,然后對樣式進(jìn)行編碼(編碼作用是對一些特殊字符进宝、中文進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換成與html不沖突的字符)枷恕,編碼后然后進(jìn)行json序列化存入后臺党晋。
下面是編碼與解碼方法
整體思路:將符號轉(zhuǎn)換為ascii碼,將中文進(jìn)行兩次encodeURI()編碼徐块。?
;!function () {
? ? ? ? String.prototype.AsciiEncode = function () {
? ? ? ? ? ? var fh = "", dg = "", asc = 0, perfix = arguments[0] || '~', str = this;
? ? ? ? ? ? for (i = 0; i < str.length; i++) {
? ? ? ? ? ? ? ? dg = str.substring(i, i + 1);
? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? asc = parseInt(str.charCodeAt(i));
? ? ? ? ? ? ? ? ? ? if ((asc < 48) || (asc > 90 && asc < 97) || (asc > 122 && asc < 127) || (asc > 57 && asc < 65)) {
? ? ? ? ? ? ? ? ? ? ? ? var s000 = asc.toString();
? ? ? ? ? ? ? ? ? ? ? ? if (asc < 100) { s000 = "0" + s000; }
? ? ? ? ? ? ? ? ? ? ? ? fh += perfix + s000;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? else {
? ? ? ? ? ? ? ? ? ? ? ? fh += dg;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } catch (e) {
? ? ? ? ? ? ? ? ? ? fh += dg;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? return fh.replace(/[\u4E00-\u9FA5\uF900-\uFA2D]/g, function () {
? ? ? ? ? ? ? ? return encodeURI(encodeURI(arguments[0]));
? ? ? ? ? ? });
? ? ? ? }
JS解碼:
? ? ? ? String.prototype.AsciiDecode = function () {
? ? ? ? ? ? var fh = "", youb = "", str = decodeURI(decodeURI(this));
? ? ? ? ? ? var array = str.split(arguments[0] || '~');
? ? ? ? ? ? for (i = 0; i < array.length; i++) {
? ? ? ? ? ? ? ? if (i > 0) {
? ? ? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? ? ? youb = array[i].substring(0, 3);
? ? ? ? ? ? ? ? ? ? ? ? array[i] = array[i].replace(youb, String.fromCharCode(youb));
? ? ? ? ? ? ? ? ? ? } catch (e) { }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? fh += array[i];
? ? ? ? ? ? }
? ? ? ? ? ? return fh;
? ? ? ? }
? ? } ();
OC解碼:
+ (NSString*)addQuotesFromHTML:(NSString*)html {
? ? NSString*fh =@"";
? ? NSString*youb;
? ? NSString*decodeS = [self? ?URLDecodedString:html];
? ? NSString*newD = [self??URLDecodedString:decodeS];
? ? NSArray *array = [newD componentsSeparatedByString:@"~"];
? ? NSMutableArray *newArray = [NSMutableArray arrayWithArray:array];
? ? for(NSIntegeri =0; i < array.count; i++) {
? ? ? ? if(i >0) {
? ? ? ? ? ? NSString*string = array[i];
? ? ? ? ? ? if(string.length==0) {
? ? ? ? ? ? ? ? return??string;
? ? ? ? ? ? }else{
? ? ? ? ? ? ? ? youb = [string? substringToIndex:3];
? ? ? ? ? ? }
? ? ? ? ? ??intintyoub = [youb? intValue];
? ? ? ? ? ? NSString*newuu = [NSString? stringWithFormat:@"%c",intyoub];
? ? ? ? ? ? NSString *wee = [string stringByReplacingOccurrencesOfString:youb withString:newuu];
? ? ? ? ? ? [newArray? replaceObjectAtIndex:iwithObject:wee];
? ? ? ? }
? ? }
? ? if(newArray.count>1) {
? ? ? ? for(NSString*ssinnewArray) {
? ? ? ? ? ? NSString*k = [fh? stringByAppendingString:ss];
? ? ? ? ? ? fh = k;
? ? ? ? }
? ? ? ? //對特殊字符處理未玻,還原在html中形態(tài)
? ? ? ? fh = [fhstringByReplacingOccurrencesOfString:@"&" withString:@"&"];
? ? ? ? fh = [fhstringByReplacingOccurrencesOfString:@"<" withString:@" <"];
? ? ? ? fh = [fhstringByReplacingOccurrencesOfString:@">" withString:@">"];
? ? ? ? returnfh;
? ? }else{
? ? ? ? fh = newD;
? ? ? ? returnfh;
? ? }
}
+(NSString*)URLDecodedString:(NSString*)str
{
? ? NSString*decodedString=(__bridge_transfer NSString*)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)str,CFSTR(""),CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
? ? returndecodedString;
}