iOS SDK 的 H5 打通方案演进 | 数据采集

一、前言 在介绍 iOS SDK 的 H5 打通方案之前,我们先了解一下什么是 App 与 H5 打通。
所谓 “打通”,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不是直接同步给服务端,而是先发给 App 端的数据采集 SDK,经 App 端数据采集 SDK 二次加工处理后缓存到本地,再经过合适的上传策略同步到服务端。
二、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 如何进行打通。
三、打通方案演进 关于 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 所示:
iOS SDK 的 H5 打通方案演进 | 数据采集
文章图片

图 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
@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 )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 优缺点
优点:
由于 iOS SDK 不用处理 JS 端的数据,只需要传少量信息到 JS 端,对 iOS SDK 的侵入较小;
可以同时兼容 UIWebView 和 WKWebView,满足更多客户需求。
缺点:
H5 产生的埋点数据,继续由 JS SDK 上报到服务端,数据丢失的风险较大;
如果 WebView 加载完成,JS SDK 尚未加载,会导致打通失败;
只要开启打通,iOS SDK 就会把 App 端的匿名 ID 发给 JS SDK 。由于不会区分项目,这样可能会导致数据错乱。例如,客户 A 的 App 内嵌了客户 B 的 H5,可能导致客户 B 的 H5 页面使用了客户 A 的匿名 ID,然后数据还是发到了客户 B 的服务端,导致客户 B 的用户数据错乱;
对于客户来说打通的集成比较复杂。例如,如果客户 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 所示:
iOS SDK 的 H5 打通方案演进 | 数据采集
文章图片

图 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)判断 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 优缺点
优点:
这种打通方案,由于 iOS SDK 不用处理 JS 端的数据,只需要传少量信息到 JS,对 iOS SDK 的侵入较小;
可以同时兼容 UIWebView 和 WKWebView,满足更多客户需要;
支持 JS SDK 在 H5 页面异步加载情况下的 App 与 H5 打通。
缺点:
H5 产生的埋点数据,继续由 JS SDK 上报到服务端,数据丢失的风险较大;
只要开启打通,JS SDK 就会把 App 的匿名 ID 发给 JS SDK 。由于不会区分项目,这样可能会导致数据错乱。例如,客户 A 的 App 内嵌了客户 B 的 H5,可能导致客户 B 的 H5 页面使用了客户 A 的匿名 ID,然后数据还是发到了客户 B 的服务端,导致客户 B 的用户数据错乱;
对于客户来说打通的集成比较复杂。例如,客户 App 项目中使用了多个 UIWebView 或 WKWebView ,需要在多处实现协议方法并调用 SDK 接口,接入的工作量会比较大。
3.3 方案三 3.3.1 背景和原理
对于上述两种方案,对功能影响较大的两个问题是:
App 与 H5 打通后,App 发送匿名 ID 给 JS SDK 时不区分项目,对其他客户数据造成较大影响;
H5 的埋点数据通过 JS SDK 上报,数据丢失的风险较大,并且部分数据无法采集。
为了解决上述两个问题,我们对 App 与 H5 打通方案做了较大的修改,从而推出了方案三:
为了解决上述第一个问题,我们单独增加了开启打通的接口。如果客户调用并开启打通,iOS SDK 会修改当前 App 环境的 UA 值,拼接当前接入 SA 的 project(项目名) 和 host(域名)。这样 JS SDK 可以判断当前 H5 页面是否需要打通,并且还可以校验是否为同一个项目;
为了解决上述第二个问题,我们在 JS SDK 增加是否需要打通的判断,如果需要打通就将 H5 产生的埋点数据发送到 App,由 iOS SDK 处理并缓存到本地,然后根据合适的策略上传到服务端。
原理:
iOS SDK 修改 UA 来标记当前 App 集成神策的项目信息,JS SDK 根据 UA 值判断是否需要打通。如果需要打通,JS SDK 将埋点数据发往 App,iOS SDK 解析并处理 JS SDK 端产生的埋点数据,再根据合适的策略上报到服务端。主要流程如图 3-3 所示:
iOS SDK 的 H5 打通方案演进 | 数据采集
文章图片

图 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 优缺点
优点:
H5 产生的事件数据,iOS SDK 先经过了加工:使用 App 的匿名 ID,增加设备信息、网络和运营商信息等,从而提高了数据采集的准确性和完整性,保证了匿名 ID 的一致,为后续的行为序列分析提供了可靠的数据基础;
通过 iOS SDK 缓存并上报,很大程度上降低了数据丢失的风险;
JS SDK 解析 H5 环境的 UA 标识判断是否打通,避免了 App 使用其他客户的 H5 可能存在的数据丢失或产生脏数据问题。
缺点:
对于每个 WebView,都需要在协议方法中调用接口拦截请求,如果一个项目有多个 WebView,集成工作相对繁琐;
如果客户项目禁用 UIWebView,WKWebView 目前只支持异步获取 UA 再进行修改,可能会导致两个问题:如果客户也需要修改 UA,可能会导致打通失败或者客户修改 UA 失败;如果 App 首页加载 WKWebView,这个页面的 H5 会打通失败。
JS SDK 发送的 iframe 请求,在客户 App 环境中,可能被误判为非法请求并进行拦截,导致打通失败。
3.4 方案四 3.4.1 背景和原理
随着客户数量的快速增加,越来越复杂的使用场景和客户环境,给我们的 App 与 H5 打通方案带来新的考验。在某些复杂客户环境中,一个 H5 页面可能存在于多个不同的 App 项目(可能是正式项目与测试项目,或一个集团内的多个业务线)中,各个 App 使用的神策服务器地址可能不同,并且都需要进行打通,目前上述的几种方案,都无法满足。
同时,Apple 准备禁用 UIWebView[1],越来越多的 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 所示:
iOS SDK 的 H5 打通方案演进 | 数据采集
文章图片

图 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 优缺点
优点:
使用 WKWebView 的相关 API 实现,相对稳定可靠;
一个公司的多个 APP,可以通过配置 H5 白名单,实现不同 ServerURL 的 App 都能打通 H5;
如果开启 App 与 H5 打通,只需要通过初始化配置开关设置即可,不需要对每个 WebView 重复调用,方便客户集成。
缺点:
目前通过 swizzle 方案 hook 了 WKWebView 加载 H5 的方法,如果 swizzle 逻辑出现问题,可能导致 WKWebView 加载 H5 失败(目前测试和使用过程中均未发现);
方案只支持 WKWebView 。
四、总结 经过艰难的摸索和持续的迭代后,iOS SDK 的 H5 打通方案目前趋于稳定。当然,目前的方案只是在当前环境的最优选择。对于以后的业务变化和技术发展,我们可能会面临新的挑战和难题。如果等到那一天,我们能做的也是积极地迎接挑战,再次投入攻关和调研,找到适合我们的选择。
这个持续探索的过程,既是对方案的一次次革新,也是提高自我认知和积累技术的过程。一路走来,感慨良多,想起同事常说的一句话:“做难事,必有所得。”
参考文献:
[1]ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs.https://developer.apple.com/f...
【iOS SDK 的 H5 打通方案演进 | 数据采集】文章来源:公众号神策技术社区

    推荐阅读