关于android:不一样的-Android-堆栈抓取方案

40次阅读

共计 11121 个字符,预计需要花费 28 分钟才能阅读完成。

图片来自:https://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:https://github.com/avs333/Nougat_dlfunctions
  • 环形缓冲区:https://baike.baidu.com/item/%E7%8E%AF%E5%BD%A2%E7%BC%93%E5%8…
  • Android 平台下的 Method Trace 实现解析:https://zhuanlan.zhihu.com/p/526960193?utm_id=0

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0