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 等問題都能經過它來檢測。