Serie de optimización del rendimiento de Android-Matriz Tencent-Monitoreo de IO-Análisis de código fuente de IOCanaryPlugin

Autor: Qiu Quwuhen

El monitoreo de Matrix de io incluye cuatro aspectos

  1. Monitorear el problema de realizar operaciones IO en el hilo principal
  2. El búfer de monitoreo es un problema demasiado pequeño
  3. Monitorear lecturas repetidas del mismo archivo
  4. Supervisar problemas de pérdida de memoria

IOCanaryPlugin, la operación real la completa internamente IOCanaryCore.

método de inicio

Instale el gancho según la configuración.

//io流hook
if (ioConfig.isDetectFileIOInMainThread() || ioConfig.isDetectFileIOBufferTooSmall() || ioConfig.isDetectFileIORepeatReadSameFile()) {
    IOCanaryJniBridge.install(ioConfig, this);
}
//内存泄漏hook
if (ioConfig.isDetectIOClosableLeak()) {
    this.mCloseGuardHooker = new CloseGuardHooker(this);
    this.mCloseGuardHooker.hook();
}

método de parada

Cancelar gancho

if (this.mCloseGuardHooker != null) {
    this.mCloseGuardHooker.unHook();
}

IOCanaryJniBridge.uninstall();

IOCanaryJniBridge.install()

Hay varios pasos para instalar el gancho subyacente, cargarlo y configurar el contenido del gancho, que corresponden a los siguientes métodos.

cargarJni

System.loadLibrary("io-canary")

Después de ejecutar System.loadLibrary ("io-canary"), ingresará el método JNI_OnLoad en io_canary_jni.cc. Hay dos operaciones clave en este método: 1. Obtener información sobre la capa Java, 2. Establecer una interfaz de devolución de llamada para carga de información de seguimiento.

InitJniEnv()

static bool InitJniEnv(JavaVM *vm) {
    ....
    jclass temp_cls = env->FindClass("com/tencent/matrix/iocanary/core/IOCanaryJniBridge");
    ....
}

Establecer devolución de llamada emitida()

iocanary::IOCanary::Get().SetIssuedCallback(OnIssuePublish)

Entre ellos, OnIssuePublish reúne la información en el objeto IOIssue de la capa Java después de obtener la información y luego la coloca en la Lista. La información se genera llamando a onIssuePublish de la clase IOCanaryJniBridge de la capa Java.

com/tencent/matrix/iocanary/core/IOIssue

com/tencent/matrix/iocanary/core/IOCanaryJniBridge

habilitarDetector

Al pasar el tipo definido a la capa inferior, se implementa este tipo de monitoreo de io, el código es el siguiente

iocanary::IOCanary::Get().RegisterDetector(static_cast<DetectorType>(detector_type));

Puede ver que el Detector correspondiente finalmente se almacena en la colección de vectores detectores_, y cada Detector es una subclase de FileIODetector.

  • FileIOMainThreadDetector
  • FileIORepeatReadDetector
  • FileIOSmallBufferDetector
void IOCanary::RegisterDetector(DetectorType type) {
    switch (type) {
        case DetectorType::kDetectorMainThreadIO:
            detectors_.push_back(new FileIOMainThreadDetector());
            break;
        case DetectorType::kDetectorSmallBuffer:
            detectors_.push_back(new FileIOSmallBufferDetector());
            break;
        case DetectorType::kDetectorRepeatRead:
            detectors_.push_back(new FileIORepeatReadDetector());
            break;
        default:
            break;
    }
}

configurarConfiguración

Establezca el umbral de monitoreo para el monitoreo de io correspondiente y guárdelo en la matriz configs_. La configuración y los valores predeterminados correspondientes son los siguientes. Si se excede el umbral, se activará el monitoreo.

  • kMainThreadThreshold = 500 milisegundos
  • kSmallBufferThreshold = 4096 kb
  • kRepeatReadThreshold = 20 次

iocanary::IOCanary::Get().SetConfig(static_cast(clave), val);

void IOCanaryEnv::SetConfig(IOCanaryConfigKey key, long val) {
    if (key >= IOCanaryConfigKey::kConfigKeysLen) {
        return;
    }

    configs_[key] = val;
}

gancho

Dohook es el método principal. Una vez preparada la información de configuración anterior, el gancho del método correspondiente comienza aquí. El archivo enganchado es

const static char* TARGET_MODULES[] = {
    "libopenjdkjvm.so",
    "libjavacore.so",
    "libopenjdk.so"
};

Con respecto al gancho GOT, puede ver el marco de código abierto XHook de iQiyi, y los detalles no se describirán aquí.

Los métodos enlazados son los siguientes: open, open64, close, android_fdsan_close_with_tag. Si es así, libjavacore.so intentará enlazar sus métodos internos read, __read_chk, write, __write_chk.

abierto

Cuando se abre un archivo, se realiza una devolución de llamada al método establecido ProxyOpen, donde se detectará si es una operación del hilo principal. Si no, no se realizará ningún procesamiento. Si es el hilo principal, se ejecutará la lógica DoProxyOpenLogic. .

int ProxyOpen(const char *pathname, int flags, mode_t mode) {
    if(!IsMainThread()) {
        return original_open(pathname, flags, mode);
    }
    int ret = original_open(pathname, flags, mode);
    if (ret != -1) {
        DoProxyOpenLogic(pathname, flags, mode, ret);
    }
    return ret;
}

La información de la pila actual se obtendrá en el método DoProxyOpenLogic

static void DoProxyOpenLogic(const char *pathname, int flags, mode_t mode, int ret) {
    ....
    //kJavaBridgeClass = com/tencent/matrix/iocanary/core/IOCanaryJniBridge
    //kMethodIDGetJavaContext = getJavaContext() 得到的是一个JavaContext,
    //是一个内部类,这个类上有一个变量stack,在java Context 创建的时候,
    //就会获取到堆栈信息,保存在stack变量上
    jobject java_context_obj = env->CallStaticObjectMethod(kJavaBridgeClass, kMethodIDGetJavaContext);
    if (NULL == java_context_obj) {
        return;
    }
    //堆栈信息
    jstring j_stack = (jstring) env->GetObjectField(java_context_obj, kFieldIDStack);
    jstring j_thread_name = (jstring) env->GetObjectField(java_context_obj, kFieldIDThreadName);
    //当前线程名
    char* thread_name = jstringToChars(env, j_thread_name);
    char* stack = jstringToChars(env, j_stack);
    JavaContext java_context(GetCurrentThreadId(), thread_name == NULL ? "" : thread_name, stack == NULL ? "" : stack);
    ....
    //pathname是被打开的文件名,java_context中包含了堆栈和线程名
    //flags和mode都是系统open方法调用传过来的值,ret是open执行的结果
    //这里进入了IOCanary OnOpen方法
    iocanary::IOCanary::Get().OnOpen(pathname, flags, mode, ret, java_context);
    ....
}
abierto64

Mismo abierto()

cerca

La detección es el hilo principal, ingrese el método IOCanary OnClose

int ProxyClose(int fd) {
    if(!IsMainThread()) {
        return original_close(fd);
    }
    int ret = original_close(fd);
    iocanary::IOCanary::Get().OnClose(fd, ret);
    return ret;
}
android_fdsan_close_with_tag

Mismo cierre()

leer

El objetivo principal es obtener el tiempo consumido por la lectura y luego transportar la información a IOCanary OnRead.

ssize_t ProxyRead(int fd, void *buf, size_t size) {
    if(!IsMainThread()) {
        return original_read(fd, buf, size);
    }
    //获取到当前时间
    int64_t start = GetTickCountMicros();
    //执行原read方法
    size_t ret = original_read(fd, buf, size);
    //记录read时间间隔
    long read_cost_us = GetTickCountMicros() - start;
    //将信息传入IOCanary OnRead
    iocanary::IOCanary::Get().OnRead(fd, buf, size, ret, read_cost_us);
    return ret;
}
__read_chk

Misma lectura

escribir
ssize_t ProxyWrite(int fd, const void *buf, size_t size) {
    if(!IsMainThread()) {
        return original_write(fd, buf, size);
    }
    //获取到当前时间
    int64_t start = GetTickCountMicros();
    //执行write
    size_t ret = original_write(fd, buf, size);
    //记录时间间隔
    long write_cost_us = GetTickCountMicros() - start;
    //将信息传入IOCanary OnRead
    iocanary::IOCanary::Get().OnWrite(fd, buf, size, ret, write_cost_us);
    return ret;
}
__escribir_chk

lo mismo escribe

IOCanario

Se puede ver en el flujo del método de lectura y escritura de apertura y cierre anterior que finalmente se reúne en la clase IOCanary de C ++. Al ingresar al método correspondiente, se muestra que la clase IOInfoCollector se llama internamente en IOCanary. .

Al abrir
void IOCanary::OnOpen(const char *pathname, int flags, mode_t mode,
                      int open_ret, const JavaContext& java_context) {
    collector_.OnOpen(pathname, flags, mode, open_ret, java_context);
}

La lógica interna del método también es muy clara: el nombre del archivo y la información relacionada se ensamblan directamente en información, y luego el descriptor del archivo se usa como clave y la información se usa como valor y se almacena en info_map_ de C++ (a std:: unordered_map) La información debe almacenarse usando Sí, lo veremos más adelante. Una vez abierto el archivo, el siguiente paso es leer o escribir, así que continúe con el método de lectura.

void IOInfoCollector::OnOpen(const char *pathname, int flags, mode_t mode
        , int open_ret, const JavaContext& java_context) {
    if (open_ret == -1) {
        return;
    }
    //open_ret参数指的是open方法调用后的结果,也就是当前被打开的文件的文件描述符,
    //如果已存在,则返回
    if (info_map_.find(open_ret) != info_map_.end()) {
        return;
    }
    std::shared_ptr<IOInfo> info = std::make_shared<IOInfo>(pathname, java_context);
    info_map_.insert(std::make_pair(open_ret, info));
}
En lectura
void IOCanary::OnRead(int fd, const void *buf, size_t size,
                      ssize_t read_ret, long read_cost) {
    collector_.OnRead(fd, buf, size, read_ret, read_cost);
}

Parece que el contenido clave está en CountRWInfo. Se puede ver en el nombre del método que la lectura y la escritura están relacionadas con este método, por lo que no veremos primero el contenido del método CountRWInfo y luego entraremos en el método CountRWInfo. leyendo la escritura.

void IOInfoCollector::OnRead(int fd, const void *buf, size_t size,
                             ssize_t read_ret, long read_cost) {
    if (read_ret == -1 || read_cost < 0) {
        return;
    }
    if (info_map_.find(fd) == info_map_.end()) {
        return;
    }
    CountRWInfo(fd, FileOpType::kRead, size, read_cost);
}
En escritura
void IOCanary::OnWrite(int fd, const void *buf, size_t size,
                       ssize_t write_ret, long write_cost) {
    collector_.OnWrite(fd, buf, size, write_ret, write_cost);
}

Como leer, ingrese el método CountRWInfo

void IOInfoCollector::OnWrite(int fd, const void *buf, size_t size,
                              ssize_t write_ret, long write_cost) {
    if (write_ret == -1 || write_cost < 0) {
        return;
    }
    if (info_map_.find(fd) == info_map_.end()) {
        return;
    }
    CountRWInfo(fd, FileOpType::kWrite, size, write_cost);
}
ContarRWInfo

CountRWInfo encapsula la información correspondiente a cada archivo en la clase IOInfo, el paquete de información encapsulada es:

  • Leer (escribir) veces
  • Tamaño del archivo
  • Tiempo dedicado a leer (escribir)
  • Duración máxima de una sola lectura (escritura)
  • El tiempo total entre intervalos de lectura (escritura) es inferior a 8000 microsegundos.
  • Tamaño del búfer
  • Leer y escribir escribir, leer o escribir

Durante el proceso de lectura y escritura de un archivo, se llamará continuamente a este método y se actualizará la información correspondiente, una vez completada la lectura y escritura se obtendrá la información final y se ejecutará el método de cierre.

void IOInfoCollector::CountRWInfo(int fd, const FileOpType &fileOpType, long op_size, long rw_cost) {
    if (info_map_.find(fd) == info_map_.end()) {
        return;
    }

    const int64_t now = GetSysTimeMicros();
    //读写次数
    info_map_[fd]->op_cnt_ ++;
    //文件大小
    info_map_[fd]->op_size_ += op_size;
    //读写消耗的时长
    info_map_[fd]->rw_cost_us_ += rw_cost;
    //单次读写最大时长
    if (rw_cost > info_map_[fd]->max_once_rw_cost_time_μs_) {
        info_map_[fd]->max_once_rw_cost_time_μs_ = rw_cost;
    }
    //读写间隔小于8000微妙的总时长
    if (info_map_[fd]->last_rw_time_μs_ > 0 && (now - info_map_[fd]->last_rw_time_μs_) < kContinualThreshold) {
        info_map_[fd]->current_continual_rw_time_μs_ += rw_cost;

    } else {
        info_map_[fd]->current_continual_rw_time_μs_ = rw_cost;
    }
    if (info_map_[fd]->current_continual_rw_time_μs_ > info_map_[fd]->max_continual_rw_cost_time_μs_) {
        info_map_[fd]->max_continual_rw_cost_time_μs_ = info_map_[fd]->current_continual_rw_time_μs_;
    }
    info_map_[fd]->last_rw_time_μs_ = now;
    //缓存区大小
    if (info_map_[fd]->buffer_size_ < op_size) {
        info_map_[fd]->buffer_size_ = op_size;
    }
    //读写类型,读还是写
    if (info_map_[fd]->op_type_ == FileOpType::kInit) {
        info_map_[fd]->op_type_ = fileOpType;
    }
}
Al cerrar
void IOCanary::OnClose(int fd, int close_ret) {
    std::shared_ptr<IOInfo> info = collector_.OnClose(fd, close_ret);
    if (info == nullptr) {
        return;
    }

    OfferFileIOInfo(info);
}

Al cerrar, registre la duración total y el tamaño del archivo, y luego regrese. Después de regresar, ingrese el método OfferFileIOInfo.

std::shared_ptr<IOInfo> IOInfoCollector::OnClose(int fd, int close_ret) {
    if (info_map_.find(fd) == info_map_.end()) {
        return nullptr;
    }
    //从打开到关闭的总时长
    info_map_[fd]->total_cost_μs_ = GetSysTimeMicros() - info_map_[fd]->start_time_μs_;
    //获取到文件大小
    info_map_[fd]->file_size_ = GetFileSize(info_map_[fd]->path_.c_str());
    std::shared_ptr<IOInfo> info = info_map_[fd];
    //从map中移除
    info_map_.erase(fd);
    //返回信息
    return info;
}

OfferFileIOInfo coloca información en la cola y llama al método notify_one para notificar al consumidor sobre el consumo. Aquí se utiliza el modelo de consumo de producción. El productor pone las frutas de producción en la cola y el consumidor las saca de la cola para su consumo. Encontremos dónde está el consumidor.

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

Como puede ver, cuando se creó IOCanary, se inició un hilo.

IOCanary::IOCanary() {
    exit_ = false;
    std::thread detect_thread(&IOCanary::Detect, this);
    detect_thread.detach();
}

Hay un bucle infinito en el hilo, que se encarga de obtener constantemente información de la cola, si la cola está vacía, el hilo se suspende y espera.

Vimos anteriormente que después de obtener una información, la colocamos en la cola y luego notificamos al consumidor para que la consuma. En este momento, el hilo del consumidor se despertará del método TakeFileIOInfo y obtendrá una información, que ser entregado a cada detector para su detección.

Una vez completada la detección, la información que cumple con las condiciones se colocará en Published_issues y luego Issued_callback_ devolverá la llamada a la información. Como se mencionó anteriormente, hay tres detectores, echemos un vistazo más de cerca a su lógica interna.

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);
        if (ret != 0) {
            break;
        }
        for (auto detector : detectors_) {
            detector->Detect(env_, *file_io_info, published_issues);
        }
        if (issued_callback_ && !published_issues.empty()) {
            issued_callback_(published_issues);
        }
        file_io_info = nullptr;
    }
}

FileIOMainThreadDetector

Detectar hilo principal io

void FileIOMainThreadDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info,
                                      std::vector<Issue>& issues) {
    //必须是主线程才会执行
    if (GetMainThreadId() == file_io_info.java_context_.thread_id_) {
        int type = 0;
        //单次io时长超过13毫秒,要记录
        //constexpr static const int kPossibleNegativeThreshold = 13*1000;
        if (file_io_info.max_once_rw_cost_time_μs_ > IOCanaryEnv::kPossibleNegativeThreshold) {
            type = 1;
        }
        //最大连续读写时长超过env.GetMainThreadThreshold()=500
        if(file_io_info.max_continual_rw_cost_time_μs_ > env.GetMainThreadThreshold()) {
            type |= 2;
        }
        if (type != 0) {
            Issue issue(kType, file_io_info);
            issue.repeat_read_cnt_ = type; 
            //存入
            PublishIssue(issue, issues);
        }
    }
}

FileIORepeatReadDetector

Escuche la lectura repetida del mismo archivo.


void FileIORepeatReadDetector::Detect(const IOCanaryEnv &env,
                                      const IOInfo &file_io_info,
                                      std::vector<Issue>& issues) {
    const std::string& path = file_io_info.path_;
    if (observing_map_.find(path) == observing_map_.end()) {
        if (file_io_info.max_continual_rw_cost_time_μs_ < env.kPossibleNegativeThreshold) {
            return;
        }

        observing_map_.insert(std::make_pair(path, std::vector<RepeatReadInfo>()));
    }
    std::vector<RepeatReadInfo>& repeat_infos = observing_map_[path];
    if (file_io_info.op_type_ == FileOpType::kWrite) {
        repeat_infos.clear();
        return;
    }

    RepeatReadInfo repeat_read_info(file_io_info.path_, file_io_info.java_context_.stack_, file_io_info.java_context_.thread_id_,
                                  file_io_info.op_size_, file_io_info.file_size_);
    if (repeat_infos.size() == 0) {
        repeat_infos.push_back(repeat_read_info);
        return;
    }
    if((GetTickCount() - repeat_infos[repeat_infos.size() - 1].op_timems) > 17) {   //17ms todo astrozhou add to params
        repeat_infos.clear();
    }
    bool found = false;
    int repeatCnt;
    for (auto& info : repeat_infos) {
        if (info == repeat_read_info) {
            found = true;

            info.IncRepeatReadCount();

            repeatCnt = info.GetRepeatReadCount();
            break;
        }
    }
    if (!found) {
        repeat_infos.push_back(repeat_read_info);
        return;
    }
    if (repeatCnt >= env.GetRepeatReadThreshold()) {
        Issue issue(kType, file_io_info);
        issue.repeat_read_cnt_ = repeatCnt;
        issue.stack = repeat_read_info.GetStack();
        PublishIssue(issue, issues);
    }
}

FileIOSmallBufferDetector

El buffer de escucha es demasiado pequeño

void FileIOSmallBufferDetector::Detect(const IOCanaryEnv &env, const IOInfo &file_io_info,
                                       std::vector<Issue>& issues) {
    if (file_io_info.op_cnt_ > env.kSmallBufferOpTimesThreshold && (file_io_info.op_size_ / file_io_info.op_cnt_) < env.GetSmallBufferThreshold()
            && file_io_info.max_continual_rw_cost_time_μs_ >= env.kPossibleNegativeThreshold) {
        PublishIssue(Issue(kType, file_io_info), issues);
    }
}

OnIssuePublicar

Una vez obtenida toda la información, comienza la devolución de llamada, lo que nos lleva de regreso al iocanary::IOCanary::Get().SetIssuedCallback(OnIssuePublish) con el que comenzamos.

void OnIssuePublish(const std::vector<Issue>& published_issues) {
    ....
    //这里new了一个Java层的List
    jobject j_issues = env->NewObject(kListClass, kMethodIDListConstruct);
    //遍历所有的info,拿到信息,每一条信息创建一个Java层的IOIssue对象,封装到这个对象中

    for (const auto& issue : published_issues) {
        jint type = issue.type_;
        jstring path = env->NewStringUTF(issue.file_io_info_.path_.c_str());
        jlong file_size = issue.file_io_info_.file_size_;
        jint op_cnt = issue.file_io_info_.op_cnt_;
        jlong buffer_size = issue.file_io_info_.buffer_size_;
        jlong op_cost_time = issue.file_io_info_.rw_cost_us_/1000;
        jint op_type = issue.file_io_info_.op_type_;
        jlong op_size = issue.file_io_info_.op_size_;
        jstring thread_name = env->NewStringUTF(issue.file_io_info_.java_context_.thread_name_.c_str());
        jstring stack = env->NewStringUTF(issue.stack.c_str());
        jint repeat_read_cnt = issue.repeat_read_cnt_;

        jobject issue_obj = env->NewObject(kIssueClass, kMethodIDIssueConstruct, type, path, file_size, op_cnt, buffer_size,
                                           op_cost_time, op_type, op_size, thread_name, stack, repeat_read_cnt);
        //讲IOIssue对象add到List中
        env->CallBooleanMethod(j_issues, kMethodIDListAdd, issue_obj);
        ....
    }
    //回调到Java层的IOCanaryJniBridge类中的静态方法onIssuePublish中
    env->CallStaticVoidMethod(kJavaBridgeClass, kMethodIDOnIssuePublish, j_issues);
    ....
}

Más tarde, en la capa Java onIssuePublish, la información de empalme comienza a convertirse a json y se imprime en la consola o se carga en el servidor, en este punto el proceso finaliza.

Resumir

IOCanaryPlugin implementa la interceptación de operaciones io conectando los métodos io subyacentes de apertura, lectura, escritura y cierre, de modo que todas las operaciones io sean monitoreadas, de modo que la información de la operación se pueda registrar durante cada operación io y se pueda analizar io. la operación supera el umbral establecido, y si se cumplen las condiciones, se informará.

notas de estudio de Android

Artículo de optimización del rendimiento de Android: https://qr18.cn/FVlo89
Artículo de principios subyacentes del marco de trabajo de Android: https://qr18.cn/AQpN4J
Artículo de vehículo de Android: https://qr18.cn/F05ZCM
Notas de estudio de seguridad inversa de Android: https://qr18.cn/CQ5TcL
Artículo de audio y video de Android: https://qr18.cn/Ei3VPD
Artículo del grupo de la familia Jetpack (incluido Compose): https://qr18.cn/A0gajp
Notas de análisis del código fuente de OkHttp: https://qr18.cn/Cw0pBD
Artículo de Kotlin: Artículo https://qr18.cn/CdjtAF
de Gradle: Artículo de https://qr18.cn/DzrmMB
Flutter : https://qr18.cn/DIvKma
Ocho cuerpos de conocimiento de Android: https://qr18.cn/CyxarU
Notas principales de Android: https://qr21.cn/CaZQLo
Preguntas de la entrevista de Android de años anteriores: https://qr18.cn/CKV8OZ
Las últimas preguntas de la entrevista de Android en 2023: https://qr18.cn/CgxrRy
Ejercicios de entrevista para el puesto de desarrollo de vehículos de Android: https://qr18.cn/FTlyCJ
Preguntas de la entrevista en audio y video:https://qr18.cn/AcV6Ap

Supongo que te gusta

Origin blog.csdn.net/weixin_61845324/article/details/132811641
Recomendado
Clasificación