ObjC和JavaScript的交互,在恰當的時機注入對象

ObjC和JavaScript的交互,在恰當的時機注入對象


原文地址: http://www.jianshu.com/p/2e53d87c826b

* 警告:文章中提到的 *

- (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame;

方法涉及私有API,有網友反饋說審覈會被拒絕,希望看到的朋友們慎用

移動端項目開發中,免不了出現Native App(以下簡稱Native)和H5頁面(以下簡稱H5)的交互,網絡上有很多第三方框架,比較WebViewJavascriptBridge,對於一些小的項目需求來說,其實不用那麼麻煩,我們還是先從基礎着手。

先了解幾個基礎方法

網頁即將加載(最先執行的代理方法),在每次載入頁面的時候都會先走這個回調,可以在此做一些自己的操作,經常會在這兒攔截協議

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 
    // do something...

    return YES;
}

網頁已經加載完成(最後執行的代理方法),執行到這個地方,網頁面已經加載完成,相關代碼也都執行完畢

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    // 加載完成 隱藏HUD

}

根據不同的場景,找一個最合適的方法

場景1

H5通信本地,告知本要做的事兒

H5 頁面在某個標籤點擊後,要關閉當前加載網頁的控制器VC

需求分析:
這應該不是最簡單的一個需求,最簡單的是本地通過url給H5頁面傳參數,告知H5要做的事。
這個需求中,H5頁面已經加載完畢,此時可以說H5頁面相關的Bug和UI缺陷都與本機無關,我每次都是這麼跟測試人員講,類似問題直接分配給他們。

功能實現:
對於這類比較簡單的需求,最常用的做法就是,通過攔截協議的方法,在點擊標籤的時候,可以調用自定義協議的超鏈接,比如定義一個yuhanle://action/close的鏈接,在頁面即將載入的時候,判斷url的協議,如果協議是yuhanle,就攔截掉這個請求,做自己的處理。
圖解:
!()[http://upload-images.jianshu.io/upload_images/545755-9a66b0a332873951.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240]

場景2

H5調用Native App的JS方法,包括同步和異步操作

H5 頁面在加載過程中,需要從Native 中取得部分數據,或調用某個功能,均包含同步
操作或異步操作,比如只是簡單的獲取token,則直接同步返回,如果需要Native 異
步拿到結果,Native 則需要考慮 JSExport 中的線程問題

需求分析:
這個需求中肯定需要Native注入JS方法,H5通過調用JS和Native通信,其中包括同步和異步兩種情況下的處理,需要注意的就是異步操作時,H5需要在調用App時傳入一個JS方法名,應用在拿到數據後可以回調H5的JS方法,在調用這個回調的時候,需要使用webView的currentThread,不然就會出現頁面卡死。
功能實現:
1-定義一個類,用於注入這個對象

// 此模型用於注入JS的模型,這樣就可以通過模型來調用方法。
@interface QWSJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>

@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@property (nonatomic, weak) G100WebViewController * webVc;

@end

2-聲明協議,實現和JS對應的方法**

#import <JavaScriptCore/JavaScriptCore.h>

@protocol JavaScriptObjectiveCDelegate <JSExport>

/**
 *  獲取客戶端的token
 *
 *  @param qwsKey 客戶端生成的密碼key
 *
 *  @return 返回值token
 */
- (NSString *)getToken:(NSString *)qwsKey;

/**
 *  H5 傳遞key 獲取newToken 在調用其 callback 方法
 *
 *  @param key      qwskey
 *  @param callback 回調方法名
 *  @param property 方法參數
 */
- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property;

/**
 *  H5 在加載完成後 告訴客戶端在返回的時候調用該方法
 *
 *  @param callback js 方法名
 */
- (void)getExitMsgCallback:(NSString *)callback;

3-我們需要在打開webView的時候,找到一個好的時機注入JS

// 首先拿到JSContext
 self.jsContext = [_jsWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    // 通過模型調用方法,這種方式更好些。
    QWSJsObjCModel *model  = [[QWSJsObjCModel alloc] init];
    self.jsContext[@"nativeObj"] = model;
    model.jsContext = self.jsContext;
    model.webView = _jsWebView;

    self.jsContext[@"getUserinfo"] = ^(){
        return @"1234";
    };

    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"異常信息:%@", exceptionValue);
    };

4-對應H5頁面的JS定義及調用

<!DOCTYPE html>
<html>
<head>
 <title>測試IOS與JS之前的互調</title>
 <style type="text/css">
   * {
    font-size: 40px;
   }
 </style>
  <script type="text/javascript">

  var jsFunc = function() {
    alert('Objective-C call js to show alert');
  }

  var jsParamFunc = function(argument) {
    document.getElementById('jsParamFuncSpan').innerHTML
    = argument['name'];
  }

  </script>

</head>

<body>

<div style="margin-top: 100px">
 <h1>Test how to use objective-c call js</h1>
 <input type="button" value="getToken" onclick="alert(nativeObj.getToken())">
 <input type="button" value="Call ObjC system alert" onclick="nativeObj.showAlertMsg('js title', 'js message')">
</div>

<div>
 <input type="button" value="Call ObjC func with JSON " onclick="nativeObj.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
 <input type="button" value="Call ObjC func with JSON and ObjC call js func to pass args." onclick="nativeObj.jsCallObjcAndObjcCallJsWithDict({'name': 'testname', 'age': 10, 'height': 170})">
</div>
<div>
  <a href="test1.html">Click to next page</a>
</div>

<div>
 <span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
</div>

</body>
</html>

按照以上的做法,就能達到本機和H5之間的相互通信,現在的問題是,在什麼時候注入JS對象,才能滿足H5頁面的需求,因爲實際情況中,H5頁面可能會隨時調用你的JS 。

需要注意的幾個問題

1-場景2中我們提到的,異步調用時的線程問題首先看下下面的代碼

- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
    if (_webVc) {
        if ([_webVc.qwsKey isEqualToString:key]) {

            __block NSString * newToken = @"";
            __block NSInteger result = 0;

            [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
                if (requestSuccess) {
                    newToken = [[G100InfoHelper shareInstance] token];
                }else{
                    newToken = @"error";
                }

                result = requestSuccess ? response.errCode : statusCode;

                JSValue * function = self.jsContext[callback];
                NSArray * params = @[@(result), newToken, property];
                [function callWithArguments:params];
            }];
        }
    }
}

這段代碼,就是想在H5頁面調用的時候,應用這邊自動登陸,重新獲取到最新的令牌,拿到結果以後並回調H5,整個過程上是異步的,看起來是沒問題的,但是一旦實際操作起來,會在這裏卡死。具體原因,我也不好解釋,解決辦法是有的,只能通過webView的currentThread來執行執行操作。

示例如下:

- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
    if (_webVc) {
        if ([_webVc.qwsKey isEqualToString:key]) {

            __block NSString * newToken = @"";
            __block NSInteger result = 0;
            NSThread * webThread = [NSThread currentThread];

            [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
                if (requestSuccess) {
                    newToken = [[G100InfoHelper shareInstance] token];
                }else{
                    newToken = @"error";
                }

                result = requestSuccess ? response.errCode : statusCode;
                // 這裏通過此方法 在當前線程操作纔不會造成卡死的現象
                [self performSelector:@selector(callQWSJSWithArgument:) onThread:webThread withObject:@[callback, @(result), newToken, property] waitUntilDone:NO];
            }];
        }
    }
}

- (void)callQWSJSWithArgument:(NSArray *)argument {
    NSString * callback = argument[0];
    JSValue * function = self.jsContext[callback];

    NSMutableArray * params = [NSMutableArray arrayWithArray:argument];
    // 移除第一個 方法名
    [params removeObjectAtIndex:0];
    [function callWithArguments:params];
}

2-同樣是場景2中的一個問題,什麼時候注入對象
需要總是虛無縹緲的,對於H5結合本機的開發結構中,本機始終扮演着服務和入口的角色,H5可能隨時都會主動和本機通信,但是本應該在這什麼時候準備好這些服務呢?

看到很多網上的資料,幾乎全部都是在頁面加載完成webViewDidFinishLoad這個回調中注入方法,但實際開發中,很多頁面在加載的時候就需要和本機通信,比如說拿到令牌,如果在這個時候才注入,肯定是來不及的,只能無功而返。

相信大多數人都沒有在這個問題,當然,如果強制讓H5的開發人員修改邏輯,將所有的通信都放在頁面加載完成以後在做,也沒問題,只不過對於用戶的體驗會變得糟糕。

深入研究官方文檔,就會發現,webView在加載過程中,會執行這麼一個方法,他的作用是

_:didCreateJavaScriptContext:for:
Notifies the delegate that a new JavaScript context has been created created


具體參見官方文檔說明didCreateJavaScriptContext
看到這裏,我們就能在收到這個消息的時候,拿到JSContext,然後注入我們的模型。

首先,新建一個NSObject的Catagory,在這個代理方法中發送一個通知

@implementation NSObject (JSTest)

- (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DidCreateContextNotification" object:ctx];
}

@end

然後,在webView的控制器中監聽這個消息

- (void)viewDidLoad {
    [super viewDidLoad];
    // 監聽可以注入js 方法的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didCreateJSContext:) name:@"DidCreateContextNotification" object:nil];
}

實現@selector方法

#pragma mark - 可以注入js 的監聽
- (void)didCreateJSContext:(NSNotification *)notification {
    NSString *indentifier = [NSString stringWithFormat:@"indentifier%lud", (unsigned long)self.webView.hash];
    NSString *indentifierJS = [NSString stringWithFormat:@"var %@ = '%@'", indentifier, indentifier];
    [self.webView stringByEvaluatingJavaScriptFromString:indentifierJS];

    JSContext *context = notification.object;

    if (![context[indentifier].toString isEqualToString:indentifier]) return;

    self.jsContext = context;
    // 通過模型調用方法,這種方式更好些。
    QWSJsObjCModel *model  = [[QWSJsObjCModel alloc] init];
    self.jsContext[@"nativeObj"] = model;
    model.jsContext = self.jsContext;
    model.webView   = self.webView;
    model.webVc     = self;

    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
        context.exception = exceptionValue;
        DLog(@"異常信息:%@", exceptionValue);
    };
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章