Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'package:analytics_sdk/manager/event_persistence.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:analytics_sdk/config/sdk_config.dart';
|
||||
import 'package:analytics_sdk/manager/event_type_config_manager.dart';
|
||||
import 'package:analytics_sdk/manager/session_behavior_manager.dart';
|
||||
import 'package:analytics_sdk/manager/session_manager.dart';
|
||||
import 'package:analytics_sdk/manager/session_metrics_manager.dart';
|
||||
import 'package:analytics_sdk/manager/session_sensor_sampler.dart';
|
||||
import 'package:analytics_sdk/manager/sdk_context.dart';
|
||||
import 'package:analytics_sdk/utils/logger.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
/// 事件处理与上报管理器
|
||||
///
|
||||
/// 负责:
|
||||
/// - 维护事件队列
|
||||
/// - 本地持久化(缓存文件)
|
||||
/// - 批量上报 & 指数退避重试
|
||||
/// - 定时上报定时器
|
||||
///
|
||||
/// 对外由 `AnalyticsSdk` 持有并调用,不直接暴露给业务方。
|
||||
class EventProcessor {
|
||||
String? _reportUrl;
|
||||
final Queue<Map<String, dynamic>> _eventQueue = Queue<Map<String, dynamic>>();
|
||||
|
||||
Timer? _uploadTimer;
|
||||
|
||||
final EventPersistenceImpl _persistence = EventPersistenceImpl();
|
||||
|
||||
int _backoffLevel = 0;
|
||||
Timer? _retryTimer;
|
||||
|
||||
/// JSON 过大时折半缩小批次;成功后重置为 null(恢复用 maxBatchSize)
|
||||
int? _overrideBatchSize;
|
||||
|
||||
http.Client? _httpClient;
|
||||
|
||||
http.Client get _client => _httpClient ??= http.Client();
|
||||
|
||||
/// 上报锁,防止并发上报
|
||||
bool _isUploading = false;
|
||||
final Lock _uploadLock = Lock();
|
||||
|
||||
/// event_id 防抖:记录最近见到该 id 的时间,窗口内重复则丢弃
|
||||
final Map<String, DateTime> _recentEventIds = {};
|
||||
|
||||
/// 防抖窗口:1000ms 秒级时间戳粒度 + 100ms 时钟抖动容差。
|
||||
/// event_id 的哈希因子包含秒级 client_ts,同秒内同事件会产生同一 id;
|
||||
/// 额外 100ms 用于覆盖调用链路上的微小时间偏移,避免临界点误判。
|
||||
static const _dedupWindow = Duration(milliseconds: 1100);
|
||||
|
||||
/// 测试包用:最近一次上报结果
|
||||
bool? lastUploadSuccess;
|
||||
String? lastUploadError;
|
||||
DateTime? lastUploadTime;
|
||||
|
||||
/// 是否已配置可用的上报 URL
|
||||
bool get hasValidReportUrl =>
|
||||
_reportUrl != null && _reportUrl!.isNotEmpty == true;
|
||||
|
||||
/// 测试包用:队列中待上报事件数
|
||||
int get queueLength => _eventQueue.length;
|
||||
|
||||
/// 测试包用:当前上报地址(脱敏)
|
||||
String? get debugReportUrl => _reportUrl;
|
||||
|
||||
/// 测试包用:完整事件 JSONL 调试文件路径(仅 Native debug 包)
|
||||
String? get debugReportEventLogPath =>
|
||||
kDebugMode && SdkConfig.enableDebugReportLog
|
||||
? _persistence.debugReportEventLogFilePath
|
||||
: null;
|
||||
|
||||
/// 更新上报 URL(域名管理器回调时调用)
|
||||
void updateReportUrl(String? reportUrl) {
|
||||
_reportUrl = reportUrl;
|
||||
}
|
||||
|
||||
/// 启动定时上报定时器
|
||||
void startAutoUploadTimer() {
|
||||
_uploadTimer?.cancel();
|
||||
_uploadTimer = Timer.periodic(SdkConfig.uploadInterval, (_) {
|
||||
try {
|
||||
uploadBatch().catchError((e) {
|
||||
Logger.analyticsSdk('定时上报 Future 异常: $e');
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('定时上报异常: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止定时上报定时器
|
||||
void stopAutoUploadTimer() {
|
||||
_uploadTimer?.cancel();
|
||||
_uploadTimer = null;
|
||||
}
|
||||
|
||||
/// 重置退避和上报状态(SDK 重新初始化时调用)
|
||||
void resetState() {
|
||||
_backoffLevel = 0;
|
||||
_overrideBatchSize = null;
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
lastUploadSuccess = null;
|
||||
lastUploadError = null;
|
||||
lastUploadTime = null;
|
||||
_isUploading = false;
|
||||
_recentEventIds.clear();
|
||||
}
|
||||
|
||||
/// 事件入队:
|
||||
/// - 负责队列容量检查
|
||||
/// - 触发必要的上报
|
||||
/// - 写入本地缓存
|
||||
///
|
||||
/// 返回值:
|
||||
/// - `true`:事件成功进入队列
|
||||
/// - `false`:因队列已满等原因被丢弃
|
||||
bool enqueueEvent(Map<String, dynamic> json) {
|
||||
// 队列容量检查,防止内存无限增长
|
||||
if (_eventQueue.length >= SdkConfig.maxQueueSize) {
|
||||
Logger.analyticsSdk('警告:事件队列已满(${_eventQueue.length}),尝试立即上报');
|
||||
// 尝试立即上报以释放队列空间(不 await)
|
||||
try {
|
||||
uploadBatch().catchError((e) {
|
||||
Logger.analyticsSdk('立即上报 Future 异常: $e');
|
||||
});
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('立即上报异常: $e');
|
||||
}
|
||||
// 队列已满时直接丢弃新事件
|
||||
return false;
|
||||
}
|
||||
|
||||
// event_id 防抖:1100ms 窗口内同一 id 只保留第一条
|
||||
final eventId = json['event_id'] as String?;
|
||||
if (eventId != null) {
|
||||
final now = DateTime.now();
|
||||
_recentEventIds.removeWhere((_, t) => now.difference(t) > _dedupWindow);
|
||||
final lastSeen = _recentEventIds[eventId];
|
||||
if (lastSeen != null && now.difference(lastSeen) <= _dedupWindow) {
|
||||
Logger.analyticsSdk('event_id 防抖,已丢弃: $eventId');
|
||||
return false;
|
||||
}
|
||||
_recentEventIds[eventId] = now;
|
||||
}
|
||||
|
||||
_eventQueue.add(json);
|
||||
SessionManager.instance.recordActivityForEvent(json);
|
||||
SessionMetricsManager.instance.recordAcceptedEvent(json);
|
||||
SessionBehaviorManager.instance.recordAcceptedEvent(json);
|
||||
SessionSensorSampler.instance.recordAcceptedEvent(json);
|
||||
if (json['event'] == 'app_session_summary') {
|
||||
_persistence.persistCriticalEvent(json);
|
||||
Logger.analyticsSdk(
|
||||
'app_session_summary 已同步写入本地缓存: ${json['event_id'] ?? ''}');
|
||||
} else {
|
||||
_persistence.bufferEvent(json);
|
||||
}
|
||||
_writeDebugReportEvent(json);
|
||||
_logAcceptedEvent(json);
|
||||
_flushIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 当队列长度达到阈值时触发上报
|
||||
void _flushIfNeeded() {
|
||||
if (_eventQueue.length >= SdkConfig.autoUploadThreshold) {
|
||||
uploadBatch().catchError((e) {
|
||||
Logger.analyticsSdk('阈值触发上报 Future 异常: $e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 强制立即上报
|
||||
/// 该方法永远不会抛出异常
|
||||
Future<void> flush() async {
|
||||
try {
|
||||
await uploadBatch();
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('flush() 发生异常,已安全处理: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量上报队列中的事件
|
||||
Future<void> uploadBatch() async {
|
||||
if (_eventQueue.isEmpty || _reportUrl == null) return;
|
||||
|
||||
return _uploadLock.synchronized(() async {
|
||||
if (_eventQueue.isEmpty || _reportUrl == null || _isUploading) return;
|
||||
|
||||
_isUploading = true;
|
||||
// 限制单次上报的批次大小,避免请求过大导致超时或内存问题
|
||||
// _overrideBatchSize 在 JSON 过大时折半缩小,上报成功后重置
|
||||
final maxBatch = _overrideBatchSize ?? SdkConfig.maxBatchSize;
|
||||
final batchSize =
|
||||
_eventQueue.length > maxBatch ? maxBatch : _eventQueue.length;
|
||||
|
||||
// 安全检查:确保批次大小有效
|
||||
if (batchSize <= 0) {
|
||||
Logger.analyticsSdk('批次大小无效: $batchSize');
|
||||
_isUploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取出批次但不从队列移除,上报成功后再移除(at-least-once 语义)
|
||||
final batch = List<Map<String, dynamic>>.from(
|
||||
_eventQueue.take(batchSize),
|
||||
);
|
||||
|
||||
try {
|
||||
// 安全检查:确保批次不为空
|
||||
if (batch.isEmpty) {
|
||||
Logger.analyticsSdk('批次为空,跳过上报');
|
||||
return;
|
||||
}
|
||||
|
||||
final requestBatch = batch
|
||||
.map(_ensureEventSignatureBeforeUpload)
|
||||
.toList(growable: false);
|
||||
|
||||
// 在后台线程执行 JSON 编码,避免阻塞主线程
|
||||
final batchJson = await _encodeBatchAsync(requestBatch);
|
||||
|
||||
// 检查编码结果
|
||||
if (batchJson == '[]' || batchJson.isEmpty) {
|
||||
Logger.analyticsSdk('JSON 编码失败或结果为空');
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 JSON 大小,避免请求过大
|
||||
final jsonSize = utf8.encode(batchJson).length;
|
||||
if (jsonSize > SdkConfig.maxJsonSize) {
|
||||
Logger.analyticsSdk(
|
||||
'警告:JSON 大小过大 (${(jsonSize / 1024 / 1024).toStringAsFixed(2)}MB),缩小批次重试');
|
||||
if (batchSize > 1) {
|
||||
// 折半批次后重试,避免同一批次反复触发 JSON 过大
|
||||
_overrideBatchSize = (batchSize / 2).ceil();
|
||||
_isUploading = false;
|
||||
Future.microtask(() {
|
||||
uploadBatch().catchError((e) {
|
||||
Logger.analyticsSdk('缩小批次后重试上报 Future 异常: $e');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 单条事件过大,移除该事件避免阻塞队列
|
||||
if (_eventQueue.isNotEmpty) {
|
||||
_eventQueue.removeFirst();
|
||||
}
|
||||
Logger.analyticsSdk(
|
||||
'警告:单条事件过大 (${(jsonSize / 1024).toStringAsFixed(2)}KB),已丢弃');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析 URL,添加错误处理
|
||||
Uri uri;
|
||||
try {
|
||||
uri = Uri.parse(_reportUrl!);
|
||||
if (!uri.hasScheme || (!uri.scheme.startsWith('http'))) {
|
||||
throw const FormatException('无效的 URL scheme');
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('URL 解析失败: $_reportUrl, 错误: $e');
|
||||
_scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
// 终止窗口内也要保证批次已落盘:否则请求已开始但进程被杀时,
|
||||
// in-flight 文件虽存在,主缓存却没有这批事件可供重试。
|
||||
// 服务端通过 event_id 去重,客户端采用 at-least-once 投递。
|
||||
await _persistence.flushBufferAsync();
|
||||
await _persistence.markInFlight(requestBatch);
|
||||
|
||||
Logger.analyticsSdk(
|
||||
'上报中:${requestBatch.length} 条事件,JSON大小: ${(jsonSize / 1024).toStringAsFixed(2)}KB(已落盘)');
|
||||
|
||||
// 复用 http.Client,利用连接池
|
||||
final response = await _client
|
||||
.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: batchJson,
|
||||
)
|
||||
.timeout(SdkConfig.totalRequestTimeout);
|
||||
final statusCode = response.statusCode;
|
||||
if (statusCode == 200) {
|
||||
// 上报成功,按 event_id 从队列中移除已上报的事件
|
||||
final reportedIds =
|
||||
batch.map((e) => e['event_id']).whereType<String>().toSet();
|
||||
if (reportedIds.isNotEmpty) {
|
||||
_eventQueue
|
||||
.removeWhere((e) => reportedIds.contains(e['event_id']));
|
||||
} else {
|
||||
// event_id 全缺失时回退到按数量移除(兜底)
|
||||
for (int i = 0; i < batchSize && _eventQueue.isNotEmpty; i++) {
|
||||
_eventQueue.removeFirst();
|
||||
}
|
||||
}
|
||||
lastUploadSuccess = true;
|
||||
lastUploadError = null;
|
||||
lastUploadTime = DateTime.now();
|
||||
_overrideBatchSize = null; // 上报成功,恢复正常批次大小
|
||||
_resetBackoff();
|
||||
await _persistence.removeEvents(requestBatch);
|
||||
final confirmedEvents = requestBatch
|
||||
.map((event) =>
|
||||
'${event['event'] ?? 'unknown'}(${event['event_id'] ?? 'missing_event_id'})')
|
||||
.join(', ');
|
||||
Logger.analyticsSdk(
|
||||
'上报成功: ${requestBatch.length} 条事件,HTTP $statusCode 批次确认,'
|
||||
'已写入 tombstone;确认事件: [$confirmedEvents]');
|
||||
} else {
|
||||
lastUploadSuccess = false;
|
||||
lastUploadError = 'HTTP $statusCode';
|
||||
lastUploadTime = DateTime.now();
|
||||
_scheduleRetry();
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
Logger.analyticsSdk('上报失败: 客户端错误,状态码: $statusCode(可能是数据格式问题)');
|
||||
} else if (statusCode >= 500) {
|
||||
Logger.analyticsSdk('上报失败: 服务器错误,状态码: $statusCode(服务器问题,将重试)');
|
||||
} else {
|
||||
Logger.analyticsSdk('上报失败: 未知状态码 $statusCode');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
lastUploadSuccess = false;
|
||||
lastUploadError = e is TimeoutException
|
||||
? '请求超时'
|
||||
: e is http.ClientException
|
||||
? '网络连接失败'
|
||||
: e.toString().length > 30
|
||||
? '${e.toString().substring(0, 30)}...'
|
||||
: e.toString();
|
||||
lastUploadTime = DateTime.now();
|
||||
_scheduleRetry();
|
||||
if (e is TimeoutException) {
|
||||
Logger.analyticsSdk('上报异常: 请求超时 - ${e.message ?? "未知超时"}',
|
||||
level: LogLevel.error);
|
||||
} else if (e is http.ClientException) {
|
||||
Logger.analyticsSdk('上报异常: 网络连接失败 - ${e.message}',
|
||||
level: LogLevel.error);
|
||||
} else {
|
||||
final errorMessage = e.toString();
|
||||
Logger.analyticsSdk('上报异常: $errorMessage', level: LogLevel.error);
|
||||
}
|
||||
} finally {
|
||||
// 无论成功、失败还是异常,确保 in-flight 文件被清除(幂等)
|
||||
try {
|
||||
await _persistence.clearInFlight();
|
||||
} catch (_) {}
|
||||
}
|
||||
} finally {
|
||||
_isUploading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, dynamic> _ensureEventSignatureBeforeUpload(
|
||||
Map<String, dynamic> event,
|
||||
) {
|
||||
final eventSig = event['event_sig'];
|
||||
if (eventSig is String && eventSig.trim().isNotEmpty) {
|
||||
return event;
|
||||
}
|
||||
|
||||
final signed = SdkContext.attachDeviceEnvIfNeeded(event);
|
||||
final signedEventSig = signed['event_sig'];
|
||||
if (signedEventSig is String && signedEventSig.trim().isNotEmpty) {
|
||||
return signed;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
void _logAcceptedEvent(Map<String, dynamic> json) {
|
||||
if (!SdkConfig.enableDebugReportLog) return;
|
||||
try {
|
||||
Logger.analyticsSdkDeveloperLog(
|
||||
'accepted event json: ${jsonEncode(json)}',
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('事件日志打印失败: $e', level: LogLevel.warn);
|
||||
}
|
||||
}
|
||||
|
||||
void _writeDebugReportEvent(Map<String, dynamic> json) {
|
||||
if (!kDebugMode || !SdkConfig.enableDebugReportLog) return;
|
||||
try {
|
||||
_persistence.appendDebugReportEvent(json);
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('debug 完整事件 JSONL 记录失败: $e', level: LogLevel.warn);
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON 编码的静态方法,用于 compute isolate
|
||||
static String _encodeBatchJson(List<Map<String, dynamic>> batch) {
|
||||
return jsonEncode(batch);
|
||||
}
|
||||
|
||||
/// 在后台 isolate 中异步执行 JSON 编码,避免阻塞主线程
|
||||
Future<String> _encodeBatchAsync(List<Map<String, dynamic>> batch) async {
|
||||
try {
|
||||
if (batch.isEmpty) {
|
||||
return '[]';
|
||||
}
|
||||
if (batch.length <= SdkConfig.isolateEncodingThreshold) {
|
||||
return jsonEncode(batch);
|
||||
}
|
||||
try {
|
||||
return await compute(_encodeBatchJson, batch);
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('compute 失败,回退到主线程编码: $e');
|
||||
return jsonEncode(batch);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('JSON 编码失败: $e');
|
||||
return '[]';
|
||||
}
|
||||
}
|
||||
|
||||
/// 安排指数退避重试(事件仍在队列中,无需重新入队)
|
||||
void _scheduleRetry() {
|
||||
_backoffLevel = (_backoffLevel + 1).clamp(0, SdkConfig.maxBackoffLevel);
|
||||
_retryTimer?.cancel();
|
||||
final delay = _nextDelay();
|
||||
_retryTimer = Timer(delay, () {
|
||||
try {
|
||||
Logger.analyticsSdk('指数退避重试 (延迟 ${delay.inSeconds}s)');
|
||||
uploadBatch();
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('重试上报异常: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 计算下次延迟时间(指数退避 + 上限)
|
||||
Duration _nextDelay() {
|
||||
final exponential = SdkConfig.baseRetryDelay * pow(2, _backoffLevel);
|
||||
return exponential > SdkConfig.maxRetryDelay
|
||||
? SdkConfig.maxRetryDelay
|
||||
: exponential;
|
||||
}
|
||||
|
||||
/// 上报成功时重置退避状态
|
||||
void _resetBackoff() {
|
||||
if (_backoffLevel > 0) {
|
||||
_backoffLevel = 0;
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
Logger.analyticsSdk('上报成功,退避机制已重置');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initPersistence() async {
|
||||
try {
|
||||
await _persistence.init();
|
||||
final debugPath = debugReportEventLogPath;
|
||||
if (debugPath != null && debugPath.isNotEmpty) {
|
||||
Logger.analyticsSdk('debug 完整事件 JSONL 文件: $debugPath');
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('持久化初始化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadCachedEvents() async {
|
||||
try {
|
||||
final recovered = await _persistence.loadEvents(
|
||||
queue: _eventQueue,
|
||||
maxQueueSize: SdkConfig.maxQueueSize,
|
||||
maxCacheLines: SdkConfig.maxCacheLines,
|
||||
isEventTypeEnabled: EventTypeConfigManager.instance.isEventTypeEnabled,
|
||||
);
|
||||
|
||||
if (recovered.isNotEmpty) {
|
||||
final availableSpace = SdkConfig.maxQueueSize - _eventQueue.length;
|
||||
if (availableSpace > 0) {
|
||||
final eventsToRecover = recovered.length > availableSpace
|
||||
? recovered.sublist(0, availableSpace)
|
||||
: recovered;
|
||||
_eventQueue.addAll(eventsToRecover);
|
||||
Logger.analyticsSdk('从缓存恢复 ${eventsToRecover.length} 条事件');
|
||||
} else {
|
||||
Logger.analyticsSdk('警告:队列已满,无法恢复缓存事件');
|
||||
}
|
||||
}
|
||||
|
||||
_flushIfNeeded();
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('缓存加载失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearCache() async {
|
||||
await _persistence.clearCache();
|
||||
}
|
||||
|
||||
/// 释放资源(定时器、HttpClient、缓存 buffer 等)
|
||||
void dispose() {
|
||||
try {
|
||||
stopAutoUploadTimer();
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
_isUploading = false;
|
||||
_persistence.flushBuffer();
|
||||
_httpClient?.close();
|
||||
_httpClient = null;
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('EventProcessor.dispose 异常: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试专用入口:触发 tombstone 写入以供单元测试验证缓存移除行为
|
||||
@visibleForTesting
|
||||
Future<void> removeEventsFromCacheForTest(
|
||||
List<Map<String, dynamic>> events) =>
|
||||
_persistence.removeEvents(events);
|
||||
|
||||
/// 测试专用入口:注入 HTTP client 以验证最终请求体。
|
||||
@visibleForTesting
|
||||
void setHttpClientForTest(http.Client client) {
|
||||
_httpClient?.close();
|
||||
_httpClient = client;
|
||||
}
|
||||
|
||||
/// 异步释放资源(先 await 缓存刷写,再释放其他资源)
|
||||
///
|
||||
/// 推荐在 SDK dispose 时使用此方法替代 [dispose],确保缓存 buffer 写入磁盘。
|
||||
Future<void> disposeAsync() async {
|
||||
try {
|
||||
stopAutoUploadTimer();
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
_isUploading = false;
|
||||
await _persistence.flushBufferAsync();
|
||||
_httpClient?.close();
|
||||
_httpClient = null;
|
||||
} catch (e) {
|
||||
Logger.analyticsSdk('EventProcessor.disposeAsync 异常: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user