乐趣区

关于ios:打造一个通用可配置多句柄的数据上报-SDK

一个 App 个别会存在很多场景去上传 App 中产生的数据,比方 APM、埋点统计、开发者自定义的数据等等。所以本篇文章就讲讲如何设计一个通用的、可配置的、多句柄的数据上报 SDK。

前置阐明

因为这篇文章和 APM 是属于姊妹篇,所以看这篇文章的时候有些货色不晓得活着好奇的时候能够看带你打造一套 APM 监控零碎。

另外看到我在上面的代码段,有些命名格调、简写、分类、办法的命名等,我简略做个阐明。

  • 数据上报 SDK 叫 HermesClient,咱们规定类的命名个别用 SDK 的名字缩写,当前情况下缩写为 HCT
  • 给 Category 命名,规定为 类名 + SDK 前缀缩写的小写格局 + 下划线 + 驼峰命名格局的性能形容。比方给 NSDate 减少一个获取毫秒工夫戳的分类,那么类名为 NSDate+HCT_TimeStamp
  • 给 Category 的办法命名,规定为 SDK 前缀缩写的小写格局 + 下划线 + 驼峰命名格局的性能形容。比方给 NSDate 减少一个依据以后工夫获取毫秒工夫戳的办法,那么办法名为 + (long long)HCT_currentTimestamp;

一、首先定义须要做什么

咱们要做的是「一个通用可配置、多句柄的数据上报 SDK」,也就是说这个 SDK 具备这么几个性能:

  • 具备从服务端拉取配置信息的能力,这些配置用来管制 SDK 的上报行为(需不需要默认行为?)
  • SDK 具备多句柄个性,也就是领有多个对象,每个对象具备本人的管制行为,彼此之间的运行、操作相互隔离
  • APM 监控作为十分非凡的能力存在,它也应用数据上报 SDK。它的能力是 App 品质监控的保障,所以针对 APM 的数据上报通道是须要非凡解决的。
  • 数据先依据配置决定要不要存,存下来之后再依据配置决定如何上报

明确咱们须要做什么,接下来的步骤就是剖析设计怎么做。

二、拉取配置信息

1. 须要哪些配置信息

首先明确几个准则:

  • 因为监控数据上报作为数据上报的一个非凡 case,那么监控的配置信息也应该非凡解决。
  • 监控能力蕴含很多,比方卡顿、网络、奔溃、内存、电量、启动工夫、CPU 使用率。每个监控能力都须要一份配置信息,比方监控类型、是否仅 WI-FI 环境下上报、是否实时上报、是否须要携带 Payload 数据。(注:Payload 其实就是通过 gZip 压缩、AES-CBC 加密后的数据)
  • 多句柄,所以须要一个字段标识每份配置信息,也就是一个 namespace 的概念
  • 每个 namespace 下都有本人的配置,比方数据上传后的服务器地址、上报开关、App 降级后是否须要革除掉之前版本保留的数据、单次上传数据包的最大体积限度、数据记录的最大条数、在非 WI-FI 环境下每天上报的最大流量、数据过期天数、上报开关等
  • 针对 APM 的数据配置,还须要一个是否须要采集的开关。

所以数据字段根本如下

@interface HCTItemModel : NSObject <NSCoding>

@property (nonatomic, copy) NSString *type;         /< 上报数据类型 */
@property (nonatomic, assign) BOOL onlyWifi;        /< 是否仅 Wi-Fi 上报 */
@property (nonatomic, assign) BOOL isRealtime;      /< 是否实时上报 */
@property (nonatomic, assign) BOOL isUploadPayload; /< 是否须要上报 Payload*/

@end

@interface HCTConfigurationModel : NSObject <NSCoding>

@property (nonatomic, copy) NSString *url;                        /< 以后 namespace 对应的上报地址 */
@property (nonatomic, assign) BOOL isUpload;                      /< 全局上报开关 */
@property (nonatomic, assign) BOOL isGather;                      /< 全局采集开关 */
@property (nonatomic, assign) BOOL isUpdateClear;                 /< 降级后是否革除数据 */
@property (nonatomic, assign) NSInteger maxBodyMByte;             /< 最大包体积单位 M (范畴 < 3M)*/
@property (nonatomic, assign) NSInteger periodicTimerSecond;      /< 定时上报工夫单位秒 (范畴 1 ~ 30 秒)*/
@property (nonatomic, assign) NSInteger maxItem;                  /< 最大条数 (范畴 < 100)*/
@property (nonatomic, assign) NSInteger maxFlowMByte;             /< 每天最大非 Wi-Fi 上传流量单位 M (范畴 < 100M)*/
@property (nonatomic, assign) NSInteger expirationDay;            /< 数据过期工夫单位 天 (范畴 < 30)*/
@property (nonatomic, copy) NSArray<HCTItemModel *> *monitorList; /< 配置我的项目 */

@end

因为数据须要长久化保留,所以须要实现 NSCoding 协定。

一个小窍门,每个属性写 encodedecode 会很麻烦,能够借助于宏来实现疾速编写。

#define HCT_DECODE(decoder, dataType, keyName)                                                    \
{                                                                                             \
_##keyName = [decoder decode##dataType##ForKey:NSStringFromSelector(@selector(keyName))]; \
};

#define HCT_ENCODE(aCoder, dataType, key)                                             \
{                                                                                 \
[aCoder encode##dataType:_##key forKey:NSStringFromSelector(@selector(key))]; \
};

- (instancetype)initWithCoder:(NSCoder *)aDecoder {if (self = [super init]) {HCT_DECODE(aDecoder, Object, type)
        HCT_DECODE(aDecoder, Bool, onlyWifi)
        HCT_DECODE(aDecoder, Bool, isRealtime)
        HCT_DECODE(aDecoder, Bool, isUploadPayload)
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {HCT_ENCODE(aCoder, Object, type)
    HCT_ENCODE(aCoder, Bool, onlyWifi)
    HCT_ENCODE(aCoder, Bool, isRealtime)
    HCT_ENCODE(aCoder, Bool, isUploadPayload)
}

抛出一个问题:既然监控很重要,那别要配置了,间接全副上传。

咱们想一想这个问题,监控数据都是不间接上传的,监控 SDK 的责任就是收集监控数据,而且监控后的数据十分多,App 运行期间的网络申请可能都有 n 次,App 启动工夫、卡顿、奔溃、内存等可能不多,然而这些数据间接上传前期拓展性十分差,比方依据 APM 监控大盘剖析出某个监控能力临时先敞开掉。这时候就无力回天了,必须等下次 SDK 公布新版本。监控数据必须先存储,如果 crash 了,则必须保留了数据等下次启动再去组装数据、上传。而且数据在生产、新数据在一直生产,如果上传失败了还须要对失败数据的解决,所以这些逻辑还是挺多的,对于监控 SDK 来做这个事件,不是很适合。答案就不言而喻了,必须要配置(监控开关的配置、数据上报的行为配置)。

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 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;
}

下面的例子是一份默认配置信息

3. 拉取策略

网络拉取应用了根底 SDK(非网络 SDK)的能力 mGet,依据 key 注册网络服务。这些 key 个别是 SDK 外部的定义好的,比方统跳路由表等。

这类 key 的共性是 App 在打包阶段会内置一份默认配置,App 启动后会去拉取最新数据,而后实现数据的缓存,缓存会在 NSDocumentDirectory 目录下依照 SDK 名称、App 版本号、打包平台上调配的打包工作 id、key 建设缓存文件夹。

此外它的特点是等 App 启动实现后才去申请网络,获取数据,不会影响 App 的启动。

流程图如下

上面是一个截取代码,比照下面图看看。

@synthesize configurationDictionary = _configurationDictionary;

#pragma mark - Initial Methods

+ (instancetype)sharedInstance {
    static HCTConfigurationService *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{_sharedInstance = [[self alloc] init];
    });
    return _sharedInstance;
}

- (instancetype)init {if (self = [super init]) {[self setUp];
    }
    return self;
}

#pragma mark - 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 * _Nonnull error) {}];
}

- (HCTConfigurationModel *)getConfigurationWithNamespace:(NSString *)namespace {if (!HCT_IS_CLASS(namespace, NSString)) {NSAssert(HCT_IS_CLASS(namespace, NSString), @"须要依据 namespace 参数获取对应的配置信息, 所以必须是 NSString 类型");
        return nil;
    }
    if (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

三、数据存储

1. 数据存储技术选型

记得在做数据上报技术的评审会议上,Android 共事说用 WCDB,特色是 ORM、多线程平安、高性能。而后就被质疑了。因为上个版本应用的技术是基于零碎自带的 sqlite2,单纯为了 ORM、多线程问题就额定引入一个三方库,是不太能压服人的。有这样几个疑难

  • ORM 并不是外围诉求,利用 Runtime 能够在根底上进行批改,也可反对 ORM 性能
  • 线程平安。WCDB 在线程平安的实现次要是基于HandleHandlePoolDatabase 三个类实现的。Handle 是 sqlite3 指针,HandlePool 用来解决连贯。

    RecyclableHandle 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 上报 */
@property (nonatomic, assign) BOOL isRealtime;      /**< 是否实时上报 */
@property (nonatomic, assign) BOOL isUploadPayload; /**< 是否须要上报 Payload*/

@end

@interface HCTConfigurationModel : NSObject <NSCoding>

@property (nonatomic, copy) NSString *url;                        /**< 以后 namespace 对应的上报地址 */
@property (nonatomic, assign) BOOL isUpload;                      /**< 全局上报开关 */
@property (nonatomic, assign) BOOL isGather;                      /**< 全局采集开关 */
@property (nonatomic, assign) BOOL isUpdateClear;                 /**< 降级后是否革除数据 */
@property (nonatomic, assign) NSInteger maxBodyMByte;             /**< 最大包体积单位 M (范畴 < 3M)*/
@property (nonatomic, assign) NSInteger periodicTimerSecond;      /**< 定时上报工夫单位秒 (范畴 1 ~ 30 秒)*/
@property (nonatomic, assign) NSInteger maxItem;                  /**< 最大条数 (范畴 < 100)*/
@property (nonatomic, assign) NSInteger maxFlowMByte;             /**< 每天最大非 Wi-Fi 上传流量单位 M (范畴 < 100M)*/
@property (nonatomic, assign) NSInteger expirationDay;            /**< 数据过期工夫单位 天 (范畴 < 30)*/
@property (nonatomic, copy) NSArray<HCTItemModel *> *monitorList; /**< 配置我的项目 */

@end

监控数据存储流程:

  1. 每个数据(监控数据、业务线数据)过去先判断该数据所在的 namespace 是否开启了收集开关
  2. 判断数据是否能够落库,依据数据接口中 type 是否命中上报配置数据中的 monitorList 中的任何一项的 type
  3. 监控数据先写入 meta 表,而后判断是否写入 payload 表。判断规范是计算监控数据的 payload 大小是否超过了上报配置数据的 maxBodyMByte。超过大小的数据就不能入库, 因为这是服务端耗费 payload 的一个下限
  4. 走监控接口过去的数据,在办法外部会为监控数据减少根底信息(比方 App 名称、App 版本号、打包工作 id、设施类型等等)

    @property (nonatomic, copy) NSString *xxx_APP_NAME;       /**<App 名称(wax)*/
    @property (nonatomic, copy) NSString *xxx_APP_VERSION;    /**<App 版本(wax)*/
    @property (nonatomic, copy) NSString *xxx_CANDLE_TASK_ID; /**< 打包平台调配的打包工作 id*/
    @property (nonatomic, copy) NSString *SYS_SYSTEM_MODEL;   /**< 零碎类型(android / iOS)*/
    @property (nonatomic, copy) NSString *SYS_DEVICE_ID;      /**< 设施 id*/
    
    @property (nonatomic, copy) NSString *SYS_BRAND;          /**< 零碎品牌 */
    @property (nonatomic, copy) NSString *SYS_PHONE_MODEL;    /**< 设施型号 */
    @property (nonatomic, copy) NSString *SYS_SYSTEM_VERSION; /**< 零碎版本 */
    @property (nonatomic, copy) NSString *APP_PLATFORM;       /**< 平台号 */
    @property (nonatomic, copy) NSString *APP_VERSION;        /**<App 版本(业务版本)*/
    
    @property (nonatomic, copy) NSString *APP_SESSION_ID;   /**<session id*/
    @property (nonatomic, copy) NSString *APP_PACKAGE_NAME; /**< 包名 */
    @property (nonatomic, copy) NSString *APP_MODE;         /**<Debug/Release*/
    @property (nonatomic, copy) NSString *APP_UID;          /**<user id*/
    @property (nonatomic, copy) NSString *APP_MC;           /**< 渠道号 */
    
    @property (nonatomic, copy) NSString *APP_MONITOR_VERSION; /**< 监控版本号。和服务端维持同一个版本,服务端降级的话,SDK 也跟着降级 */
    @property (nonatomic, copy) NSString *REPORT_ID;           /**< 惟一 ID*/
    @property (nonatomic, copy) NSString *CREATE_TIME;         /**< 工夫 */
    @property (nonatomic, assign) BOOL IS_BIZ;                 /**< 是否是监控数据 */
  5. 因为本次交给数据上报 SDK 的 crash 类型的数据是上次奔溃时的数据,所以在第 4 点说的规定不太实用,APM crash 类型是特例。
  6. 计算每条数据的大小。metaSize + payloadSize
  7. 再写入 payload 表
  8. 判断是否触发实时上报,触发后走后续流程。
- (void)sendWithType:(NSString *)type meta:(NSDictionary *)meta payload:(NSData *__nullable)payload {
    // 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. 判断以后 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];
}

业务线数据存储流程根本和监控数据的存储差不多,有差异的是某些字段的标示,用来辨别业务线数据。

四、数据上报机制

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:x86、DEC

    网络字节程序 NBO(Network Byte Order):网络默认为大端序。

  • 下面的逻辑有一步是当网络上报胜利后须要删除标记为 dirty 的数据。然而测试了一下发现,大量数据删除后数据库文件的大小不变,实践上须要腾出内存数据大小的空间。

    sqlite 采纳的是变长记录存储,当数据被删除后,未应用的磁盘空间被增加到一个外在的“闲暇列表”中,用于下次插入数据,这属于优化机制之一,sqlite 提供 vacuum 命令来开释。

    这个问题相似于 Linux 中的文件援用计数的意思,尽管不一样,然而提出来做一下参考。试验是这样的

    1. 先看一下以后各个挂载目录的空间大小:df -h
    2. 首先咱们产生一个 50M 大小的文件
    3. 写一段代码读取文件

      #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;
      }
    4. 命令行模式下应用 rm 删除文件
    5. 查看文件大小: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,第三方的 OCMockKiwiExpectaSpecta。测试应用了根底类,后续每个文件都设计继承自测试基类的类。

    Xcode 能够看到整个 SDK 的测试覆盖率和单个文件的测试覆盖率

    也能够应用 slather。在我的项目终端环境下新建 .slather.yml 配置文件,而后执行语句 slather coverage -s --scheme hermes-client-Example --workspace hermes-client.xcworkspace hermes-client.xcodeproj

    对于质量保证的最根底、牢靠的计划之一软件测试,在各个端都有一些须要留神的中央,还须要联合工程化,我会写专门的文章谈谈教训心得。

五、接口设计及外围实现

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 下的配置信息 */
@property (nonatomic, copy) NSString *metaURL;                  /**<meta 接口地址 */
@property (nonatomic, copy) NSString *payloadURL;               /**<payload 接口地址 */
@property (nonatomic, strong) HCTRequestFactory *requester;     /**< 网络申请核心 */
@property (nonatomic, strong) NSNumber *currentTimestamp;       /**< 保留的工夫戳 */
@property (nonatomic, strong) NSNumber *currentFlow;            /**< 以后已应用的流量 */
@property (nonatomic, strong) TMLoopTaskExecutor *taskExecutor; /**< 上报数据定时工作 */
@property (nonatomic, assign) BOOL isAppLaunched;               /**< 通过 KVC 的模式获取到 HermesClient 外面存储 App 是否启动实现的标识, 这种 case 是解决: mget 首次获取到 3 个 namespace, 但 App 运行期间服务端新增某种 namespace, 此时业务线如果插入数据仍旧能够失常落库、上报 */
@end

@implementation HCTService

@synthesize currentTimestamp = _currentTimestamp;
@synthesize currentFlow = _currentFlow;

#pragma mark - life cycle

- (instancetype)initWithNamespace:(NSString  * _Nonnull)namespace {if (self = [super init]) {
        _namespace = namespace;
        [self setupConfig];
        if (self.isAppLaunched) {[self executeHandlerWhenAppLaunched];
        } else {[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
                                                              object:nil
                                                               queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {[self executeHandlerWhenAppLaunched];
                [[HermesClient sharedInstance] setValue:@(YES) forKey:@"isAppLaunched"];
            }];
        }
    }
    return self;
}


#pragma mark - public Method

- (void)sendWithType:(NSString *)type meta:(NSDictionary *)meta payload:(NSData *__nullable)payload {
    // 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. 判断以后 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

六、总结与思考

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 天实现。这么做的益处很多,比方:

  1. 除非是十分优良,不然脑子想的再后面到真正开发的时候发现有出入,coding 完发现和后期方案设计不一样。所以倡议用流程图、UML 图、技术架构图、UT 也一样,设计个表格,这样等到时候编码也就是 coding 的工作了,将图翻译成代码
  2. 前期和他人探讨或者沟通或者 CTO 进行 code review 的时候不须要一行行看代码。你将相干的架构图、流程图、UML 图给他看看。他再看看一些要害逻辑的 UT,保障输入输出正确,一般来说这样就够了

3. 质量保证

UT 是质量保证的一个方面,另一个就是 MR 机制。咱们团队 MR 采纳 +1 机制。每个 merge request 必须有团队内至多 3 集体 +1,且其中一人必须为同技术栈且比你资深一些的共事 +1,一人为和你加入同一个我的项目的共事。

当有人评论或者有疑难时,你必须解答分明,他人提出的批改点要么批改好,要么解释分明,才能够 +1。当 +1 数大于 3,则合并分支代码。

连带责任制。当你的线上代码存在 bug 时,为你该次 MR +1 的共事具备连带责任。

参考资料

  • WAL
  • WCDB 的 WAL 模式和异步 Checkpoint
  • sqlite vacuum
  • 彻底弄懂函数防抖和函数节流
退出移动版