iOS Hybrid 框架 ——PhoneGap

前言

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

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開發人員可以操作熟悉的語言、工具來開發APP.

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

Cordova在系統中的層級應該是這樣子的:

二.Js 與 Objective-C 通信

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

我接下來說的主要是第二種方式,iframe bridge。
通過在 Js 端創建一個透明的 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());
            }
            // 發起請求
            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 請求,最后發到 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 中
    // 這四個參數就是最后發給原生代碼的數據
    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 執行

- (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/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容