iOS Hybrid 框架 ——PhoneGap

前言

Hybrid App(混合模式移動應用)是指介于web-app拘领、native-app這兩者之間的app庶弃,兼具“Native App良好用戶交互體驗的優(yōu)勢”和“Web App跨平臺開發(fā)的優(yōu)勢”黔龟。

Hybrid App按網頁語言與程序語言的混合,通常分為三種類型:多View混合型对竣,單View混合型难衰,Web主體型巫玻,3種類型比較如下:

今天我來談談Web主體型中Hybrid框架里面比較有名的PhoneGap

一.Cordova

說到PhoneGap丛忆,就不得不說到Cordova

Cordova 是一個可以讓 JS 與原生代碼(包括 Android 的 java,iOS 的 Objective-C 等)互相通信的一個庫仍秤,并且提供了一系列的插件類熄诡,比如 JS 直接操作本地數據庫的插件類。

Cordova的設計概念诗力,是在APP上透過Web控件來呈現Web頁面凰浮,讓Web開發(fā)人員可以操作熟悉的語言、工具來開發(fā)APP.

為了讓Web頁面能夠滿足更多的APP功能需求苇本,Cordova提供了Plugin機制袜茧,讓Web頁面能夠掛載并調用Native開發(fā)技術所開發(fā)的功能模塊

Cordova在系統(tǒng)中的層級應該是這樣子的:

二.Js 與 Objective-C 通信

Js 使用了兩種方式來與 Objective-C 通信,一種是使用 XMLHttpRequest 發(fā)起請求的方式瓣窄,另一種則是通過設置透明的 iframe 的 src 屬性笛厦。

我接下來說的主要是第二種方式,iframe bridge俺夕。
通過在 Js 端創(chuàng)建一個透明的 iframe裳凸,設置這個 ifame 的 src 為自定義的協議,而 ifame 的 src 更改時劝贸,UIWebView 會先回調其 delegate 的 webView:shouldStartLoadWithRequest:navigationType: 方法

說的還是很抽象的姨谷,來實際看一段代碼

在cordova.js 里面,是這樣子實現的

function iOSExec() {
    ...
    if (!isInContextOfEvalJs && commandQueue.length == 1)  {
        // 如果支持 XMLHttpRequest映九,則使用 XMLHttpRequest 方式
        if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
            // This prevents sending an XHR when there is already one being sent.
            // This should happen only in rare circumstances (refer to unit tests).
            if (execXhr && execXhr.readyState != 4) {
                execXhr = null;
            }
            // Re-using the XHR improves exec() performance by about 10%.
            execXhr = execXhr || new XMLHttpRequest();
            // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
            // For some reason it still doesn't work though...
            // Add a timestamp to the query param to prevent caching.
            execXhr.open('HEAD', "/!gap_exec?" + (+new Date()), true);
            if (!vcHeaderValue) {
                vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
            }
            execXhr.setRequestHeader('vc', vcHeaderValue);
            execXhr.setRequestHeader('rc', ++requestCount);
            if (shouldBundleCommandJson()) {
                // 設置請求的數據
                execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
            }
            // 發(fā)起請求
            execXhr.send(null);
        } else {
            // 如果不支持 XMLHttpRequest菠秒,則使用透明 iframe 的方式,設置 iframe 的 src 屬性
            execIframe = execIframe || createExecIframe();
            execIframe.src = "gap://ready";
        }
    }
    ...
}

iOS這邊對應的要在WebView里面寫響應的方法


// UIWebView 加載 URL 前回調的方法氯迂,返回 YES,則開始加載此 URL言缤,返回 NO嚼蚀,則忽略此 URL
- (BOOL)webView:(UIWebView*)theWebView
              shouldStartLoadWithRequest:(NSURLRequest*)request
              navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL* url = [request URL];
    
    /*
     * Execute any commands queued with cordova.exec() on the JS side.
     * The part of the URL after gap:// is irrelevant.
     */
    // 判斷是否 Cordova 的請求,對于 JS 代碼中 execIframe.src = "gap://ready" 這句
    if ([[url scheme] isEqualToString:@"gap"]) {
        // 獲取請求的數據管挟,并對數據進行分析轿曙、處理
        [_commandQueue fetchCommandsFromJs];
        return NO;
    }
    ...
}

這樣就完成了Js和OC的通信了

三.Objective-C 與 Js 通信

首先OC獲取Js的請求數據

- (void)fetchCommandsFromJs
{
    // Grab all the queued commands from the JS side.
    NSString* queuedCommandsJSON = [_viewController.webView
                                    stringByEvaluatingJavaScriptFromString:
                                    @"cordova.require('cordova/exec').nativeFetchMessages()"];
    
    [self enqueCommandBatch:queuedCommandsJSON];
    if ([queuedCommandsJSON length] > 0) {
        CDV_EXEC_LOG(@"Exec: Retrieved new exec messages by request.");
    }
}

然后OC處理Js傳過來的請求

OC再把處理結果返回Js

NSString *ret = [((HFNativeFunction*)strongSelf.actionDict[funcName]) doCall:argArr];
        NSString *js = [NSString stringWithFormat:@"if(typeof %@ == 'string') { paf.nativeInvocationObject=%@;} else {   paf.nativeInvocationObject=JSON.stringify(%@);} ", ret, ret, ret];
        DLog(@"\n\njs call fun=%@ ret=%@\n\n", funcName, ret);
        [self.webView stringByEvaluatingJavaScriptFromString: js];

四.Cordova - Js工作原理

Cordova JS 端請求方法的格式:
// successCallback : 成功回調方法
// failCallback : 失敗回調方法
// server : 所要請求的服務名字
// action : 所要請求的服務具體操作
// actionArgs : 請求操作所帶的參數

cordova.exec(successCallback, failCallback, service, action, actionArgs);

傳進來的這五個參數并不是直接傳送給原生代碼的,Cordova JS 端會做以下的處理:

1.會為每個請求生成一個叫 callbackId 的唯一標識:這個參數需傳給 Objective-C 端,Objective-C 處理完后导帝,會把 callbackId 連同處理結果一起返回給 JS 端守谓。

2.以 callbackId 為 key,{success:successCallback, fail:failCallback} 為 value您单,把這個鍵值對保存在 JS 端的字典里斋荞,successCallback 與 failCallback 這兩個參數不需要傳給 Objective-C 端,Objective-C 返回結果時帶上 callbackId虐秦,JS 端就可以根據 callbackId 找到回調方法平酿。

3.每次 JS 請求,最后發(fā)到 Objective-C 的數據包括:callbackId, service, action, actionArgs悦陋。

Js處理請求

function iOSExec() {
    ...
    // 生成一個 callbackId 的唯一標識蜈彼,并把此標志與成功、失敗回調方法一起保存在 JS 端
    // Register the callbacks and add the callbackId to the positional
    // arguments if given.
    if (successCallback || failCallback) {
        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
        {success:successCallback, fail:failCallback};
    }
    
    actionArgs = massageArgsJsToNative(actionArgs);
    
    // 把 callbackId俺驶,service幸逆,action,actionArgs 保持到 commandQueue 中
    // 這四個參數就是最后發(fā)給原生代碼的數據
    var command = [callbackId, service, action, actionArgs];
    commandQueue.push(JSON.stringify(command));
    ...
}

// 獲取請求的數據暮现,包括 callbackId, service, action, actionArgs
iOSExec.nativeFetchMessages = function() {
    // Each entry in commandQueue is a JSON string already.
    if (!commandQueue.length) {
        return '';
    }
    var json = '[' + commandQueue.join(',') + ']';
    commandQueue.length = 0;
    return json;
};

五.Cordova - OC工作原理

Native OC拿到 callbackId还绘、service、action 及 actionArgs 后送矩,會做以下的處理:

1.根據 service 參數找到對應的插件類

2.根據 action 參數找到插件類中對應的處理方法蚕甥,并把 actionArgs 作為處理方法請求參數的一部分傳給處理方法

3.處理完成后,把處理結果及 callbackId 返回給 JS 端栋荸,JS 端收到后會根據 callbackId 找到回調方法菇怀,并把處理結果傳給回調方法

Objective-C 返回結果給 JS 端

- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
    CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
    // This occurs when there is are no win/fail callbacks for the call.
    if ([@"INVALID" isEqualToString : callbackId]) {
        return;
    }
    int status = [result.status intValue];
    BOOL keepCallback = [result.keepCallback boolValue];
    NSString* argumentsAsJSON = [result argumentsAsJSON];
    
    // 將請求的處理結果及 callbackId 通過調用 JS 方法返回給 JS 端
    NSString* js = [NSString stringWithFormat:
                    @"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d)",
                    callbackId, status, argumentsAsJSON, keepCallback];
    
    [self evalJsHelper:js];
}

舉個具體的例子:

1.將收到的json轉換成Command

// Execute the commands one-at-a-time.
     NSArray* jsonEntry = [commandBatch dequeue];
     if ([commandBatch count] == 0) {
                  [_queue removeObjectAtIndex:0];
      }
     HFCDVInvokedUrlCommand* command = [HFCDVInvokedUrlCommand commandFromJson:jsonEntry];
     HF_CDV_EXEC_LOG(@"Exec(%@): Calling %@.%@", command.callbackId, command.className, command.methodName);

2.OC 執(zhí)行

- (BOOL)execute:(HFCDVInvokedUrlCommand*)command
{
    if ((command.className == nil) || (command.methodName == nil)) {
        DLog(@"ERROR: Classname and/or methodName not found for command.");
        return NO;
    }
    
    if ([command.className isEqualToString:@"DeviceReadyDummyClass"] &&
        [command.methodName isEqualToString:@"deviceReady"]) {
        [[NSNotificationCenter defaultCenter]postNotificationName:k_NOTIF_DEVICE_READY object:_viewController];
        return YES;
    }

    // Fetch an instance of this class
    HFCDVPlugin* obj = [_viewController.commandDelegate getCommandInstance:command.className];

    if (!([obj isKindOfClass:[HFCDVPlugin class]])) {
        DLog(@"ERROR: Plugin '%@' not found, or is not a HFCDVPlugin. Check your plugin mapping in config.xml.", command.className);
        return NO;
    }
    BOOL retVal = YES;
    double started = [[NSDate date] timeIntervalSince1970] * 1000.0;
    // Find the proper selector to call.
    NSString* methodName = [NSString stringWithFormat:@"%@:", command.methodName];
    SEL normalSelector = NSSelectorFromString(methodName);
    if ([obj respondsToSelector:normalSelector]) {
        // [obj performSelector:normalSelector withObject:command];
        ((void (*)(id, SEL, id))objc_msgSend)(obj, normalSelector, command);
    } else {
        // There's no method to call, so throw an error.
        DLog(@"ERROR: Method '%@' not defined in Plugin '%@'", methodName, command.className);
        retVal = NO;
    }
    double elapsed = [[NSDate date] timeIntervalSince1970] * 1000.0 - started;
    if (elapsed > 10) {
        DLog(@"THREAD WARNING: ['%@'] took '%f' ms. Plugin should use a background thread.", command.className, elapsed);
    }
    return retVal;
}

六.回調方法

Js端拿到數據根據 callbackId 回調

// 根據 callbackId 及是否成功標識,找到回調方法晌块,并把處理結果傳給回調方法
callbackFromNative: function(callbackId, success, status, args, keepCallback) {
    var callback = cordova.callbacks[callbackId];
    if (callback) {
        if (success && status == cordova.callbackStatus.OK) {
            callback.success && callback.success.apply(null, args);
        } else if (!success) {
            callback.fail && callback.fail.apply(null, args);
        }
        
        // Clear callback if not expecting any more results
        if (!keepCallback) {
            delete cordova.callbacks[callbackId];
        }
    }
}

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: https://halfrost.com/ios_hybrid_phonegap/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末爱沟,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子匆背,更是在濱河造成了極大的恐慌呼伸,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钝尸,死亡現場離奇詭異括享,居然都是意外死亡,警方通過查閱死者的電腦和手機珍促,發(fā)現死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門铃辖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人猪叙,你說我怎么就攤上這事娇斩∪示恚” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵犬第,是天一觀的道長锦积。 經常有香客問我,道長歉嗓,這世上最難降的妖魔是什么丰介? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮遥椿,結果婚禮上基矮,老公的妹妹穿的比我還像新娘。我一直安慰自己冠场,他們只是感情好家浇,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碴裙,像睡著了一般钢悲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舔株,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天莺琳,我揣著相機與錄音,去河邊找鬼载慈。 笑死惭等,一個胖子當著我的面吹牛,可吹牛的內容都是我干的办铡。 我是一名探鬼主播辞做,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寡具!你這毒婦竟也來了秤茅?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤童叠,失蹤者是張志新(化名)和其女友劉穎框喳,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體厦坛,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡五垮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了杜秸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拼余。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖亩歹,靈堂內的尸體忽然破棺而出匙监,到底是詐尸還是另有隱情,我是刑警寧澤小作,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布亭姥,位于F島的核電站,受9級特大地震影響顾稀,放射性物質發(fā)生泄漏达罗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一静秆、第九天 我趴在偏房一處隱蔽的房頂上張望粮揉。 院中可真熱鬧,春花似錦抚笔、人聲如沸扶认。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辐宾。三九已至,卻和暖如春膨蛮,著一層夾襖步出監(jiān)牢的瞬間叠纹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工敞葛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留誉察,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓惹谐,卻偏偏與公主長得像持偏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子豺鼻,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容