一個 App 通常會存在不少場景去上傳 App 中產生的數據,好比 APM、埋點統計、開發者自定義的數據等等。因此本篇文章就講講如何設計一個通用的、可配置的、多句柄的數據上報 SDK。html
前置說明
由於這篇文章和 APM 是屬於姊妹篇,因此看這篇文章的時候有些東西不知道活着好奇的時候能夠看帶你打造一套 APM 監控系統。node
另外看到我在下面的代碼段,有些命名風格、簡寫、分類、方法的命名等,我簡單作個說明。android
- 數據上報 SDK 叫
HermesClient
,咱們規定類的命名通常用 SDK 的名字縮寫,當前狀況下縮寫爲HCT
- 給 Category 命名,規則爲
類名 + SDK 前綴縮寫的小寫格式 + 下劃線 + 駝峯命名格式的功能描述
。好比給 NSDate 增長一個獲取毫秒時間戳的分類,那麼類名爲NSDate+HCT_TimeStamp
- 給 Category 的方法命名,規則爲
SDK 前綴縮寫的小寫格式 + 下劃線 + 駝峯命名格式的功能描述
。好比給 NSDate 增長一個根據當前時間獲取毫秒時間戳的方法,那麼方法名爲+ (long long)HCT_currentTimestamp;
1、 首先定義須要作什麼
咱們要作的是「一個通用可配置、多句柄的數據上報 SDK」,也就是說這個 SDK 具備這麼幾個功能:ios
- 具備從服務端拉取配置信息的能力,這些配置用來控制 SDK 的上報行爲(需不須要默認行爲?)
- SDK 具備多句柄特性,也就是擁有多個對象,每一個對象具備本身的控制行爲,彼此之間的運行、操做互相隔離
- APM 監控做爲很是特殊的能力存在,它也使用數據上報 SDK。它的能力是 App 質量監控的保障,因此針對 APM 的數據上報通道是須要特殊處理的。
- 數據先根據配置決定要不要存,存下來以後再根據配置決定如何上報
明白咱們須要作什麼,接下來的步驟就是分析設計怎麼作。c++
2、 拉取配置信息
1. 須要哪些配置信息
首先明確幾個原則:git
- 由於監控數據上報做爲數據上報的一個特殊 case,那麼監控的配置信息也應該特殊處理。
- 監控能力包含不少,好比卡頓、網絡、奔潰、內存、電量、啓動時間、CPU 使用率。每一個監控能力都須要一份配置信息,好比監控類型、是否僅 WI-FI 環境下上報、是否實時上報、是否須要攜帶 Payload 數據。(注:Payload 其實就是通過 gZip 壓縮、AES-CBC 加密後的數據)
- 多句柄,因此須要一個字段標識每份配置信息,也就是一個 namespace 的概念
- 每一個 namespace 下都有本身的配置,好比數據上傳後的服務器地址、上報開關、App 升級後是否須要清除掉以前版本保存的數據、單次上傳數據包的最大致積限制、數據記錄的最大條數、在非 WI-FI 環境下天天上報的最大流量、數據過時天數、上報開關等
- 針對 APM 的數據配置,還須要一個是否須要採集的開關。
因此數據字段基本以下github
@interface HCTItemModel : NSObject <nscoding> @property (nonatomic, copy) NSString *type; /<上報數據類型* @property (nonatomic, assign) bool onlywifi; <是否僅 wi-fi 上報* isrealtime; <是否實時上報* isuploadpayload; <是否須要上報 payload* @end @interface hctconfigurationmodel : nsobject <nscoding> @property (nonatomic, copy) NSString *url; /<當前 namespace 對應的上報地址 * @property (nonatomic, assign) bool isupload; <全局上報開關* isgather; <全局採集開關* isupdateclear; <升級後是否清除數據* nsinteger maxbodymbyte; <最大包體積單位 m (範圍 < 3m)* periodictimersecond; <定時上報時間單位秒 (範圍1 ~ 30秒)* maxitem; <最大條數 100)* maxflowmbyte; <天天最大非 wi-fi 上傳流量單位 100m)* expirationday; <數據過時時間單位 天 30)* copy) nsarray<hctitemmodel> *monitorList; /<配置項目* @end ``` 由於數據須要持久化保存,因此須要實現 `nscoding` 協議。 一個小竅門,每一個屬性寫 `encode`、`decode` 會很麻煩,能夠藉助於宏來實現快速編寫。 ```objective-c #define hct_decode(decoder, datatype, keyname) \ { _##keyname="[decoder" decode##datatype##forkey:nsstringfromselector(@selector(keyname))]; }; hct_encode(acoder, key) [acoder encode##datatype:_##key forkey:nsstringfromselector(@selector(key))]; - (instancetype)initwithcoder:(nscoder *)adecoder if (self="[super" init]) hct_decode(adecoder, object, type) bool, onlywifi) isrealtime) isuploadpayload) } return self; (void)encodewithcoder:(nscoder *)acoder 拋出一個問題:既然監控很重要,那別要配置了,直接所有上傳。 咱們想想這個問題,監控數據都是不直接上傳的,監控 sdk 的責任就是收集監控數據,並且監控後的數據很是多,app 運行期間的網絡請求可能都有 n 次,app 啓動時間、卡頓、奔潰、內存等可能很少,可是這些數據直接上傳後期拓展性很是差,好比根據 apm 監控大盤分析出某個監控能力暫時先關閉掉。這時候就無力迴天了,必須等下次 發佈新版本。監控數據必須先存儲,假如 crash 了,則必須保存了數據等下次啓動再去組裝數據、上傳。並且數據在消費、新數據在不斷生產,假如上傳失敗了還須要對失敗數據的處理,因此這些邏輯仍是挺多的,對於監控 來作這個事情,不是很合適。答案就顯而易見了,必需要配置(監控開關的配置、數據上報的行爲配置)。 ### 2. 默認配置 由於監控真的很特殊,app 一啓動就須要去收集 app 的性能、質量相關數據,因此須要一份默認的配置信息。 初始化一份默認配置 (void)setdefaultconfigurationmodel hctconfigurationmodel *configurationmodel="[[HCTConfigurationModel" alloc] init]; configurationmodel.url="@"https://***DomainName.com";" configurationmodel.isupload="YES;" configurationmodel.isgather="YES;" configurationmodel.isupdateclear="YES;" configurationmodel.periodictimersecond="5;" configurationmodel.maxbodymbyte="1;" configurationmodel.maxitem="100;" configurationmodel.maxflowmbyte="20;" configurationmodel.expirationday="15;" hctitemmodel *appcrashitem="[[HCTItemModel" appcrashitem.type="@"appCrash";" appcrashitem.onlywifi="NO;" appcrashitem.isrealtime="YES;" appcrashitem.isuploadpayload="YES;" *applagitem="[[HCTItemModel" applagitem.type="@"appLag";" applagitem.onlywifi="NO;" applagitem.isrealtime="NO;" applagitem.isuploadpayload="NO;" *appbootitem="[[HCTItemModel" appbootitem.type="@"appBoot";" appbootitem.onlywifi="NO;" appbootitem.isrealtime="NO;" appbootitem.isuploadpayload="NO;" *netitem="[[HCTItemModel" netitem.type="@"net";" netitem.onlywifi="NO;" netitem.isrealtime="NO;" netitem.isuploadpayload="NO;" *neterroritem="[[HCTItemModel" neterroritem.type="@"netError";" neterroritem.onlywifi="NO;" neterroritem.isrealtime="NO;" neterroritem.isuploadpayload="NO;" configurationmodel.monitorlist="@[appCrashItem," applagitem, appbootitem, netitem, neterroritem]; self.configurationmodel="configurationModel;" 上面的例子是一份默認配置信息 3. 拉取策略 網絡拉取使用了基礎 (非網絡 sdk)的能力 mget,根據 key 註冊網絡服務。這些 通常是 內部的定義好的,好比統跳路由表等。 這類 的共性是 在打包階段會內置一份默認配置,app 啓動後會去拉取最新數據,而後完成數據的緩存,緩存會在 `nsdocumentdirectory` 目錄下按照 名稱、 版本號、打包平臺上分配的打包任務 id、 創建緩存文件夾。 此外它的特色是等 啓動完成後纔去請求網絡,獲取數據,不會影響 的啓動。 流程圖以下 ![數據上報配置信息獲取流程](https: raw.githubusercontent.com fantasticlbp knowledge-kit master assets 2020-06-29-datauploadconfigurationstructure.png) 下面是一個截取代碼,對比上面圖看看。 @synthesize configurationdictionary="_configurationDictionary;" #pragma mark initial methods + (instancetype)sharedinstance static hctconfigurationservice *_sharedinstance="nil;" dispatch_once_t oncetoken; dispatch_once(&oncetoken, ^{ _sharedinstance="[[self" }); _sharedinstance; (instancetype)init [self setup]; public method (void)registerandfetchconfigurationinfo __weak typeof(self) weakself="self;" nsdictionary *params="@{@"deviceId":" [[hermesclient sharedinstance] getcommon].sys_device_id}; [self.requester fetchuploadconfigurationwithparams:params success:^(nsdictionary * _nonnull configurationdictionary) weakself.configurationdictionary="configurationDictionary;" [nskeyedarchiver archiverootobject:configurationdictionary tofile:[self savedfilepath]]; failure:^(nserror error) }]; (hctconfigurationmodel *)getconfigurationwithnamespace:(nsstring *)namespace (!hct_is_class(namespace, nsstring)) nsassert(hct_is_class(namespace, nsstring), @"須要根據 namespace 參數獲取對應的配置信息,因此必須是 nsstring 類型"); nil; (namespace.length="=" 0) nsassert(namespace.length> 0, @"須要根據 namespace 參數獲取對應的配置信息,因此必須是非空的 NSString"); return nil; } id configurationData = [self.configurationDictionary objectForKey:namespace]; if (!configurationData) { return nil; } if (!HCT_IS_CLASS(configurationData, NSDictionary)) { return nil; } NSDictionary *configurationDictionary = (NSDictionary *)configurationData; return [HCTConfigurationModel modelWithDictionary:configurationDictionary]; } #pragma mark - private method - (void)setUp { // 建立數據保存的文件夾 [[NSFileManager defaultManager] createDirectoryAtPath:[self configurationDataFilePath] withIntermediateDirectories:YES attributes:nil error:nil]; [self setDefaultConfigurationModel]; [self getConfigurationModelFromLocal]; } - (NSString *)savedFilePath { return [NSString stringWithFormat:@"%@/%@", [self configurationDataFilePath], HCT_CONFIGURATION_FILEPATH]; } // 初始化一份默認配置 - (void)setDefaultConfigurationModel { HCTConfigurationModel *configurationModel = [[HCTConfigurationModel alloc] init]; configurationModel.url = @"https://.com"; configurationModel.isUpload = YES; configurationModel.isGather = YES; configurationModel.isUpdateClear = YES; configurationModel.periodicTimerSecond = 5; configurationModel.maxBodyMByte = 1; configurationModel.maxItem = 100; configurationModel.maxFlowMByte = 20; configurationModel.expirationDay = 15; HCTItemModel *appCrashItem = [[HCTItemModel alloc] init]; appCrashItem.type = @"appCrash"; appCrashItem.onlyWifi = NO; appCrashItem.isRealtime = YES; appCrashItem.isUploadPayload = YES; HCTItemModel *appLagItem = [[HCTItemModel alloc] init]; appLagItem.type = @"appLag"; appLagItem.onlyWifi = NO; appLagItem.isRealtime = NO; appLagItem.isUploadPayload = NO; HCTItemModel *appBootItem = [[HCTItemModel alloc] init]; appBootItem.type = @"appBoot"; appBootItem.onlyWifi = NO; appBootItem.isRealtime = NO; appBootItem.isUploadPayload = NO; HCTItemModel *netItem = [[HCTItemModel alloc] init]; netItem.type = @"net"; netItem.onlyWifi = NO; netItem.isRealtime = NO; netItem.isUploadPayload = NO; HCTItemModel *netErrorItem = [[HCTItemModel alloc] init]; netErrorItem.type = @"netError"; netErrorItem.onlyWifi = NO; netErrorItem.isRealtime = NO; netErrorItem.isUploadPayload = NO; configurationModel.monitorList = @[appCrashItem, appLagItem, appBootItem, netItem, netErrorItem]; self.configurationModel = configurationModel; } - (void)getConfigurationModelFromLocal { id unarchiveObject = [NSKeyedUnarchiver unarchiveObjectWithFile:[self savedFilePath]]; if (unarchiveObject) { if (HCT_IS_CLASS(unarchiveObject, NSDictionary)) { self.configurationDictionary = (NSDictionary *)unarchiveObject; [self.configurationDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { if ([key isEqualToString:HermesNAMESPACE]) { if (HCT_IS_CLASS(obj, NSDictionary)) { NSDictionary *configurationDictionary = (NSDictionary *)obj; self.configurationModel = [HCTConfigurationModel modelWithDictionary:configurationDictionary]; } } }]; } } } #pragma mark - getters and setters - (NSString *)configurationDataFilePath { NSString *filePath = [NSString stringWithFormat:@"%@/%@/%@/%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject, @"hermes", [CMAppProfile sharedInstance].mAppVersion, [[HermesClient sharedInstance] getCommon].WAX_CANDLE_TASK_ID]; return filePath; } - (HCTRequestFactory *)requester { if (!_requester) { _requester = [[HCTRequestFactory alloc] init]; } return _requester; } - (void)setConfigurationDictionary:(NSDictionary *)configurationDictionary { @synchronized (self) { _configurationDictionary = configurationDictionary; } } - (NSDictionary *)configurationDictionary { @synchronized (self) { if (_configurationDictionary == nil) { NSDictionary *hermesDictionary = [self.configurationModel getDictionary]; _configurationDictionary = @{HermesNAMESPACE: hermesDictionary}; } return _configurationDictionary; } } @end
3、數據存儲
1. 數據存儲技術選型
記得在作數據上報技術的評審會議上,Android 同事說用 WCDB,特點是 ORM、多線程安全、高性能。而後就被質疑了。由於上個版本使用的技術是基於系統自帶的 sqlite2,單純爲了 ORM、多線程問題就額外引入一個三方庫,是不太能說服人的。有這樣幾個疑問objective-c
-
ORM 並非核心訴求,利用 Runtime 能夠在基礎上進行修改,也可支持 ORM 功能sql
-
線程安全。WCDB 在線程安全的實現主要是基於
Handle
,HandlePool
和Database
三個類完成的。Handle
是 sqlite3 指針,HandlePool
用來處理鏈接。shellRecyclableHandle HandlePool::flowOut(Error &error) { m_rwlock.lockRead(); std::shared_ptr<handlewrap> handleWrap = m_handles.popBack(); if (handleWrap == nullptr) { if (m_aliveHandleCount < s_maxConcurrency) { handleWrap = generate(error); if (handleWrap) { ++m_aliveHandleCount; if (m_aliveHandleCount > s_hardwareConcurrency) { WCDB::Error::Warning( ("The concurrency of database:" + std::to_string(tag.load()) + " with " + std::to_string(m_aliveHandleCount) + " exceeds the concurrency of hardware:" + std::to_string(s_hardwareConcurrency)) .c_str()); } } } else { Error::ReportCore( tag.load(), path, Error::CoreOperation::FlowOut, Error::CoreCode::Exceed, "The concurrency of database exceeds the max concurrency", &error); } } if (handleWrap) { handleWrap->handle->setTag(tag.load()); if (invoke(handleWrap, error)) { return RecyclableHandle( handleWrap, [this](std::shared_ptr<handlewrap> &handleWrap) { flowBack(handleWrap); }); } } handleWrap = nullptr; m_rwlock.unlockRead(); return RecyclableHandle(nullptr, nullptr); } void HandlePool::flowBack(const std::shared_ptr<handlewrap> &handleWrap) { if (handleWrap) { bool inserted = m_handles.pushBack(handleWrap); m_rwlock.unlockRead(); if (!inserted) { --m_aliveHandleCount; } } }
因此 WCDB 鏈接池經過讀寫鎖保證線程安全。因此以前版本的地方要實現線程安全修改下缺陷就能夠。增長了 sqlite3,雖然看起來就是幾兆大小,可是這對於公共團隊是致命的。業務線開發者每次接入 SDK 會注意App 包體積的變化,爲了數據上報增長好幾兆,這是不能夠接受的。
-
高性能的背後是 WCDB 自帶的 sqlite3 開啓了
WAL模式
(Write-Ahead Logging)。當 WAL 文件超過 1000 個頁大小時,SQLite3 會將 WAL 文件寫會數據庫文件。也就是 checkpointing。當大批量的數據寫入場景時,若是不停提交文件到數據庫事務,效率確定低下,WCDB 的策略就是在觸發 checkpoint 時,經過延時隊列去處理,避免不停的觸發 WalCheckpoint 調用。經過TimedQueue
將同個數據庫的WalCheckpoint
合併延遲到2秒後執行{ Database::defaultCheckpointConfigName, [](std::shared_ptr<handle> &handle, Error &error) -> bool { handle->registerCommittedHook( [](Handle *handle, int pages, void *) { static TimedQueue<std::string> s_timedQueue(2); if (pages > 1000) { s_timedQueue.reQueue(handle->path); } static std::thread s_checkpointThread([]() { pthread_setname_np( ("WCDB-" + Database::defaultCheckpointConfigName) .c_str()); while (true) { s_timedQueue.waitUntilExpired( [](const std::string &path) { Database database(path); WCDB::Error innerError; database.exec(StatementPragma().pragma( Pragma::WalCheckpoint), innerError); }); } }); static std::once_flag s_flag; std::call_once(s_flag, []() { s_checkpointThread.detach(); }); }, nullptr); return true; }, (Configs::Order) Database::ConfigOrder::Checkpoint, },
通常來講公共組作事情,SDK 命名、接口名稱、接口個數、參數個數、參數名稱、參數數據類型是嚴格一致的,差別是語言而已。實在萬不得已,能力不能堆砌的狀況下是能夠不一致的,可是須要在技術評審會議上說明緣由,須要在發佈文檔、接入文檔都有所體現。
因此最後的結論是在以前的版本基礎上進行修改,以前的版本是 FMDB。
2. 數據庫維護隊列
1. FMDB 隊列
FMDB
使用主要是經過 FMDatabaseQueue
的 - (void)inTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block
和 - (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block
。這2個方法的實現以下
- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block { #ifndef NDEBUG /* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue * and then check it against self to make sure we're not about to deadlock. */ FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey); assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock"); #endif FMDBRetain(self); dispatch_sync(_queue, ^() { FMDatabase *db = [self database]; block(db); if ([db hasOpenResultSets]) { NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]"); #if defined(DEBUG) && DEBUG NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]); for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue]; NSLog(@"query: '%@'", [rs query]); } #endif } }); FMDBRelease(self); }
- (void)inTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block { [self beginTransaction:FMDBTransactionExclusive withBlock:block]; } - (void)beginTransaction:(FMDBTransaction)transaction withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block { FMDBRetain(self); dispatch_sync(_queue, ^() { BOOL shouldRollback = NO; switch (transaction) { case FMDBTransactionExclusive: [[self database] beginTransaction]; break; case FMDBTransactionDeferred: [[self database] beginDeferredTransaction]; break; case FMDBTransactionImmediate: [[self database] beginImmediateTransaction]; break; } block([self database], &shouldRollback); if (shouldRollback) { [[self database] rollback]; } else { [[self database] commit]; } }); FMDBRelease(self); }
上面的 _queue
實際上是一個串行隊列,經過 _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
建立。因此,FMDB
的核心就是以同步的形式向串行隊列提交任務,來保證多線程操做下的讀寫問題(比每一個操做加鎖效率高不少)。只有一個任務執行完畢,才能夠執行下一個任務。
上一個版本的數據上報 SDK 功能比較簡單,就是上報 APM 監控後的數據,因此數據量不會很大,以前的人封裝超級簡單,僅以事務的形式封裝了一層 FMDB 的增刪改查操做。那麼就會有一個問題。假如 SDK 被業務線接入,業務線開發者不知道數據上報 SDK 的內部實現,直接調用接口去寫入大量數據,結果 App 發生了卡頓,那不得反饋你這個 SDK 超級難用啊。
2. 針對 FMDB 的改進
改法也比較簡單,咱們先弄清楚 FMDB
這樣設計的緣由。數據庫操做的環境多是主線程、子線程等不一樣環境去修改數據,主線程、子線程去讀取數據,因此建立了一個串行隊列去執行真正的數據增刪改查。
目的就是讓不一樣線程去使用 FMDB
的時候不會阻塞當前線程。既然 FMDB
內部維護了一個串行隊列去處理多線程狀況下的數據操做,那麼改法也比較簡單,那就是建立一個併發隊列,而後以異步的方式提交任務到 FMDB
中去,FMDB
內部的串行隊列去執行真正的任務。
代碼以下
// 建立隊列 self.dbOperationQueue = dispatch_queue_create(HCT_DATABASE_OPERATION_QUEUE, DISPATCH_QUEUE_CONCURRENT); self.dbQueue = [FMDatabaseQueue databaseQueueWithPath:[HCTDatabase databaseFilePath]]; // 以刪除數據爲例,以異步任務的方式向併發隊列提交任務,任務內部調用 FMDatabaseQueue 去串行執行每一個任務 - (void)removeAllLogsInTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeAllLogsInTable:tableName]; }); } - (void)removeAllLogsInTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@", tableName]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString]; }]; }
小實驗模擬下流程
sleep(1); NSLog(@"1"); dispatch_queue_t concurrentQueue = dispatch_queue_create("HCT_DATABASE_OPERATION_QUEUE", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"2"); }); sleep(1); NSLog(@"3"); dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"4"); }); sleep(1); NSLog(@"5"); 2020-07-01 13:28:13.610575+0800 Test[54460:1557233] 1 2020-07-01 13:28:14.611937+0800 Test[54460:1557233] 3 2020-07-01 13:28:15.613347+0800 Test[54460:1557233] 5 2020-07-01 13:28:15.613372+0800 Test[54460:1557280] 2 2020-07-01 13:28:17.616837+0800 Test[54460:1557277] 4
3. 數據表設計
通用的數據上報 SDK 的功能是數據的保存和上報。從數據的角度來劃分,數據能夠分爲 APM 監控數據和業務線的業務數據。
數據各有什麼特色呢?APM 監控數據通常能夠劃分爲:基本信息、異常信息、線程信息,也就是最大程度的還原案發線程的數據。業務線數據基本上不會有所謂的大量數據,最多就是數據條數很是多。鑑於此現狀,能夠將數據表設計爲 meta 表、payload 表。meta 表用來存放 APM 的基礎數據和業務線的數據,payload 表用來存放 APM 的線程堆棧數據。
數據表的設計是基於業務狀況的。那有這樣幾個背景
- APM 監控數據須要報警(具體能夠查看 APM 文章,地址在開頭 ),因此數據上報 SDK 上報後的數據須要實時解析
- 產品側好比監控大盤能夠慢,因此符號化系統是異步的
- 監控數據實在太大了,若是同步解析會由於壓力較大形成性能瓶頸
因此把監控數據拆分爲2塊,即 meta 表、payload 表。meta 表至關於記錄索引信息,服務端只須要關心這個。而 payload 數據在服務端是不會處理的,會有一個異步服務單獨處理。
meta 表、payload 表結構以下:
create table if not exists ***_meta (id integer NOT NULL primary key autoincrement, report_id text, monitor_type text, is_biz integer NOT NULL, created_time datetime, meta text, namespace text, is_used integer NOT NULL, size integer NOT NULL); create table if not exists ***_payload (id integer NOT NULL primary key autoincrement, report_id text, monitor_type text, is_biz integer NOT NULL, created_time datetime, meta text, payload blob, namespace text, is_used integer NOT NULL, size integer NOT NULL);
4. 數據庫表的封裝
#import "HCTDatabase.h" #import <fmdb fmdb.h> static NSString *const HCT_LOG_DATABASE_NAME = @"***.db"; static NSString *const HCT_LOG_TABLE_META = @"***_hermes_meta"; static NSString *const HCT_LOG_TABLE_PAYLOAD = @"***_hermes_payload"; const char *HCT_DATABASE_OPERATION_QUEUE = "com.***.HCT_database_operation_QUEUE"; @interface HCTDatabase () @property (nonatomic, strong) dispatch_queue_t dbOperationQueue; @property (nonatomic, strong) FMDatabaseQueue *dbQueue; @property (nonatomic, strong) NSDateFormatter *dateFormatter; @end @implementation HCTDatabase #pragma mark - life cycle + (instancetype)sharedInstance { static HCTDatabase *_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedInstance = [[self alloc] init]; }); return _sharedInstance; } - (instancetype)init { self = [super init]; self.dateFormatter = [[NSDateFormatter alloc] init]; [self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss A Z"]; self.dbOperationQueue = dispatch_queue_create(HCT_DATABASE_OPERATION_QUEUE, DISPATCH_QUEUE_CONCURRENT); self.dbQueue = [FMDatabaseQueue databaseQueueWithPath:[HCTDatabase databaseFilePath]]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [self createLogMetaTableIfNotExist:db]; [self createLogPayloadTableIfNotExist:db]; }]; return self; } #pragma mark - public Method - (void)add:(NSArray<hctlogmodel *> *)logs inTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself add:logs inTable:tableName]; }); } - (void)remove:(NSArray<hctlogmodel *> *)logs inTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself remove:logs inTable:tableName]; }); } - (void)removeAllLogsInTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeAllLogsInTable:tableName]; }); } - (void)removeOldestRecordsByCount:(NSInteger)count inTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeOldestRecordsByCount:count inTable:tableName]; }); } - (void)removeLatestRecordsByCount:(NSInteger)count inTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeLatestRecordsByCount:count inTable:tableName]; }); } - (void)removeRecordsBeforeDays:(NSInteger)day inTableType:(HCTLogTableType)tableType { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeRecordsBeforeDays:day inTable:tableName]; }); [self rebuildDatabaseFileInTableType:tableType]; } - (void)removeDataUseCondition:(NSString *)condition inTableType:(HCTLogTableType)tableType { if (!HCT_IS_CLASS(condition, NSString)) { NSAssert(HCT_IS_CLASS(condition, NSString), @"自定義刪除條件必須是字符串類型"); return; } if (condition.length == 0) { NSAssert(!(condition.length == 0), @"自定義刪除條件不能爲空"); return; } [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself removeDataUseCondition:condition inTable:tableName]; }); } - (void)updateData:(NSString *)state useCondition:(NSString *)condition inTableType:(HCTLogTableType)tableType { if (!HCT_IS_CLASS(state, NSString)) { NSAssert(HCT_IS_CLASS(state, NSString), @"數據表字段更改命令必須是合法字符串"); return; } if (state.length == 0) { NSAssert(!(state.length == 0), @"數據表字段更改命令必須是合法字符串"); return; } if (!HCT_IS_CLASS(condition, NSString)) { NSAssert(HCT_IS_CLASS(condition, NSString), @"數據表字段更改條件必須是字符串類型"); return; } if (condition.length == 0) { NSAssert(!(condition.length == 0), @"數據表字段更改條件不能爲空"); return; } [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself updateData:state useCondition:condition inTable:tableName]; }); } - (void)recordsCountInTableType:(HCTLogTableType)tableType completion:(void (^)(NSInteger count))completion { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); NSInteger recordsCount = [weakself recordsCountInTable:tableName]; if (completion) { completion(recordsCount); } }); } - (void)getLatestRecoreds:(NSInteger)count inTableType:(HCTLogTableType)tableType completion:(void (^)(NSArray<hctlogmodel *> *records))completion { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); NSArray<hctlogmodel *> *records = [weakself getLatestRecoreds:count inTable:tableName]; if (completion) { completion(records); } }); } - (void)getOldestRecoreds:(NSInteger)count inTableType:(HCTLogTableType)tableType completion:(void (^)(NSArray<hctlogmodel *> *records))completion { [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); NSArray<hctlogmodel *> *records = [weakself getOldestRecoreds:count inTable:tableName]; if (completion) { completion(records); } }); } - (void)getRecordsByCount:(NSInteger)count condtion:(NSString *)condition inTableType:(HCTLogTableType)tableType completion:(void (^)(NSArray<hctlogmodel *> *records))completion { if (!HCT_IS_CLASS(condition, NSString)) { NSAssert(HCT_IS_CLASS(condition, NSString), @"自定義查詢條件必須是字符串類型"); if (completion) { completion(nil); } } if (condition.length == 0) { NSAssert(!(condition.length == 0), @"自定義查詢條件不能爲空"); if (completion) { completion(nil); } } [self isExistInTable:tableType]; __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); NSArray<hctlogmodel *> *records = [weakself getRecordsByCount:count condtion:condition inTable:tableName]; if (completion) { completion(records); } }); } - (void)rebuildDatabaseFileInTableType:(HCTLogTableType)tableType { __weak typeof(self) weakself = self; dispatch_async(self.dbOperationQueue, ^{ NSString *tableName = HCTGetTableNameFromType(tableType); [weakself rebuildDatabaseFileInTable:tableName]; }); } #pragma mark - CMDatabaseDelegate - (void)add:(NSArray<hctlogmodel *> *)logs inTable:(NSString *)tableName { if (logs.count == 0) { return; } __weak typeof(self) weakself = self; [self.dbQueue inTransaction:^(FMDatabase *_Nonnull db, BOOL *_Nonnull rollback) { [db setDateFormat:weakself.dateFormatter]; for (NSInteger index = 0; index < logs.count; index++) { id obj = logs[index]; // meta 類型數據的處理邏輯 if (HCT_IS_CLASS(obj, HCTLogMetaModel)) { HCTLogMetaModel *model = (HCTLogMetaModel *)obj; if (model.monitor_type == nil || model.meta == nil || model.created_time == nil || model.namespace == nil) { HCTLOG(@"參數錯誤 { monitor_type: %@, meta: %@, created_time: %@, namespace: %@}", model.monitor_type, model.meta, model.created_time, model.namespace); return; } NSString *sqlString = [NSString stringWithFormat:@"insert into %@ (report_id, monitor_type, is_biz, created_time, meta, namespace, is_used, size) values (?, ?, ?, ?, ?, ?, ?, ?)", tableName]; [db executeUpdate:sqlString withArgumentsInArray:@[model.report_id, model.monitor_type, @(model.is_biz), model.created_time, model.meta, model.namespace, @(model.is_used), @(model.size)]]; } // payload 類型數據的處理邏輯 if (HCT_IS_CLASS(obj, HCTLogPayloadModel)) { HCTLogPayloadModel *model = (HCTLogPayloadModel *)obj; if (model.monitor_type == nil || model.meta == nil || model.created_time == nil || model.namespace == nil) { HCTLOG(@"參數錯誤 { monitor_type: %@, meta: %@, created_time: %@, namespace: %@}", model.monitor_type, model.meta, model.created_time, model.namespace); return; } NSString *sqlString = [NSString stringWithFormat:@"insert into %@ (report_id, monitor_type, is_biz, created_time, meta, payload, namespace, is_used, size) values (?, ?, ?, ?, ?, ?, ?, ?, ?)", tableName]; [db executeUpdate:sqlString withArgumentsInArray:@[model.report_id, model.monitor_type, @(model.is_biz), model.created_time, model.meta, model.payload ?: [NSData data], model.namespace, @(model.is_used), @(model.size)]]; } } }]; } - (NSInteger)remove:(NSArray<hctlogmodel *> *)logs inTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@ where report_id = ?", tableName]; [self.dbQueue inTransaction:^(FMDatabase *_Nonnull db, BOOL *_Nonnull rollback) { [logs enumerateObjectsUsingBlock:^(HCTLogModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { [db executeUpdate:sqlString withArgumentsInArray:@[obj.report_id]]; }]; }]; return 0; } - (void)removeAllLogsInTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@", tableName]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString]; }]; } - (void)removeOldestRecordsByCount:(NSInteger)count inTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@ where id in (select id from %@ order by created_time asc limit ? )", tableName, tableName]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString withArgumentsInArray:@[@(count)]]; }]; } - (void)removeLatestRecordsByCount:(NSInteger)count inTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@ where id in (select id from %@ order by created_time desc limit ?)", tableName, tableName]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString withArgumentsInArray:@[@(count)]]; }]; } - (void)removeRecordsBeforeDays:(NSInteger)day inTable:(NSString *)tableName { // 找出從create到如今已經超過最大 day 天的數據,而後刪除 :delete from ***_hermes_meta where strftime('%s', date('now', '-2 day')) >= created_time; NSString *sqlString = [NSString stringWithFormat:@"delete from %@ where strftime('%%s', date('now', '-%zd day')) >= created_time", tableName, day]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString]; }]; } - (void)removeDataUseCondition:(NSString *)condition inTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"delete from %@ where %@", tableName, condition]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString]; }]; } - (void)updateData:(NSString *)state useCondition:(NSString *)condition inTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"update %@ set %@ where %@", tableName, state, condition]; [self.dbQueue inDatabase:^(FMDatabase * _Nonnull db) { BOOL res = [db executeUpdate:sqlString]; HCTLOG(res ? @"更新成功" : @"更新失敗"); }]; } - (NSInteger)recordsCountInTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"select count(*) as count from %@", tableName]; __block NSInteger recordsCount = 0; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { FMResultSet *resultSet = [db executeQuery:sqlString]; [resultSet next]; recordsCount = [resultSet intForColumn:@"count"]; [resultSet close]; }]; return recordsCount; } - (NSArray<hctlogmodel *> *)getLatestRecoreds:(NSInteger)count inTable:(NSString *)tableName { __block NSMutableArray<hctlogmodel *> *records = [NSMutableArray new]; NSString *sql = [NSString stringWithFormat:@"select * from %@ order by created_time desc limit ?", tableName]; __weak typeof(self) weakself = self; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db setDateFormat:weakself.dateFormatter]; FMResultSet *resultSet = [db executeQuery:sql withArgumentsInArray:@[@(count)]]; while ([resultSet next]) { if ([tableName isEqualToString:HCT_LOG_TABLE_META]) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; model.is_used = [resultSet boolForColumn:@"is_used"]; [records addObject:model]; } else if ([tableName isEqualToString:HCT_LOG_TABLE_PAYLOAD]) { HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.payload = [resultSet dataForColumn:@"payload"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; model.is_used = [resultSet boolForColumn:@"is_used"]; [records addObject:model]; } } [resultSet close]; }]; return records; } - (NSArray<hctlogmodel *> *)getOldestRecoreds:(NSInteger)count inTable:(NSString *)tableName { __block NSMutableArray<hctlogmodel *> *records = [NSMutableArray array]; NSString *sqlString = [NSString stringWithFormat:@"select * from %@ order by created_time asc limit ?", tableName]; __weak typeof(self) weakself = self; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db setDateFormat:weakself.dateFormatter]; FMResultSet *resultSet = [db executeQuery:sqlString withArgumentsInArray:@[@(count)]]; while ([resultSet next]) { if ([tableName isEqualToString:HCT_LOG_TABLE_META]) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; model.is_used = [resultSet boolForColumn:@"is_used"]; [records addObject:model]; } else if ([tableName isEqualToString:HCT_LOG_TABLE_PAYLOAD]) { HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.payload = [resultSet dataForColumn:@"payload"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_used = [resultSet boolForColumn:@"is_used"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; [records addObject:model]; } } [resultSet close]; }]; return records; } - (NSArray<hctlogmodel *> *)getRecordsByCount:(NSInteger)count condtion:(NSString *)condition inTable:(NSString *)tableName { __block NSMutableArray<hctlogmodel *> *records = [NSMutableArray array]; __weak typeof(self) weakself = self; NSString *sqlString = [NSString stringWithFormat:@"select * from %@ where %@ order by created_time desc limit %zd", tableName, condition, count]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db setDateFormat:weakself.dateFormatter]; FMResultSet *resultSet = [db executeQuery:sqlString]; while ([resultSet next]) { if ([tableName isEqualToString:HCT_LOG_TABLE_META]) { HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; model.is_used = [resultSet boolForColumn:@"is_used"]; [records addObject:model]; } else if ([tableName isEqualToString:HCT_LOG_TABLE_PAYLOAD]) { HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init]; model.log_id = [resultSet intForColumn:@"id"]; model.report_id = [resultSet stringForColumn:@"report_id"]; model.monitor_type = [resultSet stringForColumn:@"monitor_type"]; model.created_time = [resultSet stringForColumn:@"created_time"]; model.meta = [resultSet stringForColumn:@"meta"]; model.payload = [resultSet dataForColumn:@"payload"]; model.namespace = [resultSet stringForColumn:@"namespace"]; model.size = [resultSet intForColumn:@"size"]; model.is_biz = [resultSet boolForColumn:@"is_biz"]; model.is_used = [resultSet boolForColumn:@"is_used"]; [records addObject:model]; } } [resultSet close]; }]; return records; } - (void)rebuildDatabaseFileInTable:(NSString *)tableName { NSString *sqlString = [NSString stringWithFormat:@"vacuum %@", tableName]; [self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) { [db executeUpdate:sqlString]; }]; } #pragma mark - private method + (NSString *)databaseFilePath { NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; NSString *dbPath = [docsPath stringByAppendingPathComponent:HCT_LOG_DATABASE_NAME]; HCTLOG(@"上報系統數據庫文件位置 -> %@", dbPath); return dbPath; } - (void)createLogMetaTableIfNotExist:(FMDatabase *)db { NSString *createMetaTableSQL = [NSString stringWithFormat:@"create table if not exists %@ (id integer NOT NULL primary key autoincrement, report_id text, monitor_type text, is_biz integer NOT NULL, created_time datetime, meta text, namespace text, is_used integer NOT NULL, size integer NOT NULL)", HCT_LOG_TABLE_META]; BOOL result = [db executeStatements:createMetaTableSQL]; HCTLOG(@"確認日誌Meta表是否存在 -> %@", result ? @"成功" : @"失敗"); } - (void)createLogPayloadTableIfNotExist:(FMDatabase *)db { NSString *createMetaTableSQL = [NSString stringWithFormat:@"create table if not exists %@ (id integer NOT NULL primary key autoincrement, report_id text, monitor_type text, is_biz integer NOT NULL, created_time datetime, meta text, payload blob, namespace text, is_used integer NOT NULL, size integer NOT NULL)", HCT_LOG_TABLE_PAYLOAD]; BOOL result = [db executeStatements:createMetaTableSQL]; HCTLOG(@"確認日誌Payload表是否存在 -> %@", result ? @"成功" : @"失敗"); } NS_INLINE NSString *HCTGetTableNameFromType(HCTLogTableType type) { if (type == HCTLogTableTypeMeta) { return HCT_LOG_TABLE_META; } if (type == HCTLogTableTypePayload) { return HCT_LOG_TABLE_PAYLOAD; } return @""; } // 每次操做前檢查數據庫以及數據表是否存在,不存在則建立數據庫和數據表 - (void)isExistInTable:(HCTLogTableType)tableType { NSString *databaseFilePath = [HCTDatabase databaseFilePath]; BOOL isExist = [[NSFileManager defaultManager] fileExistsAtPath:databaseFilePath]; if (!isExist) { self.dbQueue = [FMDatabaseQueue databaseQueueWithPath:[HCTDatabase databaseFilePath]]; } [self.dbQueue inDatabase:^(FMDatabase *db) { NSString *tableName = HCTGetTableNameFromType(tableType); BOOL res = [db tableExists:tableName]; if (!res) { if (tableType == HCTLogTableTypeMeta) { [self createLogMetaTableIfNotExist:db]; } if (tableType == HCTLogTableTypeMeta) { [self createLogPayloadTableIfNotExist:db]; } } }]; } @end
上面有個地方須要注意下,由於常常須要根據類型來判讀操做那個數據表,使用頻次很高,因此寫成內聯函數的形式
NS_INLINE NSString *HCTGetTableNameFromType(HCTLogTableType type) { if (type == HCTLogTableTypeMeta) { return HCT_LOG_TABLE_META; } if (type == HCTLogTableTypePayload) { return HCT_LOG_TABLE_PAYLOAD; } return @""; }
5. 數據存儲流程
APM 監控數據會比較特殊點,好比 iOS 當發生 crash 後是沒辦法上報的,只有將 crash 信息保存到文件中,下次 App 啓動後讀取 crash 日誌文件夾再去交給數據上報 SDK。Android 在發生 crash 後因爲機制不同,能夠立刻將 crash 信息交給數據上報 SDK。
因爲 payload 數據,也就是堆棧數據很是大,因此上報的接口也有限制,一次上傳接口中報文最大包體積的限制等等。
能夠看一下 Model 信息,
@interface HCTItemModel : NSObject <nscoding> @property (nonatomic, copy) NSString *type; /**<上報數據類型* @property (nonatomic, assign) bool onlywifi; **<是否僅 wi-fi 上報* isrealtime; **<是否實時上報* isuploadpayload; **<是否須要上報 payload* @end @interface hctconfigurationmodel : nsobject <nscoding> @property (nonatomic, copy) NSString *url; /**<當前 namespace 對應的上報地址 * @property (nonatomic, assign) bool isupload; **<全局上報開關* isgather; **<全局採集開關* isupdateclear; **<升級後是否清除數據* nsinteger maxbodymbyte; **<最大包體積單位 m (範圍 < 3m)* periodictimersecond; **<定時上報時間單位秒 (範圍1 ~ 30秒)* maxitem; **<最大條數 100)* maxflowmbyte; **<天天最大非 wi-fi 上傳流量單位 100m)* expirationday; **<數據過時時間單位 天 30)* copy) nsarray<hctitemmodel> *monitorList; /**<配置項目* @end ``` 監控數據存儲流程: 1. 每一個數據(監控數據、業務線數據)過來先判斷該數據所在的 namespace 是否開啓了收集開關 2. 判斷數據是否能夠落庫,根據數據接口中 type 可否命中上報配置數據中的 monitorlist 中的任何一項的 3. 監控數據先寫入 meta 表,而後判斷是否寫入 payload 表。判斷標準是計算監控數據的 大小是否超過了上報配置數據的 `maxbodymbyte`。超過大小的數據就不能入庫,由於這是服務端消耗 的一個上限 4. 走監控接口過來的數據,在方法內部會爲監控數據增長基礎信息(好比 app 名稱、app 版本號、打包任務 id、設備類型等等) ```objective-c @property (nonatomic, copy) nsstring *xxx_app_name; **<app 名稱(wax)* *xxx_app_version; 版本(wax)* *xxx_candle_task_id; **<打包平臺分配的打包任務id* *sys_system_model; **<系統類型(android ios)* *sys_device_id; **<設備 id* *sys_brand; **<系統品牌* *sys_phone_model; **<設備型號* *sys_system_version; **<系統版本* *app_platform; **<平臺號* *app_version; 版本(業務版本)* *app_session_id; **<session *app_package_name; **<包名* *app_mode; **<debug release* *app_uid; **<user *app_mc; **<渠道號* *app_monitor_version; **<監控版本號。和服務端維持同一個版本,服務端升級的話,sdk也跟着升級* *report_id; **<惟一id* *create_time; **<時間* assign) bool is_biz; **<是不是監控數據* 5. 由於本次交給數據上報 sdk 的 crash 類型的數據是上次奔潰時的數據,因此在第4點說的規則不太適用,apm 類型是特例。 6. 計算每條數據的大小。metasize + payloadsize 7. 再寫入 表 8. 判斷是否觸發實時上報,觸發後走後續流程。 - (void)sendwithtype:(nsstring *)type meta:(nsdictionary *)meta payload:(nsdata *__nullable)payload { 檢查參數合法性 *warning="[NSString" stringwithformat:@"%@不能是空字符串", type]; if (!hct_is_class(type, nsstring)) nsassert1(hct_is_class(type, nsstring), warning, type); return; } (type.length="=" 0) nsassert1(type.length> 0, warning, type); return; } if (!HCT_IS_CLASS(meta, NSDictionary)) { return; } if (meta.allKeys.count == 0) { return; } // 2. 判斷當前 namespace 是否開啓了收集 if (!self.configureModel.isGather) { HCTLOG(@"%@", [NSString stringWithFormat:@"Namespace: %@ 下數據收集開關爲關閉狀態", self.namespace]); return ; } // 3. 判斷是不是有效的數據。能夠落庫(type 和監控參數的接口中 monitorList 中的任一條目的type 相等) BOOL isValidate = [self validateLogData:type]; if (!isValidate) { return; } // 3. 先寫入 meta 表 HCTCommonModel *commonModel = [[HCTCommonModel alloc] init]; [self sendWithType:type namespace:HermesNAMESPACE meta:meta isBiz:NO commonModel:commonModel]; // 4. 若是 payload 不存在則退出當前執行 if (!HCT_IS_CLASS(payload, NSData) && !payload) { return; } // 5. 添加限制(超過大小的數據就不能入庫,由於這是服務端消耗 payload 的一個上限) CGFloat payloadSize = [self calculateDataSize:payload]; if (payloadSize > self.configureModel.maxBodyMByte) { NSString *assertString = [NSString stringWithFormat:@"payload 數據的大小超過臨界值 %zdKB", self.configureModel.maxBodyMByte]; NSAssert(payloadSize <= self.configureModel.maxBodyMByte, assertString); return; } // 6. 合併 meta 與 Common 基礎數據,用來存儲 payload 上報所須要的 meta 信息 NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary]; NSDictionary *commonDictionary = [commonModel getDictionary]; // Crash 類型爲特例,外部傳入的 Crash 案發現場信息不能被覆蓋 if ([type isEqualToString:@"appCrash"]) { [metaDictionary addEntriesFromDictionary:commonDictionary]; [metaDictionary addEntriesFromDictionary:meta]; } else { [metaDictionary addEntriesFromDictionary:meta]; [metaDictionary addEntriesFromDictionary:commonDictionary]; } NSError *error; NSData *metaData = [NSJSONSerialization dataWithJSONObject:metaDictionary options:0 error:&error]; if (error) { HCTLOG(@"%@", error); return; } NSString *metaContentString = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding]; // 7. 計算上報時 payload 這條數據的大小(meta+payload) NSMutableData *totalData = [NSMutableData data]; [totalData appendData:metaData]; [totalData appendData:payload]; // 8. 再寫入 payload 表 HCTLogPayloadModel *payloadModel = [[HCTLogPayloadModel alloc] init]; payloadModel.is_used = NO; payloadModel.namespace = HermesNAMESPACE; payloadModel.report_id = HCT_SAFE_STRING(commonModel.REPORT_ID); payloadModel.monitor_type = HCT_SAFE_STRING(type); payloadModel.is_biz = NO; payloadModel.created_time = HCT_SAFE_STRING(commonModel.CREATE_TIME); payloadModel.meta = HCT_SAFE_STRING(metaContentString); payloadModel.payload = payload; payloadModel.size = totalData.length; [HCT_DATABASE add:@[payloadModel] inTableType:HCTLogTableTypePayload]; // 9. 判斷是否觸發實時上報 [self handleUploadDataWithtype:type]; }
業務線數據存儲流程基本和監控數據的存儲差很少,有差異的是某些字段的標示,用來區分業務線數據。
4、數據上報機制
1. 數據上報流程和機制設計
數據上報機制須要結合數據特色進行設計,數據分爲 APM 監控數據和業務線上傳數據。先分析下2部分數據的特色。
-
業務線數據可能會要求實時上報,須要有根據上報配置數據控制的能力
-
整個數據聚合上報過程須要有根據上報配置數據控制的能力定時器週期的能力,隔一段時間去觸發上報
-
整個數據(業務數據、APM 監控數據)的上報與否須要有經過配置數據控制的能力
-
由於 App 在某個版本下收集的數據可能會對下個版本的時候無效,因此上報 SDK 啓動後須要有刪除以前版本數據的能力(上報配置數據中刪除開關打開的狀況下)
-
一樣,須要刪除過時數據的能力(刪除距今多少個天然天前的數據,一樣走下發而來的上報配置項)
-
由於 APM 監控數據很是大,且數據上報 SDK 確定數據比較大,因此一個網絡通訊方式的設計好壞會影響 SDK 的質量,爲了網絡性能不採用傳統的
key/value
傳輸。採用自定義報文結構 -
數據的上報流程觸發方式有3種:App 啓動後觸發(APM 監控到 crash 的時候寫入本地,啓動後處理上次 crash 的數據,是一個特殊 case );定時器觸發;數據調用數據上報 SDK 接口後命中實時上報邏輯
-
數據落庫後會觸發一次完整的上報流程
-
上報流程的第一步會先判斷該數據的 type 可否名字上報配置的 type,命中後若是實時上報配置項爲 true,則立刻執行後續真正的數據聚合過程;不然中斷(只落庫,不觸發上報)
-
因爲頻率會比較高,因此須要作節流的邏輯
不少人會搞不清楚防抖和節流的區別。一言以蔽之:「函數防抖關注必定時間連續觸發的事件只在最後執行一次,而函數節流側重於一段時間內只執行一次」。此處不是本文重點,感興趣的的能夠查看這篇文章
-
上報流程會首先判斷(爲了節約用戶流量)
- 判斷當前網絡環境爲 WI-FI 則實時上報
- 判斷當前網絡環境不可用,則實時中斷後續
- 判斷當前網絡環境爲蜂窩網絡, 則作是否超過1個天然天內使用流量是否超標的判斷
- T(當前時間戳) - T(上次保存時間戳) > 24h,則清零已使用的流量,記錄當前時間戳到上次上報時間的變量中
- T(當前時間戳) - T(上次保存時間戳) <= 24h,則判斷一個天然天內已使用流量大小是否超過下發的數據上報配置中的流量上限字段,超過則 exit;不然執行後續流程
-
數據聚合分表進行,且會有必定的規則
- 優先獲取 crash 數據
- 單次網絡上報中,總體數據條數不能數據上報配置中的條數限制;數據大小不能超過數據配置中的數據大小
-
數據取出後將這批數據標記爲 dirty 狀態
-
meta 表數據須要先
gZip
壓縮,再使用AES 128
加密 -
payload 表數據需組裝自定義格式的報文。格式以下
Header 部分:
2字節大小、數據類型 unsigned short 表示 meta 數據大小 + n 條 payload 數據結構(2字節大小、數據類型爲 unsigned int 表示單條 payload 數據大小)
header + meta 數據 + payload 數據
-
發起數據上報網絡請求
- 成功回調:刪除標記爲
dirty
的數據。判斷爲流量環境,則將該批數據大小疊加到1個天然天內已使用流量大小的變量中。 - 失敗回調:更新標記爲
dirty
的數據爲正常狀態。判斷爲流量環境,則將該批數據大小疊加到1個天然天內已使用流量大小的變量中。
- 成功回調:刪除標記爲
整個上報流程圖以下:
2. 踩過的坑 && 作得好的地方
-
以前作�����對網絡接口基本上都是使用現有協議的
key/value
協議上開發的,它的優勢是使用簡單,缺點是協議體太大。在設計方案的時候分析道數據上報 SDK 網絡上報確定是很是高頻的因此咱們須要設計自定義的報文協議,這部分的設計上能夠參考TCP 報文頭結構
。 -
當時和後端對接接口的時候發現數據上報過去,服務端解析不了。斷點調試發現數據聚合後的大小、條數、壓縮、加密都是正常的,在本地 Mock 後徹底能夠反向解析出來。但爲何到服務端就解析不了,聯調後發現是字節端序(Big-Endian)的問題。簡單介紹以下,關於大小端序的詳細介紹請查看個人這篇文章
主機字節順序HBO(Host Byte Order):與 CPU 類型有關。Big-Endian: PowerPC、IBM、Sun。Little-Endian:x8六、DEC
網絡字節順序 NBO(Network Byte Order):網絡默認爲大端序。
-
上面的邏輯有一步是當網絡上報成功後須要刪除標記爲 dirty 的數據。可是測試了一下發現,大量數據刪除後數據庫文件的大小不變,理論上須要騰出內存數據大小的空間。
sqlite 採用的是變長記錄存儲,當數據被刪除後,未使用的磁盤空間被添加到一個內在的「空閒列表」中,用於下次插入數據,這屬於優化機制之一,sqlite 提供
vacuum
命令來釋放。這個問題相似於 Linux 中的文件引用計數的意思,雖然不同,可是提出來作一下參考。實驗是這樣的
-
先看一下當前各個掛載目錄的空間大小:
df -h
-
首先咱們產生一個50M大小的文件
-
寫一段代碼讀取文件
#include<stdio.h> #include<unistd.h> int main(void) { FILE *fp = NULL; fp = fopen("/boot/test.txt", "rw+"); if(NULL == fp){ perror("open file failed"); return -1; } while(1){ //do nothing sleep(1); } fclose(fp); return 0; }
-
命令行模式下使用
rm
刪除文件 -
查看文件大小:
df -h
,發現文件被刪除了,可是該目錄下的可用空間並未變多
解釋:實際上,只有當一個文件的引用計數爲0(包括硬連接數)的時候,纔可能調用 unlink 刪除,只要它不是0,那麼就不會被刪除。所謂的刪除,也不過是文件名到 inode 的連接刪除,只要不被從新寫入新的數據,磁盤上的 block 數據塊不會被刪除,所以,你會看到,即使刪庫跑路了,某些數據仍是能夠恢復的。換句話說,當一個程序打開一個文件的時候(獲取到文件描述符),它的引用計數會被+1,rm雖然看似刪除了文件,實際上只是會將引用計數減1,但因爲引用計數不爲0,所以文件不會被刪除。
-
-
在數據聚合的時候優先獲取 crash 數據,總數據條數須要小於上報配置數據的條數限制、總數據大小須要小於上報配置數據的大小限制。這裏的處理使用了遞歸,改變了函數參數
- (void)assembleDataInTable:(HCTLogTableType)tableType networkType:(NetworkingManagerStatusType)networkType completion:(void (^)(NSArray<hctlogmodel *> *records))completion { // 1. 獲取到合適的 Crash 類型的數據 [self fetchCrashDataByCount:self.configureModel.maxFlowMByte inTable:tableType upperBound:self.configureModel.maxBodyMByte completion:^(NSArray<hctlogmodel *> *records) { NSArray<hctlogmodel *> *crashData = records; // 2. 計算剩餘須要的數據條數和剩餘須要的數據大小 NSInteger remainingCount = self.configureModel.maxItem - crashData.count; float remainingSize = self.configureModel.maxBodyMByte - [self calculateDataSize:crashData]; // 3. 獲取除 Crash 類型以外的其餘數據,且須要符合相應規則 BOOL isWifi = (networkType == NetworkingManagerStatusReachableViaWiFi); [self fetchDataExceptCrash:remainingCount inTable:tableType upperBound:remainingSize isWiFI:isWifi completion:^(NSArray<hctlogmodel *> *records) { NSArray<hctlogmodel *> *dataExceptCrash = records; NSMutableArray *dataSource = [NSMutableArray array]; [dataSource addObjectsFromArray:crashData]; [dataSource addObjectsFromArray:dataExceptCrash]; if (completion) { completion([dataSource copy]); } }]; }]; } - (void)fetchDataExceptCrash:(NSInteger)count inTable:(HCTLogTableType)tableType upperBound:(float)size isWiFI:(BOOL)isWifi completion:(void (^)(NSArray<hctlogmodel *> *records))completion { // 1. 根據剩餘須要數據條數去查詢表中非 Crash 類型的數據集合 __block NSMutableArray *conditions = [NSMutableArray array]; [self.configureModel.monitorList enumerateObjectsUsingBlock:^(HCTItemModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (isWifi) { if (![obj.type isEqualToString:@"appCrash"]) { [conditions addObject:[NSString stringWithFormat:@"'%@'", obj.type]]; } } else { if (!obj.onlyWifi && ![obj.type isEqualToString:@"appCrash"]) { [conditions addObject:[NSString stringWithFormat:@"'%@'", HCT_SAFE_STRING(obj.type)]]; } } }]; NSString *queryCrashDataCondition = [NSString stringWithFormat:@"monitor_type in (%@) and is_used = 0 and namespace = '%@'", [conditions componentsJoinedByString:@","], self.namespace]; // 2. 根據是否有 Wifi 查找對應的數據 [HCT_DATABASE getRecordsByCount:count condtion:queryCrashDataCondition inTableType:tableType completion:^(NSArray<hctlogmodel *> *_Nonnull records) { // 3. 非 Crash 類型的數據集合大小是否超過剩餘須要的數據大小 float dataSize = [self calculateDataSize:records]; // 4. 大於最大包體積則遞歸獲取 maxItem-1 條非 Crash 數據集合並判斷數據大小 if (size == 0) { if (completion) { completion(records); } } else if (dataSize > size) { NSInteger currentCount = count - 1; return [self fetchDataExceptCrash:currentCount inTable:tableType upperBound:size isWiFI:isWifi completion:completion]; } else { if (completion) { completion(records); } } }]; }
-
整個 SDK 的 Unit Test 經過率 100%,代碼分支覆蓋率爲 93%。測試基於 TDD 和 BDD。測試框架:系統自帶的
XCTest
,第三方的OCMock
、Kiwi
、Expecta
、Specta
。測試使用了基礎類,後續每一個文件都設計繼承自測試基類的類。Xcode 能夠看到整個 SDK 的測試覆蓋率和單個文件的測試覆蓋率
也可使用 slather。在項目終端環境下新建
.slather.yml
配置文件,而後執行語句slather coverage -s --scheme hermes-client-Example --workspace hermes-client.xcworkspace hermes-client.xcodeproj
。關於質量保證的最基礎、可靠的方案之一軟件測試,在各個端都有一些須要注意的地方,還須要結合工程化,我會寫專門的文章談談經驗心得。
5、 接口設計及核心實現
1. 接口設計
@interface HermesClient : NSObject - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; /** 單例方式初始化全局惟一對象。單例以後必須立刻 setUp @return 單例對象 */ + (instancetype)sharedInstance; /** 當前 SDK 初始化。當前功能:註冊配置下發服務。 */ - (void)setup; /** 上報 payload 類型的數據 @param type 監控類型 @param meta 元數據 @param payload payload類型的數據 */ - (void)sendWithType:(NSString *)type meta:(NSDictionary *)meta payload:(NSData *__nullable)payload; /** 上報 meta 類型的數據,須要傳遞三個參數。type 代表是什麼類型的數據;prefix 表明前綴,上報到後臺會拼接 prefix+type;meta 是字典類型的元數據 @param type 數據類型 @param prefix 數據類型的前綴。通常是業務線名稱首字母簡寫。好比記帳:JZ @param meta description元數據 */ - (void)sendWithType:(NSString *)type prefix:(NSString *)prefix meta:(NSDictionary *)meta; /** 獲取上報相關的通用信息 @return 上報基礎信息 */ - (HCTCommonModel *)getCommon; /** 是否須要採集上報 @return 上報開關 */ - (BOOL)isGather:(NSString *)namespace; @end
HermesClient
類是整個 SDK 的入口,也是接口的提供者。其中 - (void)sendWithType:(NSString *)type prefix:(NSString *)prefix meta:(NSDictionary *)meta;
接口給業務方使用。
- (void)sendWithType:(NSString *)type meta:(NSDictionary *)meta payload:(NSData *__nullable)payload;
給監控數據使用。
setup
方法內部開啓多個 namespace 下的處理 handler。
- (void)setup { // 註冊 mget 獲取監控和各業務線的配置信息,會產生多個 namespace,彼此平行、隔離 [[HCTConfigurationService sharedInstance] registerAndFetchConfigurationInfo]; [self.configutations enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { HCTService *service = [[HCTService alloc] initWithNamespace:obj]; [self.services setObject:service forKey:obj]; }]; HCTService *hermesService = [self.services objectForKey:HermesNAMESPACE]; if (!hermesService) { hermesService = [[HCTService alloc] initWithNamespace:HermesNAMESPACE]; [self.services setObject:hermesService forKey:HermesNAMESPACE]; } }
2. 核心實現
真正處理邏輯的是 HCTService
類。
#define HCT_SAVED_FLOW @"HCT_SAVED_FLOW" #define HCT_SAVED_TIMESTAMP @"HCT_SAVED_TIMESTAMP" @interface HCTService () @property (nonatomic, copy) NSString *requestBaseUrl; /**<須要配置的baseurl* @property (nonatomic, copy) hctconfigurationmodel *configuremodel; **<當前 namespace 下的配置信息* nsstring *metaurl; **<meta 接口地址* *payloadurl; **<payload strong) hctrequestfactory *requester; **<網絡請求中心* nsnumber *currenttimestamp; **<保存的時間戳* *currentflow; **<當前已使用的流量* tmlooptaskexecutor *taskexecutor; **<上報數據定時任務* assign) bool isapplaunched; **<經過 kvc 的形式獲取到 hermesclient 裏面存儲 app 是否啓動完成的標識,這種 case 是處理: mget 首次獲取到 3個 namespace, 但 運行期間服務端新增某種 此時業務線若是插入數據依舊能夠正常落庫、上報* @end @implementation hctservice @synthesize currenttimestamp="_currentTimestamp;" currentflow="_currentFlow;" #pragma mark - life cycle (instancetype)initwithnamespace:(nsstring * _nonnull )namespace { if (self="[super" init]) _namespace="namespace;" [self setupconfig]; (self.isapplaunched) executehandlerwhenapplaunched]; } else [[nsnotificationcenter defaultcenter] addobserverforname:uiapplicationdidfinishlaunchingnotification object:nil queue:[nsoperationqueue mainqueue] usingblock:^(nsnotification note) [[hermesclient sharedinstance] setvalue:@(yes) forkey:@"isapplaunched"]; }]; return self; public method (void)sendwithtype:(nsstring *)type meta:(nsdictionary *)meta payload:(nsdata *__nullable)payload 1. 檢查參數合法性 *warning="[NSString" stringwithformat:@"%@不能是空字符串", type]; (!hct_is_class(type, nsstring)) nsassert1(hct_is_class(type, nsstring), warning, type); return; (type.length="=" 0) nsassert1(type.length> 0, warning, type); return; } if (!HCT_IS_CLASS(meta, NSDictionary)) { return; } if (meta.allKeys.count == 0) { return; } // 2. 判斷當前 namespace 是否開啓了收集 if (!self.configureModel.isGather) { HCTLOG(@"%@", [NSString stringWithFormat:@"Namespace: %@ 下數據收集開關爲關閉狀態", self.namespace]); return ; } // 3. 判斷是不是有效的數據。能夠落庫(type 和監控參數的接口中 monitorList 中的任一條目的type 相等) BOOL isValidate = [self validateLogData:type]; if (!isValidate) { return; } // 3. 先寫入 meta 表 HCTCommonModel *commonModel = [[HCTCommonModel alloc] init]; [self sendWithType:type namespace:HermesNAMESPACE meta:meta isBiz:NO commonModel:commonModel]; // 4. 若是 payload 不存在則退出當前執行 if (!HCT_IS_CLASS(payload, NSData) && !payload) { return; } // 5. 添加限制(超過大小的數據就不能入庫,由於這是服務端消耗 payload 的一個上限) CGFloat payloadSize = [self calculateDataSize:payload]; if (payloadSize > self.configureModel.maxBodyMByte) { NSString *assertString = [NSString stringWithFormat:@"payload 數據的大小超過臨界值 %zdKB", self.configureModel.maxBodyMByte]; NSAssert(payloadSize <= self.configureModel.maxBodyMByte, assertString); return; } // 6. 合併 meta 與 Common 基礎數據,用來存儲 payload 上報所須要的 meta 信息 NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary]; NSDictionary *commonDictionary = [commonModel getDictionary]; // Crash 類型爲特例,外部傳入的 Crash 案發現場信息不能被覆蓋 if ([type isEqualToString:@"appCrash"]) { [metaDictionary addEntriesFromDictionary:commonDictionary]; [metaDictionary addEntriesFromDictionary:meta]; } else { [metaDictionary addEntriesFromDictionary:meta]; [metaDictionary addEntriesFromDictionary:commonDictionary]; } NSError *error; NSData *metaData = [NSJSONSerialization dataWithJSONObject:metaDictionary options:0 error:&error]; if (error) { HCTLOG(@"%@", error); return; } NSString *metaContentString = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding]; // 7. 計算上報時 payload 這條數據的大小(meta+payload) NSMutableData *totalData = [NSMutableData data]; [totalData appendData:metaData]; [totalData appendData:payload]; // 8. 再寫入 payload 表 HCTLogPayloadModel *payloadModel = [[HCTLogPayloadModel alloc] init]; payloadModel.is_used = NO; payloadModel.namespace = HermesNAMESPACE; payloadModel.report_id = HCT_SAFE_STRING(commonModel.REPORT_ID); payloadModel.monitor_type = HCT_SAFE_STRING(type); payloadModel.is_biz = NO; payloadModel.created_time = HCT_SAFE_STRING(commonModel.CREATE_TIME); payloadModel.meta = HCT_SAFE_STRING(metaContentString); payloadModel.payload = payload; payloadModel.size = totalData.length; [HCT_DATABASE add:@[payloadModel] inTableType:HCTLogTableTypePayload]; // 9. 判斷是否觸發實時上報 [self handleUploadDataWithtype:type]; } - (void)sendWithType:(NSString *)type prefix:(NSString *)prefix meta:(NSDictionary *)meta { // 1. 校驗參數合法性 NSString *prefixWarning = [NSString stringWithFormat:@"%@不能是空字符串", prefix]; if (!HCT_IS_CLASS(prefix, NSString)) { NSAssert1(HCT_IS_CLASS(prefix, NSString), prefixWarning, prefix); return; } if (prefix.length == 0) { NSAssert1(prefix.length > 0, prefixWarning, prefix); return; } NSString *typeWarning = [NSString stringWithFormat:@"%@不能是空字符串", type]; if (!HCT_IS_CLASS(type, NSString)) { NSAssert1(HCT_IS_CLASS(type, NSString), typeWarning, type); return; } if (type.length == 0) { NSAssert1(type.length > 0, typeWarning, type); return; } if (!HCT_IS_CLASS(meta, NSDictionary)) { return; } if (meta.allKeys.count == 0) { return; } // 2. 私有接口處理 is_biz 邏輯 HCTCommonModel *commonModel = [[HCTCommonModel alloc] init]; [self sendWithType:type namespace:prefix meta:meta isBiz:YES commonModel:commonModel]; } #pragma mark - private method // 基礎配置 - (void)setupConfig { _requestBaseUrl = @"https://***DomainName.com"; _metaURL = @"hermes/***"; _payloadURL = @"hermes/***"; } - (void)executeHandlerWhenAppLaunched { // 1. 刪除非法數據 [self handleInvalidateData]; // 2. 回收數據庫磁盤碎片空間 [self rebuildDatabase]; // 3. 開啓定時器去定時上報數據 [self executeTimedTask]; } /* 1. 當 App 版本變化的時候刪除數據 2. 刪除過時數據 3. 刪除 Payload 表裏面超過限制的數據 4. 刪除上傳接口網絡成功,可是突發 crash 形成沒有刪除這批數據的狀況,因此啓動完成後刪除 is_used = YES 的數據 */ - (void)handleInvalidateData { NSString *currentVersion = [[HermesClient sharedInstance] getCommon].APP_VERSION; NSString *savedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:HCT_SAVED_APP_VERSION] ?: [currentVersion copy]; NSInteger threshold = [NSDate HCT_currentTimestamp]; if (![currentVersion isEqualToString:savedVersion] && self.configureModel.isUpdateClear) { [[NSUserDefaults standardUserDefaults] setObject:currentVersion forKey:HCT_SAVED_APP_VERSION]; } else { threshold = [NSDate HCT_currentTimestamp] - self.configureModel.expirationDay * 24 * 60 * 60 *1000; } NSInteger sizeUpperLimit = self.configureModel.maxBodyMByte * 1024 * 1024; NSString *sqlString = [NSString stringWithFormat:@"(created_time < %zd and namespace = '%@') or size > %zd or is_used = 1", threshold, self.namespace, sizeUpperLimit]; [HCT_DATABASE removeDataUseCondition:sqlString inTableType:HCTLogTableTypeMeta]; [HCT_DATABASE removeDataUseCondition:sqlString inTableType:HCTLogTableTypePayload]; } // 啓動時刻清理數據表空間碎片,回收磁盤大小 - (void)rebuildDatabase { [HCT_DATABASE rebuildDatabaseFileInTableType:HCTLogTableTypeMeta]; [HCT_DATABASE rebuildDatabaseFileInTableType:HCTLogTableTypePayload]; } // 判斷數據是否能夠落庫 - (BOOL)validateLogData:(NSString *)dataType { NSArray<hctitemmodel *> *monitors = self.configureModel.monitorList; __block BOOL isValidate = NO; [monitors enumerateObjectsUsingBlock:^(HCTItemModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if ([obj.type isEqualToString:dataType]) { isValidate = YES; *stop = YES; } }]; return isValidate; } - (void)executeTimedTask { __weak typeof(self) weakself = self; self.taskExecutor = [[TMLoopTaskExecutor alloc] init]; TMTaskOption *dataUploadOption = [[TMTaskOption alloc] init]; dataUploadOption.option = TMTaskRunOptionRuntime; dataUploadOption.interval = self.configureModel.periodicTimerSecond; TMTask *dataUploadTask = [[TMTask alloc] init]; dataUploadTask.runBlock = ^{ [weakself upload]; }; [self.taskExecutor addTask:dataUploadTask option:dataUploadOption]; } - (void)handleUploadDataWithtype:(NSString *)type { __block BOOL canUploadInTime = NO; [self.configureModel.monitorList enumerateObjectsUsingBlock:^(HCTItemModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if ([type isEqualToString:obj.type]) { if (obj.isRealtime) { canUploadInTime = YES; *stop = YES; } } }]; if (canUploadInTime) { // 節流 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self upload]; }); } } // 對內和對外的存儲都走這個流程。經過這個接口設置 is_biz 信息 - (void)sendWithType:(NSString *)type namespace:(NSString *)namespace meta:(NSDictionary *)meta isBiz:(BOOL)is_biz commonModel:(HCTCommonModel *)commonModel { // 0. 判斷當前 namespace 是否開啓了收集 if (!self.configureModel.isGather) { HCTLOG(@"%@", [NSString stringWithFormat:@"Namespace: %@ 下數據收集開關爲關閉狀態", self.namespace]); return ; } // 1. 檢查參數合法性 NSString *warning = [NSString stringWithFormat:@"%@不能是空字符串", type]; if (!HCT_IS_CLASS(type, NSString)) { NSAssert1(HCT_IS_CLASS(type, NSString), warning, type); return; } if (type.length == 0) { NSAssert1(type.length > 0, warning, type); return; } if (!HCT_IS_CLASS(meta, NSDictionary)) { return; } if (meta.allKeys.count == 0) { return; } // 2. 判斷是不是有效的數據。能夠落庫(type 和監控參數的接口中 monitorList 中的任一條目的type 相等) BOOL isValidate = [self validateLogData:type]; if (!isValidate) { return; } // 3. 合併 meta 與 Common 基礎數據 NSMutableDictionary *mutableMeta = [NSMutableDictionary dictionaryWithDictionary:meta]; mutableMeta[@"MONITOR_TYPE"] = is_biz ? [NSString stringWithFormat:@"%@-%@", namespace, type] : type; meta = [mutableMeta copy]; commonModel.IS_BIZ = is_biz; NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary]; NSDictionary *commonDictionary = [commonModel getDictionary]; // Crash 類型爲特例,外部傳入的 Crash 案發現場信息不能被覆蓋 if ([type isEqualToString:@"appCrash"]) { [metaDictionary addEntriesFromDictionary:commonDictionary]; [metaDictionary addEntriesFromDictionary:meta]; } else { [metaDictionary addEntriesFromDictionary:meta]; [metaDictionary addEntriesFromDictionary:commonDictionary]; } // 4. 轉換爲 NSData NSError *error; NSData *metaData = [NSJSONSerialization dataWithJSONObject:metaDictionary options:0 error:&error]; if (error) { HCTLOG(@"%@", error); return; } // 5. 添加限制(超過 10K 的數據就不能入庫,由於這是服務端消耗 meta 的一個上限) CGFloat metaSize = [self calculateDataSize:metaData]; if (metaSize > 10 / 1024.0) { NSAssert(metaSize <= 10 / 1024.0, @"meta 數據的大小超過臨界值 10KB"); return; } NSString *metaContentString = [[NSString alloc] initWithData:metaData encoding:NSUTF8StringEncoding]; // 6. 構造 MetaModel 模型 HCTLogMetaModel *metaModel = [[HCTLogMetaModel alloc] init]; metaModel.namespace = namespace; metaModel.report_id = HCT_SAFE_STRING(commonModel.REPORT_ID); metaModel.monitor_type = HCT_SAFE_STRING(type); metaModel.created_time = HCT_SAFE_STRING(commonModel.CREATE_TIME); metaModel.meta = HCT_SAFE_STRING(metaContentString); metaModel.size = metaData.length; metaModel.is_biz = is_biz; // 7. 寫入數據庫 [HCT_DATABASE add:@[metaModel] inTableType:HCTLogTableTypeMeta]; // 8. 判斷是否觸發實時上報(對內的接口則在函數內部判斷,若是是對外的則在這裏判斷) if (is_biz) { [self handleUploadDataWithtype:type]; } } - (BOOL)needUploadPayload:(HCTLogPayloadModel *)model { __block BOOL needed = NO; [self.configureModel.monitorList enumerateObjectsUsingBlock:^(HCTItemModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if ([obj.type isEqualToString:model.monitor_type] && obj.isUploadPayload) { needed = YES; *stop = YES; } }]; return needed; } /* 計算 數據包大小,分爲2種狀況。 1. 上傳前使用數據表中的 size 字段去判斷大小 2. 上報完成後則根據真實網絡通訊中組裝的 payload 進行大小計算 */ - (float)calculateDataSize:(id)data { if (HCT_IS_CLASS(data, NSArray)) { __block NSInteger dataLength = 0; NSArray *uploadDatasource = (NSArray *)data; [uploadDatasource enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if (HCT_IS_CLASS(obj, HCTLogModel)) { HCTLogModel *uploadModel = (HCTLogModel *)obj; dataLength += uploadModel.size; } }]; return dataLength / (1024 * 1024.0); } else if (HCT_IS_CLASS(data, NSData)) { NSData *rawData = (NSData *)data; return rawData.length / (1024 * 1024.0); } else { return 0; } } // 上報流程的主函數 - (void)upload { /* 1. 判斷可否上報 2. 數據聚合 3. 加密壓縮 4. 1分鐘內的網絡請求合併爲1次 5. 上報(全局上報開關是開着的狀況) - 成功:刪除本地數據、調用更新策略的接口 - 失敗:不刪除本地數據 */ [self judgeCanUploadCompletionBlock:^(BOOL canUpload, NetworkingManagerStatusType networkType) { if (canUpload && self.configureModel.isUpload) { [self handleUploadTask:networkType]; } }]; } /** 上報前的校驗 - 判斷網絡狀況,分爲 wifi 和 非 Wi-Fi 、網絡不通的狀況。 - 從配置下發的 monitorList 找出 onlyWifi 字段爲 true 的 type,組成數組 [appCrash、appLag...] - 網絡不通,則不能上報 - 網絡通,則判斷上報校驗 1. 當前GMT時間戳-保存的時間戳超過24h。則認爲是一個新的天然天 - 清除 currentFlow - 觸發上報流程 2. 當前GMT時間戳-保存的時間戳不超過24h - 當前的流量是否超過配置信息裏面的最大流量,未超過(<):觸發上報流程 - 當前的流量是否超過配置信息裏面的最大流量,超過:結束流程 */ - (void)judgeCanUploadCompletionBlock:(void (^)(BOOL canUpload, NetworkingManagerStatusType networkType))completionBlock { // WIFI 的狀況下不判斷直接上傳;不是 WIFI 的狀況須要判斷「當日最大限制流量」 [self.requester networkStatusWithBlock:^(NetworkingManagerStatusType status) { switch (status) { case NetworkingManagerStatusUnknown: { HCTLOG(@"沒有網絡權限哦"); if (completionBlock) { completionBlock(NO, NetworkingManagerStatusUnknown); } break; } case NetworkingManagerStatusNotReachable: { if (completionBlock) { completionBlock(NO, NetworkingManagerStatusNotReachable); } break; } case NetworkingManagerStatusReachableViaWiFi: { if (completionBlock) { completionBlock(YES, NetworkingManagerStatusReachableViaWiFi); } break; } case NetworkingManagerStatusReachableViaWWAN: { if ([self currentGMTStyleTimeStamp] - self.currentTimestamp.integerValue > 24 * 60 * 60 * 1000) { self.currentFlow = [NSNumber numberWithFloat:0]; self.currentTimestamp = [NSNumber numberWithInteger:[self currentGMTStyleTimeStamp]]; if (completionBlock) { completionBlock(YES, NetworkingManagerStatusReachableViaWWAN); } } else { if (self.currentFlow.floatValue < self.configureModel.maxFlowMByte) { if (completionBlock) { completionBlock(YES, NetworkingManagerStatusReachableViaWWAN); } } else { if (completionBlock) { completionBlock(NO, NetworkingManagerStatusReachableViaWWAN); } } } break; } } }]; } - (void)handleUploadTask:(NetworkingManagerStatusType)networkType { // 數據聚合(2張表分別掃描) -> 壓縮 -> 上報 [self handleUploadTaskInMetaTable:networkType]; [self handleUploadTaskInPayloadTable:networkType]; } - (void)handleUploadTaskInMetaTable:(NetworkingManagerStatusType)networkType { __weak typeof(self) weakself = self; // 1. 數據聚合 [self assembleDataInTable:HCTLogTableTypeMeta networkType:networkType completion:^(NSArray<hctlogmodel *> *records) { if (records.count == 0) { return; } // 2. 加密壓縮處理:(meta 總體先加密再壓縮,payload一條條先加密再壓縮) __block NSMutableString *metaStrings = [NSMutableString string]; __block NSMutableArray *usedReportIds = [NSMutableArray array]; // 2.1. 遍歷拼接model,取出 meta,用 \n 拼接 [records enumerateObjectsUsingBlock:^(HCTLogModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if (HCT_IS_CLASS(obj, HCTLogMetaModel)) { HCTLogMetaModel *metaModel = (HCTLogMetaModel *)obj; BOOL shouldAppendLineBreakSymbol = idx < (records.count - 1); [usedReportIds addObject:[NSString stringWithFormat:@"'%@'", metaModel.report_id]]; [metaStrings appendString:[NSString stringWithFormat:@"%@%@", metaModel.meta, shouldAppendLineBreakSymbol ? @"\n" : @""]]; } }]; if (metaStrings.length == 0) { return; } // 2.2 拼接後的內容先壓縮再加密 NSData *data = [HCTDataSerializer compressAndEncryptWithString:metaStrings]; // 3. 將取出來用於接口請求的數據標記爲 dirty NSString *updateCondtion = [NSString stringWithFormat:@"report_id in (%@)", [usedReportIds componentsJoinedByString:@","]]; [[HCTDatabase sharedInstance] updateData:@"is_used = 1" useCondition:updateCondtion inTableType:HCTLogTableTypeMeta]; // 4. 請求網絡 NSString *requestURL = [NSString stringWithFormat:@"%@/%@", weakself.requestBaseUrl, weakself.metaURL]; [weakself.requester postDataWithRequestURL:requestURL bodyData:data success:^{ [weakself deleteInvalidateData:records inTableType:HCTLogTableTypeMeta]; if (networkType == NetworkingManagerStatusReachableViaWWAN) { float currentFlow = [weakself.currentFlow floatValue]; currentFlow += [weakself calculateDataSize:data]; weakself.currentFlow = [NSNumber numberWithFloat:currentFlow]; } } failure:^(NSError *_Nonnull error) { [[HCTDatabase sharedInstance] updateData:@"is_used = 0" useCondition:updateCondtion inTableType:HCTLogTableTypeMeta]; if (networkType == NetworkingManagerStatusReachableViaWWAN) { float currentFlow = [weakself.currentFlow floatValue]; currentFlow += [weakself calculateDataSize:data]; weakself.currentFlow = [NSNumber numberWithFloat:currentFlow]; } }]; }]; } - (NSData *)handlePayloadData:(NSArray *)rawArray { // 1. 數據校驗 if (rawArray.count == 0) { return nil; } // 2. 加密壓縮處理:(meta 總體先加密再壓縮,payload一條條先加密再壓縮) __block NSMutableString *metaStrings = [NSMutableString string]; __block NSMutableArray<nsdata *> *payloads = [NSMutableArray array]; // 2.1. 遍歷拼接model,取出 meta,用 \n 拼接 [rawArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if (HCT_IS_CLASS(obj, HCTLogPayloadModel)) { HCTLogPayloadModel *payloadModel = (HCTLogPayloadModel *)obj; BOOL shouldAppendLineBreakSymbol = idx < (rawArray.count - 1); [metaStrings appendString:[NSString stringWithFormat:@"%@%@", HCT_SAFE_STRING(payloadModel.meta), shouldAppendLineBreakSymbol ? @"\n" : @""]]; // 2.2 判斷是否須要上傳 payload 信息。若是須要則將 payload 取出。 if ([self needUploadPayload:payloadModel]) { if (payloadModel.payload) { NSData *payloadData = [HCTDataSerializer compressAndEncryptWithData:payloadModel.payload]; if (payloadData) { [payloads addObject:payloadData]; } } } } }]; NSData *metaData = [HCTDataSerializer compressAndEncryptWithString:metaStrings]; __block NSMutableData *headerData = [NSMutableData data]; unsigned short metaLength = (unsigned short)metaData.length; HTONS(metaLength); // 處理2字節的大端序 [headerData appendData:[NSData dataWithBytes:&metaLength length:sizeof(metaLength)]]; Byte payloadCountbytes[] = {payloads.count}; NSData *payloadCountData = [[NSData alloc] initWithBytes:payloadCountbytes length:sizeof(payloadCountbytes)]; [headerData appendData:payloadCountData]; [payloads enumerateObjectsUsingBlock:^(NSData *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { unsigned int payloadLength = (unsigned int)obj.length; HTONL(payloadLength); // 處理4字節的大端序 [headerData appendData:[NSData dataWithBytes:&payloadLength length:sizeof(payloadLength)]]; }]; __block NSMutableData *uploadData = [NSMutableData data]; // 先添加 header 基礎信息,不須要加密壓縮 [uploadData appendData:[headerData copy]]; // 再添加 meta 信息,meta 信息須要先壓縮再加密 [uploadData appendData:metaData]; // 再添加 payload 信息 [payloads enumerateObjectsUsingBlock:^(NSData *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { [uploadData appendData:obj]; }]; return [uploadData copy]; } - (void)handleUploadTaskInPayloadTable:(NetworkingManagerStatusType)networkType { __weak typeof(self) weakself = self; // 1. 數據聚合 [self assembleDataInTable:HCTLogTableTypePayload networkType:networkType completion:^(NSArray<hctlogmodel *> *records) { if (records.count == 0) { return; } // 2. 取出能夠上傳的 payload 數據 NSArray *canUploadPayloadData = [self fetchDataCanUploadPayload:records]; if (canUploadPayloadData.count == 0) { return; } // 3. 將取出來用於接口請求的數據標記爲 dirty __block NSMutableArray *usedReportIds = [NSMutableArray array]; [canUploadPayloadData enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (HCT_IS_CLASS(obj, HCTLogModel)) { HCTLogModel *model = (HCTLogModel *)obj; [usedReportIds addObject:[NSString stringWithFormat:@"'%@'", model.report_id]]; } }]; NSString *updateCondtion = [NSString stringWithFormat:@"report_id in (%@)", [usedReportIds componentsJoinedByString:@","]]; [[HCTDatabase sharedInstance] updateData:@"is_used = 1" useCondition:updateCondtion inTableType:HCTLogTableTypePayload]; // 4. 將取出的數據聚合,組成報文 NSData *uploadData = [self handlePayloadData:canUploadPayloadData]; // 5. 請求網絡 NSString *requestURL = [NSString stringWithFormat:@"%@/%@", weakself.requestBaseUrl, weakself.payloadURL]; [weakself.requester postDataWithRequestURL:requestURL bodyData:uploadData success:^{ [weakself deleteInvalidateData:canUploadPayloadData inTableType:HCTLogTableTypePayload]; if (networkType == NetworkingManagerStatusReachableViaWWAN) { float currentFlow = [weakself.currentFlow floatValue]; currentFlow += [weakself calculateDataSize:uploadData]; weakself.currentFlow = [NSNumber numberWithFloat:currentFlow]; } } failure:^(NSError *_Nonnull error) { [[HCTDatabase sharedInstance] updateData:@"is_used = 0" useCondition:updateCondtion inTableType:HCTLogTableTypePayload]; if (networkType == NetworkingManagerStatusReachableViaWWAN) { float currentFlow = [weakself.currentFlow floatValue]; currentFlow += [weakself calculateDataSize:uploadData]; weakself.currentFlow = [NSNumber numberWithFloat:currentFlow]; } }]; }]; } // 清除過時數據 - (void)deleteInvalidateData:(NSArray<hctlogmodel *> *)data inTableType:(HCTLogTableType)tableType { [HCT_DATABASE remove:data inTableType:tableType]; } // 以秒爲單位的時間戳 - (NSInteger)currentGMTStyleTimeStamp { return [NSDate HCT_currentTimestamp]/1000; } #pragma mark-- 數據庫操做 /** 根據接口配置信息中的條件獲取表中的上報數據 - Wi-Fi 的時候都上報 - 不爲 Wi-Fi 的時候:onlyWifi 爲 false 的類型進行上報 */ - (void)assembleDataInTable:(HCTLogTableType)tableType networkType:(NetworkingManagerStatusType)networkType completion:(void (^)(NSArray<hctlogmodel *> *records))completion { // 1. 獲取到合適的 Crash 類型的數據 [self fetchCrashDataByCount:self.configureModel.maxFlowMByte inTable:tableType upperBound:self.configureModel.maxBodyMByte completion:^(NSArray<hctlogmodel *> *records) { NSArray<hctlogmodel *> *crashData = records; // 2. 計算剩餘須要的數據條數和剩餘須要的數據大小 NSInteger remainingCount = self.configureModel.maxItem - crashData.count; float remainingSize = self.configureModel.maxBodyMByte - [self calculateDataSize:crashData]; // 3. 獲取除 Crash 類型以外的其餘數據,且須要符合相應規則 BOOL isWifi = (networkType == NetworkingManagerStatusReachableViaWiFi); [self fetchDataExceptCrash:remainingCount inTable:tableType upperBound:remainingSize isWiFI:isWifi completion:^(NSArray<hctlogmodel *> *records) { NSArray<hctlogmodel *> *dataExceptCrash = records; NSMutableArray *dataSource = [NSMutableArray array]; [dataSource addObjectsFromArray:crashData]; [dataSource addObjectsFromArray:dataExceptCrash]; if (completion) { completion([dataSource copy]); } }]; }]; } - (NSArray *)fetchDataCanUploadPayload:(NSArray *)datasource { __weak typeof(self) weakself = self; __block NSMutableArray *array = [NSMutableArray array]; if (!HCT_IS_CLASS(datasource, NSArray)) { NSAssert(HCT_IS_CLASS(datasource, NSArray), @"參數必須是數組"); return nil; } [datasource enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { if (HCT_IS_CLASS(obj, HCTLogPayloadModel)) { HCTLogPayloadModel *payloadModel = (HCTLogPayloadModel *)obj; // 判斷是否須要上傳 payload 信息 if ([weakself needUploadPayload:payloadModel]) { [array addObject:payloadModel]; } } }]; return [array copy]; } // 遞歸獲取符合條件的 Crash 數據集合(count < maxItem && size < maxBodySize) - (void)fetchCrashDataByCount:(NSInteger)count inTable:(HCTLogTableType)tableType upperBound:(NSInteger)size completion:(void (^)(NSArray<hctlogmodel *> *records))completion { // 1. 先經過接口拿到的 maxItem 數去查詢表中的 Crash 數據集合 NSString *queryCrashDataCondition = [NSString stringWithFormat:@"monitor_type = 'appCrash' and is_used = 0 and namespace = '%@'", self.namespace]; [HCT_DATABASE getRecordsByCount:count condtion:queryCrashDataCondition inTableType:tableType completion:^(NSArray<hctlogmodel *> *_Nonnull records) { // 2. Crash 數據集合大小是否超過配置接口拿到的最大包體積(單位M) maxBodySize float dataSize = [self calculateDataSize:records]; // 3. 大於最大包體積則遞歸獲取 maxItem-- 條 Crash 數據集合並判斷數據大小 if (size == 0) { if (completion) { completion(records); } } else if (dataSize > size) { NSInteger currentCount = count - 1; [self fetchCrashDataByCount:currentCount inTable:tableType upperBound:size completion:completion]; } else { if (completion) { completion(records); } } }]; } - (void)fetchDataExceptCrash:(NSInteger)count inTable:(HCTLogTableType)tableType upperBound:(float)size isWiFI:(BOOL)isWifi completion:(void (^)(NSArray<hctlogmodel *> *records))completion { // 1. 根據剩餘須要數據條數去查詢表中非 Crash 類型的數據集合 __block NSMutableArray *conditions = [NSMutableArray array]; [self.configureModel.monitorList enumerateObjectsUsingBlock:^(HCTItemModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (isWifi) { if (![obj.type isEqualToString:@"appCrash"]) { [conditions addObject:[NSString stringWithFormat:@"'%@'", obj.type]]; } } else { if (!obj.onlyWifi && ![obj.type isEqualToString:@"appCrash"]) { [conditions addObject:[NSString stringWithFormat:@"'%@'", HCT_SAFE_STRING(obj.type)]]; } } }]; NSString *queryCrashDataCondition = [NSString stringWithFormat:@"monitor_type in (%@) and is_used = 0 and namespace = '%@'", [conditions componentsJoinedByString:@","], self.namespace]; // 2. 根據是否有 Wifi 查找對應的數據 [HCT_DATABASE getRecordsByCount:count condtion:queryCrashDataCondition inTableType:tableType completion:^(NSArray<hctlogmodel *> *_Nonnull records) { // 3. 非 Crash 類型的數據集合大小是否超過剩餘須要的數據大小 float dataSize = [self calculateDataSize:records]; // 4. 大於最大包體積則遞歸獲取 maxItem-1 條非 Crash 數據集合並判斷數據大小 if (size == 0) { if (completion) { completion(records); } } else if (dataSize > size) { NSInteger currentCount = count - 1; return [self fetchDataExceptCrash:currentCount inTable:tableType upperBound:size isWiFI:isWifi completion:completion]; } else { if (completion) { completion(records); } } }]; } #pragma mark - getters and setters - (HCTRequestFactory *)requester { if (!_requester) { _requester = [[HCTRequestFactory alloc] init]; } return _requester; } - (NSNumber *)currentTimestamp { if (!_currentTimestamp) { NSInteger currentTimestampValue = [[NSUserDefaults standardUserDefaults] integerForKey:HCT_SAVED_TIMESTAMP]; _currentTimestamp = [NSNumber numberWithInteger:currentTimestampValue]; } return _currentTimestamp; } - (void)setCurrentTimestamp:(NSNumber *)currentTimestamp { [[NSUserDefaults standardUserDefaults] setInteger:[currentTimestamp integerValue] forKey:HCT_SAVED_TIMESTAMP]; _currentTimestamp = currentTimestamp; } - (NSNumber *)currentFlow { if (!_currentFlow) { float currentFlowValue = [[NSUserDefaults standardUserDefaults] floatForKey:HCT_SAVED_FLOW]; _currentFlow = [NSNumber numberWithFloat:currentFlowValue]; } return _currentFlow; } - (void)setCurrentFlow:(NSNumber *)currentFlow { [[NSUserDefaults standardUserDefaults] setFloat:[currentFlow floatValue] forKey:HCT_SAVED_FLOW]; _currentFlow = currentFlow; } - (HCTConfigurationModel *)configureModel { return [[HCTConfigurationService sharedInstance] getConfigurationWithNamespace:self.namespace]; } - (NSString *)requestBaseUrl { return self.configureModel.url ? self.configureModel.url : @"https://common.***.com"; } - (BOOL)isAppLaunched { id isAppLaunched = [[HermesClient sharedInstance] valueForKey:@"isAppLaunched"]; return [isAppLaunched boolValue]; } @end
6、 總結與思考
1. 技術方面
多線程技術很強大,可是很容易出問題。普通作業務的時候用一些簡單的 GCD、NSOperation 等就能夠知足基本需求了,可是作 SDK 就不同,你須要考慮各類場景。好比 FMDB 在多線程讀寫的時候,設計了 FMDatabaseQueue 以串行隊列的方式同步執行任務。可是這樣一來假如使用者在主線程插入 n 次數據到數據庫,這樣會發生 ANR,因此咱們還得維護一個任務派發隊列,用來維護業務方提交的任務,是一個併發隊列,以異步任務的方式提交給 FMDB 以同步任務的方式在串行隊列上執行。
AFNetworking 2.0 使用了 NSURLConnection,同時維護了一個常駐線程,去處理網絡成功後的回調。AF 存在一個常駐線程,假如其餘 n 個 SDK 的其中 m 個 SDK 也開啓了常駐線程,那你的 App 集成後就有 1+m 個常駐線程。
AFNetworking 3.0 使用 NSURLSession 替換 NSURLConnection,取消了常駐線程。爲何換了? 😂 逼不得已呀,Apple 官方出了 NSURLSession,那就不須要 NSURLConnection,併爲之建立常駐線程了。至於爲何 NSURLSession 不須要常駐線程?它比 NSURLConnecction 多作了什麼,之後再聊
建立線程的過程,須要用到物理內存,CPU 也會消耗時間。新建一個線程,系統會在該進程空間分配必定的內存做爲線程堆棧。堆棧大小是 4KB 的倍數。在 iOS 主線程堆棧大小是 1MB,新建立的子線程堆棧大小是 512KB。此外線程建立得多了,CPU 在切換線程上下文時,還會更新寄存器,更新寄存器的時候須要尋址,而尋址的過程有 CPU 消耗。線程過多時內存、CPU 都會有大量的消耗,出現 ANR 甚至被強殺。
舉了 🌰 是 FMDB 和 AFNetworking 的做者那麼厲害,設計的 FMDB 不包裝會 ANR,AFNetworking 必須使用常駐線程,爲何?正是因爲多線程太強大、靈活了,開發者騷操做太多,因此 FMDB 設計最簡單保證數據庫操做線程安全,具體使用能夠本身維護隊列去包一層。AFNetworking 內的多線程也嚴格基於系統特色來設計。
因此有必要再研究下多線程,建議讀 GCD 源碼,也就是 libdispatch
2. 規範方面
不少開發都不作測試,咱們公司都嚴格約定測試。寫基礎 SDK 更是如此,一個 App 基礎功能必須質量穩定,因此測試是保證手段之一。必定要寫好 Unit Test。這樣子不斷版本迭代,對於 UT,輸入恆定,輸出恆定,這樣內部實現如何變更不須要關心,只須要判斷恆定輸入,恆定輸出就足夠了。(針對每一個函數單一原則的基礎上也是知足 UT)。還有一個好處就是當和別人討論的的時候,你畫個技術流程圖、技術架構圖、測試的 case、測試輸入、輸出表述清楚,聽的人再看看邊界狀況是否都考慮全,基本上很快溝通完畢,效率考高。
在作 SDK 的接口設計的時候,方法名、參數個數、參數類型、參數名稱、返回值名稱、類型、數據結構,儘可能要作到 iOS 和 Android 端一致,除非某些特殊狀況,沒法保證一致的輸出。別問爲何?好處太多了,成熟 SDK 都這麼作。
好比一個數據上報 SDK。須要考慮數據來源是什麼,我設計的接口須要暴露什麼信息,數據如何高效存儲、數據如何校驗、數據如何高效及時上報。 假如我作的數據上報 SDK 能夠上報 APM 監控數據、同時也開放能力給業務線使用,業務線本身將感興趣的數據並寫入保存,保證不丟失的狀況下如何高效上報。由於數據實時上報,因此須要考慮上傳的網絡環境、Wi-Fi 環境和 4G 環境下的邏輯不同的、數據聚合組裝成自定義報文並上報、一個天然天內數據上傳須要作流量限制等等、App 版本升級一些數據可能會失去意義、固然存儲的數據也存在時效性。種種這些東西就是在開發前須要考慮清楚的。因此基礎平臺作事情基本是 設計思考時間:編碼時間 = 7:3。
爲何?假設你一個需求,預期10天時間;前期架構設計、類的設計、Uint Test 設計估計7天,到時候編碼開發2天完成。 這麼作的好處不少,好比:
-
除非是很是優秀,否則腦子想的再前面到真正開發的時候發現有出入,coding 完發現和前期方案設計不同。因此建議用流程圖、UML圖、技術架構圖、UT 也同樣,設計個表格,這樣等到時候編碼也就是 coding 的工做了,將圖翻譯成代碼
-
後期和別人討論或者溝通或者 CTO 進行 code review 的時候不須要一行行看代碼。你將相關的架構圖、流程圖、UML 圖給他看看。他再看看一些關鍵邏輯的 UT,保證輸入輸出正確,通常來講這樣就夠了
3. 質量保證
UT 是質量保證的一個方面,另外一個就是 MR 機制。咱們團隊 MR 採用 +1
機制。每一個 merge request 必須有團隊內至少3我的 +1,且其中一人必須爲同技術棧且比你資深一些的同事 +1,一人爲和你參加同一個項目的同事。
當有人評論或者有疑問時,你必須解答清楚,別人提出的修改點要麼修改好,要麼解釋清楚,才能夠 +1。當 +1 數大於3,則合併分支代碼。
連帶責任制。當你的線上代碼存在 bug 時,爲你該次 MR +1 的同事具備連帶責任。
參考資料
</hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></nsdata></hctlogmodel></hctitemmodel></須要配置的baseurl*></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></unistd.h></stdio.h></配置項目*></當前></上報數據類型*></nscoding></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></hctlogmodel></fmdb></std::string></handle></handlewrap></handlewrap></handlewrap></配置項目*></當前></上報數據類型*></nscoding>