图片来自:unsplash.com
本文作者: zy
布景
曾几何时,咱们只需求简简略单的一行 Thread.currentThread().getStackTrace() 代码就能够轻轻松松的获取到当时线程的仓库信息,然后剖析各种问题。跟着需求的不断迭代,APP 遇到的问题越来越多,卡顿,ANR,反常等等问题接踵而来,那么简简略单某个时间的仓库信息现已不能满足咱们的需求了,咱们的目光逐渐转移到了每个时间的仓库上,假如能获取一个时间段内,每个时间的仓库信息,那么卡顿,以及 ANR 的问题也将被解决。
抓栈计划
现在关于一段时间内的抓栈计划有两种:
- 办法插桩抓栈
- Native 抓栈
代码插桩抓栈
基本思路
APP 编译阶段,对每个办法进行插桩,在插桩的一起,填入当时办法 ID,发生卡顿或许反常的时候,将之前搜集到的办法 ID 进行聚合输出。
插桩流程图:
长处:简略高效,无兼容性问题
缺点:插桩导致一切类都非 preverify,一起 verify 与 optimize 操作会在加载类时被触发。增加类加载的压力照成一定的功能损耗。别的也会导致包体积变大,影响代码 Debug 以及代码溃散反常后错误行数
Native 抓栈
运用 Native 抓栈之前,咱们先了解一下 Java 抓栈的整个流程
JAVA仓库获取流程图
抓栈当时线程
抓栈其他线程
Java仓库获取原理剖析
因为当时线程抓栈和其他线程抓栈流程类似,这儿咱们从其他线程抓栈的流程进行剖析
首先从入口代码出发,Java 层经过 Thread.currentThread().getStackTrace()
开端获取当时仓库数据
Thread.java
public StackTraceElement[] getStackTrace() {
StackTraceElement ste[] = VMStack.getThreadStackTrace(this);
return str!=null?ste:EmptyArray.STACK_TRACE_ELEMENT;
}
Thread 中的 getStackTrace 仅仅一个空壳,底层的完成是经过 native 来获取的,持续往下走,经过 VMStack 来获取咱们需求的线程仓库数据
dalvik_system_vmstack.cc
static jobjectArray VMStack_getThreadStackTrace(JNIEnv* env, jclass, jobject javaThread) {
ScopedFastNativeObjectAccess soa(env);
// fn 办法是线程挂起回调
auto fn = [](Thread* thread, const ScopedFastNativeObjectAccess& soaa)
REQUIRES_SHARED(Locks::mutator_lock_) -> jobject {
return thread->CreateInternalStackTrace(soaa);
};
// 获取仓库
jobject trace = GetThreadStack(soa, javaThread, fn);
if (trace == nullptr) {
return nullptr;
}
// trace 是一个包括 method 的数组,有这个数据之后,咱们进行数据反解,就能获取到办法仓库明文
return Thread::InternalStackTraceToStackTraceElementArray(soa, trace);
}
上述代码中,需求留意三个元素
-
fn={return thread->CreateInternalStackTrace(soaa);}。 // 这个是线程挂起后的回调函数
-
GetThreadStack(sao,javaThread,fn) // 用来获取实际的线程仓库信息
-
Thread::InternalStackTraceToStackTraceElementArray(sao,trace),这儿 trace 便是咱们拿到的方针产物,这儿面就包括了当时线程此时此刻的仓库信息,需求对仓库进行进一步的解析,才干获取到可辨认的仓库文本
接下来咱们从获取仓库信息函数着手,看看 GetThreadStack 的具体行为。
dalvik_system_vmstack.cc
static ResultT GetThreadStack(const ScopedFastNativeObjectAccess& soa,jobject peer,T fn){
********
********
********
ThreadList* thread_list = Runtime::Current()->GetThreadList();
// 【Step1】: 挂起线程
Thread* thread = thread_list->SuspendThreadByPeer(peer,SuspendReason::kInternal,&timed_out);
if (thread != nullptr) {
{
ScopedObjectAccess soa2(soa.Self());
// 【Step2】: FN 回调,这儿面履行的便是抓栈操作,回到外层的回调函数逻辑中
trace = fn(thread, soa);
}
// 【Step3】: 康复线程
bool resumed = thread_list->Resume(thread, SuspendReason::kInternal);
}
}
return trace;
}
在该操作的三个过程中,就包括了抓栈的整个流程,
-
【Step1】: 挂起线程,线程每时每刻都在履行办法,这样就导致当时线程的办法仓库在不停的增加,假如想要抓到瞬时仓库,就需求把当时线程暂停,保留瞬时的仓库信息,这样抓出来的数据才是精确的。
-
【Step2】: 履行 FN 的回调,这儿的 FN 回调,便是上文介绍的回调办法 fn={return thread->CreateInternalStackTrace(soaa)}
-
【Step3】: 康复线程的正常运行。
上述流程中,咱们需求重点重视一下 FN 回调里边做了什么,以及怎么做到的
thread.cc
jobject Thread::CreateInternalStackTrace(const ScopedObjectAccessAlreadyRunnable& soa) const {
// 创立仓库回溯观察者
FetchStackTraceVisitor count_visitor(const_cast<Thread*>(this),&saved_frames[0],kMaxSavedFrames);
count_visitor.WalkStack(); // 回溯中心办法
// 创立仓库回溯观察者 2 号,具体的仓库数据便是 2 号处理回来的
BuildInternalStackTraceVisitor build_trace_visitor(soa.Self(), const_cast<Thread*>(this), skip_depth);
mirror::ObjectArray<mirror::Object>* trace = build_trace_visitor.GetInternalStackTrace();
return soa.AddLocalReference<jobject>(trace);
}
-
创立堆回溯观察者 1 号 FetchStackTraceVisitor,最大深度 256 进行回溯,假如深度超过了 256,则运用 2 号持续进行回溯
-
创立堆回溯观察者 2 号 BuildInternalStackTraceVisitor,承接 1 号的回溯结果,1 号没回溯完,2 号接着回溯。
栈回溯的具体进程
回溯是经过 WalkStack 来完成的。StackVisitor::WalkStack 是一个用于在当时线程仓库上单步遍历帧的函数。它能够用来搜集当时线程仓库上特定帧的信息,以便进行调试或其他剖析操作。 例如,它能够用来找出当时线程仓库上哪些函数调用了特定函数,或许搜集特定函数的参数。 也能够用来找出线程调用的函数层次结构,以及每一层调用的函数参数。 运用这个函数,能够更好地了解代码的履行流程,并帮助进行反常处理和调试。
stack.cc
void StackVisitor::WalkStack(bool include_transitions) {
for (const ManagedStack* current_fragment = thread_->GetManagedStack();current_fragment != nullptr; current_fragment = current_fragment->GetLink()) {
cur_shadow_frame_ = current_fragment->GetTopShadowFrame();
****
****
****
do {
// 告诉子类,进行栈帧的获取
bool should_continue = VisitFrame();
cur_depth_++;
cur_shadow_frame_ = cur_shadow_frame_->GetLink();
} while (cur_shadow_frame_ != nullptr);
}
}
ManagedStack 是一个单链表,保存了当时 ShadowFrame 或许 QuickFrame 栈指针,先顺次遍历 ManagedStack 链表,然后遍历其内部的 ShadowFrame 或许 QuickFrame 复原一个可读的调用栈,然后复原出当时的 Java 仓库
复原操作是经过 VisitFrame 来完成的,它是一个笼统接口,完成类咱们需求看 BuildInternalStackTraceVisitor 的完成
thread.cc
class BuildInternalStackTraceVisitor : public StackVisitor {
mirror::ObjectArray<mirror::Object>* trace_ = nullptr;
bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
****
****
****
// 每循环一帧,将其增加到 arrObj 中
ArtMethod* m = GetMethod();
AddFrame(m, m->IsProxyMethod() ? dex::kDexNoIndex : GetDexPc());
return true;
}
void AddFrame(ArtMethod* method, uint32_t dex_pc) REQUIRES_SHARED(Locks::mutator_lock_) {
ObjPtr<mirror::Object> keep_alive;
if (UNLIKELY(method->IsCopied())) {
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
keep_alive = class_linker->GetHoldingClassLoaderOfCopiedMethod(self_, method);
} else {
keep_alive = method->GetDeclaringClass();
}
// 增加每一次遍历到的 artMethod 方针,在增加完成之后,进行 count++,进行 Arr 的偏移
trace_->Set<false,false>(static_cast<int32_t>(count_) + 1, keep_alive);
++count_;
}
}
在履行 VisitFrame 的进程中,会将每次的 method 拎出来,然后增加至 ObjectArray 的调集中。当一切办法查找完成之后,会进行 method 的反解。
仓库信息反解要害操作
反解的流程在文章最初,经过 Thread::InternalStackTraceToStackTraceElementArray(soa,trace)
来进行反解。
thread.cc
jobjectArray Thread::InternalStackTraceToStackTraceElementArray(const ScopedObjectAccessAlreadyRunnable& soa,jobject internal,jobjectArray output_array,int* stack_depth) {
int32_t depth = soa.Decode<mirror::Array>(internal)->GetLength() - 1;
for (uint32_t i = 0; i < static_cast<uint32_t>(depth); ++i) {
ObjPtr<mirror::ObjectArray<mirror::Object>> decoded_traces = soa.Decode<mirror::Object>(internal)->AsObjectArray<mirror::Object>();
const ObjPtr<mirror::PointerArray> method_trace = ObjPtr<mirror::PointerArray>::DownCast(decoded_traces->Get(0));
// 【Step1】: 提取数组中的 ArtMethod
ArtMethod* method = method_trace->GetElementPtrSize<ArtMethod*>(i, kRuntimePointerSize);
uint32_t dex_pc = method_trace->GetElementPtrSize<uint32_t>(i + static_cast<uint32_t>(method_trace->GetLength()) / 2, kRuntimePointerSize);
// 【Step2】: 将 ArtMethod 转换成事务上层可辨认的 StackTraceElement 方针
const ObjPtr<mirror::StackTraceElement> obj = CreateStackTraceElement(soa, method, dex_pc);
soa.Decode<mirror::ObjectArray<mirror::StackTraceElement>>(result)->Set<false>(static_cast<int32_t>(i), obj);
}
return result;
}
static ObjPtr<mirror::StackTraceElement> CreateStackTraceElement(
const ScopedObjectAccessAlreadyRunnable& soa,
ArtMethod* method,
uint32_t dex_pc) REQUIRES_SHARED(Locks::mutator_lock_) {
// 【Step3】: 获取行号
line_number = method->GetLineNumFromDexPC(dex_pc);
// 【Step4】: 获取类名
const char* descriptor = method->GetDeclaringClassDescriptor();
std::string class_name(PrettyDescriptor(descriptor));
class_name_object.Assign(mirror::String::AllocFromModifiedUtf8(soa.Self(), class_name.c_str()));
// 【Step5】: 获取类路径
const char* source_file = method->GetDeclaringClassSourceFile();
source_name_object.Assign(mirror::String::AllocFromModifiedUtf8(soa.Self(), source_file));
// 【Step6】: 获取办法名
const char* method_name = method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetName();
Handle<mirror::String> method_name_object(hs.NewHandle(mirror::String::AllocFromModifiedUtf8(soa.Self(), method_name)));
// 【Step7】: 数据封装回抛
return mirror::StackTraceElement::Alloc(soa.Self(),class_name_object,method_name_object,source_name_object,line_number);
}
到这儿咱们现已剖析完一次由 Java 层触发的仓库调用链路一直到底层的完成逻辑。
中心流程
咱们的方针是抓栈,因而咱们只需求重视 count_visitor.WalkStack
之后的栈回溯流程。
耗时阶段
这儿最后阶段将 ArtMethod 转换成事务上层可辨认的 StackTraceElement,因为涉及到很多的字符串操作,给 Java 仓库的履行贡献了很大的耗时占比。
抓栈新思路
传统的抓栈发生的数据很完善,进程也比较耗时。咱们是否能够简化这个流程,进步抓栈效率呢,理论上是能够的,咱们只需求自己将这个流程复写一份,然后扔掉部分的数据,优化数据获取时间,相同能够做到更高效的抓栈体会。
Native抓栈逻辑完成
依据体系抓栈流程,咱们能够梳理出要做的几个事情点
要做的事情:
-
挂起线程【获取挂起线程办法内存地址】
-
进行抓栈【获取抓栈办法内存地址】【优化抓栈耗时】
-
康复线程的履行【获取康复线程办法内存地址】
遇到的问题及解决计划:
- 如何获取体系 threadList 方针
threadList 是线程履行挂起和康复的要害方针,体系未露出该方针的直接拜访操作,因而咱们只能另辟蹊径来获取它,threadList 获取依靠流程图如下:
假如想要履行线程的挂起 thread_->SuspendThreadByPeer 或许康复 thread_list->Resume ,首先需求获取到 thread_list 体系方针,该方针是经过 Runtime::Current()->getThreadList() 获取而来,,因而咱们要先获取 Runtime , Runtime 的获取能够经过 JavaVmExt 来获取,而 JavaVmExt 能够经过 JNI_OnLoad 时的 JavaVM 来获取,完好流程如下代码所示
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
JavaVM *javaVM;
env->GetJavaVM(&javaVM);
auto *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;
// JavaVMExt 结构
// 10.0 https://android.googlesource.com/platform/art/+/refs/tags/android-10.0.0_r1/runtime/jni/java_vm_ext.h
// 【Step1】. 找到 Runtime_instance_ 的方位
if (api < 30) {
runtime_instance_ = runtime;
} else {
int vm_offset = find_offset(runtime, MAX_SEARCH_LEN, javaVM);
runtime_instance_ = reinterpret_cast<void *>(reinterpret_cast<char *>(runtime) + vm_offset - offsetof(PartialRuntimeR, java_vm_));
}
// 【Step2】. 以 runtime_instance_ 的地址为起点,开端找到 JavaVMExt 在 【https://android.googlesource.com/platform/art/+/refs/tags/android-10.0.0_r29/runtime/runtime.h】中的方位
// 7.1 https://android.googlesource.com/platform/art/+/refs/tags/android-7.1.2_r39/runtime/runtime.h
int offsetOfVmExt = findOffset(runtime_instance_, 0, MAX, (size_t) javaVMExt);
if (offsetOfVmExt < 0) {
ArtHelper::reduce_model = 1;
return;
}
// 【Step3】. 依据 JavaVMExt 的方位,依据各个版别的结构,进行偏移,生成 PartialRuntimeSimpleTenR 的结构
if (ArtHelper::api == ANDROID_P_API || ArtHelper::api == ANDROID_O_MR1_API) {
PartialRuntimeSimpleNineR *simpleR = (PartialRuntimeSimpleNineR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleNineR, java_vm_));
thread_list = simpleR->thread_list_;
}else if (ArtHelper::api <= ANDROID_O_API) {
PartialRuntimeSimpleSevenR *simpleR = (PartialRuntimeSimpleSevenR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleSevenR, java_vm_));
thread_list = simpleR->thread_list_;
}else{
PartialRuntimeSimpleTenR *simpleR = (PartialRuntimeSimpleTenR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleTenR, java_vm_));
thread_list = simpleR->thread_list_;
}
}
经过三个过程,咱们就能够获取到底层的 Runtime 方针,以及最要害的 thread_list 方针,有了它,咱们就能够对线程履行暂停和康复操作。
- 线程的暂停和康复
因为 SuspendThreadByPeer 和 Resume 办法咱们拜访不到,但假如咱们能够找到这两个办法的内存地址,那么就能够直接履行了,怎么获取到内存地址呢?这儿运用 Nougat_dlfunctions 的 fake_dlopen() 和 fake_dlsym() 来获取已被加载到内存的动态链接库 libart.so 中办法内存地址。
WalkStack_ = reinterpret_cast<void (*)(StackVisitor *, bool)>(dlsym_ex(handle,"_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb"));
SuspendThreadByThreadId_ = reinterpret_cast<void *(*)(void *, uint32_t, SuspendReason, bool *)>(dlsym_ex(handle,"_ZN3art10ThreadList23SuspendThreadByThreadIdEjNS_13SuspendReasonEPb"));
Resume_ = reinterpret_cast<bool (*)(void *, void *, SuspendReason)>(dlsym_ex(handle, "_ZN3art10ThreadList6ResumeEPNS_6ThreadENS_13SuspendReasonE"));
PrettyMethod_ = reinterpret_cast<std::string (*)(void *, bool)>(dlsym_ex(handle, "_ZN3art9ArtMethod12PrettyMethodEb"));
到这儿,咱们现已现已能够完成线程的挂起和康复了,接下来便是抓栈的操作处理流程。
- 自定义抓栈
相同的,因为咱们现已获取到用于栈回溯的 WalkStack 办法地址,咱们只需求提供一个自定义的 TraceVisitor 类即可完成栈回溯
class CustomFetchStackTraceVisitor : public StackVisitor {
bool VisitFrame() override {
// 【Step1】: 体系仓库调用时咱们剖析到的流程,每帧遍历时会走一次当时流程
void *method = GetMethod();
// 【Step2】: 获取到 Method 方针之后,运用 circular_buffer 存起来,没有剩余的过滤逻辑,不反解字符串
if (CustomFetchStackTraceVisitorCallback!= nullptr){
return CustomFetchStackTraceVisitorCallback(method);
}
return true;
}
}
获取到 Method 之后,为了节省本次的抓栈耗时,咱们运用固定巨细的 circular_buffer 将数据存储起来,新数据主动掩盖老数据,依据需求,进行异步反解 Method 中的具体仓库数据。到这儿,自定义的 Native 抓栈逻辑就完成了。
总结
现在自定义 native 抓栈的多个阶段需求兼容不同体系版别的 thread_list 获取,以及不同版别的线程挂起,线程康复的函数地址获取。这些都会导致出现或多或少的兼容性问题,这儿能够经过两种计划来躲避,第一种是过滤读取到的不合法地址,关于这类不合法地址,需求跳过抓栈流程。别的一种便是动态配置下发过滤这些不兼容版别机型。
参考资料
- Nougat_dlfunctions:github.com/avs333/Noug…
- 环形缓冲区:baike.baidu.com/item/%E7%8E…
- Android 平台下的 Method Trace 完成解析:zhuanlan.zhihu.com/p/526960193…
本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。咱们终年接收各类技术岗位,假如你预备换作业,又恰好喜欢云音乐,那就参加咱们 grp.music-fe(at)corp.netease.com!