关于前端:数据上报那些事

5次阅读

共计 5152 个字符,预计需要花费 13 分钟才能阅读完成。

1. 前言

神策剖析是依靠于数据进行的,数据是剖析的根基。因而,数据上报的时效性是至关重要的。那么 iOS SDK(前面简称 SDK)是如何保证数据上报的时效性呢?

接下来,咱们就围绕这个问题来看看 SDK 到底做了什么。

2. 上报策略

直观来说,要解决数据上报的时效性问题仿佛很简略:实时上报(当触发事件后立即上报到服务端)不就能够保障时效性了吗?然而,事实并非如此简略。

不同于服务端,挪动设施上的资源是十分无限的,采取实时上报的形式势必会造成 App 整体性能的降落,如何均衡性能与数据上报的时效性是 SDK 须要面临的一个挑战。

目前 SDK 中应用的数据上报策略是事件触发后不立刻上报,而是先将事件缓存在本地,而后满足肯定的条件再进行上报。

SDK 每次触发事件时会查看如下条件,用于判断是否向服务端上报数据:

以后网络是否合乎发送策略 flushNetworkPolicy(默认 3G、4G、5G、WiFi);
与上次发送的工夫距离是否大于指定的工夫距离 flushInterval(默认 15 秒);
本地缓存的事件条数是否大于最大缓存事件数 flushBulkSize(默认 100 条)。
只有 1、2 或者 1、3 满足时,SDK 才会发送数据。当然,为了满足不同的需要,能够通过批改 flushNetworkPolicy、flushInterval、flushBulkSize 的值来管制事件上报。

SDK 的数据上报流程如图 2-1 所示:

图 2-1 SDK 的数据上报流程图

3. 时效性优化

依照咱们指定的上报策略进行数据上报,对于个别的自定义埋点事件及全埋点事件是能够满足时效性的要求。然而,这种上报策略存在一些弊病:

App 退到后盾或终止后如果不再关上,最初未上报的数据不会及时地上报到神策剖析平台;
无奈满足一些事件的实时上报需要。
为了解决这两个问题,SDK 进行了如下的优化。

3.1. App 进入后盾时上报

33.1.1. iOS 后盾机制

在理解 App 进入后盾时如何上报之前,咱们先来看下 iOS 的后盾机制。iOS 零碎中,App 在执行时可能会呈现 Active、Inactive、Background、Not Running、Suspended 这几种状态 [1]。当咱们的 App 由前台进入到后盾时,会有 5 秒的工夫执行工作,在尔后 App 将被零碎置为挂起状态(Suspended)。此时 App 运行在后盾,但无奈执行代码。

对于大多数 App 来说,5 秒的工夫足够执行一些进入后盾的要害工作。思考一些 App 须要更多后盾工夫来解决工作,iOS 提供了用于缩短利用后盾执行工夫 [2] 的接口,能够申请额定的后盾运行工夫以保障工作执行实现。通过测试,在 iOS 12 及以上的零碎最多能够申请 30 秒的运行工夫。

3.1.2. 后盾数据上报

咱们曾经晓得 App 在进入后盾时会在很短的工夫内被零碎挂起,因为数据上报时网络环境的不确定性,很难保障 SDK 在 5 秒工夫内实现数据上报。因而,SDK 在 App 进入后盾时会被动申请 App 后台任务。代码如下所示:

UIApplication *application = UIApplication.sharedApplication;
__block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
void (^endBackgroundTask)(void) = ^() {

[application endBackgroundTask:backgroundTaskIdentifier];
backgroundTaskIdentifier = UIBackgroundTaskInvalid;

};
backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:endBackgroundTask];

dispatch_async(self.serialQueue, ^{
// 上传所有的数据
[self.eventTracker flushAllEventRecords];
// 完结后台任务
endBackgroundTask();
});

3.2. App 终止时上报

上一节讲述了 App 进入后盾时如何进行数据上报,如果 App 终止了,数据还能及时上报吗?答案是必定的。

App 终止之前,零碎会先收回 UIApplicationDidEnterBackgroundNotification 告诉。此时,SDK 会申请后台任务进行数据入库及上报。

接下来,零碎就会立即尝试终止后台任务,而 SDK 采集的退出事件($AppEnd)以及退到后盾时采集的一些自定义事件可能没有足够的工夫入库。这种状况岂但无奈实现数据上报,甚至会导致数据失落。因而,咱们须要在 App 行将终止时取得一段时间用于保留咱们的事件数据。

在 App 行将终止时零碎会收回 UIApplicationWillTerminateNotification[3] 告诉。因而,咱们只有监听该告诉并阻塞以后线程,从而实现数据保留及上报。代码如下所示:

  • (void)applicationWillTerminateNotification:(NSNotification *)notification {
    dispatch_sync(self.serialQueue, ^{});
    }

    3.3. 被动上报

    在 SDK 的预置事件里,有一些预置事件对时效性要求比拟高。例如:用来剖析 UV(日活)的 $AppStart(App 启动)事件,咱们心愿能够实时剖析到实在的 UV 数据。因而,在触发 $AppStart 事件时 SDK 会被动上报一次数据。

3.4. 手动上报

因为 SDK 被动上报只能针对一些预置事件做解决,因而 SDK 对外提供了一个接口用于自定义事件的被动上报。代码如下所示:

// 触发自定义事件

[[SensorsAnalyticsSDK sharedInstance] flush];

4. 踩过的坑

通过下面的介绍能够晓得,SDK 采取了很多形式用于保证数据上报的时效性。看起来仿佛曾经很完满了,然而在测试过程中还是发现了一些问题 …

4.1. App 终止时导致的问题

在测试过程中遇到一个问题:当 App 终止时,数据上报呈现了解体。

此时,咱们未免会有疑难:只是数据上报为什么会造成解体?真的是 SDK 的起因造成的吗?带着这些疑难咱们剖析了解体堆栈。如图 4-1 所示:


图 4-1 Watchdog 造成的零碎强杀

首先,咱们看到解体的起因是触发了零碎的 Watchdog[4] 机制从而导致 App 被零碎强杀,而此时 SDK 正在执行数据上报工作。

联合 SDK 源码咱们发现:App 终止时 SDK 为了保证数据上报胜利,会采取阻塞以后线程的形式来缩短 App 后盾存活工夫。代码如下所示:

  • (void)applicationWillTerminateNotification:(NSNotification *)notification {
    dispatch_sync(self.serialQueue, ^{});
    }
    而在弱网环境下数据可能始终无奈上报胜利,最终触发零碎的 Watchdog 机制,从而导致解体。

问题起因咱们曾经明确了,然而修复这个问题会波及到“鱼和熊掌不可兼得”的问题:是保证数据及时上传还是保障 App 不触发 Watchdog 机制?

因为这个场景是在 App 终止时产生的,自身并不会影响应用 App 的体验。其次,因为只在弱网环境下呈现,产生的概率较小。因而,在之前版本的 SDK 默认会强制上报所有数据,同时提供手动敞开的接口,敞开后退到后盾时不再上报数据。代码如下所示:

// 敞开后盾上报
options.flushBeforeEnterBackground = NO;

4.2. 事件上报导致的问题

同样是弱网问题。因为 SDK 数据上报和数据采集是在同一个串行队列,数据上报时会阻塞该队列,导致数据无奈失常入库,此时 App 终止可能会造成数据失落。

数据是剖析的根基,保证数据不失落是神策的红线。

鉴于 iOS 系统对后台任务愈发严格的要求以及强制上报数据造成的影响,最终咱们决定重构后盾上报的逻辑。次要有以下几点变动:

flushBeforeEnterBackground 作用扭转,由退到后盾是否上报数据更改为是否同步上报数据;
flushBeforeEnterBackground 默认值批改为 NO(即异步上报),不再阻塞以后线程。
代码如下所示:

  • (void)flushEventRecords:(NSArray<SAEventRecord *> *)records completion:(void (^)(BOOL success))completion {
    __block BOOL flushSuccess = NO;
    // 当设置 flushBeforeEnterBackground 为 YES 或 debug 模式下,应用线程锁
    BOOL isWait = self.flushBeforeEnterBackground || self.isDebugMode;
    [self requestWithRecords:records completion:^(BOOL success) {

      if (isWait) {
          flushSuccess = success;
          dispatch_semaphore_signal(self.flushSemaphore);
      } else {completion(success);
      }

    }];
    if (isWait) {

      dispatch_semaphore_wait(self.flushSemaphore, DISPATCH_TIME_FOREVER);
      completion(flushSuccess);

    }
    }
    这段代码的含意如下:

如果 flushBeforeEnterBackground = YES,- flushEventRecords:completion: 办法为同步执行。当办法执行实现时,意味着数据上传也实现了;
如果 flushBeforeEnterBackground = NO,- flushEventRecords:completion: 办法为异步执行,不会期待数据上传实现,等数据上报实现后被动完结后台任务即可,代码如下所示:

if (newState == SAAppLifecycleStateEnd) {

UIApplication *application = UIApplication.sharedApplication;
__block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
void (^endBackgroundTask)(void) = ^() {[application endBackgroundTask:backgroundTaskIdentifier];
    backgroundTaskIdentifier = UIBackgroundTaskInvalid;
};
backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:endBackgroundTask];

dispatch_async(self.serialQueue, ^{
    // 上传所有的数据
    [self.eventTracker flushAllEventRecordsWithCompletion:^{
        // 完结后台任务
        endBackgroundTask();}];
});
return;

}
目前,SDK 中 flushBeforeEnterBackground 默认值即为 NO。然而,这样会引入另外一个问题:在弱网环境下,可能会反复上报事件。起因如下:

SDK 只会在服务端返回胜利时才会删除本地保留的数据;
异步上报数据发动网络申请后就会继续执行下一个工作;
在弱网环境下可能服务端曾经接管到数据但 SDK 没有接管到返回胜利,此时 App 终止会导致无奈删除本地保留的数据;
下次启动 App 时会从新上报该数据导致反复上报。

这个问题能够通过服务端去重机制解决,保障雷同数据不会反复入库。

5. 总结

数据分析是个简单的零碎,须要保障每一个环节都不会出错。

在数据上报这一环节,对于时效性的优化咱们始终在致力,并且肯定会继续上来。

  1. 参考文献
    [1] https://developer.apple.com/d…

[2] https://developer.apple.com/d…

[3] https://developer.apple.com/d…

[4] https://developer.apple.com/d…

文章起源公众号——神策技术社区

正文完
 0