不一樣的Android堆棧抓取方案
圖片來自:http://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
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 http://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 在 【http://android.googlesource.com/platform/art/+/refs/tags/android-10.0.0_r29/runtime/runtime.h】中的位置
// 7.1 http://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:http://github.com/avs333/Nougat_dlfunctions
- 環形緩衝區:http://baike.baidu.com/item/%E7%8E%AF%E5%BD%A2%E7%BC%93%E5%86%B2%E5%99%A8/22701730
- Android 平台下的 Method Trace 實現解析:http://zhuanlan.zhihu.com/p/526960193?utm_id=0
本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!
- 不一樣的Android堆棧抓取方案
- 雲音樂 Swift 混編 Module 化實踐
- iOS雲音樂APM性能監控實踐
- 雲音樂iOS端代碼靜態檢測實踐
- 網易雲音樂全面開源一款雲原生應用部署平台:Horizon
- dex 優化編年史
- 如何實現 iOS 16 帶來的 Depth Effect 圖片效果
- 雲音樂 iOS 跨端緩存庫 - NEMichelinCache
- 雲音樂 Android 內存監控探索篇
- Android APP 出海實踐
- Android 調試實戰與原理詳解
- 社交場景下iOS消息流交互層實踐
- 你構建的代碼為什麼這麼大
- 扒一扒 Jetpack Compose 實現原理
- Recoil 狀態管理方案的淺入淺出
- 基於自建 VTree 的全鏈路埋點方案
- 雲音樂 iOS 啟動性能優化「開荒篇」
- 雲音樂播放頁直播推薦實戰
- 雲音樂iOS端網絡圖片下載優化實踐
- 雲音樂 iOS 啟動性能優化「開荒篇」