1. 前言

在 App 的运营活动中,对用户进行弹窗提示,是一种常见的运营方式。例如:用户已经下单但未付款的时候,可以给用户一个优惠券的弹窗提示。

神策 Android 弹窗 SDK[1] 主要针对的就是上述运营场景,运营人员可以在神策智能运营中配置弹窗的 UI 以及触发弹窗的一些条件,当用户满足配置的条件时,集成了弹窗 SDK 的 App 会展示弹窗。UI 效果如图 1-1 所示:

图 1-1 弹窗 UI 效果图

2. 弹窗的实时性

在很多场景下,弹窗需要很高的实时性。如果弹窗的计算规则通过后端处理,符合条件时再下发给客户端,实时性将得不到保障。

为了解决这一问题,把计算逻辑等放在了客户端。简单来说,就是触发埋点事件之后会判断是否触发弹窗。

此外,弹窗 SDK 为了保证实时性,也做了很多工作,下面就逐一为大家进行介绍。

3. 方案演进

3.1. 方案一:共用埋点数据采集线程

触发弹窗的时机,取决于运营同学在神策智能运营中的配置,那弹窗 SDK 如何来决定是否弹窗呢?

举个例子,运营同学的配置条件是:有商品页面的浏览数据就弹窗。因此,在 App 端监控到有商品页面浏览的埋点数据产生就会弹窗。

埋点数据采集任务是在埋点数据采集线程中执行的。因此,最初的想法是在埋点数据采集线程中监控埋点数据,如果有符合弹窗条件的数据,那么就展示弹窗。示例代码如下

//判断是否弹窗
PlanManager.ensureShowDialog(data);
//数据缓存到数据库
enqueueEventMessage(data);

这种做法很简单,也能满足需求。它的优点如下:

  1. 从代码上来看,逻辑比较清晰;
  2. 判断是否弹窗和埋点数据采集都是在一个线程,方便维护。

但是,它的缺点也很明显:

  1. 如果在判断是否弹窗这一步,有阻塞或者异常,那么会影响埋点数据缓存到数据库;
  2. 耦合度非常高。现在是弹窗的业务需要监控数据,如果将来其他业务也需要监控数据,那么在埋点数据缓存之前,还需要增加更多的业务逻辑。

为了解决上述问题,我们拆分了埋点数据采集线程和弹窗判断线程。

3.2. 方案二:拆分埋点数据采集线程和弹窗判断线程

考虑到共用埋点数据采集线程的缺点,我们做了线程的拆分,如图 3-1 所示:

图 3-1  线程拆分示意图

此时,埋点数据采集线程和弹窗判断线程,是两个独立的线程。当埋点数据采集线程有新的数据时,会主动通知弹窗判断线程,让其处理弹窗业务。

这样做不仅降低了耦合度,并且弹窗业务不会影响到埋点数据采集。即使最极端的情况,比如弹窗判断线程因为某些原因出现了异常,埋点数据采集线程仍然能正常工作。

埋点数据采集线程只需要根据接口,回调给数据接收端就行,示例代码如下:

// 监控数据,并传给注册接口的地方
DataMonitorInterface.trackEvent(data);
// 数据缓存到数据库
mMessages.enqueueEventMessage(eventType.getEventType(), dataObj);

在弹窗判断线程中,收到数据后会缓存在队列,示例代码如下:

public class SFDataMonitorImpl{
    public void trackEvent(String data){  
        mSFPlanTaskManager.addTriggerTask(new Runnable() {
         //判断是否弹窗
            PlanManager.ensureShowDialog(data)
        }
   }
}

在新的线程中去执行此队列的任务,示例代码如下:

public class SFPlanTriggerRunnable implements Runnable {  
    @Override
    public void run() {
        Runnable downloadTask = mSFPlanTaskManager.getTriggerTask();
        mPool.execute(downloadTask);
    }
}

这种方案看上去已经很完美了,降低了埋点数据缓存和弹窗业务之间的耦合,并且代码上也做了拆分,互相之间的影响很小,同时也能满足各种业务场景。

但是,在测试过程中,我们发现其实弹窗 SDK 的网络请求是最耗时的一步,为了提高实时性,需要进行优化

这一步主要是为了请求后端,拿到业务同学配置的弹窗信息。要介绍这一部分的优化,首先需要对弹窗 SDK 运行的流程有所了解,如图 3-2 所示

图 3-2  弹窗 SDK 运行流程图

大致流程如下:

  1. 弹窗 SDK 初始化后,首先会读取本地缓存的弹窗数据;
  2. 在 App 进入前台时,会请求后端的弹窗数据,请求完成后会把后端返回的弹窗数据和本地的弹窗数据做对比,只取更新的部分;
  3. 接着处理埋点数据的任务。

将埋点数据采集和弹窗判断放到各自的串行队列,具有如下优点:

  1. 里面的任务会按照我们添加的顺序进行执行;
  2. 一般情况下,不需要考虑并发导致数据不安全的问题。

但是,也有缺点:

  1. 当串行队列中的某一个任务发生阻塞时,其后的任务都会延迟执行,特别是此队列中还存在网络请求的任务。因为需要使用网络请求的结果,所以当网络请求完成后才能继续处理其他任务
  2. 所有的任务都由一个线程来执行,特别在初始化的时候,串行队列的负担会比较重,除了图 3-2 中的两次 IO 操作,还有其他的 IO 操作,以及弹窗判断等任务。

那么,在此基础上还可以优化吗?答案是肯定的。

3.3. 方案三:抽离数据加载线程

其实,仔细思考之后可知:弹窗数据请求的任务不必和弹窗判断在同一个串行队列。当完成本地弹窗数据读取之后,就可以启动弹窗判断线程。

至于网络请求,本身是不可靠的。在弹窗的业务中,如果有本地数据,那么就用本地数据,不必等到网络的数据返回后再处理弹窗业务。

基于以上的思路,将数据加载线程抽离,读取到本地数据后,就进行业务的处理,详情参考示意图 3-3

图 3-3   抽离数据加载线程示意图

流程看上去比之前的方案要复杂,同时牵扯到三个线程,但只要捋清楚它们之前的关系,以及在什么时候进行通信,就比较好理解:

  1. 初始化 SDK 后,我们启动了两个线程,分别是数据加载线程和弹窗判断线程,并且让弹窗判断线程处于等待状态,而让数据加载线程去加载本地的弹窗数据;
  2. 加载数据完成后,如果数据不为空,那么启动弹窗判断线程;
  3. 当 App 进入前台时,通知数据加载线程,加载网络请求返回的数据。

这里有一个场景需要特别注意:弹窗数据正在加载中,同时产生了埋点数据。此时需要根据弹窗数据判断埋点数据是否应该弹窗,但是弹窗数据还没有加载成功,应该怎么办呢?

此时,应该让埋点的数据先缓存在队列中。只有当弹窗数据加载完成后,才会执行缓存队列中的任务,这也是弹窗判断线程启动后就让它等待的原因。另外,在数据更新的过程中,因为更新的是同一份数据,所以也需要对这一步加锁。

在初始化 SDK 内部,等到弹窗数据加载成功时,才会启动弹窗判断线程。示例代码如下:

readLocalPlanData(new Callback() {
    @Override
    public void onFailure(int code, String errorMessage) {
        
    }
    @Override
    public void onResponse(String response) {
        //弹窗判断线程启动
        new TriggerThread().start();
    }
});

其他的代码和方案二中的代码很类似,此处不再展示。

需要注意的是:如果使用了线程池,线程池也会缓存任务。如果有业务场景需要停止线程,那么就不能让线程池缓存任务,可以让线程阻塞执行:

public class SFPlanTriggerRunnable implements Runnable {  
    @Override
    public void run() {
        Runnable downloadTask = mSFPlanTaskManager.getTriggerTask();                
        mPool.submit(downloadTask).get();
    }
}

这种方案将数据加载线程抽离,解决了网络请求阻塞弹窗判断的场景,它有两个优点:

  1. 使线程的职责更加清晰;
  2. 网络请求弹窗数据,不再阻塞弹窗线程。

当然,也有缺点:

  1. 代码变得比较复杂;
  2. 需要保证并发场景下数据的安全性。

不过,基于目前的方案,既能降低延迟,也能保证业务上的需求,目前看来是一次成功的改造。

4. 总结

本文讲述了为了保证弹窗的实时性,弹窗 SDK 线程相关方案的演进过程。

弹窗 SDK 的实时性,经过不断努力终于取得了一定的成果。但在实际使用过程中,我们仍然碰到了不少难点:前后台判断的准确性、弹窗被遮盖等问题。不过,我们有信心在不久的未来一定会突破这些难点。

很多场景,只要我们朝着目标不断前行,不断优化自己的方案与代码,总能达成目标。

仔细斟酌,勇于尝试,犹如心有猛虎,细嗅蔷薇。

5. 参考文献

[1]https://github.com/sensorsdata/sf-sdk-android

 

文章来自公众号——神策技术社区