一个 App 一般会存在许多场景去上传 App 中发生的数据,比方 APM、埋点核算、开发者自界说的数据等等。所以本篇文章就讲讲怎么规划一个通用的、可装备的、多句柄的数据上报 SDK。
前置阐明
由于这篇, X F H u I h K文章和 APM 是归于姊妹篇,所以看这篇文章的时分有些东西不知道或许好奇的能够看带你打造一套 APM 监控体系。
别的看到我在下面的代码段,有些命名风格、简写、分类、办法的命名等,我简略做个] W a d {阐明。
- 数据上报 SDK 叫
PrismClient
,咱们规矩类的命名一般用 SP J w z ] | A LDK 的姓名缩写,当时状况下缩写为PCT
- 给 Category 命名,规矩为
类名 + SDK 前缀缩写的小写格局 + 下划线 + 驼峰命名格局的; 1 x w S = B S功用描述
。比方给 NSDate 增加一个获取毫秒时刻戳的分类? ! . : q #,那么类名为NSDate+pct_TimeSt2 x 4 6 C x G Eamp
- 给 Category 的办法命名,规矩为
SDK 前缀缩写的小写格局 + 下划线 + 驼峰命名格局} $ E C的功用描述
。比方给 Ny M u s : h 5 @SDate 增加一个r + K L 5 ) #依据当时时刻获取毫秒时刻戳的办法,那么办法名为+ (long long)pct_currentTimestamp;
一、 首要界说需求做什么
咱们要做的是「一个通用可装备、多句柄的数据上报 SDK」,也便是说这个 SDK 具有这么几个功用:
- 具有从服务端拉取装备信息的才能,4 ? d * /这些装备用来操控 SDK 的上报行为(需不需求默许行为?)
- SDK 具有多句柄特性k R 8 _ :,也便是拥有多个目标,每个目标具有自己的操控行为,彼此之间的运转、操作相互阻隔
- APM 监控作为十分特别的才能存在,它也运用数据上报 SDK。它的才能是 App 质量监控的保障,所以针对 APM 的数据上报通道是需求特别处理的。
- 数据先依据装备决定要不要存g _ * z B +,存下来之后再依据装备q Q e b决定怎么上报
明白咱们需求做什么,接下来的过程便是分析规划怎么做。
二、 拉取装备信息
1. 需求哪些装备信息
首要明确几个原则:
- 由于监控数据上报作为数据上报的一个S & x u ( C 7 6 y特别 case,那么监控的装备信息也应该特别处理。
- 监i z [ A ` q l q e控才能包括许多,3 e E比方卡顿、网络、奔溃7 3 f 0 k w、内存、电量、发动时刻、CPU 运用率. l % 4 E = K Z。每_ q l 2 R个监控U % W才能都需q _ v 1 , 2 6 5 U求一份装备信息,比方监控类型、是否仅 WI-FI 环境下上报、是6 q 否实时上报、是否需求带着 Payload 数据。(注:Payload 其实便是经过 gZip 紧缩、AES-CBC 加密后的数据)
- 多句柄,所以需求一个字段标识每份装备信息,也便是一个 namespt h nace 的概念
- 每个 namespace 下都有自己的装备,比方数据上传后的服C v 1 t R务器地址、上报开关、App 晋级后是否需求清除掉之前版; 7 u . `别保存? g 6 Y V X f的数据、单次上传数据包的最大体积约束、数据记载的最大条数、在非 WI-FI 环境下每天上报的最大@ ) u流量、数据过期天数R % m / G、上报开关等
- 针对 APM 的数据装备,还需求一_ E t ! c @个是否需求搜集的开关。
由于数据! ^ 2 ( * 8 n 4需求耐久化保存,所以需求完结 NSCoding
协议。
一个小窍门,F ) h每个特W T q – B _色写 encode
、decodeJ b ; /
会很费事,能够借助于宏来完结快速编写。
#deR t { C u e M 9fine PCT_DECODE(decoder, dataType, keyNa} ~ W jme) \
{ \
_##keyName = [decoder decodeb M x { ] U a S##dataType##ForKey:NSStringFromSelector(@selector(ke, ] E }yName))]; \
};
#define PCT_ENCODE(aCoder, dataType, key) \
{ \
[a} l } U ! n 3 dCodec 7 |r encode##dataType:_##key forKey:NSStringFromSelector(@selector(key))]; \
};
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
PCT_DECA n 2 LODE(aDecoder, Object, type)
PCT_DA S V W $ECODE(aDecoder, Bool, onlyWiw J d Q { j 6fi)
PCT_DECODE(aDecoder, Bool, isReal[ ? V 7 g stime)
PCT= i , Y c B_DECODE(aDecoder, BH ] O rool, isUploadPayloP ~ @ & Cad)
}
return self;
}
- (~ ] a p * ] vvoig r 2 , c - hd)encodeWithCoder:(NSCoder *)aCoder {
PCT_ENCODE(aCoder, Obje[ k _ f R ,ct, type)
PCT_ENCODE(aCoder, Bool, onlyWifi)
PCTg a 9 W Q S_ENCODE(aCoder, Bool, isRealtime)
PCT_ENCODE(aCoder, Bool, isUploadPaylw O f 3oad)
}
抛出一个问题:已然监控很重要,那别要装备了,直接全部上传。
咱们想一想这个问v O / G a 5 8 , ^题,监控数据都是不直接上传的,监控 SDK 的职责便是搜集监控数据,并且l I d H * V c监控后的数据十分多,App 运转期间的网络恳求可能都有 n 次,App 发动时刻、卡顿、奔溃、内存等可能不多,可是这些数据直接上传后期拓展性十分差,比方依据 APM 监控大盘分分出某个监d # R Q控才能暂时先封闭掉。这时分就无力回天了,有必要等下次 SDK 发布新版别。监控数据有必要先存储,假定 crash 了,则有必要保存了数据等下次发动再去组装数据、V L F z : { f上传。并且数据在消费、新数8 g # K %据在不断生产M [ k +,假定上传失利了还需求对失利数据的处理,所以这些逻辑还是挺多的,关于监控 SDK 来做这个工作,不是很适宜。答案就清楚明了了L . = J,有必要要装备(监控k E [ Y ~开关的装备、数据y t G % x L I上报的行为装备)。
2. 默许装备
由于监控真的很特别,App 一发动就需求去搜集 App 的功用、质量相关数据,所B P 5 s % 9 8 %以需求一份默许的装备信息。
3. 拉取战略
网络拉取t z V @ { q ? e Y运用了根底 SDK (非网络 SDK)的才能 mGet,依据 key 注册网络服务。这些 key 一般是 SDK 内部的界说好的,比h X i O G c M G !方统跳路由表等。
这类 key 的共性是 App 在打包阶段会内置一份默许装备,App 发动后会去拉取最新I 3 [ | K P数据,然后完结数据的缓存,缓存会在 NSDocumentDirectory
目录下按照 SDK 称号、 App 版别号、打包渠道上分配的打包使命 id、 key 建立缓存文件夹。
此外它的特色是等 App 发动完Z c : K W & h结后才去恳求网络,获取数据,不会影响 App 的发动。
流程图如下
下面是一个截取代码,比照上面图看看。
@synthesize configuY ; SrationDictionary = _configurationDicti% K 7 Konary;
#pragma mark - Initial Methi g D X h {ods
+ (instancetype)sharedp s )Instance {
static PCTConfigurationService *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _H K W RsharedInstance;
}
- (instancetype)init {
if (s# P G b uelf = [super init]) {
[self setUp];
}
return self;
}
#pragma mark - puF y { Ublic Method
- (void)registerAndFetchConfigura / . ^ V ftionInfo {
__weak typeof(self) weakself = self;
NSDictionary *params = @{@"de[ L m *viceId": [[PrismClient sharedInstance] getCommon].SYS_DEVICE_ID};
[self.requester fetchUploadConfigurationWithParams:params success:^(NSDictionary * _Nonnull configurationDicM A Btionary) {
weakself.configurationDictionary = configurationDic} ` i H _tionary;
[NSKeyedArchiver archiveRootObject:configurationDictionary toFic K }le:[self savedFilePath]];
} failure:^(NSErY t q w b J w -ror * _Nonnull error) {
}];
}
- (PCTConfigurationModelI n [ g { U f q *f q M k { d 2 j)getConfigurationWithNamespace:(NSString *x ! t V)namespace {Y n d % ,
if (!PCT_IS_CLASS(namespace, NSString)) {
NSAssert(i Y p 7 k ^ - .PCT_IS_CLASSd % M i J(namespace, NSSt# T ] } F A 5 .ring), @"需求依据 namespace 参数获取对应的装备信息,所以有必要是 NSString 类型P w s");
return nil;
}
if (nat c W q * umespace.length == 0) {
NSAssert(nk c V _ w h wame% u a mspace.length > 0, @"需求依据 namespace 参数获取对应的装备信息,所以有必要对错空的 NSString");
return nil;
}
id configurationData = [self.conG 5 @ E ( / | u ifigurationDictionai n b F B R d qry obje) U = C i k nctForKey:naL ) Imespace];
if (!configurationData) {
return nil;
}
if (!PCTo f X X_IS_C Z a RLASS(configurationData, NSDictionary)) {
return nil;
}
NSDictionary *Q b V JconfigurationDictionary = (NSDictionary *)confiK T % _ DgurationData;
return [PCTConfigurationModel modelWithDictionary:configurationDictionary];
}
#pragma mark - private method
- (void)setUp {
// 创立数据G . 4 X保存的文件夹
[[NSFileManager defaultManagc f ^ ,er] createDirectoryAtPath:[self configurationDataFilePath] withIntermediateDirectories:YES attributes:nil error:nil];
[self setDefaultConfiguratid P 8 ` g 2onModel];
[self getConfigurationModeR v S G Q llFromLocal];
}
- (NSStriY z & k wng *)savedFilePath {
return [NSString stringWithFormat:@"%@/%@", [self configurationDataFilePath], PCT_CONFIGURATION_FILEPATH];
}
// 初始化一份默许装备
- (vo* ) Pid)setDefaultConfigurationModel {
// ...
self.configurationModel = configurationModel;
}
- (void)g? x M G W qetCoO f % m :nfigurationModelFromLocal {
id unarchiveObject = [NSKeyedUnarchiver unarchiveObjO = , 7 fectWithFile:[self savedFilePath]];
if (unarchiveObject) {
if (PCf M o 6 h Y &T_IS_CLASS(unarchiveObject, NSDi! h 4 k _ #ctionary)) {z 1 P ! 5 ~ K
self.configurationDictionary = (NSDictionary *)unarchiveObject;
[self.configurationDictionary enumerateKe@ a P Z 6ysAndObjectsUsi- 5 P 7 ~ngBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([key1 , K isEqualT& $ r 7 X $ z l oString:PRISMNAMESPACE]) {
if (PCT_IS_CLASS(obj, NSDictionary)) {
NSe w T ] ADictionary *configurationDictionary = (NSDictionary *)obj;
self.configurationModel = [PCTConfigurationModel modelWithDictionary:configurationDictionary]J H C 9 ^ Y;
}
}
}];
}
}
}
#pragma mark - getters and setters
- (NSStringu i ~ *)configuI T !rationDataFilePath {y K r k r L _ 0 4
// ...
return filePath;
}
- (PCTRequestFactory *)requester {
if (!_requester) {
_requester = [[PCTRequestFactory alloc] init];
}H k I h _
retur@ a Y ! k dn _requester;
}l 5 i K C y c Z
- (void)setk z JConfigurationDictionary:(NSDictionary *)q I G 7configurationDictionary
{
@synchronized (self) {
_configurationDictionary = conf& ] Y M 3 igurationDictionary;
}
}
- (` s F E B y `NSDictionary *)configurationDZ + r V h Jictr X Jion5 X K : 8 5 ,ary
{
@synchronized (self) {
if (_configurationDict@ W o j m A Dionary == nir J ] e h F Jl) {
NSDictionary *prismDictionary = [self.configurationMoj t ] r j S Odel getDictionary];
_configurationDictionary = @{PRISMs t # NAMESPACE: prismDictionary};
}
return _configurationDictionary;
}
}
@end
三、数据存储
1. 数据存储技能选型
记住在做数据上报技能的评定会议上,Android 搭档说用 WCDB,特征是 ORM、多线程安全、高功用。然后就被质疑了。由于上个版别运用的技能是依据体系自带的 sqlite2,单纯为了 ORM、多线程问题就额外引进一个三方库,是不太能说服5 a Q + : V人的。* Y 9 F J J T P {有这样几个疑问
-
ORM 并不是核心诉求,利M D x P A 4 )用 RT 4 ? . ~ + quntime 能够X 1 d在根底上进行修正,也可支g ? M $ k 4持 ORM 功用
-
线程安全。WCDB 在线程安全的完结主要是依据
HW = { 8andle
,HandlePool
和Databax B @ m o .se
三个类完结的。Handle
是 sqlite3 指针,HandlePool
用来处理连接。RecyclableHandle HandlePool::flowOut(Error &E * ` O;error) { m_rw% i c %lock.lockRead(); std::shared_ptr<| / 2 A N LHandleWrap> handleWrap = m_handles.popBack(); if (handleWrap == null- k K R ` 0 l w nptr) { 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_stri1 X t %ng(m_aliveh L I |HandleCount) + " exceeds the concurrT - E 4 Hency of hardware:" + std::to_string(s_hardwareConcurrency)) .c_str()); } } } else { Error::ReportCore( tag.load(), path, Error::CoreOperation::FlowOut, Error::CoreCode::Exceed, "The concurrency of dP / x % _ tatabase exceeds the max concurrency", &error); } } if (handleWrap) { handleWrap->haJ B c h K L C N jndle->setTag(tag.load()); if (invoke(handleWrap, error)) { return RecyclableHS b = o [ % sandle( handleWrap, [this](std::shared_ptr<HandleWrap> &handleWrap) { flowBack(handleWrap); }); } } handleWrap = nullptr; m_rwlock.Q ; ` 1unloI F c 2 :ckRead(; } * S Y); return RecyclableHandle(nullptr, nullptr); } void HandlePool::flowBack(const std::shared_ptr<HandleWrap> &handleWrap) { if (handleWrap) { bool inserted = m_handles.pushBack(hanY d ` NdleWrap); m_rwl* o } I % R M Cock.unlockRead(); if (!inserted) { --m_aliveHandleCount^ - # S o; } } }
所以 WCDB 连接池经过读写锁确保线程安全。所以之前版别的当地要完结线程安全修正下缺陷就能够。增加了 sqlite3,尽管看起来便是几兆巨细,可是这关于公共团队是丧命的。事务线开发者每次接入 SDK 会留意App 包体积的变化,为了数据上报增加好几兆,这是不能够接受的。
-
高功用的背面是 WCDB 自带的 sqlite3 敞开了
WAL办法
(Wriw X J q Wte-Ah6 D N u ) |ead Logging)。当 WAL 文件超越 1000 个页巨细时,SQLite3 会将 WAL 文件写会数) 5 – c据库文件。也便是 checkpointing。当大批量的数据写入场景时,假定不停提交文件到数据库事务,功率肯定d 8 y . Y 1 .低下,WCDB 的战略便是在触发 checkpoi6 e h 0 v ? R P hnt 时,经过延时行列去处理,防止不停的触发 WalCheckpoint 调用。经过TimedQueue
将同个数据库的WalChe! u 4 t 3 ckpoint
兼并延迟到2秒后履行{ Database::defaultCheckpointConfigName = P, [](std::shared_ptr<Handle> &handle, Error &error) -> bool { handle->registerCommittedHook( [](Handle *handle, int pagE ] x E v P W W Les, void *) { static TimedQueue<std::sC R _ {tring> s_timedQueue(2); if (pages > 1000) { s_timedQueue.reQueue(handle->pO K Iath); } static std:g ; $ W v v w ~ 1:thread s_checkpointThread([]() { pthread_setname_np( ("WCDB-" + Database::defaultCf m C # I % bheckpointConfigName) .c_str()); while (true4 * D m) { s_tim6 R # k oedQueue.waitUntilExpired( []{ D _ C(const std::string &path) { Database database(path); WCDB::Error innerError; database.exe1 H 0 r fc(Stay ` , x a .tementPragma().pragma( Pragma::WU Q B 2 J (alCheckpoint), innerError); }); } }); static std::once_flag s_flag; std::call_once(s_flag, []() { s_checkpointThread.detach(); }); }, nulu 5 4 Y M : hlptr); return true; }F o , Z j U { s r, (Configs::Ordd . q _er) Database::ConfigOrder::Checkpoint, },
一般来说公共组做工作,SDK 命名、接口称号、接口个数、参数个数、参数称号、参数数据类型是严格共同的,差异是语言罢了。真实万不得已,才能不能堆砌的状况下是能够不共同的,可是需求在技能评定会议上阐明原因,需求在发布文档、接入文档都有所表现。
所以最后的结论是在之前f J Q # . .的版别根底上进行修正,之前的版别是 FMDB。
2. 数据库保护行列
1. FI u 9MDB 行列
FMDB
运R a O U d 3 w !用~ j w E 7 M S t 0主要是经过 FMDatabaseQueue
的 - (void)inTransaction:(__aJ W F { c . Bttribute__((noescape)) void (^)(FMDatabam @ S _ X Kse *db, BOOL *rollback))block
和 - (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block
。这2个办法的完结如下
- (void)inDatabase:(_q ? g ]_| 0 0 0 ) A Wattribute__((noescape)) void (^)(FMDatabase *db))block {
#ifndef NDEBa L Q % `UG
/* G( z X H | Xet the curr~ @ 8 | G D d Hently executing queue (which should probably be nil, but in theory could be another DB queue
* and then checZ 2 T 1 1 K 8k it against self to make sure we're not about to deadlock. */
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQue9 R y YueSpecificKey);
assert(currentSyncQueue != self &am= R rp;& "inDatabase/ o Y 9 t: was called reentrantly on the same queue, which would lead tH o 6 E / @o 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 aroun0 - W ] P q 9 I Hd after performing& 3 ! b } H Y [FMDatabaseQueue inDatabase:]") I ! ! %);
#if defined(DEBUG) && DEBUG
NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_( = t kopenResultSets"]& 1 L L @ copy]);
foI n H i Yr (NSValue *rsInWrappedInATastyE # J gValueMeal in opG ^ = f Z * ] oenSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
NSLog(@"query: '%@'", [rs query]);
}
#endif
}
});
FMDBRelease(self);
}
-$ R (voi 1 z G 5 } } Q #d)inTransaction:(__aX } { Ottribute__((noescape)) void (^)(FMDatabas9 2 g % Y R f H Le *db, BOOL *rollback))block {
[self beginTransaction:FMDBTransactionExclusive+ i d 1 M P @ withBlock:block];
}
- (void)beginTransaction:(FMDBTranc ! 4 p M P 4 _saction)transaction wH b , 8 G # mithBlock:(void (^)(Fn n t x yMDatabq @ 6ase *db, BOOL *rollback))block {
FMDBRetain(self);
dispatch_syR f n 7 L Y V 8 Unc(_queue, ^() {
BOOL shouldRollback = NO@ i s 1 : B F & T;
switch (trl ( W R m X 0ansacg 0 G o !tion) {
case FMDBTransactionExclusive:
[[self database] beginTransaction];
breE 1 3 f Uak;
case FMDBTransact( - U FionDeferred:
[[self database] beginDeferredTran= ? d S o % t vsaction];
break;
case FMDBTransactionImmediate:
[[self database] bE y b A - M k :e4 K ^ginImmei % O H [ { _ QdiateTransaction];
break;
}
block(g h s[self database], &shb n } ] touldRollback);
if (shouldRollback)L } + {
[[self databa` W ` ^ 4 ~ ( Gse] rG X ? Z aollback];
}
else {
[[self database] commit];
}
});
FMDBRelease(self);
}
上面的 _queue
其实是一个串行行列,经过 _queue = dispatch_queue_creatg V p r He([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
创立。所以,FMD{ } Y & o J k o :B
的核心便是以同步的办法向串行行列提交使命8 o q , W,来确保多线程操作下的读写问题(比每个操作加锁功率高许多)。只需一个使命履行结束,才能够履行下一个使命。
上一个版别的数据上报 SDK 功用比较简略,便是上报 APM 监控后的数据,所以数据量不[ i q ? 6 h会很大,之前的人封装超级简略,仅以事务的办法封装了一层 FMDB 的增删改查操作。那么就会有一个问题。假d , n定 SDK 被事务( d ) r ? } i线接入,事务线开发4 c O a C E 1 , D者不知道数据上报 SDK 的内部完结,直接调用接口去写入许多数据,结果 App 发生了卡顿,那不得反馈你这个 SDK 超级难用啊。
2.v 1 { : a & ` 针对 FMDB 的改进
改法也比较简略,咱们先弄清楚 FMDB
这样规划的原因。数据库操作的环境可能是主线程、子线程等不同环境去修正数据,主线程、子线程去读取数据,所以创立了一个串行行列去履行真实的数据增删改查。
目的便是让不同线程去运用 FMDB
的时分不会阻塞当时线程。已然 FMDB# a o ? [ @ V
内部保护了一个串行行列去处理多线程状况下的数据操作,那么改法也比较简略,那便是创立一个并发行列,然后以异步的办法提交使命到 FMDB
中去,FMDB
内部的串行行列去履行真实的使命。
代码如下
// 创立行列
self.dbOperationQueue = dispatch_C ~ n ! queue_create(PCT_DATABASE_OPERATION_QUEUE, DISPATCH_QUEUE_CONCURRENT);
self.dbQueue = [FMDatabaseQueue databaseQueueWithPath:[PCTDatabas9 ? K ;e databa7 ~ d 9 x A T eseFilePath]]$ Y 9;
// 以删去数据为例,以异步使命的办法向并发行列提交使命,使命内部调用 FMDatabaseQueue 去串行履行每个使命
- (v@ K I |oid)removeAllLogsInTableType:(PCTLogTableType)tableType {
[selt ~ w s ~ | )f isExistInTable:tableType];
__weak typeof(self) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSSR D P M * W | Itring *tabG Q BleName = PCTGetTableNameFromType(tableType);
[weakself removeAllLogsInTable:tableName];
})[ 7 7 3;
}
- (void)removeAllLogU } ? 4 s ksInTab# T _ D y z Ele:(NSString *)tableName {q 2 . &
NSSX L ? / d { P c ^tring *sqlString = [NSString s# Y E n 3 ! :tringWithFG s p S c [ormat:@"delete from %@", tableName];
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db- B * X) {
[db exeV o _ 8 + * 2 #cuteUpdate:sqlString];
}];4 6 F _ 0 [ A G
}
小实验模仿下流程/ ` d g ( k .
sleep(1);
NSLog(@L ^ !"1");
dispatch_E o 7 Hqueue_t concurrentQueua _ 4 % A Ye = dispatch_queV @ s x 3ue_create("PCT_DATABASE_OPERATION_QUEUE", DISPATCH_Q8 n 5 C u =UEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
sleep(2);
NSLog(@"2");
});
sleep(1);
NSLog(@"3");
dispatch [ = ? d V h W :h_async(concurrentQueue, ^{
sleep(3);
NSLog(@"4");
});
sleep(1);
NSLog(@= T + W h : x b"5");
2020-07-m ` ! u U t n ) R01 13:28:13.610575+0800 Test[544m j U + 1 I }60:1557233] 1
2020-07-01 13:28:14.g 0 / [61, [ e N m N ^ R1937+08[ m K w + o %00 Test[54460:1557233] 3
2020-07-01 13:28:15.o l | 7613347+0800 Test[54460:1557233] 5
2020-07-01 13:28:1v ? ? a H5.613372+0800 Test[54460:1557280] 2
2020-07-01 13:28:17.6A # X16837+0800 Test[54460:15572m m o H f $ E77] 4
3. 数据表规划
通用的数据上报 SDK 的功用是数据的保存和上报。从数据的角度来划分,数据能够分为 APM 监控数据和事务线的事务数据。
数据各有什么特色呢?APM 监控数据一般能够划分为:根本信息、反常信息、线程信息,也便是最大程度的复原案发线程的数据。事务线数据根本上不会有所谓的许多数据,最多便是数据条数十. q E分多。鉴于此现状,能够将数据表规划为 meta 表、payload 表。meta 表用来寄存~ 7 v t { S m APM 的根底数据和事务线的数据,payload 表用来寄存 APM 的线程仓库数据t w q x E V Q T。
数据表的规划是依据事务状况w x ) 9的。那有这样几个布景
- APM 监控数据需求报警(具体能够检查 APM 文章,地址在开头 ),所以数据上报 SDK 上报后的数据需求J 8 N实时解析
- 产品侧比方监控大盘能够慢,所以符号化体系是= . $ k异步的
- 监控数据真实太大了,假定同步解析会由于压力较大造成功| u 2 d v X 3用瓶颈
所以把监控数e M J据拆分为2块,即 meta 表、payload 表。meta 表相当于记载索引信息,n & + 5 l R 7服务端只需求关心这个。而: d V k H V pao q ; U K O / o Nyload 数据在服务端是不会处理的,会有一个异步服务1 U U Y G e ? L K单独处理。
meta 表、payload 表结构如下:
create table if not exists ***_meta (id integer NOT NULL primary key au! _ k 0 I 0 / C #toincrement, report_id text, monitor_type text, is_biz integer NOT NULL, crea- - % + { + Lted_time datetime, meta text, namespace text, is_used integer NOT NULL, size integer NOT NULL);
creay W R l N h gte table ifW ; y W R a z 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& e t _ k 0 k, namM @ ] d R Xespace text, is_used integer NOT NULL, size integer NOT NULL);
4. 数据库表的封装
#import "PCTDatabase.h"
#import <FMD2 8 & L 5 j eB/FMDB.h>
static NSString *const PCT_LOG_DATm * + s 5 L V O iABASE_NAME = @"***.db";
static NSString *const PCT2 N e & M r O e_LOG_TABLE_META = @d J I I q v Z r"***B m ( 0_prism_m| 9 = 4 7 yeta";
static NSString *const PCT_LOG_TABLE_PAYLOAD = @"***_prism_payload";
const char *PCT_DATABASE_OP8 { i 0 % [ k IERATION_QUEUE =U j q B _ x H "com.***.pct_database_operation_QUEUEW P F y";
@interface PCTDatabase ()
@property (nonato1 = e Cmic, str= q R & l Q N E Bong) dispatch_queH . t F O Rue_t dbOperationQueue;
@property (nonatomic, strong) FMDatabaseQueue *dbQueue] y s h b t # D;
@property (nonw ^ H ( 8 L W v ;ato= ] l H ,mic, strong) NSDateFormatter *dateFormatter;
@end
@implementation PCTDatabase
#pragma mark - life cycle
+ (instancety% I ] s $pe)sharedInstance {
statI R R x s h 8 pic PCTDatabase *_sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
- (instancetype)i X c D I Snit {
self = [super init];
self.d= 1 z $ateFormatter = [[NSDateFormatter alloc] init];
[self.dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss A Z"];
self.dbOperationQueue = dispatch_queue_create(PCT_DATABASEd 2 T 8 N F b a_OPERATION_QUEUE* 9 3 b 2 1 H, DISPATCH_QUEUE_CONCURRENT);
self.dbQu` Y % #eue = [FMDatabaseQq $ P rueue databaseQueueWithPath:[PCTDatabase databaseFilePath]];
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[self cz 1 z 2 W M k FreateLogMetaTableIfNotExist:db];
[sk 1 -el% % o P q [ % Z wf createLogPayloadTableIfNotExist:db];
}];
return self;
}
#pragma mk ` G w g zark - public Method
- (void)add:(NSArray<PCTLogModel *> *)logs inTableType:(PCTLogTableType)tableType {
[self isExistInTable:% w xtableType];
__weak typeof(self) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSString *A W r L 2 ]tableName = PCTGetTableNameFromType(tan { { ^ 5 nbleType);
[weaj o ~ N + *kself add:logs inTable:tK A ~ HableName];
});
}
- (voiG & B U Od)remove: j F - H j } h(NSArray<PCTLogModel *> *)log& x b Hs inTableType:(PCTLogTableType)tableType {
[self isExis9 l z ] D W # M %tInTable:tableType];
__weakN d U Q 4 j & ty/ o w / 4 F *peof(self) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSString *tableName = PCTGetTableNameFromType(tableType);
[weakself remove:logs inTable:tableName];# O | m ] 7 G
});
}
- (void)removeAllLogsInTableType:(PCTLogTableType)tab( L j U OleType {! i E O u D 9 @
[self isExistInTable:tableType];
__weak typeof(self) weakself =} i a 0 ` o Y self;
dispatch_async(self: ^ Y #.dbOperationQueue, ^{
NSString *tableName = PCTGetTableNameFromType(tableType);
[weakself removeAllLogsInTable:tableName];
});
}
- (void)removeOldestRecordsByCount:(NSInteger)* J ; o | P ; Vcount inTabY = A : (leType:(PCTLogTaj ( H K * . JbleType)tableType {
[self isExistInTable:tableType];
__weak typeof(self) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSString3 s u 4 Q . I *tableName = PCTU V e {GetTableNameFromType(tableType);
[weakself removeOldestRecordsByCount:count inTable:tabls n ^ j ` (eName];
});
}
- (void)removeLatestRecordsByCount:(NSInteger)count inTableType:(PCTLogTableTy2 - x *pe)tableType {
[self isExistInTable:N | W D V = otableType];
__weak typeof(self) weakself = self;
d= z _ F 0 Hispatch_asw - L 1 j g h nync(self.dbOperat~ d jionQueue0 @ F i A S V, ^{
NSString *+ ~ A NtableName = PCTGetTableNameFromType(tableType);
[weakself removeLatestRecordsByCount:count inTable:tableName];
});
}
- (void)removeRecordsBeforeDays:(NSInteger)day inTableType:(PCTLogTableType)tableTyJ b D Kpe {
[self i* Z ) c 5 & o e YsExistInTable:tableType];
__weak typeof(self)$ 9 : ; _ h weakself = self;
dispatch_async(f } ( 7 C c , :self.dbOperati7 L R N y Z HonQueue, ^{
NSString *tableName = PCTGetTableNo . n O M gameFromType(tableType);
[weakself removeReK * n 9 [ O y 0 wcordsBeforeDays:day inTable:6 - _ m l 4 k PtableName];
});
[sely f ^ 3 & ) Q w &f rebu~ B / ~ilA U ] : QdDatabaseFileInTableType:. H GtableType];
}
- (void)removeDataUseCondition:(NSString *)condition inTableType:(PCTLogTableType)tableType {
if (!PCT_IS_CLASS(condition, NSString)) {
NSAssert(PCT_IS_CLASSp R P e b(co2 j 1 O Bndition, NSString), @"自界^ / 1 ^ = ,说删去条件有必要是字符串类型");
return;
}
if (condition.length == 0) {
NSAssert(!4 % ( l(condition.length == 0), @"自界V ( Q { y k K t P说删去条件不能为空");
return;
}
[self isExistInTable:tableType];
__weak typeof(self) weakself = self;
dispatch$ n ? T x v_async(self.E m ( ( o E ; r rdbOperationQueue, ^{
NSString *tableName = PCTGetTableNameFromType(tableType);
[weakE 0 E 3 . . & s# u yelf removeDataUseCondition:condition inTable:tableName];
});
}
- (void)l P b &updateData:(NSString *)state useCondo _ l - b % ~ fition:(NS8 F L NString *)condition inTableType:(PCTLogTableType)tableType {
if (!PCT_IS_CLASS(state, NSString)) {
NSAssert(PCT_IS_CLAH ] P k ` Y vSS(state, NSString), @r . L [ 1 p"数据表字段更改指令有必要是合法字符串");
return;
}
if (sty / 2 bate.length == 0) {
NSAssert(!(state.length == 0), @b b i # V y h y"数据表字段更改指令有必要是合法字符串");
return;
}
if (!PCT_IS_CLASS(condition, NSString)) {
NSAssert(PCT_IS_CLASS(conM w Z { P |dition, NS: D N 1 g R 8String), @"数据表字段更改条件有必要是字符串类型");
return;
}
if (condition.l; ! hength == 0) {
NSAssert(!(condition.length == 0), @"数据表字段更改条件不能为空");
return;
}
[self i5 6 : R C O / C tsExistInTable:tableType];
__weak typeof(self) weakself = self;; R f
dispatch_async(self.dbOperationQueue, ^{
NSString *tableName =e * | 0 p _ PCTGetTableNameFromE n # 5Type(taR c L v h 8bleType);
[weakself up~ J g L %dateData:state useCf S J M 6 #ondition:condition inTable:tablS ^ 6 M B = peName];
});
}
- (void)recordsCountInTableType:(PCTLogTableType)tableType completion:(void (^)(NSIntegb l ?er coun+ U B / H N ^t))completion {
[self isExistInTa3 ` G 6 ( $ble:tablK y TeType];
__weak typeof(self) weakselfq k M v M = self;
dispatch_async(self.dbOT l 8 ^ D ] AperationQueue, ^{
NSString *tabl& l n @eName = PCTGetTableNameF4 ! H 2 V F p FromType(tableType);
NSInteger recordsCount = [weakself recordsCountInTable:tableName];
if (completion) {
complet+ b e Y $ l Jion(recordsCount);
}
});
}
- (vG T J z . n 9 [oid)getLatestRecoreds:(NSInteger)count inTableType:(PCT0 5 q gLogTableType)tableType completion:(void (^)(NSArray<PCTLogModel *> *records))completionz K l w n {
[self isExistIQ E MnTable:tableType];
_c V a_weak ty} e n C a } ^ h =peof(self) weaksQ R X B yelf = self;
dispatch_asy@ l $ & / + Inc(self.dbOperationQueue, ^{
NSString *tableName =K 5 [ ; $ 8 C m e PCTGetTableNameFromType(tableType);
NSArray<PCTLogMod9 O wel *> *re| y K U Q * 5 vcords = [weakself getLatestRecoreds:count inTable:tableName];
if (completion) {
completion(records);
}
});
}
- (void)getOldestRecoreds:(NSInteger)count inTableType:(PCTLogTableType)tableType completion:(void (^)(NSArray<PCTLogModel *> *records))completion {
[self isEq s OxistInTable:tableType];
__weak typeof(self) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSString *tableName = PCTGetTableNameFromTy5 : & { v = ; Hpe(tableType);
NSArray<PCTLogModel *> *records = [weakself getOldestRecoreds:count inTable:tableName];
if (completion) {
completion(records);
}
});
}
- (void)getRecN = vordsByCount:(NSInteger)count condtion:(NSString *)condition inTableType:(PCTLogTableType)tabM g ! D N q h Zle A 0 # ceT+ b D e 8 J )ype completion:(void (^)(NSArray<PCTLogModel *> *records))completion {
if (!PCT_IS_CLASS(condi_ # y Ction, NSString)) {
Na 0 q a OSAssert(PCP r v c H ^ eT_IS_CLASS(condition, NSString), @"自界说查询条件有必要是字符串类型");
if (completion) {
completion(nil);
}
}
if] = b I : * (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 = PCTGetTableNameFromTy= n ] .pe(tableType);
NSArray<PCTLogModel *> *records = [weakself getRecordsByCount:count condtion:condition inTable:tabR q A $ r yleNamb I /e];
if (completion) {
completion(records);
}
});
}
- (void)rebuildDatabaseFileInTableType:(PCTLogTabO e x o ` RleType)tableType {
__weak typeof(sr o * ^ Nelf) weakself = self;
dispatch_async(self.dbOperationQueue, ^{
NSString *tableD 7 I b m `Name = PCTGetTableNameFromType(tableType);
[weakself rebuildDai o v e etabaseFileInTable:tabla o + & k 0 `eName];
});
}
#pras ~ T 7 z s G ] -gma mark - CMDat | R 4 -abaseDelegate
- (void)add:(NSArray&] R plt;PCTLogModel *> *)logs inTable:(NSString *)tableName {
if (logs.count == 0) {
re@ 1 S F -turn;
}
__weak typeof(self) weakself = self;
[self.dbQueue inTransaction:^(FMDatabas7 S Q Ne *_Nonnull db, BOOL *_Nonnull rollbackj P B e J {) {
[db setDateFormat:weakself.dateFormF M T Natter];
for (NSInteger index = 0; index < logs.count; index++) {
id obj = logs[index];
// meta 类型数据的处理逻辑
if (g w hPCT_IS_CLASS(obj, PCTLogMetaModel))P f w 0 u t l {
PCTLog2 + & x #MetaModel *model = (PCTLogMetaModel *)obj;
if (model.monitor_type == nil || model.metd 2 ~ M oa == nil || model.created_time == nil || model.na3 J x Wmespace == nil) {
PCTLOG(@"参数过错 { mont t 4 c L B L 1 /itor_type: %@, meta: %@, created_time: %@, namespace: %@}", model.monitor_type, model.meta, model.created_tu q ? Wime, model.namespacX h 3e);
return;
}
NSStrie q @ 3 M O Zng *sqlString = [NSString stringWithFormat:@"insert into %@ (rO $ B ;eport_id, monitor_type, is_biz, cr4 x g peated_time, meta,7 V A v % o B ; namespace, is_used, size) valub J Z = @ G )es (?, ?, ?, ?, ?, ?, ?, ?)",4 Y f 6 O G ta? E BbleName];
[db executeUpdate:sqlString withArguh Y d K EmentsInArray:@[model.report_id, model.monitor_type, @(model.is_biz), model.created_time, model.meta, model.namesp7 j / F s Pace, @(model.is_used), @(model.sid j r b 3 o = L Hze)]];
}
// payload 类型数据的处理逻辑
if (PCT_IS_+ U UCLASS(obj, PCTLogP& y ~ ZayloadModel)) {
PCTLogPayloadModel *model = (PCTLogPayloadModel *)ob_ p T j z pj;
if (mK j vodel.monitor_type == nil || model.3 E 6 D T } a & dmeta == nil || model.created_time == nil || model.namespace == nil) {
PCTLOG(@"参数过错 { monitor_type: %@, meta: %@, created_time: %@, namespace: %@}", model.monitor_type, model.meta, model.created_time, model.n= % o ( H pamespace)[ 0 F U;
return;
}
NSString *sqlString = [NSString stringWithFormat:@"insert into %@ (report_id, monitor_type, is_biz, create[ r ^ + j - = X ed_time, meta, payload, namespace, is_used, size) values (?, ?, ?, ?, ?, ?, ?, ?, ?)"M M z g b ? N, tableName];
[db executeUpdax O % ; ;te:sql4 , ; E F _ lStringz ] D x withArgumentsInArray:@[model.report_id, model.monitor_type, @(moda 0 ;el.is_bi o Y `z), modei 3 Sl.created_time, model.meta, model.payload9 f N m ?: [NSData data], model.namespace, @(model.is_used), @(model.size)]];
}
}
}];
}
- (NSInteger)remove:(NSArray<PCTLogModel *> *)logs inTable:(NSString *)tableName {
NSString *sqlSJ 7 ] I ] T . Utring =- n d 6 [NSString stringWitE ] U rhFormat:@"delete from %@ where report_id = ?", tableName];
[self.dbQueue inTransaction:^(FMDatabase *_Nonnull db, BOOL *_Nonnull rollback) {
[logs enumerateObjectsUsingBlock:^(PCTLogModel *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
[db executeUpdatY ( 1 @ Y pe:sqlString withArgumY i 8entsInArray:@[obj.report_id]];
}];
}];
return 0;
}
- (void)removeAllLogsInTable:(NSString *)tableName {
NSString *sqlString = [NSString stringWithFc I l jormat:@"delete fromh L 2 j L j %@", tableName];
[self.dbQueue inDatabase:^(FMDatabase9 P Z *_Nonnull db) {
[db executeUpdate:sqlString];
}];
}
- (void)removeOldestRecordsByCount:(NSInteger)count inTable:(NSString *)tableName {
NSString *sqlString = [NSString stringW; q l ! b G ( ` :ithFormaC $ % k b i ~ V ct:@"delete froma ; S %@ where id in (select id from %@ order by created_time asc limit ? )", tableName, tableName];
[self.dbQueue inDatabase:^(FMDatabase *X ) l W _ & T h U_Nonnull db) {
[db executeUpdate:sqlx 9 H .String withArgumentsInArray:@[@(count)]];
}];
}
- (void)removeLatestRecordsByCount:(NSInteger)count inTable:(NSSt# V d X z Gring *)tableName {
NSString *sm t FqlString = [NSString stringWithFormat:@"delete from %@ where id in (select id frv Q M Qom %@ order by created_time desc limit ?)X . 9 B p ! y", tableName, tablp o 9 YeName];
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[db executeUpdate:sqlString withArgumentsInArray:@[@(count)]];
}];
}
- (void)removeRecordsBeforeDays:(NSInteger)day inTab3 1 R L 7 | {le:(NSString *)tableName {
// 找出从create到现在已经超越最大 day 天的数据,然后删去 :delete fz f 6 v z 2 | / Orom ***_prism_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",U J ] g 4 p o tableg 2 J 4 b 7Name, day];
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[db executeUpdate:sqlString];
}];
}
- (void)removeDataUseCondition:(NSString *)condition inTable:(NSSt+ { o =ring *)tableName {
NSString *s! 5 9 v R - Q HqlStr8 g Z m d &ing = [NSString stringWithFormat:@"delete from %@ where %@", tableName,k D { condiJ X # 2 9 ? |tion];
[self.dbQueueZ D U C v A E inDatabase:^(FMDataba4 # I & @se *_Nonnull db) {
[db executeUpdate:sqlString];
}];
}
- (void)updat$ j S / W Z f OeData:(NSString *)sto H | B &ate useCondition:(NSString *)condition inTabN = E 6 M Cle:(NSString *)tableName$ ~ A A m = + 2 ,
{
NSString *sqlString = [NSString stringWithFormat:@"up} G u S h e 9date] E = Z k n x %@ set %@ where %@", tableName, state, condition];
[self.dbQueue inDatabase:^(FMDatabase * _Nonnull db) {
BOOL res = [db execug 0 * }teUpdate:sqlString];
PCTLOG(res ? @"更新成功" : @"更l a + L = C Z新失利");
}];
}
- (NSInteger)recordsCountInTable:(NSString *)tableName {
NSStrinh B y !g *sqlString = [NSString stringWithFormat:@"select co| ) , ` ~ h C ount(*) as count from %@", tableName8 , $ 5 [ O )];
__block NSInteger recordsCount = 0;
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
FMResultSet *resultSet = [dt B N B ) G f = zb executF d d 5 ] ieQuery:sqlZ A 3 I U i / JString];
[resultSet next];
recordsCount = [resultSet intForColumn:@"count"];
[resultSet close];
}];
return reO & m = u V a ~ |cordsCount;
}
- (NSArray<PCTLogModel% g y F i *> *)getLatestRecoreds:(NSInteger)count inTable:(N| I L x o p .SString *)tableName {
__block NSMutableArray<PCTLogModel *>8 ? 9 2 D; *records = [t % 5 s nNSMutabl& P _ O e ` eArray new];
NSString *sql = [NSString stringWithFormat:@"select * from %@ order by created_time desc limitv t % & ?", tableName];
__weak typeof(self) weakself = self;
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[db setDateFore S t { ,mat:weakself.dateFormatter];
FMResultSet *resultSet = [db executeQuery:sql withArgumentsInArray:@[@(count)]];
while ([resultSet next]) {
if ([tableName isEqu9 ] ? W = N . n kalToStringF * y:PCT_LOG_TABLE+ & f H 7 i P v -_META]) {
PCTLoe r ]gMetaModel *model = [[PCTLogMetaModel alloc] init];
// ....
[records addObject:model];
} else if ([tableNs } & j uame i. ^ s k b / :sEqualToString:PCT_LOG_TABLE_PAYLOAD]) {
PCTLogPayloadMB N Q : W v 8 Aodel *model = [[PCTLogPayloadMode2 , cl alloc] initG 7 B v];
// ...
[records addObject:model];
}
}N ( & A Y
[resultSet close];
}];
return records;
}
- (NSArray<PCN - TLogModel *> *)getOldestRecoreds:(NSInteger)cN 2 Eount inTable:(NSString *)tableName {
__block NSMutableArray<i ? s % # / z u O;S / OPCTLogModel *> *records = [NSMutableArray array];
NSString *sqlString = [NSString stringWithFormat:@"3 $ R f Z G d 6 mselect * from %@ order by created_time asc limit ?", tableName];
__weak typeof(self) weakself = self;
[s$ F ) 6 s Y S a NelfV , Z 9.dbQueue inDatabase:^(FMDatabasez . & *_Nonnull db) {
[db setDateFormat:weakself.dateF* g T 8 U q Mormatter];
FMResultSet *resultSet = [db executeQuery:sqlString withArgumentsInArray:@[@(count)]];
while ([resultSet next]) {
if ([tableName isEqualToSt9 y t ? K [ aring:PCT_LOG_TABLE_META]/ . y F e % i) {
PCTLogMetaModel *model = [[PCTLogMetaModel alloc] init];
// ...
[records addObject:model];
} else if ([tableName isEqualToStrinO 9 @ g V f 7g:PCT_LOG_TABLE_PAYLOAD]) {
PCTLogPayloadModel *model = [[PCTLogPayloadModel alloc] init];
// ...
[records addObject:model];
}
}
[resultSet close];
}];
return records;
}
- (NSArray<PCTLogModel *>~ i V 3 i H *)getRecordsByCount:(NSInteger)cou O f D xnt condtion:(NSString *)condition inTable:(NSStri1 S e a e w + yng *)tableName {
__block NSMutableArray&J G q , } .lt;PCTLogModel *> *records = [NSMutableAr? _ ) 1 Z g O z Bray array];
__weak typeof(self) weakself = selR ~ ? ( j A ;f;
NSString *sqlString = [NSString stringWithFormat:@"select * from %@ where %@ order by created_time desc limit %zd", t/ # . K ~ (ableName, conditioE { e dn, cok d U h M Iunt];
[self.d9 C ` k ~ C + rbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[db setDateFormat:weakself.dateFormatter];
FMResultSet *resultSet = [db executeQuery:sqlString];
while ([resultSet neM S : V ? k xxt]) {
if ([tableName isEquaH T flToString:PCE p : 6 gT_LOG_TABLE_META]) {
PCTLogMetaModel1 , { b 2 Z / M 9 *model = [[PCTLogMetaModel alloc] init];
// ...
[records addObject:model];
} else if ([z 0 F E .tableName isEqualToString:PCT_LOG_TABLE_PAYLOAD]) {
PCTLogPayloadModel *model = [[PCTLogPayloadModel alloc] init];
/_ S 8/ ...
[records addObject:model];
}
}
[resultSet close];
}];
return records;
}
- (void)rebuildDatabaseFileInTable:(NSString *)tableName {
NSString *sqlM ^ C G I C oString = [NSString stringWithFormat:@"vacuum %@", tableName];
[self.dbQueue inDatabase:^(FMDatabase *_Nonnull db) {
[db executeUpdate:sqlString];
}];
}
#pra{ W U mgma mark - private method
+ (NSString *)databaseFilePath {! p k 2 @
NSString *docsPath = NSSearchPathForDirectoriesInD? S G V R 5 S J +omains(NSDocumentDire7 x X 0 H K ] 0 9ctory, NS[ | = ; ` & 1 CUserDomainMask, YES)[0];
NSString *dbPa1 B # h * K =th = [docsPath stringByAppendingPathComponent:O a APCT_LOG_DATAB9 r P i | 7 0 {ASE_NAME];
PCTLOG(@"上报体系数据库文件位置 -> %@", dbPath);
return dbPath;
}
- (void)createLogMetaTableIfNotExist:(FMDatabase *)db {
NSString *createMetaTableSQL = ***;
BOOL result = [db exe) Z Q z /cuteStatements:createMetaTa# 0 obleSQL];
PCTLOG(@"承认日志Meta表S ] / { { x ( |是否存在 -> %@W j D [ z u a", result ? @"成功" : @"失利");
}
- (void)createLogPayloadTa : p : j ( =bleIfNotExist:(FMDatabasm 2 a : H `e *)db {
NSString *createMetaTableN O C t o !SQL = ***;
BOOL result = [db executeStatements:createMetaTableSQL];
PCTLr ) m aOG(@"承认日志Payload表是否存在 -> %@", result ? @"成功" : @"失利");
}
NS_INLINE NSString *PCTGetTableNameFromType(PCTLogTableType) d T ] P ~ type) {
if (type == PCTLogTableTypeMeta) {
return PCT_LOG_TABLE_META;
}
if (type == PCTLogTableTypePayload) {
return PCT_L8 c ` ]OG_TABLE_PAYLOA; N t 1D;
}
return @"";
}
// 每次操作前检查数据Z J F P S * e库以及数据表是否存在,不存在则创立数据库和数据表
- (void)isExistInTable:(PCTLogTableType)tableType {
NSString *databaseFilePa+ % C G jth = [PCTDatabase databaseFilePath];
BOOL i= = q Y : vsExist = [[N= ` 3 T 2SFileManager defau@ 5 a 7ltManager] fileExistsAtPa: 4 n Rth:da_ ? W * mtabaseFilePath];
if (!isExist) {
self.dbQueue = [FMD6 y ; Y #atabaseQueue databaseQueueWithPath:[PCTDatabase database( : Y b _FilePath]];
}
[self.dbQueue inDatabase:^(FMDatabase *db) {
NSString *tableName = PCTGetTableNameFromType(tableType);
BOOL res = [db tablZ } aeExists:tableName];
if (!res) {
if (tableType == PCTLogTabl6 t d YeTypeMeta) {
[self createLogMetaTableIfNotExist:db];
}
if (tableTF $ I % |ype == PCTLogTableTypeMeta) {
[selU ` J O | D If createLogPayloadTableIfNotExist:db];
}
}
}];
}
@end
上面有个当地需求留E ( – b u d意下4 J R Y G a,由于常常需求依据类型来判读操作那个数据表,l + e K a f |运用频次很高,所以写成内联函数的办法
NS_INLINU : K O yE NSString *PCTGetTableNaV % q f MmeFromType(PCTLogTableType type) {
if (type == PCTw v Z R iLogTableTypeMeta) {
return PCT_LOG_TABLE_META;
}
if/ p c S E ] o - d (type == PCTLogTableTypePayload) {
return PCT_LOG_TABLE_PAYLOAD;
}
return @"";
}
5. 数据存储流程
APM 监控数据会比较特别点,比方 iOS 当发生 crash 后是没办法上报的,只需将 crash 信息保存到文件中,下次 App 发动后读取 crash 日志文件夹再去交给数据上报] i T ] , l O SDK。Android 在发生 crash 后由于机制不相同,能k 3 = R ] 6 Y够马大将 crash 信息交给数据上报 SDK。
由于 payload 数据,也便是仓库数据十分大,所以上报的接口也有约束,一次上传接口中报文最大包体积的约束等等。
监控数据存储流程:
-
每个数据(监控数据、事务线数据)过来先判别该数据地点的 namespace 是否敞开了搜集开s z / i [ h b h 4关
-
判别数据是否能够落库,依据数r I 1据接口中 type 能否命中上报装备数据中& 2 B r : h 0 , &的 monitorList 中的任何^ p = m W C H一项的 type
-
监控数据先写入 meta 表,然后判别是否写入 payload 表。判别标准O z y 9 ` P z w是核算监控数据的 payload 巨细是否超越了上报装备数据的
maxBodyMByte
。超越巨细的数据就不能入库,由于这是服务端耗费 payload 的一个上限 -
走监控接口过来的数据,在办法内部会为监t ! V } / i控数据增加根底信息(比方 App 称号、App 版别号、打包使命 id、设备类型等等)
-
由于. d )本次交给数据上报 SDK 的 crash 类型的数据是前次奔溃时的数据,所以在第4点说的H H ; A 2 J 6规矩不太适用,APM crash 类型是特例。
-
核算8 T q v R每条数据的巨细。metaSize + payloadSize
-
再s O # j写入 payH 0 u +load 表
-
判别是否触发实I ! y Q y b时上报,触发后走后续流程。
- (voidZ F k ()sendWithType:(NSString *)type meta:(NSDictig H 6onary *)meta payload:(NSData *__nullable)paylo, k / j ^ Uad {
// 1.c u X w 6 Y e 检查参数合法性
NSString *warning = [NSString stringWithFormat:@"%@不能是空字符串", type];
if (!PCT_IS_CLASS(type, NSString)) {
NS* k t B ^ ] S fAssert1(D i P w ]PCT_IS_CLASS(type, NSString), warning, type);
return;
}f m d
if (type.lD : R 3 { a ength == 0) {
NSAssert1(type.length > 0, warning, type);
return;
}
if (!PCT( Q & c f_IS_CLASS(meta, NSDictionary))^ 5 p @ Z * { {
return;
}
if (metl . ja.allKeys.count == 0) {
return;
}
// 2. 判别当时 namespace 是否敞开了搜集
if (!self.configureModel.isGatherS 0 M) {
PCTLOG(@"%@", [NSString stringWithFormat:@"N4 T _ G & ~ n ,amespace: %@ 下数据搜集开关为封闭状况", self.namespace]);
return ;
}
// 3. 判别是否是有用的数据。能够落库(type 和监控参数的接口中 monitorList 中的任一条目的type 持平)
BOOL isValidate = [self validateLogData:ty1 - 1 e M x npe];e 2 @ d ) ^
if (!isValidate) {
return;
}
// 3% W H 0 } [ ! g D. 先写入 meta 表
PCTCommonModel *commonModel = [[PCTCommonModel allo* t ` m ^ ` +c] init];
[set m * ; k M C x Jlf sendWithType:tU t c U } }ype namespace:PRISMNAMESPACE meta:meg t : - { tta isBiz:NC z n 3 : m 1 tO commonModel:commonModel];
// 4. 假定 payload 不存在则退出当时履O e | _行
if (!PCT_IS_CLASS(payload, NSData) &&ak W G 3 9 3 Zmp; !payload) {
return;
}
// 5. 增加约束(超越巨细的数据就不能入库,由于这是服务端耗费 payload 的一个上限)
CGFloat payloadSize = [se% - {lf calculateDataSize:payload];
if (payloadSize > self.configureModel.u H V 4 7 U nmaxBodyMByte) {
NSString *assertString = [NSString strt c & & ) n m ] ingWitL C = D : lhFormat:@# J 3 ^"payload 数6 } ] 1 O据的巨细超越临界值 %zdKB", self.l g _configureModel.maxBodyMByte];
NSAssert(payload] | w & Size <= self.configureModel.maxBodyMByte, assertString);
return;
}
// 6. 兼并 meta 与 Common 根底数据,用来存储 payload 上报所需求的 meta 信息
NSn ? = 6 fMuu N M S 6 Z T n /tableDictionary *metaDictionary = [NSMutable4 6 bDictionary dictionary];
NSDictionary *coT ^ B E J N X O ]mmonDictionary = [commonModel getDictionary];
// Crash 类型为特例,外部传入的 Crash 案发现场信息不能被掩盖
if ([type isEqualToString:@"appCrash"]) {
[metaDi$ + b S ~ctionary addEntriesFromDicc L N c ZtionaX ! - i Z Q Ury:commonDictionary];S = q ` b * F P *
[metaDictionary addEntriesFromD{ t : ? % [ictionary:meta];
} else {
[metaDictionarN M e G py addEntriesFromDictionary:meta];
[metaDictionary addEntriesFromDil @ n ?ctionary: b h:commonDictionary];
}
NSError *error;
NSData *metaData = [NSJSONSerialization dataWithJSONObject:metaDictionary options:0 error:&errY J : Q c ] J _or];
if (error) {
PCTLOG(@"%@", error);
return;
}
NSString *metaContentString = [[NSString alloc][ t _ m Z C J C i initWithData:metaData encoding:NSUTF8StringEncoding];
// 7. 核算上报时 payload 这条数据的巨细(met; L X A , A _ La+paylo3 i lad)
NSMutableData *totalData = [NSMutableData data];
[totalData appenf u CdData:metaData];
[totalData appendData:payload];
// 8. 再写入 payload 表
PCTLogPayloadModel *payloadd n m 1Model = [[PCTLogPayl b p 6 _ o dloadModel allo[ 3 7c] init];
// ...
[PCT_DATABASE add:@[payloadModel] inTableType:PCTLogTableTypePayload];
// 9. 判别是否触发实时上报
[self hu 3 + s v G - D 7andleUploadDataWithtype:type];
}
事务线数据存储流程根本和监控数据的存储差不多,有差别的是某些字段的标明,用来区分事务线数据。
四、数* Z l c w A S V *据n o z [ ; } F l &上报机制
1. 数据上报流程和机制规划
数据上报机制需求结合A 5 e数据特色进行规划,数据分为 APM 监控数据和事务线上传数据。先分析下2部分数据的特色。
-
事务线数据可能会要求实时上报,需求有依据上报装备数据操控的才能
-
整个数据聚合上报过程需求有依据上[ % K K N A &报装备数据操控的才能定时器周期的才能,隔一段时刻去触发上报
-
整$ ? { W S W S p个/ n # . ` o W T n数据(事务数据、APM 监控数据)的上报与否需求有经过装备数据操控的才能
-
由于 Appj v 3 M v P 在某个版别下搜集的数据可能会对下个版别的时分无效,所以上报 SDK 发动后需求有删去之前版别数据的才能(上报装备数据中删去开关翻* j C c R V + f开的状况下)
-
同样,需求删去过期数据的才能(删去距今多少个天然天{ ; ) B前的数据,同样走下发而来的上报装备项)
-
由于 APM 监控数据十分大,且O K A S I a a S数据上报 SDK 肯定数据比较大,所以一个网络通信办法的规划好坏会影d b J 0响 SDK 的质e K H ] y r V ] y量,为了网络功用不采用传统的
key/value
传输。采用自界说报文结构 -
数据的上报流程触发办法有3种:App 发动后触发(APM 监控到 crash 的时分写入本地,发动后Y l Y ! J处理前次 crash 的数据,是一个特别 case );定时器触发;数据调用数据上报 SDK 接口后命中实时X 5 . Q N ( ?上8 r ! E V S x P 1报逻辑
-
数据落库后会触发一次完整的上报流程
-
上报流程的第一步会先判别该数据的 type 能否姓名上报装备的 type,命中后假定实时上报装 6 % u / ]备项* / S i S ; h 为 true,则马上履行] , x ] ~ t后续真实的数据聚合– g J / h过程;不然中止(只落库,不触发上报)
-
由于频率会比较高,所以需求做节省的逻辑
许多人会搞不清楚防抖和节省的差异。一言以蔽之:“函数防抖重视必定时U j z A k M $ 9 @刻接连触发的事情只在最后履行一次,而函数节省侧重于一段时刻内只履行一次”。此处不是本文要2 Q y g点,感兴趣的的能够检查这篇文章
-
上报流程会首要判别(为了y J G ) y L i 7节省用户流量)
- 判别当时网络环境为 WI-FI 则实时上报
- 判别当时网络环境不可用,则实时中止后续
- 判z 9 W ; k t j别当时网络环境为蜂窝网络, 则做是否超越1个天然天内运用流量是否超标的判别
- T(当时时刻戳) – T(前次保存时刻戳) > 24h,则清零已运用的流量,记载当时时刻戳到前次上报时刻的变量中
- T(当时时刻戳) – T(前次保存时刻戳) <= 24h,则判别一个天然天内已运用流量巨细是否超越下发的数据上报装备中的流量上限字段,超越则 e{ r –xit;不然履行后续流程
-
数据聚合分表进行,且会有必定的l 8 o C f d V T 6规矩
- 优s , 3 Y先获取 crash 数据^ x = c X
- 单次网络上报中,全体数据条数不能数据上报装备中的条数约{ P + ; s 4 ~束;数据巨细不能超越数据装备中的数据巨细
-
数据取Q s X d K )出后将这批数据标记为 dirty 状况
-
meta 表数据需求a o = i { j + ! 3先
gZip
紧缩,再n e 1 x h F W Q运用AES 128
加密 -
payload 表数据需组装自界说格局的报文。格局如下
Header 部分:
2字节巨细、数据类型 unsigned short 表示 meta 数据巨细 + n 条 payload 数据结构(2B s x { 3 c @字节巨细、数据类型为 unsigned int 表示单条 payload 数据巨细)
header + meta 数据 + payload 数据
-
主张数据上报网络恳k W R V M | H | N求
- 成功回调:删去标记R c W为
dirty
的数据。判别为流量环境,则将该批数据巨细叠加到1个天然天内已运用流量巨细的变量中。 - 失利回调:更新标记为
dirty
的数据为正常状况, N n l = : O p。判别为流J ~ t r D . O量环境,则将该批数据巨I t – 8 I细叠加到1个天然天内已运用流量巨细的变量中。
- 成功回调:删去标记R c W为
整个上报流程图j , E } j f G % V如下:
2. 踩过的坑 && 做得好的当地
-
之前做针对网络接口根本上都是运用现有协议的
key/value
协议上开发的,它的长处是N C o i 7 ? 2运用简略,缺点是协议体 e v h o a i太大。在规划计划的时分分析道数据N H ` & y ^ (上报 SDK 网络上报肯定是十分高频的所以咱们需e S 0 / U + 3 : J求规划自界说f % o的报文协议,这部分的] 6 f % X规划上能够参阅TCP 报文头结构
。 -
当时和后端对接接口的时分k J } F发现数据( * H F V上报曩昔,Q q 4 6 f 1 X服务端解析不了。断点调试发现数据聚合后的巨细、条数、紧缩、加密都是正常的,在本地 Mock 后完全能够反向解分出来。但为什么到服务X r a / ( j J端就解析不了,联调后发现是字节端序(Big-Endian)的问题。简略介绍如下,关于巨细端序的详细介绍请检查我的这篇文章
主机字节顺序HBO(Host Byte Order):与 CPU 类型有关。Big-Endian: PowerPC、IBM、Sun。Little-Endian:x86、Dn 1 A w #EC
网络字节顺序 NBO(Network Byte Order):网络默许为大端序。
-
上面的逻辑有一步g 0 ] ( | C是当网络上报成功后需求k P ` Z删去标记为 dirty 的数据。可是测验了一下发现,许多数据删去后数据库文件的巨细不变,理论上需求腾出内存数据巨细的空间。
sqlite 采用的是变长记载存储,当数据被删去后,未运用的磁盘空间被增加到一个内在的“空闲列表”中,用于下次插入数据,这归于优化机制之一,sqlite 提供
vacuum
指令来开释2 A e y Y。这个问题类似于 Linux 中的文件引证计数的意思,尽管不相同,可是提出来做一下参阅。实验是这样的
-
先看一下当时各个挂载目录的空间巨细:
df -h
-
首要咱们发生一个50M巨细的文件
-
写一段代码读取文件
#include<q , % - ~ Lstdio.h> #include<unistd.h> intmain(void)Y B + s 6 Y r Q o {FILE*fp=NULL; fp=fopen("/b+ ) R D * a ( Zoot/test.txt",8 ) E : p -"rw+"); if(f / 0NULL==fp){ perror("openfilefailed"); return-1; } while(1){ //donothingsleep(1); } fclose(fp); return0; }x ! v 3 = J H &
-
指令行办法下运用
rm
删去文件 -
检查文件巨细:
df -h
,发现文件被删去了,可是该目录下的可用空– g w间并未变多
解说:实际上,只需当一个文件的引证计数为0(包括硬链接数)的时分,才可能调用 unlink 删去,只需它不是0,那么就不会被删去。所谓的删去,也不过是文件 s I Q 5 W h z _名到 inode 的链接删去,只需不被n 5 Y ;从w 6 )头写入新的数据,磁盘上的 block 数据块不会被删去,因而,你会看到,即使删库跑路了,某些数据还是能够恢复的。换句话说,当一个程序翻开一个文件的时分(获取到文件描述符)T % z 9 x p *,它的引证计数会被+1,rm尽管看似删去了文l D @件,实际上仅仅会将引证计数减1,但由于引证计数不为0,因而文件不会被删去。
-
-
在数据聚合的时分优先获取 crash 数据,总数据条数需求小于上报装备数据的条数约束、总数据巨细需求小于上报装备数据的巨细约束。这里的处理运用了递归,改变了函数参数
- (void)assembleDataInTable:(PCTLogTableType)tableType net; L oworkType:(NetworkingManagerSta~ T D 8 p . _ JtusType)networkType completion:(void (^)(NSArray<PCTLogModel *> *r2 P s e | } [ecords| { a ^ & , ;))completion[ W # G R u { // 1. 获取到适宜的 Crash 类型C q ` r P的数据 [self fetchCrashDataByCount:self^ x X H Q U , $.configureModel.maxFlowMByte inTable:tableType upperBound:self.configureModel.maxBodyMByte completion:^(NSArray<PCTLogModel *> *records) { NSArray<PCTLogModel *> *crashData = recor! @ Y , + h c yds; // 2. 核算剩下需求的数据条数和剩下需求的数据巨细 NSC P H . w J / 9Integer remainingCount = self.configureModel.maxItem{ . r F ( - crashData.count; flk q v %oat remainingSiz( f i #e = self.confie k _gureModel.maxBodyMByte - [self calculateDataSize:crashData]; // 3. 获取除 Crash 类型之外的其他数据,且需求契合相应规矩 BOOL isWifi = (networkType == NetworkingManagerStatusReac~ ; O @ X X 9hableViaWiFi); [self feW j T M u / O = 8tchDG l o o s !ataExceptCrash:remainingCount ind ) [ P - A P OTable:tabK 9 ] 9 7 leType upperBound:remainingSize isWiFI:isWifi completionf 5 N f % ^ ? = q:^(NSArray<PCTLogModel *> *records) { NSArrO t ] x f aay<PCTLogModel *> *dataExceptCrash = records; NSMutableArray *dataSource = [NSMutableArray aR - f ) ` M Zrrayy [ / ~ E c]; [dataSource addObjectsFromArray:crashData]; [dataSource addObjectsFromArray:dataExceptCrash]; if (completion) { completion([dataSource copy]); } }]; }]; } - (void)fetchDataExceptCrash:(NSInteger)count inTable:(PCTLogTableType)tableType upperBound:(float)size isWiFI:(BOOL)isWifi completion:(void (^)(NSArray<PCTLogModel *> *records))completion { // 1. 依据剩下需求a A G数据条数去查询表中非 Crash 类型的数据调集 __block NSMutableArray *conditions = [NSMl ; 0 /utableArray aF h Prray]; [self.configureModel.monitorList enumerateObjectsUsingBlock:s G F Q @^(PCTItemModel * _Nonnull obj, NSUInteger idx, BOOL * _@ l &Nonnull stop) { iI D 1 (f (isWifi) { if (![obj.type isEqualToString:@ & @ 7 ? J 6"appCrash"]) { [conditions addObject:[NSS, x # } m P c : 6tring stringWithFormat:@"'%@'", obj.type]]; } } else { if (!obj.onlyWifi &&S A r y C * X { tamp$ t I; ![obj.type isEqualToString:@"appCrash"]) { [conditions addObject:[NSString stringWithFormat:@"'%Z 2 J J & d !@'", PCT_SAFE_STRING(obj.type)]]; } } }]; NSStt R k V k U M Wring *queryCf ~ ) 5 a `rashDataCondition = [NSS~ @ V g & k tring stringWithFormat:@"monitor_type in (%@) and is_used = 0 aM B 3 ~ E j ~ ) rnd namespace = '%@'", [conditionsB @ M V _ u % / componentsJoinedByo R t x 9 . ^String:@","], self.namespace]; // 2. 依据是否有 Wifi 查找对应的数据 [PCT_DATABASE getRecordsByCount:count condtion:queryCrashDataCondi^ q ~tion i` [ 0 ? . $ } j +nTableTyp; B Me:tableType completion:^(NSArray<PC5 U z 8 : y U OTLogModel *> *_Nonnull records) { // 3. 非 Crash 类型的数据调集巨$ j ? ] k M细是否超越剩下需求的数Y & o f ! f O ~ )据巨细 float dataSize: 8 i d v + / = [ser : Y * 7lf calculateDataSize:records]; // 4. 大于最大包体积则递归获取 maxItem-1 条非 CraW } / $ Y ysh 数据集兼并判别数据巨细 if (size == 0) { if (completion) { completion(records); } } else if (dataSize > size) { NSInteger currentCount = count - 1; return [self fetchDataExceptCrash:curr. E m 1 { o O T wentCount inTable:t; 2 !ableType upperBound:size isWiFI:isWifi completion:G : y @ t f W Kcompletio& 9 w U * E ` tn]; } else { if (completion) { completion(r) X P B 0 P Qecords); } } }]; }
-
整个 SDK 的 Unit Test 经过率 100%,代码分支掩盖率为 93%。测验依据 TDD 和 BDD。测验结构:体系自带的
XCTest
,第三方的OCMock
、Kiwi
、Expecta
、Specta
。测验运用了根底类,后续每个文件都规划承继自测验基类的类。Xcode 能够看到整个 SDKG h h 的测验掩盖率和单个文件的测验掩盖率
也能够运用 slather。在项目终端环境下新建
.slather.yml
装备文件U r ] b,然后履. @ V U行句子slather coverv v } d 8 Kage -s --sch( % H Q ~ w ; Leme prism-client-Exampled x d v F R --workspace prism-client.xcworkspace prism-client.xcodeproj
。关于质量确保的最根底、可靠的计划之一软件% V [ [ 8 K |测验,在各个端都有一些需求留意的当地,还c { ) A N C 6需求结合工程化,我会写专门的文章谈谈经验心得。
五、 接口规划% : F ) E ,及核心完结
1. 接口规划
@intt + b , . + e Oerface PrismClient : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/**
单例办法初始化全局唯一目标。单例之后有必要马上 se( e 1 t & D J L 2tUp
@return 单例目n & w A = 2标
*/
+ (instancetype( o 2 -)sharedIB v Snstance;
/**
当时 SDK 初始化。当时功用:注册装备下发服务。
*/
- (void)setup;
/**
上报 payload 类型的数据
@param type 监控类型
@param meta 元数据
@param payload payloal { e ~ % od类型的数据
*/
- (void)send) 8 V 7 N xWithType:(NSString *)type meta:(NSDictionary *)meta payload:(NSData *__nullable)payload;
/y 1 x C**
上报 meta 类型的数据,需求传递三个参数。ty: Z L V % M =pe 标明是什么类型的数据;prefix 代表前缀,上报到后台会拼接 prefix+type;meta 是字典类型的元数据
@param type 数据类型
@param prefix 数据类型的前缀。一般是事l B U ( { ` f $ -务线称号首字母简写。比方记账:JZ
@param meta description元数据
*/
- (void)sendWit+ 3 O fhType:(NSString *)tyz 9 O Q +pe prefix:(NSString *)o d b C T D R uprefix meta:(NSDictionary *)meta;
/**
获取上报相关的通用信息
@return 上报根底信息
*c - ) B E 4 c X/
- (PCTCommonModel *o g 1 -)getCommon;
/**
是否需求搜集上报
@ret5 ^ F R 8urn 上报开关@ ` 2 N
*/
- (BOOL)isGather:(NSString *)namespace;
@end
PrismClient
类是整个 SDK 的入口,也是接口的提供者。其间 - (void)sendWithType:(NSString *)t| q P 2 type prefix:(NSString *)prefix meta:(NSDictionary *)meta;
接口给事务b n 3 * f # | 4 &方运用。
- (void)sendWithType:(NSString *)type meta:(NSDictionary *)meta pa& W 2 Dyload:(NSData *__nullable)payload;
给监控数据运用。
setup
办法内部敞开多个 namespace 下的处理 handler。
- (void)setup {
// 注册 mget 获取监控和各事务线的装备信息,会发生= Y 6多个2 w 6 7 % 5 q namespace,彼此平行、G # _ 6阻隔
[[PA _ w w ? W /CTConfigurationService share0 6 ^ s t K XdInstance] registerAndFet{ ) @ S ` r J PchConfigurationInfo];
[se^ : {lf.configutations enumer9 p 0 * 0 mateO, I h * +bjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
PCTz k 0 ~ Z )Service *service = [[PCTService alloc] initWithNamel N W % : c F & Bspace:obj];
[self.services setObject:service forKey:obj]l E ) n e f W s =;
}];
PCTService *prism1 } J r Z T . ^ ]Service = [self.services objectForKey:PRISMNAMESPACE];
if (!prismService) {
prismService = [[PCTService alloc] initWithNamespace:PRISMNAMESP_ A , k ! i Q %ACE];
[self.seo T e } k N k A zrvic- ; E z u Q g D 4es setObject:prismService forKey:PRISMNAMESPACE];
}
}
六、 总结与思考
1. 技能方面
多线程技能很强壮,可是很容易出问题。一般做事务的时分用一些简略的 GCD、NSOperation 等就能够满足根本需求了,可是做 SDK 就不相同,你需求考虑F W H }各种场景。比方 FMDB 在多线程读写的时分,规划了 FMDatabaseQueue 以串行行列的办法同步履行使命。可是这样一来假定运用者在主线程插入 n 次数据到数据库,这样会发生 ANR,所以咱们还得保护一个使命派发行列,用n [ o来保护a F m l / M l z事务方提交的使命,是一个并发行列,以异步使命的办法提交给 FMDB 以同步使命的办法在串行行列上履行。
AFNetworking 2.0 运用了 NSURLConnection,一起保护了一P N +个常驻线程,去处理网络成功后的回调。9 z 4 & 9 : E %AF 存在一个常驻线程,假定其他 n 个 SDK 的其间 m 个 SDK 也敞开了常驻线程,那你的 App 集成后就有7 N & k 8 p e 1+m 个常驻线程。
AFNetworking 3.0 运( } e T 1 ^用 NSURb l 2 j S }LSessiD s u 4 ! R lon 替换 NSURLConnection,取消了常驻线程。为什么换了? 逼不得已呀,Apple 官方出了 NSURLSession,那就不需求 NSURLConnection,并为之创立常驻线程了。至于为什么 NSURLSession 不需求常2 ; b Y P驻线程?它比 NSURLConnecction 多做9 ^ { I o I i N了什么,今后再聊
创立Z f e t线程的过程,需求用到物理内存,CPU 也会耗费时刻。新建一个线程,体系会在该进程空间分配必定的内存作为线程仓库。仓库巨细是 4KB 的倍数。在 iOS 主线程仓库巨细是 1MB,新创立的子线程仓库巨细是 512KB。此外线程创立得多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时a ^ c }分需求寻址,而寻址的过程有 CPU 耗费。} P {线程过多时内存、CPU 都会有许多的耗费,呈现 ANR 乃至被强杀。
举了 是 FMD! 3 @ m f q )B 和 AFNetworking 的作者那么凶猛,规划的 FMDB 不包装会 ANR,AFNetworking 有必要运用常驻线程,为什么?正是由于多线程太强壮、灵活了,开发者骚操作太多,所以 FMDB 规划最简略确保数据库操作线程安全,具体运用能够自己保护行列去包一层。AFNetV l ] – l r &working 内的多线* k P | t + / z p程也严格依据体系特色来规划。
所以有必要再研讨下多线程,主张读 GCD 源码,也便是 libdispatch
2. 标准方面
许多开发都不做测验,z % ! U = : L d $咱们公司都严格约好测验。写根底 SDKh / I I Y V l o 更是如此,一个 App 根底功用有必要质量G Q x 2 x g ? o R稳定,所以测验是确保手法之一E G 7 8 _ ^。必定p r y ; 0 I I p要写好 Unit Test。这姿态不断版别迭代,关于 Uw y Z fT,输入稳定,l m 7 8 G $ W输出稳定,这样内部完结怎么变动不需求关心,只需求判别稳定输入,稳定输出就足够了。(针7 z c 3 k d对每个函数单一原则的根底上也是满足 UT)。还有一个优点便是当和他人谈论的的时分,你画个技能流程图、技能架构图、测验的 case、测验输入、P g 8 r输出表述清楚,听的人再看看鸿沟状况是否都考虑全,根本上很快交流结束,功率考高。
在做 SDK 的接口规划的时分,办法名、参数个数、参数类型、参数称号、返回值称号、类型、数据结构,尽量要做到 iOS 和 Android 端共同,除非某些特别2 K ~ S w 5 L L状况,无法确保共同的输出。别问为什么?优点太多了,老练 SDK 都这么做。
比方一个数据上I L } B ? X z % 7报 SDK。需求H x s考虑数据来源是什么,我规划的接口需求露出什么信息, } e G Z I o t a,数据怎么高效存Z O g @ B储、数据怎么校验、数据怎么高效及时上报。 假定我做的数据上报 SDK 能够上报 APM 监控数据、一起也开放4 F t R z 0 / #才能给事务线运用,事务线自己将感兴趣的2 n E M | M 0 n }数据并写入保存,确保不丢失的状况下怎5 G l ) K b么高效上报。由于数据实时上报,所以需求考虑上传的5 % f A y 0 l网络环境、Wi-Fi 环境和 4G 环境下的逻辑不相同的、数据聚合组装成自界说报文并上报3 , ^、一个天然天内数据上传需求做流量约束等等、App 版别晋级一些数据可能X N [ u h a M会失掉意义、当然存储的数据也存在时效性。种种这些东西便是在开发前需求考虑清楚的。所以根底渠道做工作根本是 规划思考时刻:编码时刻 = 7:3。
为什么?假定你一个] 2 q需求,预期10天时刻;前期架构规划、类的规划、Uint Test 规划估量7天,到时分编码开发2天完结。 这么做的优点许多,比方:
-
除非是十分优异,S , ,不然脑子想的再前面y 6 d U T ` 9 7到真实开发的时分发现有出入,coding 完发现和前期计划规划不相同。所以主张用流程图、` ` W 9 n UMLc { t / N T 3图、技能架构图、UT 也相同,规划个表格,这样比及时分编码也便是 coding 的工作了,将图翻译成代码
-
后期和他人谈论或许交流或许 CTO 进行 code review 的时分不需求一行行看代码。你将相关的架构图、流程图、UMLb d W 图给他看看。他再看看一些关键逻辑的 UT,确保输入输出正确,一般来说这2 q / Z w [样就够了
3. 质量确保
UT 是质量确保的一个方面,K B j , y M J另一个便是 MR 机制。咱们团队 MR 采用 +1
机制。每个 merge reqH h f J | ;uest 有必要有团队内O a 5 C e ) @ =至少3个人 +1,且其间一人有必要为同技能栈且比你资深一些的搭档 +1,一人为和你参与同一个项目的搭档。
当有人谈论或许有疑问时,你有必要解答清楚,他人提出的修正点要么修正好,要么解说清楚,. s ( * _ . { B才1 ! +能够 +1。当 +1 数大于3,则兼并分支代码。
连带职责制。当你的线上代码存在 bug 时,为你该次 MR +1 的搭档具有连带职责。
参阅资料
- WAL
- WCDB 的 WAL 办法和异步 Checkpoint
- sqlite vacuum
- 完全弄懂函数防抖M _ 1和函数节省