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] https://developer.apple.com/d…
[2] https://developer.apple.com/d…
[3] https://developer.apple.com/d…
[4] https://developer.apple.com/d…
文章起源公众号——神策技术社区