如何应对Android 面试官 -> 内存如何进行优化(下)?玩转 Koom

前言


在这里插入图片描述

本章我们继续进行上一章未完的进行讲解;

线上内存监测方案思路优化


  • Activity 在执行销毁的时候,我们如何得知?
  • 如何判断一个 Activity 无法被 GC 回收?

上一章我们讲了线上内存如何监测的思路,主要是借助 ActivityLifeCycleCallbacks + WeakHashMap 但是在和方案也是有一些弊端的,例如:需要我们在 onDestory 的回调中手动的触发 GC,但是 GC 又比较耗费资源,就有会导致我们的项目卡顿;

所以,我们在触发 GC 的时候,应该给予一个阈值,当达到这个阈值之后才触发 GC;

常见内存泄漏原因

原因:长生命周期对象持有短生命周期对象,导致短生命周期对象在不再需要时无法被 GC 回收。

  • 静态变量或单例导致的内存泄漏(静态变量或者单例不要持有 activity 或 view 等对象的引用,如果必须持有可以改为 WeakReference);
  • 内部类导致的内存泄漏(内部类会持有外部类的引用,使用静态内部类实现,静态内部类相当于普通类,不会持有对外部类的引用);
  • 匿名内部类同样会持有对外部类对象的引用(经典的 Handler 问题,在销毁 Activity 的时候调用 handler.removeCallbacksAndMessages(); 考虑是否可以使用静态内部类实现,静态内部类相当于普通类,不会持有外部类的引用;当业务确实需要调用 Activity 的方法时,使用 WeakReference);
  • 动画问题(Activity 销毁的时候,未调用动画的 cancel 方法);
  • InputStream、OutputStream、Cursor、File文件等没有 Close;

Native 层内存泄漏监测原理


  • hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据「大小、堆栈、地址等」
  • 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息「地址、大小」
  • 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据「不可达内存块地址、大小、分配堆栈等」

Koom 源码浅析


我们来分析下 Koom 是如何监测 native、java 层泄漏的;

Native 层泄漏监测

Koom 的 native 层泄漏监测模块是 koom-native-leak
在这里插入图片描述

Java 层其实没什么逻辑,一个是配置相关信息,一个是对 native 层的调用,主要集中在 LeakMonitor 和 LeakMonitorConfig;

LeakMonitor 主要是 init 初始化 和 startLoop 开启监测、以及 checkLeaks 监测泄漏并上报;

真正的监测逻辑在 jni 层,我们来详细看下 jni 层,是如何实现 native 层的监测的;

我们先进入 CMakeList.txt 中看下:

cmake_minimum_required(VERSION 3.6)

set(TARGET koom-native)
set(THIRD_PARTY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../koom-common/third-party)
set(KWAI_ANDROID_BASE_DIR ${CMAKE_SOURCE_DIR}/../../../../koom-common/kwai-android-base)

add_compile_options(-Oz)

project(${TARGET})

include_directories(
        ${CMAKE_CURRENT_SOURCE_DIR}/include/
        ${KWAI_ANDROID_BASE_DIR}/src/main/cpp/include/
        ${KWAI_ANDROID_BASE_DIR}/src/main/cpp/liblog/include/
        ${THIRD_PARTY_DIR}/xhook/src/main/cpp/xhook/src/
        )

link_directories(
        ${KWAI_ANDROID_BASE_DIR}/src/main/libs/${ANDROID_ABI}/
        ${THIRD_PARTY_DIR}/xhook/src/main/libs/${ANDROID_ABI}/
)

add_library( # Sets the name of the library.
        ${TARGET}

        SHARED

        src/memory_map.cpp
        src/jni_leak_monitor.cpp
        src/leak_monitor.cpp
        src/memory_analyzer.cpp
        src/utils/hook_helper.cpp
        src/utils/stack_trace.cpp
        )

find_library( # Sets the name of the path variable.
        log-lib
        log)

target_link_libraries( # Specifies the target library.
        ${TARGET}
        xhook_lib
        kwai-android-base
        ${log-lib})

可以看到,Koom 中借助了爱奇艺开源的 xhook 框架,用来 hook native 层的逻辑,xhook 感兴趣的可以 github 上搜索一下,这里主要讲内存监测,不做过多概述;

如果想知道 so 中有哪些函数,可以借助 ida 来查看一个 so 中有哪些函数;

我们接着看下 jni_leak_monitor.cpp

static const char *kLeakMonitorFullyName =
    "com/kwai/koom/nativeoom/leakmonitor/LeakMonitor";
static const char *kLeakRecordFullyName =
    "com/kwai/koom/nativeoom/leakmonitor/LeakRecord";
static const char *kFrameInfoFullyName =
    "com/kwai/koom/nativeoom/leakmonitor/FrameInfo";

java 层定义的三个类;

static void Clean(JNIEnv *env) {
  if (g_leak_record.global_ref) {
    // 删除全局引用
    env->DeleteGlobalRef(g_leak_record.global_ref);
    // 清除内存空间
    memset(&g_leak_record, 0, sizeof(g_leak_record));
  }
  if (g_frame_info.global_ref) {
    env->DeleteGlobalRef(g_frame_info.global_ref);
    memset(&g_frame_info, 0, sizeof(g_frame_info));
  }
}

Clean 函数主要是用来清除内存空间的;

而如何做监听,是通过 UninstallMonitor 和 InstallMonitor 来实现的

static void UninstallMonitor(JNIEnv *env, jclass) {
  LeakMonitor::GetInstance().Uninstall();
  g_memory_map.~MemoryMap();
  Clean(env);
}

InstallMonitor

static bool InstallMonitor(JNIEnv *env, jclass clz, jobjectArray selected_array,
                           jobjectArray ignore_array,
                           jboolean enable_local_symbolic) {
  jclass leak_record;
  FIND_CLASS(leak_record, kLeakRecordFullyName);
  g_leak_record.global_ref =
      reinterpret_cast<jclass>(env->NewGlobalRef(leak_record));
  if (!CheckedClean(env, g_leak_record.global_ref)) {
    return false;
  }
  GET_METHOD_ID(g_leak_record.construct_method, leak_record, "<init>",
                "(JILjava/lang/String;[Lcom/kwai/koom/nativeoom/leakmonitor/"
                "FrameInfo;)V");

  jclass frame_info;
  FIND_CLASS(frame_info, kFrameInfoFullyName);
  g_frame_info.global_ref =
      reinterpret_cast<jclass>(env->NewGlobalRef(frame_info));
  if (!CheckedClean(env, g_frame_info.global_ref)) {
    return false;
  }
  GET_METHOD_ID(g_frame_info.construct_method, frame_info, "<init>",
                "(JLjava/lang/String;)V");

  g_enable_local_symbolic = enable_local_symbolic;

  auto array_to_vector =
      [](JNIEnv *env, jobjectArray jobject_array) -> std::vector<std::string> {
    std::vector<std::string> ret;
    int length = env->GetArrayLength(jobject_array);

    if (length <= 0) {
      return ret;
    }

    for (jsize i = 0; i < length; i++) {
      auto str = reinterpret_cast<jstring>(
          env->GetObjectArrayElement(jobject_array, i));
      const char *data = env->GetStringUTFChars(str, nullptr);
      ret.emplace_back(data);
      env->ReleaseStringUTFChars(str, data);
    }

    return std::move(ret);
  };

  std::vector<std::string> selected_so = array_to_vector(env, selected_array);
  std::vector<std::string> ignore_so = array_to_vector(env, ignore_array);
  // 最终调用到的是 CheckedClean 函数
  return CheckedClean(
      env, LeakMonitor::GetInstance().Install(&selected_so, &ignore_so));
}

我们接着往下看 GetLeakAllocs 函数

static void GetLeakAllocs(JNIEnv *env, jclass, jobject leak_record_map) {
  ScopedLocalRef<jclass> map_class(env, env->GetObjectClass(leak_record_map));
  jmethodID put_method;
  GET_METHOD_ID(put_method, map_class.get(), "put",
                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
  std::vector<std::shared_ptr<AllocRecord>> leak_allocs =
      LeakMonitor::GetInstance().GetLeakAllocs();

  for (auto &leak_alloc : leak_allocs) {
    if (leak_alloc->num_backtraces <= kNumDropFrame) {
      continue;
    }

    leak_alloc->num_backtraces -= kNumDropFrame;
    std::vector<std::pair<jlong, std::string>> frames;
    for (int i = 0; i < leak_alloc->num_backtraces; i++) {
      uintptr_t offset;
      // 计算引用链
      auto *map_entry = g_memory_map.CalculateRelPc(
          leak_alloc->backtrace[i + kNumDropFrame], &offset);

      if (!map_entry) {
        continue;
      }

      if (map_entry->NeedIgnore()) {
        leak_alloc->num_backtraces = i;
        break;
      }

      std::string symbol_info =
          g_enable_local_symbolic
              ? g_memory_map.FormatSymbol(
                    map_entry, leak_alloc->backtrace[i + kNumDropFrame])
              : basename(map_entry->name.c_str());
      // 通过取阈值来判断是否发生了泄漏        
      frames.emplace_back(static_cast<jlong>(offset), symbol_info);
    }

    if (!leak_alloc->num_backtraces || frames.empty()) {
      continue;
    }

    char address[sizeof(uintptr_t) * 2 + 1];
    snprintf(address, sizeof(uintptr_t) * 2 + 1, "%lx",
             CONFUSE(leak_alloc->address));
    ScopedLocalRef<jstring> memory_address(env, env->NewStringUTF(address));
    ScopedLocalRef<jobjectArray> frames_ref(env, BuildFrames(env, frames));
    ScopedLocalRef<jobject> leak_record_ref(
        env, BuildLeakRecord(env, leak_alloc->index, leak_alloc->size,
                             leak_alloc->thread_name, frames_ref.get()));
    ScopedLocalRef<jobject> no_use(
        env,
        env->CallObjectMethod(leak_record_map, put_method, memory_address.get(),
                              leak_record_ref.get()));
  }
}

数组 kLeakMonitorMethods 中比较重要的是 GetLeakAllocs

static const JNINativeMethod kLeakMonitorMethods[] = {
    {"nativeInstallMonitor", "([Ljava/lang/String;[Ljava/lang/String;Z)Z",
     reinterpret_cast<void *>(InstallMonitor)},
    {"nativeUninstallMonitor", "()V",
     reinterpret_cast<void *>(UninstallMonitor)},
    {"nativeSetMonitorThreshold", "(I)V",
     reinterpret_cast<void *>(SetMonitorThreshold)},
    {"nativeGetAllocIndex", "()J", reinterpret_cast<void *>(GetAllocIndex)},
    {"nativeGetLeakAllocs", "(Ljava/util/Map;)V",
     reinterpret_cast<void *>(GetLeakAllocs)}};

GetLeakAllocs

static jlong GetAllocIndex(JNIEnv *, jclass) {
  return LeakMonitor::GetInstance().CurrentAllocIndex();
}

可以看到,比较核心的逻辑在 LeakMonitor 中,我们进入这个 cpp 文件看下,这里面进行了一系列的 hook 操作来进行泄漏的监测;

#define HOOK(ret_type, function, ...) \
  static ALWAYS_INLINE ret_type WRAP(function)(__VA_ARGS__)

hook free 函数

HOOK(void, free, void *ptr) {
  free(ptr);
  if (ptr) {
    // 一旦 hook 了就会通过 UnregisterAlloc 来监测有没有进行释放
    LeakMonitor::GetInstance().UnregisterAlloc(
        reinterpret_cast<uintptr_t>(ptr));
  }
}

hook 了 free 释放内存函数之后就会通过 UnregisterAlloc 来监测有没有进行释放;

hook malloc 函数

HOOK(void *, malloc, size_t size) {
  auto result = malloc(size);
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
                                       size);
  CLEAR_MEMORY(result, size);
  return result;
}

一旦执行了 malloc 分配内存函数,立马用 OnMonitor 进行监测了,监测的就是 malloc 之后有没有 free

hook realloc 函数

HOOK(void *, realloc, void *ptr, size_t size) {
  auto result = realloc(ptr, size);
  if (ptr != nullptr) {
    LeakMonitor::GetInstance().UnregisterAlloc(
        reinterpret_cast<uintptr_t>(ptr));
  }
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
                                       size);
  return result;
}

一旦执行了 realloc 重新分配内存函数,立马用 OnMonitor 进行监测了;

hook calloc 函数

HOOK(void *, calloc, size_t item_count, size_t item_size) {
  auto result = calloc(item_count, item_size);
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
                                       item_count * item_size);
  return result;
}

hook memalign、posix_memalign 函数;

HOOK(void *, memalign, size_t alignment, size_t byte_count) {
  auto result = memalign(alignment, byte_count);
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),
                                       byte_count);
  CLEAR_MEMORY(result, byte_count);
  return result;
}

HOOK(int, posix_memalign, void **memptr, size_t alignment, size_t size) {
  auto result = posix_memalign(memptr, alignment, size);
  LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(*memptr),
                                       size);
  CLEAR_MEMORY(*memptr, size);
  return result;
}

那么这些函数是怎么被 hook 的呢?就是借助 xHook 框架来实现的,我们进入 hook_helper.cpp 中看下:

#define LOG_TAG "hook_helper"
#include "utils/hook_helper.h"

#include <dlopencb.h>
#include <log/log.h>
#include <xhook.h>

std::vector<const std::string> HookHelper::register_pattern_;
std::vector<const std::string> HookHelper::ignore_pattern_;
std::vector<std::pair<const std::string, void *const>> HookHelper::methods_;

bool HookHelper::HookMethods(
    std::vector<const std::string> &register_pattern,
    std::vector<const std::string> &ignore_pattern,
    std::vector<std::pair<const std::string, void *const>> &methods) {
  if (register_pattern.empty() || methods.empty()) {
    ALOGE("Hook nothing");
    return false;
  }

  register_pattern_ = std::move(register_pattern);
  ignore_pattern_ = std::move(ignore_pattern);
  methods_ = std::move(methods);
  DlopenCb::GetInstance().AddCallback(Callback);
  return HookImpl();
}

void HookHelper::UnHookMethods() {
  DlopenCb::GetInstance().RemoveCallback(Callback);
  register_pattern_.clear();
  ignore_pattern_.clear();
  methods_.clear();
}

void HookHelper::Callback(std::set<std::string> &, int, std::string &) {
  HookImpl();
}

bool HookHelper::HookImpl() {
  pthread_mutex_lock(&DlopenCb::hook_mutex);
  xhook_clear();
  for (auto &pattern : register_pattern_) {
    for (auto &method : methods_) {
      // 执行真正的 hook 逻辑
      if (xhook_register(pattern.c_str(), method.first.c_str(), method.second,
                         nullptr) != EXIT_SUCCESS) {
        ALOGE("xhook_register pattern %s method %s fail", pattern.c_str(),
              method.first.c_str());
        pthread_mutex_unlock(&DlopenCb::hook_mutex);
        return false;
      }
    }
  }

  for (auto &pattern : ignore_pattern_) {
    for (auto &method : methods_) {
      if (xhook_ignore(pattern.c_str(), method.first.c_str()) != EXIT_SUCCESS) {
        ALOGE("xhook_ignore pattern %s method %s fail", pattern.c_str(),
              method.first.c_str());
        pthread_mutex_unlock(&DlopenCb::hook_mutex);
        return false;
      }
    }
  }

  int ret = xhook_refresh(0);
  pthread_mutex_unlock(&DlopenCb::hook_mutex);
  return ret == 0;
}

通过 xhook_register 来执行 hook 逻辑;

内存泄露如何被检测就是通过 hook 的方式来实现的;至于内存分析这块,koom 借助了 libmemunreachable 库来进行分析的;

所以 Koom native 层泄漏监测的原理可以总结为如下:

  • hook malloc/free 等内存分配器方法,用于记录 Native 内存分配元数据『大小、堆栈、地址等』;
  • 周期性的使用 mark-and-sweep 分析整个进程 Native Heap,获取不可达的内存块信息『地址、大小』;
  • 利用不可达的内存块的地址、大小等从我们记录的元数据中获取其分配堆栈,产出泄漏数据『不可达内存块地址、大小、分配堆栈等』;

Java 层内存泄漏监测

我们知道 上一章讲 LeakCanary 的内存泄漏监测是通过利用弱引用的特性,为 Activity 创建弱引用,当 Activity 对象变成弱可达时,弱引用会被加入到引用队列中,通过在 Activity.onDestory() 后连续两次的 GC 操作,并检查引用队列,可以判定 Activity 是否发生了泄漏。但是频繁的 GC 会造成用户可感知的卡顿,那么怎么解决这种性能损耗呢?Koom 采用了 无性能损耗的阈值监控策略 来实现的,我们具体来看一下;

为什么 LeakCanary 不能作为线上内存监控策略呢?

我们来看下系统中 dump Hprof 的时候发生了什么,我们通过 Debug.dumpHprofData() 方法最终跟到的是 native 层

// art/runtime/native/dalvik_system_VMDebug.cc

static void VMDebug_dumpHprofData(JNIEnv* env, jclass, jstring javaFilename, jint javaFd) {
  // Only one of these may be null.
  if (javaFilename == nullptr && javaFd < 0) {
    ScopedObjectAccess soa(env);
    ThrowNullPointerException("fileName == null && fd == null");
    return;
  }

  std::string filename;
  if (javaFilename != nullptr) {
    ScopedUtfChars chars(env, javaFilename);
    if (env->ExceptionCheck()) {
      return;
    }
    filename = chars.c_str();
  } else {
    filename = "[fd]";
  }

  int fd = javaFd;

  hprof::DumpHeap(filename.c_str(), fd, false);
}

最终执行的是 DumpHeap 我们进入这个方法看下:

// art/runtime/hprof/hprof.cc

void DumpHeap(const char* filename, int fd, bool direct_to_ddms) {
  CHECK(filename != nullptr);
  Thread* self = Thread::Current();
  gc::ScopedGCCriticalSection gcs(self,
                                  gc::kGcCauseHprof,
                                  gc::kCollectorTypeHprof);
  ScopedSuspendAll ssa(__FUNCTION__, true /* long suspend */);
  Hprof hprof(filename, fd, direct_to_ddms);
  hprof.Dump();
}

从源码中可以看到,在执行 Dump 之前,会构造一个 ScopedSuspendAll 对象,用来暂停所有的线程,然后再析构方法中恢复;

// /art/runtime/thread_list.cc

ScopedSuspendAll::ScopedSuspendAll(const char* cause, bool long_suspend) {
  Runtime::Current()->GetThreadList()->SuspendAll(cause, long_suspend);
}

ScopedSuspendAll::~ScopedSuspendAll() {
  Runtime::Current()->GetThreadList()->ResumeAll();
}

Koom 是如何规避这个问题的呢?看源码可以得知,Koom 做了如下操作:

  • fork 子进程,在子进程中执行 Debug.dumpHprofData();
  • fork 子进程,采用的是 Copy-On-Write 的技术,只有在进行写入操作的时候,才会为子进程拷贝分配独立的内存空间,默认情况下,子进程可以和父进程共享同个内存空间,所以,当我们要执行 dumpHprofData() 的时候,可以先 fork 一个子进程,它拥有父进程的内存副本,然后在子进程去执行 Debug.dumpHprofData() 方法,而父进程则可以继续正常运行;
public synchronized boolean dump(String path) {
    MonitorLog.i(TAG, "dump " + path);
    if (!sdkVersionMatch()) {
      throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");
    }
    init();
    if (!mLoadSuccess) {
      MonitorLog.e(TAG, "dump failed caused by so not loaded!");
      return false;
    }

    boolean dumpRes = false;
    try {
      MonitorLog.i(TAG, "before suspend and fork.");
      int pid = suspendAndFork();
      if (pid == 0) {
        // Child process
        Debug.dumpHprofData(path);
        exitProcess();
      } else if (pid > 0) {
        // Parent process
        dumpRes = resumeAndWait(pid);
        MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
      }
    } catch (IOException e) {
      MonitorLog.e(TAG, "dump failed caused by " + e);
      e.printStackTrace();
    }
    return dumpRes;
}

可以看到调用 native 方法 suspendAndFork fork 一个子进程,如果子进程 id = 0 则执行 dump 操作;我们来看下 suspendAndFork 是如何 fork 的;

pid_t HprofDump::SuspendAndFork() {
  KCHECKI(init_done_)

  if (android_api_ < __ANDROID_API_R__) {
    suspend_vm_fnc_();
  } else if (android_api_ <= __ANDROID_API_U__) {
    void *self = __get_tls()[TLS_SLOT_ART_THREAD_SELF];
    sgc_constructor_fnc_((void *)sgc_instance_.get(), self, kGcCauseHprof,
                         kCollectorTypeHprof);
    ssa_constructor_fnc_((void *)ssa_instance_.get(), LOG_TAG, true);
    // avoid deadlock with child process
    exclusive_unlock_fnc_(*mutator_lock_ptr_, self);
    sgc_destructor_fnc_((void *)sgc_instance_.get());
  }

  pid_t pid = fork();
  if (pid == 0) {
    // Set timeout for child process
    alarm(60);
    prctl(PR_SET_NAME, "forked-dump-process");
  }
  return pid;
}

可以看到 pid_t pid = fork(); 使用 fork 函数进行子进程的 fork 操作;

整体流程如下

  1. 开始 dump
  2. 调用虚拟机 Suspend API
  3. Fork 创建子进程
  4. 子进程开始 dump
  5. 通知父进程 dump 完成
  6. 父进程开始分析

好了,Koom 的 native 层和 java 层的内存泄漏监控就讲完了;

Hprof


这块不做过多的解释了,讲一下裁剪思路;

如何裁剪

裁剪可以参考下 Martix,它里面的裁剪功能的目标是将 Bitmap 和 String 之外的所有对象的基础数据类型的值移除,因为 Hprof 文件的分析功能只需要用到字符串数组和 Bitmap 的 buffer 数组。另一方面,如果存在不同的 Bitmap 对象其 buffer 数组值相同的情况,则可以将它们指向同一个 buffer,以进一步减小文件尺寸。裁剪后的 Hprof 文件通常比源文件小 1/10 以上,代码结果和 ASM 很像,主要有 HprofVisitor、HprofReader、HprofWriter 组成,分别对应 ASM 的 ClassVisitor、ClassReader、ClassWriter;

下一张预告


App 启动流程优化

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值