1. 前言

近年来,混合开发越来越流行,App 与 H5 的打通需求也越来越迫切。

那什么是 App 与 H5 打通呢?

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

本文的内容,主要是回答以下两个问题:

  • App 与 H5 为什么要打通?
  • App 与 H5 该如何打通?

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 标识匿名用户的规则不一样(Android 一般使用 Android ID,H5 一般使用 Cookie),进而导致一个用户出现两个匿名 ID 的情况。如果 App 与 H5 打通,就可以将两个匿名 ID 做归一化处理(以 App 端匿名 ID 为准)。

2.4. 基础功能

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

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

3. 打通方案演进

在打通方面,神策积累了丰富的经验,同时也踩了许多的坑。目前摸索出了三种打通方案,我们将按照技术演进的顺序为大家一一介绍这几种方式,并分析其背景、原理和不足。

3.1. 早期版本(1.0)

3.1.1. 背景和原理

上一节介绍了为什么要进行 H5 打通,其中有一个点非常关键,“App 与 H5 打通,就可以将两个匿名 ID 做归一化处理”,简单而言就是使用 App 的用户 ID 去标识 H5 的行为,即将 H5 传到服务端的数据添加上 App 的用户 ID,然后上传到服务端,从而统一移动端的用户行为。

本着让 H5 产生的事件数据使用 App 的用户 ID 的思路,首先想到的是将 App 的用户信息发给 H5,神策的早期打通方案也确实是这么做的。基本原理是将 JSBridge 注入到 WebView(读者可查看官方的 Building web apps in WebView 了解 Android 和 H5 页面相互调用的操作),JSBridge 中提供方法给 H5 中的 JS 调用,提供的方法会返回 is_login(标识客户是否在 App 登录)、distinct_id(用户 ID)等信息。如图 3-1 所示:


图 3-1 早期版本的打通方案

图 3-1 描述了早期版本的打通方案,就是将 App 的用户信息通过 JSBridge 传给 H5 ,然后由 H5 将用户信息添加到事件中从而实现 App 和 H5 页面的用户标识的统一。此种方式对应的代码片段如下:

注入 JSBridge
webView.addJavascriptInterface(new AppWebViewInterface(mContext, properties), "SensorsData_APP_JS_Bridge");
JSBridge 类
class AppWebViewInterface {
    private static final String TAG = "SA.AppWebViewInterface";
    private Context mContext;
    private JSONObject properties;
    AppWebViewInterface(Context c, JSONObject p) {
        this.mContext = c;
        this.properties = p;
    }
    @JavascriptInterface
    public String sensorsdata_call_app() {
        try {
            if (properties == null) {
                properties = new JSONObject();
            }
            properties.put("type""Android");
            String loginId = SensorsDataAPI.sharedInstance(mContext).getLoginId();
            if (!TextUtils.isEmpty(loginId)) {
                properties.put("distinct_id", loginId);
                properties.put("is_login"true);
            else {
                properties.put("distinct_id", SensorsDataAPI.sharedInstance(mContext).getAnonymousId());
                properties.put("is_login"false);
            }
            return properties.toString();
        catch (JSONException e) {
            SALog.i(TAG, e.getMessage());
        }
        return null;
    }
}

3.1.2. 方案缺陷

在了解了这种打通方案后,你可能会注意到这种方式是 H5 直接将数据发送到服务端,跟我们在第一章中介绍的 “进行 H5 打通可以降低数据丢失率” 正好相反;同时你也可能注意到一旦将 WebView 注入了 JSBridge 对象后,那么这个 WebView 加载的所有 H5(这里特指集成了神策 Web JS SDK 的页面) 都会使用 App 提供的 is_login 和 distinct_id 字段。假如 WebView 加载了另外一家集成了神策 Web JS SDK 的 H5 页面就会出现很大的问题,因为 App 提供的信息添加在了另外一家客户的 H5 里,这样就会给另外一家客户带来很大的麻烦。

注意

神策 Web JS SDK 提供了是否打通的标志位,可以选择性的对部分 H5 页面进行打通,这一点在上面的流程图中没有体现,本篇文章默认 H5 的标志位都是打通的。

3.2. 中期版本(2.0)

3.2.1. 背景和原理

1.0 方案介绍了 H5 打通的早期版本的实现方式以及存在的两个问题:一是数据是通过 H5 页面发送的;二是无差别的对待方式会给其他客户的 H5 带来很大的麻烦。为了解决这两个问题,我们修改了 1.0 方案。

首先我们将 H5 页面产生的数据发送到 App,接着 App 端提供校验标识位,用来判断是否校验 H5 数据的数据接收地址和 App 端的数据接收地址。修改上面的流程图,如图 3-2 所示:

图 3-2 中期版本的打通方案

图 3-2 描述了 2.0 版本的逻辑,首先 JSBridge 对象提供了 boolean sensorsdata_verify(String event) 方法用来接收和校验 H5 数据,注意这个方法的返回值,true 表示校验通过,数据会通过 App 发送;false 表示校验未通过,数据会通过 H5 发送。通过这种方式解决了客户自己的 H5 数据可以通过 App 发送,对于其他集成了神策 Web JS SDK 的 H5 页面,因为校验 server_url 不通过,H5 自己发送数据。

了解了原理以后,我们来看一下代码实现:

注入 JSBridge
webView.addJavascriptInterface(new AppWebViewInterface(mContext, enableVerify), "SensorsData_APP_JS_Bridge");

此处代码是给 WebView 注入 JSBridge 对象,注意 AppWebViewInterface 构造方法中有一个 enableVerify 参数,作用是 App 端控制是否需要校验,我们再看 AppWebViewInterface 的代码:

AppWebViewInterface 类
class AppWebViewInterface {
    private static final String TAG = "SA.AppWebViewInterface";
    private Context mContext;
    private boolean enableVerify;
    AppWebViewInterface(Context c, boolean b) {
        this.mContext = c;
        this.enableVerify = b;
    }
    @JavascriptInterface
    public boolean sensorsdata_verify(String event) {
        try {
            if (!enableVerify) {
                sensorsdata_track(event);
                return true;
            }
            return SensorsDataAPI.sharedInstance(mContext)._trackEventFromH5(event);
        catch (Exception e) {
            SALog.printStackTrace(e);
            return false;
        }
    }
}

这里要注意的是 sensorsdata_verify 方法,当 enableVerify 为 false 的时候表示不校验。因此只要是 H5 发过来的任何数据都通过 App 发送,并且 H5 调用这个方法得到的返回值为 true,表示数据已经在 App 端处理了,H5 将不会再发送此条数据;如果 enableVerify 为 true 的时候,App 会校验 H5 发送数据的 server_url 和 App 的 server_url 是否相同。如果相同也会返回 true 表示 App 处理此条数据,如果不同会返回 false,表示校验失败,数据还是通过 H5 端去发送。

3.2.2. 方案缺陷

通过上一节的原理介绍和代码展示可以发现几个问题

  • 假如 App 端调用如下代码为 WebView 注入 JSBridge :

    webView.addJavascriptInterface(new AppWebViewInterface(mContext, false), "SensorsData_APP_JS_Bridge");

    其中,enableVerify 总是设置为 false,那么还是会存在其他客户集成神策 Web JS SDK 的 H5 数据发送到 App 上,造成其他客户数据的丢失以及当前客户脏数据的增多;

  • 假如客户的 App 中有很多 WebView 需要打通,那么我们就需要给每一个 WebView 调用上面这段代码,显得不够优雅;
  • 还有一个原因使得我们必须去改善,那就是我们的可视化全埋点功能依赖于打通功能,或者说就算不打通也希望能做到客户使用可视化全埋点功能的时候可以提示客户去打通。关于可视化全埋点功能可以参考我们官网的『可视化全埋点介绍』。

3.3. 成熟版本(3.0)

3.3.1. 背景和原理

我们可以看到 2.0 版本是对 1.0 版本缺陷的一个改善,但是并没有解决当客户不校验数据时产生的 “其他客户集成有神策 Web JS SDK 的 H5 页面发送到 App 上,造成其他客户数据的丢失以及当前客户脏数据的增多” 问题。如果 App 中 WebView 有很多,不得不为每一个 WebView 都注入 JSBridge,使得客户的工作量会很大。同时,也需要为可视化全埋点功能做好技术准备。那么为了解决这些问题,神策对 2.0 版本进行了升级,具体方案分两步:

  1. 为每个 WebView 建立一个通道。这个通道不光可以注入 AppWebViewInterface Bridge 还可以注入其他的 JSBridge,例如可视化全埋点功能需要的 JSBridge;
  2. 更改数据校验规则。2.0 版本是将校验放在 App 端,现在将校验放在 H5 端,由 H5 端来判断是否需要将数据发送到 App,而 App 只提供 H5 端用于校验的 server_url(server_url 是服务端地址,采集的数据会发往该地址)

此方案的流程如图 3-3 所示:


图 3-3 成熟版本的打通方案

方案的第一步是建立通道,这需要用到神策 SDK 插件从字节码层面上去实现。具体原理是插件扫描 class 文件中的方法,方法中如果有类似 webview.loadUrl(String url) 这样的方法,我们会将其替换成 SensorsDataAutoTrackHelper.loadUrl(webview, url) ,这个方法就是我们建立的通道,代码如下

SensorsDataAutoTrackHelper
public static void loadUrl(View webView, String url) { //webView 可能是原生的 Android WebView 也可能是腾讯 X5WebView,这里设置类型为 View,是为了做兼容
     if (webView == null) {
         throw new NullPointerException("WebView has not initialized.");
     }
     setupH5Bridge(webView); //设置 JSBridge
     //... 其他类型的 JSBridge
     invokeWebViewLoad(webView, "loadUrl"new Object[]{url}, new Class[]{String.class});//通过反射调用 webview.loadUrl
}
private static void setupH5Bridge(View webView) {
     if (SensorsDataAPI.sharedInstance() instanceof SensorsDataAPIEmptyImplementation) {
         return;
     }
     if (isSupportJellyBean() && SensorsDataAPI.sharedInstance().getConfigOptions() != null && SensorsDataAPI.sharedInstance().getConfigOptions().isAutoTrackWebView) {
         setupWebView(webView);
     }
     if (isSupportJellyBean()) {
         addWebViewVisualInterface(webView);
     }
 }
 private static void invokeWebViewLoad(View webView, String methodName, Object[] params, Class[] paramTypes) {
     try {
         Class<?> clazz = webView.getClass();
         Method loadMethod = clazz.getMethod(methodName, paramTypes);
         loadMethod.invoke(webView, params);
     catch (Exception e) {
         SALog.printStackTrace(e);
     }
 }

上面的代码是神策 Android SDK 中 SensorsDataAutoTrackHelper 类的代码片段。当集成了神策 Android SDK 插件后,插件会将如下的代码替换掉:

H5Activity
class H5Activity : BaseActivity() {
    private val TAG: String = "H5Activity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_h5)
        androidWebView.loadUrl("https://www.sensorsdata.cn") 此处的代码将会被插件替换成  SensorsDataAutoTrackHelper.loadUrl(androidWebView, "https://www.sensorsdata.cn")
    }
}

通过这种操作,我们将原始加载页面的方法替换为通道方法,并且在通道方法中通过反射的方式去调用原本加载页面的逻辑。当然,我们不光可以在通道中注入打通的 JSBridge,还可以注入其他业务需要的 JSBridge,而且不需要客户再去写代码来实现了。

上面介绍了建立通道的方案,那么如何实现在 H5 端进行校验呢,这个比较容易实现了,看一下我们的 JSBridge 类的代码:

AppWebViewInterface
class AppWebViewInterface {
    private static final String TAG = "SA.AppWebViewInterface";
 
    AppWebViewInterface() {
   
    }
    //提供给 H5 端调用,用来获取 App 配置的 server_url
    @JavascriptInterface
    public String sensorsdata_get_server_url() {
        return SensorsDataAPI.sharedInstance().getConfigOptions().isAutoTrackWebView ? SensorsDataAPI.sharedInstance().getServerUrl() : "";
    }
}

可以看到我们只提供了一个 sensorsdata_get_server_url 方法,H5 会调用此方法获取 App 的 server_url,然后与自己的白名单列表对比。如果白名单中存在此 server_url,就认为校验通过,数据会发往 App;如果 App 的 server_url 为空或者跟自己的白名单不匹配就认为校验失败,H5 直接发送数据。通过这种方式把校验的主动权放在了 H5 端,解决了 “其他客户集成有神策 Web JS SDK 的 H5 页面发送到 App 上,造成其他客户数据的丢失以及当前客户脏数据的增多” 这个问题。关于神策 Web JS SDK 的信息可以参考这里

3.3.2. 方案缺陷

从上一节的介绍能够知道神策 Android 插件需要扫描方法里的代码,这增加了插件编译的时间,不过目前没有更好的办法。后面会将更多需要在 SDK 端完成的配置通过插件来完成,尽量让客户不写代码或者尽量少些代码就能使用我们的 SDK 功能。

4. 总结

神策 Android SDK 的 H5 打通方案演进可以说是建立在业务驱动和需求的基础上不断发展的,现在到了 3.0 版本。从目前来看,此版本的思路更好、扩展性也更好。未来有没有更好的方案还是要看具体的业务需要,我们也会持续探索,看看有没有更好的方案。读者如果有更好的想法,也希望能加入开源社区与我们分享。

5. 本文作者

神策数据 | Android 研发工程师

我是张伟,神策数据 Android 研发工程师,主要从事神策 Android SDK 和 Android Plugin 的开发工作,希望通过开源社区这个平台与大家共同学习进步。生活中热爱篮球、看书、旅游,希望我们相聚神策,一起维护神策开源社区和打篮球。

 

6、交流合作

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