免责声明:本文为私人前期存货,内容营养无质保,请挑选性阅读 peace
前语
开发进程中日志的重要性不言而喻,它协助开发者了解代码履行状况整理事务流程,经过输出的日志能发现危险或排查问题。移动端开发进程中可经过 IDE 的控制台来输出、检查日志,也便是所谓的在线调试。在线调试适宜追寻可安稳复现的问题,但关于随机性或有严厉边界条件的问题却力不从心。
一方面大多数不行安稳复现的问题多产生在用户侧,用户侧的问题不行能有在线调试的机遇(即使有成本也会相对较大)。另一方面移动端出于功能、安全等要素的考虑会在线上版别封闭日志输出,因为缺失日志导致在用户侧呈现问题时开发往往只能依据现象去猜想问题原因,有点开盲盒的感觉。靠猜想处理问题严重影响工作功率,已成为本团队开发中的一大阻碍,相信其它团队也会有相似的困扰。
其本质原因是开发者对用户侧的问题没有有用日志获取手法,能像开发阶段相同获取用户侧的全量日志来剖析问题将是本篇所评论的要点。
关于写日志
在开发进程中输出有用的日志是一个开发者的根本素养,好像不写注释的开发不是好程序员相同,不输出有用日志的程序员也不是一个合格的开发者。
完善的日志能协助开发快递定位问题,一个有水平的开发往往都有杰出注释习气,但好开发关于日志的输出却是有会所顾忌因为他潜意识里会考虑到日志对功能的影响、会不会有安全方面的危险,根据此大多数开发实践是不太乐意打印日志的,当然也会有开发考虑不到功能和安全问题,这另当别论。所以在实践项目中大概率会呈现的四种状况:
- 日志与注释根本没有整个项目处在裸奔状况
- 有少量注释但日志随处可见
- 有注释,根本无日志,这种状况有或许是开发洁癖在上线前删掉了日志代码,
- 日志注释相互交错没有规则,何时打日志何时写注释是为所欲为。
日志 == 注释 ?
在开发进程即要写注释又要写日志,他们在功能上好像有重叠的部分,在实践工作中好像难以区别什么时候去写日志,什么时侯去写注释。有开发或许会把日志当成注释在用,张狂写日志来便利日后出问题能够快速剖析、排查。
在此需求清晰的是:日志决不等同于注释。
- 日志要表达的是代码履行的进程,能经过日志复原代码履行状况
- 注释要表达的是开发思路&意图,能经过注释快速了解代码
日志与注释是两个维度的东西,不行混为一谈。关于代码阅读者能经过注释快速了解整个体系的结构,阅读者能以上帝视角审视整个体系的运转流程,剖析局部时再经过日志了解代码履行细节,审视每个流程的走向,反常逻辑分支的处理。注释过多日志太少就无法了解代码动态履行进程,日志过多注释缺失会对阅读&了解代码形成困扰,无关紧要的日志过多会掩盖要害细节。 日志与注释结合才能完成 1 + 1 > 2 的作用,把注释与日志的价值一起最大化。关于日志与注释有一点跑题,扯远了,回到写日志自身。
怎么写有用的日志
关于写日志的一些个人总结,不一定对欢迎探讨:
- 流程开始&结束时
- 检查边界条件不满足时
- 二选一的逻辑分支选其一输出日志
- 多选一的逻辑分支输出判别成果
- 反常逻辑处理时
- 内部状况产生变化时
- 防止在遍历或循环中打印日
- IO 操作失败时
- 调用三方库时的输入与输出
- 决议流程流向的要害变量
- 耗时操作的时长打印
- 自动进行线程切换时
全体计划比照
咱们的意图是在有需求时拿到用户端的日志来帮忙剖析问题,到达这个意图好像很简略。只需求将日志经过接口在适宜的机遇上传到服务端供开发者查阅即可。将日志上传有两个挑选:
计划一
在产线上敞开日志输出,将日志写入内存缓存。后端供给日志上传接口,可实时按条或批量上传日志。后端做日志清洗、聚合,开发有需求时去查日志即可。
计划二
在产线上敞开日志输出,将日志写入日志文件中,规划一套机制在需求时读取日志文件上传服务端存储,然后交由开发下载查阅。
证明
计划一正是埋点事情计算类东西的思路。因为对实时性要求较高而且数据量相对较小,对埋点这类场景计划一是可行的。但套用到日志体系上就会有问题,咱们需求全量日志来剖析问题,代码中全量日志数据量会十分多,悉数上传后端会占用许多存储资源。而且大多状况下上传的日志是无用数据,只要出问题极少数用户的日志才开发人员关心的。而且在内存中缓存的日志在遇到反常闪退时缓存数据会丢掉,在上传及后端处等环节也不行能排除数据丢掉的问题。
计划二好像可行,将日志写入文件而文件体系相对写入内存更加牢靠,经过下发指令的方法来上传日志文件能够做到按需运用而且日志不会在服务端处理环节丢掉,缺陷在于写文件明显没有写内存速度快,日志时效性不如实时上报。
计划一存在致命缺陷:丢日志,计划二在时效性和功能上又有所扣头。实时性的日志不是必需,因为从用户发现问题到开发收到反应是会有相当长时延,实时日志没有实践含义,只需求拿到出问题那一刻之前一段时刻完好日志即可。这儿的完好十分重要,如此计划二能够做全量日志体系根本方向。计划二两个要点,一是将日志存储到闪存(耐久化)、二是在适宜的机遇将日志上传。
耐久化计划探索
不同于开发阶段日志东西,在线日志体系需求考虑更加全面。用户设备的碎片化、操作高度随机,网络环境之复杂都对日志体系提出更高的要求。这儿最要害的是日志的耐久化功能,从事务代码中接收日志字符到向存入闪存的进程中不该存在功能瓶颈。而这个进程中往往会交叉一些耗时操作,格式化、紧缩、加密、耐久化等等。
对低端机而言耐久化日志不该对功能产生影响,换而言之不能因为某一场景日志较多而形成界面卡顿。闪退时用户端产生的终究一条日志不能丢掉,不然不能精确复原闪退原因。开发有或许把账号、鉴权或其它灵敏信息写入日志,所以日志明文不行走漏。日志文件不该占用过多的存储空间,代码级的日志量是十分大,跟着时刻的积累日志占用的存储也不行忽视。
根据以上这些考量,需求对在线日志体系的耐久化提出一些要求:高功能、高牢靠、高安全、低存储占用。
高功能
是的,一个能让开发甩手打日志的体系首要考虑的是 「功能」。
无论是 Android 仍是 iOS,用体系函数写下一条日志代码其背面的逻辑都是将字符写入到日志体系中(在线调试时会同步输出到 IDE)。写日志本质上是一个写文件的进程,写文件也就意味着磁盘 I/O。当日志量不大时,用写文件的方法耐久化日志是可行的,但当日志量较大时,较多的磁盘 I/O 将严重影响功能。写日志的 I/O 操作或许堵塞事务 I/O 操作从而对事务产生影响。
每一次写文件操作都会有两次内存数据的复制进程。1、从用户态复制到内核态 2、从内核态到闪存。数据从内核态复制到磁盘这个进程的机遇是不行控且从内核态到闪存会涉及到内核态与用户态的频频切换。除此之外关于智能手机 NAND 存储还会存在写入扩大的问题。
写入扩大:因为闪存在写入新数据前有必要先擦除原始数据(全新的闪存都是擦除状况),写入以页(page, 4KB)为单位,擦除以块(block, 128 个 page)为单位。当一个 block 写满了,就要找一个新的 block 来持续写。当一切闲暇 block 用完了或快用完时,就要对那些已被符号删去的 block 进行收回操作,把尚存数据不多的脏block擦除洁净,从头利用。而要擦除一个block,有必要先把其尚存的有用数据 page 移走,重写到其他block里边去,这就需求额定的写入操作。
在 Andrioid 渠道上高频的日志写入也会产生许多的 GC(Flutter 同理),这些都将对功能产生负面影响,严重时会影响用户正常操作。
直接写文件会有功能问题,那能不能去直接写内存一起确保反常退出时数据不丢掉呢?Memory Map 刚好能够做到!
mmap
mmap 是运用逻辑内存对磁盘文件进行映射,中心仅仅进行映射没有任何复制操作,防止了写文件的数据复制。操作内存就相当于在操作文件,防止了内核空间和用户空间的频频切换。如此运用 mmap 写日志相当于直接把数据写入内存,相关于写文件其佣有无可比拟的速度优势。
牢靠性
移动端 App 或许会因为各种反常状况产生闪退,理想中的状况是:当 App 产生闪退,反常捕获模块捕获到反常,然后调用日志模块把日志输出到日志文件。然而现实的状况是 App 被体系杀掉的状况十分多,不是每次被杀掉都有事情产生。Android 这种既答应后台运转又保存随时杀掉 App 权力的渠道这种状况更为严重,这些不能被捕获的反常往往又是咱们重视的要点,假如不能完好的记录日志那剖析问题无从谈起。
所以一个好的日志体系应该具有十分高的牢靠性,在任何极端状况都不该丢掉日志、确保日志的完好性。如此在产生问题是才或许精准复原用户端运转时状况。
同样运用 mmap 能够处理牢靠性的问题, mmap 相当于在用户空间拓荒一块专属虚拟内存,让这块内存和文件印射起来。只需求往内存中写入数据,操作体系担任在适宜的机遇把内存数据同步到闪存文件。mmap 同步内存到闪存文件机遇有:
- 调用
msync
函数自动进行数据同步(自动) - 调用
munmap
函数对文件进行解除映射联系时(自动) - 进程退出时(被迫)
- 体系关机时(被迫)
因为内存数据回写到闪存由操作体系控制,App 即使产生反常闪退也能确保数据不会丢掉,一起确保了与写内存相同的速度。
安全性
开发进程不行防止的需求打印一些灵敏信息的日志来辅佐问题剖析,将包括灵敏信息的日志耐久化后那怎么确保灵敏信息的安全性是不得不考虑的问题。
咱们能够经过加密来处理安全的问题。但挑选哪种加密方法才是最佳挑选呢?对称加密,功能好但密钥的保存是个问题,密钥保存在客户端,一旦走漏加密将失去含义。非对称加密相关于对称加密功能较差,公钥放在客户端,用公钥加密日志后需求用私钥解密,不走漏私钥安全性会大大进步。毫无疑问,需求挑选对非称加密做为加密手法。
运用非对称加密后加密机遇又存在问题,上传服务端前一致加密, 保存在磁盘上的未加密日志仍是有走漏的危险而且文件级的加密操作使 CPU 负载呈现峰值对功能产生影响。单条日志加密后写入日志好像是一个可行的计划,加密量小能够滑润 CPU 曲线防止峰值产生。
紧缩率
与加密相同,是全体紧缩仍是单行紧缩看起来是个挑选题。文件全体紧缩的紧缩率能到达最大但若紧缩后的数据在任意方位产生损坏则日志将无法复原,且存在 CPU 峰值对功能产生影响。单行紧缩,文本过短会严重影响紧缩率,但单行紧缩量小,有利于滑润 CPU 负载曲线,任意行损坏不影响其它日志数据。
单行紧缩与全体紧缩都无法满足要求,那可不行分块紧缩?把需求紧缩的字符串分成固定巨细的段,累积到指定巨细时再进行紧缩。只需求找个一个适宜的块巨细使得紧缩率和 CPU 负载都到达一个可接受的水平。这个方法是可行的,只不过仍是能够先进行单行紧缩,紧缩后的数据再分块进行进二次紧缩能够进一步进步紧缩率。即使产生数据损坏也仅仅影响某一个数据块,其它数据块仍能复原成日志明文。
耐久化计划终究形状
前面提到功能、牢靠、加密、紧缩等问题的处理计划,在移动端单独完成每个计划都是一个不小的应战。在造轮子之前先看看有没有现成的轮子可用,究竟咱们是要处理问题而不是为了造一个轮子。
xlog
经过调研发现微信开源了跨渠道组件集 Mars, 包括许多有用小组件,其中最核心最根底的日志组件 「xlog」。xlog 是根据 mmap 用 C++ 开发的跨渠道高功能日志组件,完美的处理了功能、牢靠、加密、紧缩等问题。实践上上面提到的处理计划正是 xlog 所用到的。依赖微信海量用户长时刻的打磨,xlog 验证了其在 iOS/Android 上的安稳性。关于 xlog 的原理说明能够参阅链接: 高功能日志模块xlog
xlog 做到了开箱即用,但因其为跨渠道组件,不能在事务代码里直接运用 xlog 接口来打印日志。需求渠道做好事务层封装,并需求兼容接入 xlog 后在 IDE 控制台日志输出。考虑到日志输出的扩展性,渠道应该按如下结构封装日志接口:
多端兼容
WebView
咱们的方针是规划一个全量日志体系,所以仅仅仅仅搜集原生渠道日志还远远不够。需求将日志搜集规模扩展到 Webview & Futter。将搜集到的日志一致经过 xlog 搜集并耐久化。
关于 Webview 能够经过向每个 Webview 目标注入 Javascript 代码的方法 Hook 掉 console 目标的方法完成。在 Hook 方法里先把日志发送到原生日志接口层,原生再将日志异步发送到 xlog 进行加密、紧缩、耐久化。
Flutter
关于 Flutter 就略微复杂一点,Flutter 端现在不支持 mmap (Dart 库函数约束),所以在 Flutter 端不存在和 xlog 相似的处理计划,只能凭借原生的能力来完成 Flutter 端高功能高牢靠的日志耐久化。前端同学都知道 Flutter 与原生进行通讯都是经过 Channel 的方法进行,但 Channel 因存在跨端数据编/解码操作导致通讯功率偏低,假如用 Channel 进行日志搜集势必会影响低端机流畅性。好在 Dart 供给一套调用 C/C++ 言语接口的处理计划:FFI, FFI 在 Flutter 2.0 之后的版别到达了生产可用。
FFI (Foreign Function Interface) 是用来与其它言语交互的接口,在有些言语里边称为言语绑定(language bindings),Java 里边一般称为 JNI(Java Native Interface) 或 JNA(Java Native Access)。因为现实中许多程序是由不同编程言语写的,必然会涉及到跨言语调用,比方 A 言语写的函数假如想在 B 言语里边调用,这时一般有两种处理计划:一种是将函数做成一个服务,经过进程间通讯(IPC)或网络协议通讯(RPC, RESTful等);另一种便是直接经过 FFI 调用。前者需求至少两个独立的进程才能完成,而后者直接将其它言语的接口内嵌到本言语中,所以调用功率比前者高。
Dart 经过 FFI 调用 xlog 供给的 C 接口比 Channel 的方法调用具有更高的功率,这个计划在抹平了 Dart 与 C/C++ 言语鸿沟的一起能够充分利分 xlog 供给的优势,防止重复造轮子、提高整个计划的开发功率与安稳性。
处理了 xlog 在 Flutter 侧的调用之后,Flutter 侧的日志搜集也应该选用原生渠道相似结构进行封装,终究依赖结构如下
Flutter FFI 集成 xlog
实操
参阅 mars 的官方指引进行静态库(iOS)、so (Android)编译。直接运转 build_ios.py/build_android.py 脚本即可。iOS 示例:
cd ~/Donwload/mars/
python build_ios.py
接下来挑选「Clean && build xlog.」 就可完成 iOS 静态库编译。可是要在 Flutter FFI 运用还有几个问题需求处理。
依据文档 在 iOS 中运用 dart:ffi 调用本地代码 FFI 只能与 C 函数绑定,也便是说 Dart 只能直接调用 C 函数。Dart 侧的类型只会转成 C 的根本类型(int char * stract 等),但 xlog 内部运用 C++ 目标完成装备,FFI 无法转化 C++ 目标。因此首先要处理的问题便是 C 根底类型与 C++ 目标之间的转化。
好在 XLogConfig 目标内部都是根底类型, 处理计划是在 C++ 函数上再包装一层 C 函数接口曝露给 FFI 运用。最直接的方法是把 XLogConfig 内的特点都拆到 C 函数参数内,但这样参数会过长,不太优雅。
终究我挑选将 XLogConfig 结构体内的 sdt::string 类型替换换为 char * 类型,由 Dart 直接请求内存结构 XLogConfig 结体实例,然后 xlog 内运用原 XLogConfig 内 C++ string 目标的方位用 std::string(config->logdir) 进行复原。
1、替换为 C 的根本类型
2、在原结构体特点引用地方复原为 C++ 目标
3、包装 C 函数 (需求留意这儿的 config 结构体是 Dart 侧 calloc 出来的,你需求在 Dart 侧动手 free 它)
4、Dart 侧调用(Dart 侧 FFI 调用代码运用 ffigen 生成)
你需求留意
Dart 类型转 C 类型
Dart 侧可运用 toNativeUtf8 函数将 Dart String 类型转化为 C char* 类型以供 C 侧访问,但这儿需求留意内存开释问题,看注释!
只看终究一句话,返回的指针指向由 allocator 额定分配的内存。
因此在运用完 toNativeUti8 函数后你需求留意他的开释问题。这儿 Dart/C 双侧都能够开释此块内存,谁担任开释呢?我的建议是「谁创建,谁开释」,意思是由 Dart 侧请求的内存,Dart 侧担任开释超出作用域的内存 。如遇到异步的 C 函数,C 函数内部再复制一份自行进行内存办理。
xlog日志约束
xlog 默许有单条 16K 巨细的约束,16K刚好为一个页存页,mmap 在进行内存请求时能削减内存碎片。你也能够修改源码来改动这个约束,但我仍然建议你在上层应用做好日志切断,将大日志拆分为多条日志写入。
功能比照
日志上传计划
经过一定手法获取在用户设备存储的日志,获取包括两层意思:
- 自动上传
- 被迫上传
被迫上传
是指经过后台向指定用户发送上传指令,指定用户接收到后再经过接口上传日志文件。因为日志文件往往较大,被迫上传需求用户具有较高的运用频次不然时效生难以确保。
日志被迫上传的要害是接收上传指含,也便是说开发者可依据自定义战略决哪些设备需求上传指定时刻规模的日志。这儿咱们能够依据现有的离线包或 App 灰度战略来指定设备或用户进行上传。
指令的接收用推拉结合的方法,利用 App 的推送能力发送静默奉告,App 收到静默奉告后会被唤醒,解析奉告带着的上报字符命令后再经过接口去查询当时设备是否需求上传日志以及上传的具体信息。整流程如下图所示:
被迫上传最核心的点在于查询上报信息,上报信息需求带上此上报的唯一标识,此标识用于在办理后台符号此次上传使命的状况。之所以需求使命状况是考虑到上报进程是一个长异步进程会存在中止的或许性所以需求一个唯一符号奉告办理后台使命成功的状况,假如没有成功,下次启动去查询时会返回相同的成果。依据此符号 App 端还能够康复上次未完成的上传使命。
自动上报
能处理时效性的问题,在产生可预见的反常或指定等级的反常产生后能够触发一次自动上传日志文件,在用户反应问题后能够直接拿到日志而不再需求下发指令等待上传。
自动上传是指依据实践需求决议是否需求将当时日志进行打包上传。需求自动上传的场景有:
- App 溃散
- 能够预见的严重事务过错
- 严厉的边界条件被触发
- 未被捕获的反常
假如能穷举出一切或许出问题的场景,便能够在收到任何反应后第一时刻拿到日志,但在实践项目中咱们不行能穷举出一切场景,那些不能被举列出的场景就能够用被迫上报来覆盖,在实践项目中被迫上报的运用频次会占大多数。这儿需求留意的是开发者切不行图省劲,不区别等级一股脑的将一切过错/反常都上传,这样不只会形成资源糟蹋还会导致严重过错因信息太多被忽视。自动上传的流程相关于被迫上传要简略得多,全体流程如下:
经过上面的流程发现因为不需求同步使命状况所以整个流程比较简略,App 端做好文件分片及继传后将成果上报给办理后台,后台办理体系只需搜集不同用户、不同设备日志下载地址。
总结
以上便是移动端全量日志系的全体规划思路,全体的规划思路解释的相比照较细。日志作为一个根底东西以往长时间被开发忽视,仅仅是会用远远不够还需求用好。充分发挥日志东西作为根底组件的职能,日志的重要性不只仅只在开发中表现,更重要的是服务产品的全生命周期。
后记
因为这篇水文所涉内容实践、成文、发布,时刻跨度比较大,本想开源根据 Dart FFI 桥接 xlog 源码但时至今日(2023-12-10零时)所涉技能已有过期的嫌疑就先不贴代码地址了。
假如你仍然十分需求本文涉及到的 Dart FFI 桥接 xlog 相关代码,能够在评论区留言奉告我,呼声较高的话我整理一下代码给大家开箱即用的版别。
终究 love&peace