Matrix 中用于 I/O 监控的模块是 IOCanary,它是一个在开发、测试或者灰度阶段辅助发现 I/O 问题的工具,目前主要包括文件 I/O 监控和 Closeable Leak 监控两部分。java
具体的问题类型有 4 种:android
IOCanary 采用 hook(ELF hook) 的方案收集 IO 信息,代码无侵入,从而使得开发者能够无感知接入。配置并启动 IOCanaryPlugin 便可:算法
IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder()
.dynamicConfig(dynamicConfig)
.build());
builder.plugin(ioCanaryPlugin);
复制代码
与 IO 相关的配置选项有:数组
enum ExptEnum {
// 监测在主线程执行 IO 操做的问题
clicfg_matrix_io_file_io_main_thread_enable,
clicfg_matrix_io_main_thread_enable_threshold, // 读写耗时
// 监测缓冲区太小的问题
clicfg_matrix_io_small_buffer_enable,
clicfg_matrix_io_small_buffer_threshold, // 最小 buffer size
clicfg_matrix_io_small_buffer_operator_times, // 读写次数
// 监测重复读同一文件的问题
clicfg_matrix_io_repeated_read_enable,
clicfg_matrix_io_repeated_read_threshold, // 重复读次数
// 监测内存泄漏问题
clicfg_matrix_io_closeable_leak_enable,
}
复制代码
出现资源泄漏(好比未关闭读写流)时,报告信息示例以下:markdown
{
"tag": "io",
"type": 4,
"process": "sample.tencent.matrix",
"time": 1590410170122,
"stack": "sample.tencent.matrix.io.TestIOActivity.leakSth(TestIOActivity.java:190)\nsample.tencent.matrix.io.TestIOActivity.onClick(TestIOActivity.java:103)\njava.lang.reflect.Method.invoke(Native Method)\nandroid.view.View$DeclaredOnClickListener.onClick(View.java:4461)\nandroid.view.View.performClick(View.java:5212)\nandroid.view.View$PerformClick.run(View.java:21214)\nandroid.app.ActivityThread.main(ActivityThread.java:5619)\njava.lang.reflect.Method.invoke(Native Method)\ncom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:853)\ncom.android.internal.os.ZygoteInit.main(ZygoteInit.java:737)\n",
}
复制代码
写入太多、缓冲区过小的报告示例以下:app
{
"tag": "io",
"type": 2, // 问题类型
"process": "sample.tencent.matrix",
"time": 1590409786187,
"path": "/sdcard/a_long.txt", // 文件路径
"size": 40960000, // 文件大小
"op": 80000, // 读写次数
"buffer": 512, // 缓冲区大小
"cost": 1453, // 耗时
"opType": 2, // 1 读 2 写
"opSize": 40960000, // 读写总内存
"thread": "main",
"stack": "sample.tencent.matrix.io.TestIOActivity.writeLongSth(TestIOActivity.java:129)\nsample.tencent.matrix.io.TestIOActivity.onClick(TestIOActivity.java:99)\njava.lang.reflect.Method.invoke(Native Method)\nandroid.view.View$DeclaredOnClickListener.onClick(View.java:4461)\nandroid.view.View.performClick(View.java:5212)\nandroid.view.View$PerformClick.run(View.java:21214)\nandroid.app.ActivityThread.main(ActivityThread.java:5619)\njava.lang.reflect.Method.invoke(Native Method)\ncom.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:853)\ncom.android.internal.os.ZygoteInit.main(ZygoteInit.java:737)\n",
"repeat": 0 // 重复读次数
}
复制代码
须要注意的是,字段 repeat 在主线程 IO 事件中有不一样的含义:"1" 表示单次读写耗时过长;"2" 表示连续读写耗时过长(大于配置指定值);"3" 表示前面两个问题都存在。框架
IOCanary 将收集应用的全部文件 I/O 信息并进行相关统计,再依据必定的算法规则进行检测,发现问题后再上报到 Matrix 后台进行分析展现。流程图以下:jvm
IOCanary 基于 xHook 收集 IO 信息,主要 hook 了 os posix 的四个关键的文件操做接口:函数
int open(const char *pathname, int flags, mode_t mode); // 成功时返回值就是 fd
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size);
int close(int fd);
复制代码
以 open 为例,追根溯源,能够发现 open 函数最终是 libjavacore.so 执行的,所以 hook libjavacore.so 便可,找到 hook 目标 so 的目的是把 hook 的影响范围尽量地降到最小。不一样的 Android 版本可能会有些不一样,目前兼容到 Android P。工具
另外,不一样于其它 IO 事件,对于资源泄漏监控,Android 自己就支持了该功能,这是基于工具类 dalvik.system.CloseGuard 来实现的,所以在 Java 层经过反射 hook 相关 API 便可实现资源泄漏监控。
想要了解 hook 技术,首先须要了解动态连接,了解动态连接以前,又须要从静态连接提及。
静态连接可让开发者们相对独立地开发本身的程序模块,最后再连接到一块儿,但静态连接也存在浪费内存和磁盘更新、更新困难等问题。好比 program1 和 program2 都依赖 Lib.o 模块,那么,最终连接到可执行文件中的 Lib.o 模块将会有两份,极大地浪费了内存空间。同时,一旦程序中有任何模块更新,整个程序就要从新连接、发布给用户。
所以,要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,造成独立的文件,而再也不将它们静态地连接在一块儿。也就是说,要在程序运行时进行连接,这就是动态连接的基本思想。
虽然动态连接带来了不少优化,但也带来了一个新的问题:共享对象在装载时,如何肯定它在进程虚拟地址空间中的位置?
解决思路是把指令中那些须要修改的部分分离出来,和数据部分放在一块儿。
对于模块内部的数据访问、函数调用,由于它们之间的相对位置是固定的,所以这些指令不须要重定位。
对于模块外部的数据访问、函数调用,基本思想就是把地址相关的部分放到数据段里面,创建一个指向这些变量的指针数组,这个数据也被称为全局偏移表(Global Offset Table,GOT)。连接器在装载模块的时候会查找每一个变量所在的地址,而后填充 GOT 中的各个项,以确保每一个指针指向的地址正确。
但 GOT 也带来了新的问题——性能损失,动态连接比静态连接慢的主要缘由就是动态连接对于全局和静态的数据访问都要进行复杂的 GOT 定位,而后间接寻址。
对于这个问题,在一个程序运行过程当中,可能不少函数直到程序执行完毕都不会被用到,好比一些错误处理函数等,若是一开始就把全部函数都连接好其实是一种浪费,因此 ELF 采用了延迟绑定的方法,基本思想是当函数第一次被用到时才由动态连接器来进行绑定(符号查找、重定位等)。延迟绑定对应的就是 PLT(Procedure Linkage Table) 段。也就是说,ELF 在 GOT 之上又增长了一层间接跳转。
所以,所谓 hook 技术,实际上就是修改 PLT/GOT 表中的内容。
IOCanary 的源码结构是很清晰的,流程大体以下:
IOCanary 的 hook 目标 so 文件包括 libopenjdkjvm.so、libjavacore.so、libopenjdk.so,每一个 so 文件的 open 和 close 函数都会被 hook,若是是 libjavacore.so,read 和 write 函数也会被 hook。源码以下所示,
const static char* TARGET_MODULES[] = {
"libopenjdkjvm.so",
"libjavacore.so",
"libopenjdk.so"
};
const static size_t TARGET_MODULE_COUNT = sizeof(TARGET_MODULES) / sizeof(char*);
JNIEXPORT jboolean JNICALL
Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
const char* so_name = TARGET_MODULES[i];
void* soinfo = xhook_elf_open(so_name);
// 将目标函数替换为本身的实现
xhook_hook_symbol(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
xhook_hook_symbol(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
if (is_libjavacore) {
xhook_hook_symbol(soinfo, "read", (void*)ProxyRead, (void**)&original_read);
xhook_hook_symbol(soinfo, "__read_chk", (void*)ProxyReadChk, (void**)&original_read_chk);
xhook_hook_symbol(soinfo, "write", (void*)ProxyWrite, (void**)&original_write);
xhook_hook_symbol(soinfo, "__write_chk", (void*)ProxyWriteChk, (void**)&original_write_chk);
}
xhook_hook_symbol(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
xhook_elf_close(soinfo);
}
}
复制代码
为了分析是否出现主线程 IO、缓冲区太小、重复读同一文件等问题,首先须要对每一次的 IO 操做进行统计,记录 IO 耗时、操做次数、缓冲区大小等信息。
这些信息最终都会由 Collector 保存,为此,在执行 open 操做时,须要建立一个 IOInfo,并保存到 map 里面,key 为文件句柄:
int ProxyOpen(const char *pathname, int flags, mode_t mode) {
int ret = original_open(pathname, flags, mode);
if (ret != -1) {
DoProxyOpenLogic(pathname, flags, mode, ret);
}
return ret;
}
static void DoProxyOpenLogic(const char *pathname, int flags, mode_t mode, int ret) {
... // 经过 Java 层的 IOCanaryJniBridge 获取 JavaContext
iocanary::IOCanary::Get().OnOpen(pathname, flags, mode, ret, java_context);
}
void IOCanary::OnOpen(...) {
collector_.OnOpen(pathname, flags, mode, open_ret, java_context);
}
void IOInfoCollector::OnOpen(...) {
std::shared_ptr<IOInfo> info = std::make_shared<IOInfo>(pathname, java_context);
info_map_.insert(std::make_pair(open_ret, info));
}
复制代码
接着,在执行 read/write 操做时,更新 IOInfo 的信息:
void IOInfoCollector::OnWrite(...) {
CountRWInfo(fd, FileOpType::kWrite, size, write_cost);
}
void IOInfoCollector::CountRWInfo(int fd, const FileOpType &fileOpType, long op_size, long rw_cost) {
info_map_[fd]->op_cnt_ ++;
info_map_[fd]->op_size_ += op_size;
info_map_[fd]->rw_cost_us_ += rw_cost;
...
}
复制代码
最后,在执行 close 操做时,将 IOInfo 插入到队列中:
void IOCanary::OnClose(int fd, int close_ret) {
std::shared_ptr<IOInfo> info = collector_.OnClose(fd, close_ret);
OfferFileIOInfo(info);
}
void IOCanary::OfferFileIOInfo(std::shared_ptr<IOInfo> file_io_info) {
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_.push_back(file_io_info); // 将数据保存到队列中
queue_cv_.notify_one(); // 唤醒后台线程,队列有新的数据了
lock.unlock();
}
复制代码
后台线程被唤醒后,首先会从队列中获取一个 IOInfo:
int IOCanary::TakeFileIOInfo(std::shared_ptr<IOInfo> &file_io_info) {
std::unique_lock<std::mutex> lock(queue_mutex_);
while (queue_.empty()) {
queue_cv_.wait(lock);
}
file_io_info = queue_.front();
queue_.pop_front();
return 0;
}
复制代码
接着,将 IOInfo 传给全部已注册的 Detector,Detector 返回 Issue 后再回调上层 Java 接口,上报问题:
void IOCanary::Detect() {
std::vector<Issue> published_issues;
std::shared_ptr<IOInfo> file_io_info;
while (true) {
published_issues.clear();
int ret = TakeFileIOInfo(file_io_info);
for (auto detector : detectors_) {
detector->Detect(env_, *file_io_info, published_issues); // 检查该 IO 事件是否存在问题
}
if (issued_callback_ && !published_issues.empty()) { // 若是存在问题
issued_callback_(published_issues); // 回调上层 Java 接口并上报
}
}
}
复制代码
以 small_buffer_detector 为例,若是 IOInfo 的 buffer_size_ 字段大于选项给定的值就上报问题:
void FileIOSmallBufferDetector::Detect(...) {
if (file_io_info.op_cnt_ > env.kSmallBufferOpTimesThreshold // 连续读写次数
&& (file_io_info.op_size_ / file_io_info.op_cnt_) < env.GetSmallBufferThreshold() // buffer size
&& file_io_info.max_continual_rw_cost_time_μs_ >= env.kPossibleNegativeThreshold) /* 连续读写耗时 */ {
PublishIssue(Issue(kType, file_io_info), issues);
}
}
复制代码
Android framework 已实现了资源泄漏监控的功能,它是基于工具类 dalvik.system.CloseGuard 来实现的。以 FileInputStream 为例,在 GC 准备回收 FileInputStream 时,会调用 guard.warnIfOpen 来检测是否关闭了 IO 流:
public class FileInputStream extends InputStream {
private final CloseGuard guard = CloseGuard.get();
public FileInputStream(File file) {
...
guard.open("close");
}
public void close() {
guard.close();
}
protected void finalize() throws IOException {
if (guard != null) {
guard.warnIfOpen();
}
}
}
复制代码
CloseGuard 的部分源码以下:
final class CloseGuard {
public void warnIfOpen() {
REPORTER.report(message, allocationSite);
}
}
复制代码
能够看到,执行 warnIfOpen 时若是未关闭 IO 流,就调用 REPORTER 的 report 方法。
所以,利用反射把 REPORTER 换成本身的就好了:
public final class CloseGuardHooker {
private boolean tryHook() {
Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard");
Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter");
Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter");
Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls);
Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class);
sOriginalReporter = methodGetReporter.invoke(null);
methodSetEnabled.invoke(null, true);
ClassLoader classLoader = closeGuardReporterCls.getClassLoader();
methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader,
new Class<?>[]{closeGuardReporterCls},
new IOCloseLeakDetector(issueListener, sOriginalReporter)));
}
}
复制代码
framework 不少代码都用了 CloseGuard ,所以,诸如文件资源没 close、Cursor 没有 close 等问题都能经过它来检测。
IOCanary 是一个在开发、测试或者灰度阶段辅助发现 I/O 问题的工具,目前主要包括文件 I/O 监控和 Closeable Leak 监控两部分。具体的问题类型有 4 种:
基于 xHook,IOCanary 将收集应用的全部文件 I/O 信息并进行相关统计,再依据必定的算法规则进行检测,发现问题后再上报到 Matrix 后台进行分析展现。
流程以下:
不一样于其它 IO 事件,对于资源泄漏监控,Android 自己就支持了该功能,这是基于工具类 dalvik.system.CloseGuard 来实现的,所以在 Java 层经过反射 hook CloseGuard 便可实现资源泄漏监控。由于 Android 框架层不少代码都用了 CloseGuard ,所以,诸如文件资源没 close、Cursor 没有 close 等问题都能经过它来检测。