1. 前言

在介绍 iOS SDK 的 H5 打通方案之前,我们先了解一下什么是 App 与 H5 打通。

所谓 “打通”,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不是直接同步给服务端,而是先发给 App 端的数据采集 SDK,经 App 端数据采集 SDK 二次加工处理后缓存到本地,再经过合适的上传策略同步到服务端。

2. App 与 H5 打通原因

关于 App 与 H5 打通的原因,我们主要是从以下几个角度考虑:

2.1. 数据丢失率

在业界,App 端采集数据的丢失率一般在 1% 左右,而 H5 采集数据的丢失率一般在 5% 左右(主要是因为缓存、网络或切换页面等原因)。因此,如果 App 与 H5 打通,H5 触发的所有事件都可以先发给 App 端数据采集 SDK,经过 App 端二次加工处理后存入本地缓存,在符合特定策略之后再进行数据同步,即可把数据丢失率由 5% 降到 1% 左右。

2.2. 数据准确性

众所周知,H5 无法直接获取设备相关的信息,只能通过解析 UserAgent 值获取到有限的信息。而解析 UserAgent 值,至少会面临以下两个问题:

  1. 有些信息通过解析 UserAgent 值根本获取不到,比如应用程序的版本号、设备具体型号等;
  2. 有些信息通过解析 UserAgent 值可以获取到,但内容可能不正确。

如果 App 与 H5 打通,由 App 端数据采集 SDK 补充这些信息,即可确保事件信息的准确性和完整性。

2.3. 用户标识

如果用户在 App 端注册或登录之前使用我们的产品,我们一般都是使用匿名 ID 来标识用户。而 App 与 H5 标识匿名用户的规则不一样(iOS 一般使用 IDFA 或 IDFV,H5 一般使用 Cookie),从而就会导致一个用户使用了我们的产品,结果产生了两个匿名用户。如果 App 与 H5 打通,就可以将两个匿名 ID 做归一化处理(以 App 端匿名 ID 为准)。

2.4. 基础功能

基于 App 与 H5 打通,可以实现诸如 App 内嵌 H5 可视化全埋点、App 内嵌 H5 弹框等更加高级的功能。

介绍完打通的原因之后,我们来看下 App 与 H5 如何进行打通。

3. 打通方案演进

关于 App 与 H5 的打通,曾经一直是我们的一个痛点,为此我们也做了持续地探索和迭代。在这个过程中我们踩了很多坑,也积累了一些经验,现在将按照方案演进的顺序为大家一一介绍这几种方式,并分析其背景、实现和优缺点。

3.1. 方案一

3.1.1. 背景和原理

iOS SDK 从 v1.6.8 版本开始支持 App 与 H5 打通,也就是打通方案的原始版本

众所周知,在实际开发中,一个技术方案的使用,都是为了解决某些问题。在早期的事件分析中,H5 的匿名 ID(一般使用 Cookie)经常变化,并且与 App 端(iOS 一般使用 IDFA 或 IDFV)不同。因此,导致一个 App 用户在内嵌 H5 页面的行为序列无法和原生页面的行为联系起来,为业务分析造成很大困扰。但是,如果将 App 端的匿名 ID 传给 H5,就能保证 App 内嵌 H5 页面使用的匿名 ID 和原生页面一致,从而解决上述问题。

原理:

在 WebView 加载 H5 页面完成后,iOS SDK 调用 JS 方法将匿名 ID 传给 JS SDK,JS SDK 采集的埋点数据就可以使用 App 的匿名 ID,从而使得 App 进入 H5 页面前后的用户行为序列准确关联。主要流程如图 3-1 所示:

图 3-1 打通方案一的流程图

3.1.2. 具体实现

具体实现主要分为下面几个步骤:

1. 从图 3-1 的打通方案流程可以知道,为了保证 JS SDK 已经加载完成,iOS SDK 需要在 H5 页面加载完成后才能调用 JS 方法传值。那么监听 H5 页面加载完成的时机,便成了打通的关键。

对于 UIWebView 而言,我们知道从 UIWebViewDelegate 的代理方法中可以获取 UIWebView 加载 H5 页面的进度,UIWebViewDelegate 的代理方法如下:

API_UNAVAILABLE(tvos) @protocol UIWebViewDelegate <NSObject>
@optional
/// 是否开始加载 H5
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType API_DEPRECATED("No longer supported.", ios(2.0, 12.0));
/// 已经开始加载 H5
- (void)webViewDidStartLoad:(UIWebView *)webView API_DEPRECATED("No longer supported.", ios(2.0, 12.0));
/// H5 页面加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView API_DEPRECATED("No longer supported.", ios(2.0, 12.0));
/// H5 页面加载失败
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error API_DEPRECATED("No longer supported.", ios(2.0, 12.0));
@end

因此,我们在 UIWebView 的 H5 页面加载完成调用 JS 方法,只需要实现 – webViewDidFinishLoad: 方法即可,如下所示:

UIWebView
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [[SensorsAnalyticsSDK sharedInstance] showUpWebView:webView];
}

对于 WKWebView 而言,查阅 Apple 的 WKWebView 相关 API 文档没有合适的代理方法去监听 H5 页面加载完成,不过发现了 loading 这个属性,说明如下:

/*! @abstract A Boolean value indicating whether the view is currently
 loading content.
 @discussion @link WKWebView @/link is key-value observing (KVO) compliant
 for this property.
 */
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

loading 属性表示当前页面是否正在加载,如果 loading = NO(即页面不再加载了),表示 H5 页面已经加载完成。因此,我们使用 KVO 监听 loading 的属性变化,就可以知道 WKWebView 加载 H5 页面是否完成,具体实现如下所示:

WKWebView
// 通过观察者监听 WKWebView 加载进度
[_webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if (!_webView.loading) {
        [[SensorsAnalyticsSDK sharedInstance] showUpWebView:_webView];
    }
}

2. 监听 H5 页面加载完成后,即表示 JS SDK 已经准备就绪。此时使用 WebView 调用 JS 方法,将 App 端使用的匿名 ID 传给 JS SDK 即可,具体实现如下所示:

- (void)showUpWebView:(id)webView {
    NSString *js = [NSString stringWithFormat:@"sensorsdata_app_js_bridge_call_js('%@')", [self webViewJavascriptBridgeCallbackInfo]];
    if ([webView isKindOfClass:[UIWebView class]]) {//UIWebView
        // UIWebView 调用 JS 方法
        [webView stringByEvaluatingJavaScriptFromString:js];
    else if([webView isKindOfClass:[WKWebView class]]) {//WKWebView
        // WKWebView 调用 JS 方法
        [webView evaluateJavaScript:js completionHandler:^(id _Nullable response, NSError * _Nullable error) {
            NSLog(@"response: %@ error: %@", response, error);
        }];
    }
}

3. JS SDK 后续采集的所有事件,都使用 iOS SDK 传递的匿名 ID 作为当前的用户标识,具体实现如下所示:

window.sensorsdata_app_js_bridge_call_js = function(data) {
    setAppInfo(data);
};
function setAppInfo(data) {
// 解析 $type 和 $distinct_id,拼接数据
}

3.1.3. 优缺点

优点:

  1. 由于 iOS SDK 不用处理 JS 端的数据,只需要传少量信息到 JS 端,对 iOS SDK 的侵入较小;
  2. 可以同时兼容 UIWebView 和 WKWebView,满足更多客户需求。

缺点:

  1. H5 产生的埋点数据,继续由 JS SDK 上报到服务端,数据丢失的风险较大;
  2. 如果 WebView 加载完成,JS SDK 尚未加载,会导致打通失败;
  3. 只要开启打通,iOS SDK 就会把 App 端的匿名 ID 发给 JS SDK 。由于不会区分项目,这样可能会导致数据错乱。例如,客户 A 的 App 内嵌了客户 B 的 H5,可能导致客户 B 的 H5 页面使用了客户 A 的匿名 ID,然后数据还是发到了客户 B 的服务端,导致客户 B 的用户数据错乱;
  4. 对于客户来说打通的集成比较复杂。例如,如果客户 App 项目中使用了多个 UIWebView 或 WKWebView ,需要在多处实现协议方法并调用 SDK 接口,接入的工作量会比较大

3.2. 方案二

3.2.1. 背景和原理

为了减小加载 JS SDK 对客户 H5 页面的影响,后期 JS SDK 支持异步加载(即 H5 页面加载完成后才开始加载 JS SDK)。这就导致一个问题:可能 WebView 加载完成时,JS SDK 尚未加载,此时方案一会打通失败。为了解决这个问题,我们开发出了方案二。

因为 JS SDK 支持异步加载,所以 iOS SDK 不再依赖于 WebView 加载完成的状态去判断 JS SDK 是否加载完成。因此,方案二的关键问题是:iOS SDK 应该什么时机去调用 JS 的方法发送 App 端的数据,即 iOS SDK 怎么才能知道 JS SDK 加载完成?

原理

在 JS SDK 加载完成并初始化后,发送一个 iframe 请求(sensorsanalytics://getAppInfo)。然后在 App 的 WebView 的协议方法中拦截 H5 的页面请求。如果出现 sensorsanalytics://getAppInfo 这个请求,即认为 JS SDK 加载完成。此时,调用 JS 的方法发送 App 端的数据,就能保证打通成功。主要流程如图 3-2 所示:

图 3-2 打通方案二的流程图

3.2.2. 具体实现

具体实现主要分为下面几个步骤:

1. JS SDK 初始化后发送 iframe 请求:

function calliOS() {
    if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
        var iframe = document.createElement("iframe");
        iframe.setAttribute("src""sensorsanalytics://getAppInfo");
        document.documentElement.appendChild(iframe);
        iframe.parentNode.removeChild(iframe);
        iframe = null;
    }
}

2. iOS SDK 拦截请求。关于拦截 H5 页面中的请求,在 UIWebView 和 WKWebView 实现略有不同,分别示例如下。

对于 UIWebView 拦截请求,需要实现 UIWebViewDelegate 中的如下代理方法:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[SensorsAnalyticsSDK sharedInstance] showUpWebView:webView WithRequest:request]) {
        return NO;
    }
    return YES;
}

对于 WKWebView 拦截请求,需要实现 WKNavigationDelegate 中的如下代理方法:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if ([[SensorsAnalyticsSDK sharedInstance] showUpWebView:webView WithRequest:navigationAction.request]) {
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

3. iOS SDK 判断 sensorsanalytics://getAppInfo 请求,调用 JS 方法发送数据:

- (BOOL)showUpWebView:(id)webView WithRequest:(NSURLRequest *)request {
    NSString* jsonString =  [self webViewJavascriptBridgeCallbackInfo];
    NSString *scheme = @"sensorsanalytics://getAppInfo";
    NSString *js = [NSString stringWithFormat:@"sensorsdata_app_js_bridge_call_js('%@')", jsonString];
    //判断系统是否支持WKWebView
    Class wkWebViewClass = NSClassFromString(@"WKWebView");
    if ([webView isKindOfClass:[UIWebView class]]) {//UIWebView
        // 判断当前 request 是否为 JS SDK 发送的 iframe 请求
        if ([request.URL.absoluteString rangeOfString:scheme].location != NSNotFound) {
            [webView stringByEvaluatingJavaScriptFromString:js];
            return YES;
        }
        return NO;
    else if(wkWebViewClass && [webView isKindOfClass:wkWebViewClass]) {//WKWebView
        // WKWebView 逻辑和上述类似
    else{
        SADebug(@"showUpWebView: not UIWebView or WKWebView");
        return NO;
    }
}

4. JS SDK 采集的事件,都使用 iOS SDK 发送的匿名 ID 作为当前用户的标识。

3.2.3. 优缺点

优点

  1. 这种打通方案,由于 iOS SDK 不用处理 JS 端的数据,只需要传少量信息到 JS,对 iOS SDK 的侵入较小;
  2. 可以同时兼容 UIWebView 和 WKWebView,满足更多客户需要;
  3. 支持 JS SDK 在 H5 页面异步加载情况下的 App 与 H5 打通。

缺点

  1. H5 产生的埋点数据,继续由 JS SDK 上报到服务端,数据丢失的风险较大;
  2. 只要开启打通,JS SDK 就会把 App 的匿名 ID 发给 JS SDK 。由于不会区分项目,这样可能会导致数据错乱。例如,客户 A 的 App 内嵌了客户 B 的 H5,可能导致客户 B 的 H5 页面使用了客户 A 的匿名 ID,然后数据还是发到了客户 B 的服务端,导致客户 B 的用户数据错乱;
  3. 对于客户来说打通的集成比较复杂。例如,客户 App 项目中使用了多个 UIWebView 或 WKWebView ,需要在多处实现协议方法并调用 SDK 接口,接入的工作量会比较大。

3.3. 方案三

3.3.1. 背景和原理

对于上述两种方案,对功能影响较大的两个问题是:

  1. App 与 H5 打通后,App 发送匿名 ID 给 JS SDK 时不区分项目,对其他客户数据造成较大影响;
  2. H5 的埋点数据通过 JS SDK 上报,数据丢失的风险较大,并且部分数据无法采集。

为了解决上述两个问题,我们对 App 与 H5 打通方案做了较大的修改,从而推出了方案三。

  1. 为了解决上述第一个问题,我们单独增加了开启打通的接口。如果客户调用并开启打通,iOS SDK 会修改当前 App 环境的 UA 值,拼接当前接入 SA 的 project(项目名) 和 host(域名)。这样 JS SDK 可以判断当前 H5 页面是否需要打通,并且还可以校验是否为同一个项目;
  2. 为了解决上述第二个问题,我们在 JS SDK 增加是否需要打通的判断,如果需要打通就将 H5 产生的埋点数据发送到 App,由 iOS SDK 处理并缓存到本地,然后根据合适的策略上传到服务端。

原理

iOS SDK 修改 UA 来标记当前 App 集成神策的项目信息,JS SDK 根据 UA 值判断是否需要打通。如果需要打通,JS SDK 将埋点数据发往 App,iOS SDK 解析并处理 JS SDK 端产生的埋点数据,再根据合适的策略上报到服务端。主要流程如图 3-3 所示:

图 3-3 打通方案三的流程图

3.3.2. 实现

具体实现主要分为下面几个步骤:

1. iOS SDK 开启打通的接口,默认校验当前数据接收地址中的项目。也就是将神策数据接收地址中的 host 和 project 写入当前的 UA 环境,以便 JS SDK 解析:

// 开启打通,默认校验 serverURL
- (void)addWebViewUserAgentSensorsDataFlag {
    dispatch_async(dispatch_get_main_queue(), ^{
        
        SAServerUrl *url = [[SAServerUrl alloc] initWithUrl:self.serverURL];
        UIWebView *tempWebView = [[UIWebView alloc] initWithFrame:CGRectZero];
        // 获取 UA
        NSString *userAgent = [tempWebView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
        if ([userAgent rangeOfString:@"sa-sdk-ios"].location != NSNotFound) {
            return;
        }
        // 拼接当前 SA 项目中的 host 和 project
        userAgent = [userAgent stringByAppendingString:[NSString stringWithFormat: @" /sa-sdk-ios/sensors-verify/%@?%@ ", url.host, url.project]];
        
        // 写入 UA
        NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:userAgent, @"UserAgent", nil];
        [[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
        [[NSUserDefaults standardUserDefaults] synchronize];
    });
}

2. JS SDK 获取当前环境的 UA 值,如果判断当前环境为 App 内嵌 H5 并且开启打通(UA 中包含 /sa-sdk-ios/sensors-verify),就会解析 UA 中的 host 和 project。如果根据 host 和 project 判断当前 H5 与 App 集成的是同一个神策项目,则表示需要进行 App 与 H5 打通。此时 JS SDK 触发 iframe 请求,发送埋点数据到 App :

JS SDK
   if (sd.bridge.iOS_UA_bridge()) {
   iframe = document.createElement('iframe');
   iframe.setAttribute('src''sensorsanalytics://trackEvent?event=' + encodeURIComponent(JSON.stringify(_.extend({
     server_url: sd.para.server_url
   }, originData))));
   document.documentElement.appendChild(iframe);
   iframe.parentNode.removeChild(iframe);
}

3. iOS SDK 拦截 WebView 请求,解析 JS 埋点数据并进行处理和缓存(此处以 UIWebView 的实现为例,关于 WKWebView 拦截请求的方案,上文已有介绍,此处不再赘述):

webView 中拦截请求
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[SensorsAnalyticsSDK sharedInstance] showUpWebView:webView WithRequest:request]) {
        return NO;
    }
    return YES;
}

解析 JS SDK 触发 iframe 请求中的 URL 参数,从而获取 JS SDK 发送的埋点数据:

- (BOOL)showUpWebView:(id)webView WithRequest:(NSURLRequest *)request {
    /* 其他合法性判断*/
    NSString *urlString = request.URL.absoluteString;
    if (!urlString) {
        return YES;
    }
    if (![urlString rangeOfString:@"sensorsanalytics://trackEvent"].length) {
        return NO;
    }
    //解析 url 中的事件数据
    NSDictionary *paramsDic = [SANetwork queryItemsWithURLString:urlString];
    if ([paramsDic count] > 0) {
        NSString *eventInfo = paramsDic[@"event"];
        if (eventInfo) {
            NSString *encodedString = [eventInfo stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            [self trackFromH5WithEvent:encodedString];
        }
    }
}

3.3.3. 优缺点

优点:

  1. H5 产生的事件数据,iOS SDK 先经过了加工:使用 App 的匿名 ID,增加设备信息、网络和运营商信息等,从而提高了数据采集的准确性和完整性,保证了匿名  ID 的一致,为后续的行为序列分析提供了可靠的数据基础;
  2. 通过 iOS SDK 缓存并上报,很大程度上降低了数据丢失的风险;
  3. JS SDK 解析 H5 环境的 UA 标识判断是否打通,避免了 App  使用其他客户的 H5 可能存在的数据丢失或产生脏数据问题。

缺点:

  1. 对于每个 WebView,都需要在协议方法中调用接口拦截请求,如果一个项目有多个 WebView,集成工作相对繁琐;
  2. 如果客户项目禁用 UIWebView,WKWebView 目前只支持异步获取 UA 再进行修改,可能会导致两个问题:
    • 如果客户也需要修改 UA,可能会导致打通失败或者客户修改 UA 失败;
    • 如果 App 首页加载 WKWebView,这个页面的 H5 会打通失败。
  3. JS SDK 发送的 iframe 请求,在客户 App 环境中,可能被误判为非法请求并进行拦截,导致打通失败。

3.4. 方案四

3.4.1.  背景和原理

随着客户数量的快速增加,越来越复杂的使用场景和客户环境,给我们的 App 与 H5 打通方案带来新的考验。在某些复杂客户环境中,一个 H5 页面可能存在于多个不同的 App 项目(可能是正式项目与测试项目,或一个集团内的多个业务线)中,各个 App 使用的神策服务器地址可能不同,并且都需要进行打通,目前上述的几种方案,都无法满足。

同时,Apple 开始准备禁用 UIWebView(参考:ITMS-90809: Deprecated API Usage – Apple will stop accepting submissions of apps that use UIWebView APIs),越来越多的 App 开始从 UIWebView 迁移到 WKWebView 。但是,我们的 App 与 H5 打通方案,在 WKWebView 打通中存在一些遗留问题,影响了客户的使用体验。

面对客户诉求和 Apple 的新规定,我们的 App 与 H5 打通方案,也亟待再次进行优化。因此,第四版打通方案应运而生。

原理:

通过技术调研,我们发现在 iOS 的 WKWebView 中,Apple 在 js runtime 环境里事先注入了一个 window.webkit.messageHandlers.xxxx.postMessage() 方法,我们可以使用这个方法直接向 Native 层传值。基于这一原理,iOS 开发中 Native 与 H5 的交互可以使用全新的解决方案。iOS 已经将 window.webkit.messageHandlers 向 H5 共享了当前的 webkit 环境,只要在 WKWebView 的 configuration.userContentController 中注入实现 WKScriptMessageHandler 协议的对象(即 JSBridge),H5 端通过调用 postMessage() 方法,即可直接向 Native 发送消息。

为了减轻客户开启 App 与 H5 打通的接入成本,方案四使用动态 swizzle 技术 hook 了 WKWebView 加载 URL 的方法。在拿到当前 WKWebView 对象后自动注入 JSBridge,从而避免在每个 WebView 中单独调用,提升 SDK 的集成体验。

在复杂的客户环境,针对不同校验策略的诉求,方案四使用了白名单策略。JS SDK 提供了接口设置 serverURL 的白名单集合,只要 App 设置的 serverURL 包含在白名单中,则集成 JS SDK 的 H5 都可以打通成功,巧妙地兼容了同一个 H5 需要在不同 App 项目中进行打通的场景。

方案四的主要流程如图 3-4 所示:

图 3-4 打通方案四的流程图

3.4.2.  实现

具体实现主要分为下面几个步骤:

1. 开启打通后,自动执行 swizzle:

- (void)swizzleWebViewMethod {
    static dispatch_once_t onceTokenWebView;
    dispatch_once(&onceTokenWebView, ^{
        NSError *error = NULL;
        [WKWebView sa_swizzleMethod:@selector(loadRequest:) withMethod:@selector(sensorsdata_loadRequest:) error:&error];
        /* 为了兼容不同调用方式,还需要 hook 以下三种方法,上文已做介绍,此处不再赘述:
         loadHTMLString:baseURL:
         if (@available(iOS 9.0, *)) {
            loadFileURL:allowingReadAccessToURL:
            loadData:MIMEType:characterEncodingName:baseURL:
         }
         */
    });
}

2. WKWebView 加载 H5,调用 swizzle 的方法,注入 SAScriptMessageHandler  对象(JavaScriptBridge)到 ScriptMessageHandler:

- (WKNavigation *)sensorsdata_loadRequest:(NSURLRequest *)request {
    [[SensorsAnalyticsSDK sharedInstance] addScriptMessageHandlerWithWebView:self];
    return [self sensorsdata_loadRequest:request];
}
// 注入 JavaScriptBridge 到 WKWebView
- (void)addScriptMessageHandlerWithWebView:(WKWebView *)webView {
   /* webView 合法性判断等处理,细节略 ... */
    WKUserContentController *contentController = webView.configuration.userContentController;
    [contentController removeScriptMessageHandlerForName:SA_SCRIPT_MESSAGE_HANDLER_NAME];
    [contentController addScriptMessageHandler:[SAScriptMessageHandler sharedInstance] name:SA_SCRIPT_MESSAGE_HANDLER_NAME];
    
    if (![self.network.serverURL isKindOfClass:[NSURL class]] || ![self.network.serverURL absoluteString]) {
        return;
    }
    NSMutableString *javaScriptSource = [NSMutableString string];
    [javaScriptSource appendString:@"window.SensorsData_iOS_JS_Bridge = {};"];
    [javaScriptSource appendFormat:@"window.SensorsData_iOS_JS_Bridge.sensorsdata_app_server_url = '%@';", [self.network.serverURL absoluteString]];
    /* 判断是否已经被注入,防止重复注入等,细节略 ... */
  
    // forMainFrameOnly:标识脚本是仅应注入主框架(YES)还是注入所有框架(NO)
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:[NSString stringWithString:javaScriptSource] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [contentController addUserScript:userScript];
}

3. JS SDK 发送埋点数据:

if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.sensorsdataNativeTracker && window.webkit.messageHandlers.sensorsdataNativeTracker.postMessage && _.isObject(window.SensorsData_iOS_JS_Bridge) && window.SensorsData_iOS_JS_Bridge.sensorsdata_app_server_url) {
    // 判断 serverURL 是否校验通过
    if (sd.bridge.is_verify_success) {
        window.webkit.messageHandlers.sensorsdataNativeTracker.postMessage(JSON.stringify({
            callType: 'app_h5_track',
            data: _.extend({
                server_url: sd.para.server_url
            }, originData)
        }));
        (typeof callback == = 'function') && callback();
    }
}

4. 在 SAScriptMessageHandler 中接收 js 发送的埋点数据:

// 实现 WKScriptMessageHandler 协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
        
    // 获取 JS 发送的埋点数据
    NSString *body = message.body;
    /*
    1. 数据合法性校验等判断
    2. JSON 数据解析并获取埋点事件数据,再转为 JSON String
    */
    NSString *trackMessageString = [[NSString alloc] initWithData:trackMessageData encoding:NSUTF8StringEncoding];
    // 加工 js 埋点数据
    [[SensorsAnalyticsSDK sharedInstance] trackFromH5WithEvent:trackMessageString];
}

3.4.3. 优缺点

优点:

  1. 使用 WKWebView 的相关 API 实现,相对稳定可靠;
  2. 一个公司的多个 APP,可以通过配置 H5 白名单,实现不同 ServerURL 的 App 都能打通 H5;
  3. 如果开启 App 与 H5 打通,只需要通过初始化配置开关设置即可,不需要对每个 WebView 重复调用,方便客户集成。

缺点:

  1. 目前通过 swizzle 方案 hook 了 WKWebView 加载 H5 的方法,如果 swizzle 逻辑出现问题,可能导致 WKWebView 加载 H5 失败(目前测试和使用过程中均未发现);
  2. 方案只支持 WKWebView 。

4. 总结

经过艰难的摸索和持续的迭代后,iOS SDK 的 H5 打通方案目前趋于稳定。当然,目前的方案只是在当前环境的最优选择。对于以后的业务变化和技术发展,我们可能会面临新的挑战和难题。如果等到那一天,我们能做的也是积极地迎接挑战,再次投入攻关和调研,找到适合我们的选择。这个持续探索的过程,既是对方案的一次次革新,也是提高自我认知和积累技术的过程。一路走来,感慨良多,想起同事常说的一句话:“做难事,必有所得”。

5. 本文作者

神策数据 | iOS SDK 研发工程师

我叫储强盛,神策数据 iOS SDK 研发工程师,参与神策 iOS SDK 和 macOS SDK 的开发和维护。希望通过开源社区平台,与大家分享和探讨,互相学习,共同进步。生活中,喜欢足球、爬山、羽毛球等运动的奔放,爱好钓鱼、阅读的幽静,保持精气神积极向上,动静结合,张弛有度。当然,最近痴迷健身。

目前我们仍然还有很多技术难题有待攻克,在此我们也做了很多有趣的事情,神策数据 iOS 战队,竭诚期待您的加入。

5、交流合作

本文著作权归神策数据开源社区所有。商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区公众号二维码。
你还可以扫描二维码,加入社区交流群,与大家一同讨论。
也欢迎关注我们的公众号,博客更新尽在掌握。