一、前言
在介绍 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 所示:
图 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] showU