Android Runtime | Trace文件的生成機制


本文分析基於Android S(12)

當App發生ANR或是System觸發watchdog時,系統都希望生成一份trace文件,用來記錄各個線程的調用棧信息,以及一些進程/線程的狀態信息。這份文件通常存放在/data/anr目錄下,APP開發者拿不到。不過從Android R(11)開始,App便可以通過AMS的getHistoricalProcessExitReasons接口讀取該文件的詳細信息。以下是一份典型trace文件中的內容。

----- pid 8331 at 2021-11-26 09:10:03 -----
Cmd line: com.hangl.test
Build fingerprint: xxx
ABI: 'arm64'
Build type: optimized
Zygote loaded classes=9118 post zygote classes=475
Dumping registered class loaders
#0 dalvik.system.PathClassLoader: [], parent #1
#1 java.lang.BootClassLoader: [], no parent
...
(進程整體的一些狀態,譬如GC的統計數據)

suspend all histogram:	Sum: 161us 99% C.I. 2us-60us Avg: 16.100us Max: 60us
DALVIK THREADS (14):
"Signal Catcher" daemon prio=5 tid=7 Runnable
  | group="system" sCount=0 dsCount=0 flags=0 obj=0x14dc0298 self=0x7c4c962c00
  ...

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7263ee78 self=0x7c4c7dcc00
  | sysTid=8331 nice=-10 cgrp=default sched=0/0 handle=0x7c4dd45ed0
  | state=S schedstat=( 387029514 32429484 166 ) utm=28 stm=10 core=6 HZ=100
  | stack=0x7feacb5000-0x7feacb7000 stackSize=8192KB
  | held mutexes=
  native: #00 pc 00000000000d0f48  /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
  native: #01 pc 00000000000180bc  /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
  native: #02 pc 0000000000017f8c  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+56)
  native: #03 pc 000000000013b920  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:336)
  at android.os.Looper.loop(Looper.java:174)
  at android.app.ActivityThread.main(ActivityThread.java:7397)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)

"Jit thread pool worker thread 0" daemon prio=5 tid=2 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x14dc0220 self=0x7bb9a05000
  ...

本文既無意於討論ANR的觸發類型,也無意於流水賬式地展示每塊內容的生成順序,因為這些已經有不少文章寫過,而且其中不乏精品。鑒於此,本文將重點分析調用棧的生成流程,而它將幫助我們更好地理解trace信息。

前言

不論是ANR還是Watchdog,trace的生成過程都放在target process中進行。以ANR為例,它的判定過程發生在system_server(AMS)中,而trace的生成過程卻發生在APP中。那么如何讓APP開始這個過程呢?答案是給它發送SIGQUIT(信號3)。之所以這樣處理,是因為跨進程的信息收集通常采用ptrace方案,它要求收集方要么擁有特殊權限,要么滿足進程間父子關系,而這些都沒有進程內收集方便。

因此,分析的第一步便是去查看信號3在進程中的處理方式。

1. Signal Catcher線程

"Signal Catcher"線程在每個Java進程中都會存在。正常運行時,它將掛起等待信號3(以及信號10)的到來。當該進程接收到信號3時,將會交由"Signal Catcher"線程處理,處理的函數為HandleSigQuit

void SignalCatcher::HandleSigQuit() {
  Runtime* runtime = Runtime::Current();
  std::ostringstream os;
  os << "\n"
      << "----- pid " << getpid() << " at " << GetIsoDate() << " -----\n";

  DumpCmdLine(os);

  // Note: The strings "Build fingerprint:" and "ABI:" are chosen to match the format used by
  // debuggerd. This allows, for example, the stack tool to work.
  std::string fingerprint = runtime->GetFingerprint();
  os << "Build fingerprint: '" << (fingerprint.empty() ? "unknown" : fingerprint) << "'\n";
  os << "ABI: '" << GetInstructionSetString(runtime->GetInstructionSet()) << "'\n";

  os << "Build type: " << (kIsDebugBuild ? "debug" : "optimized") << "\n";

  runtime->DumpForSigQuit(os);

  if ((false)) {
    std::string maps;
    if (android::base::ReadFileToString("/proc/self/maps", &maps)) {
      os << "/proc/self/maps:\n" << maps;
    }
  }
  os << "----- end " << getpid() << " -----\n";
  Output(os.str());
} 

中間的跳轉過程就不展示了,直接進入我們關心的議題:調用棧的收集過程。通過ThreadList::Dump函數,我們可以收集所有線程的調用棧信息。

void ThreadList::Dump(std::ostream& os, bool dump_native_stack) {
  Thread* self = Thread::Current();
  {
    MutexLock mu(self, *Locks::thread_list_lock_);
    os << "DALVIK THREADS (" << list_.size() << "):\n";
  }
  if (self != nullptr) {
    DumpCheckpoint checkpoint(&os, dump_native_stack);
    size_t threads_running_checkpoint;
    {
      // Use SOA to prevent deadlocks if multiple threads are calling Dump() at the same time.
      ScopedObjectAccess soa(self);
      threads_running_checkpoint = RunCheckpoint(&checkpoint);
    }
    if (threads_running_checkpoint != 0) {
      checkpoint.WaitForThreadsToRunThroughCheckpoint(threads_running_checkpoint);
    }
  } else {
    DumpUnattachedThreads(os, dump_native_stack);
  }
}

其中關鍵的環節是執行RunCheckpoint函數。它將每個線程的信息收集分為單獨的任務:如果該線程正處在Runnable狀態(運行java代碼),則將收集的任務派發給線程自己處理;如果該線程處於其他狀態,則由"Signal Catcher"線程代為完成。請記住這句話,因為下面2、3小節分析的就是它的兩種不同情況。

2. Checkpoint機制

將任務派發給Runnable狀態的線程采用的是checkpoint機制,它分為兩個部分:

  1. "Signal Catcher"線程調用RequestCheckpoint去改變目標線程的art::Thread對象的內部數據,具體而言改變的是以下兩個字段。
   tls32_.state_and_flags.as_struct.flags |= kCheckpointRequest;
   tlsPtr_.checkpoint_function = function;
   (tls32_和tlsPtr_均是art::Thread對象的內部數據)
  1. 對ART虛擬機而言,目標線程在每個方法的起始位置和循環語句的跳轉位置都會去檢查state_and_flags字段,如果checkpoint位被置上,則執行相應的checkpoint函數。這樣安插檢查點可以保證線程”及時“地處理checkpoint任務:因為所有向前執行(直線型、帶條件分支都算)的代碼都會在有限時間內執行完,而可能導致長時間執行的代碼,要么是循環,要么是方法調用,所以只要在這兩個地方插入檢查點就可以保證及時性了。(參考R大知乎回答

關於目標線程的檢查點,這里我還想舉個例子,讓大家真切地感受到它的存在。

字節碼在ART虛擬機中可以解釋執行,也可以編譯成機器碼執行。當一個方法被編譯為機器碼后(如下所示),我們可以在函數的入口處看到有檢測state_and_flags的操作。當有標志位被置上時,則執行pTestSuspend動作。

CODE: (code_offset=0x003f9ae0 size=788)...
  0x003f9ae0: d1400bf0	sub x16, sp, #0x2000 (8192)
  0x003f9ae4: b940021f	ldr wzr, [x16]
    StackMap[0] (native_pc=0x3f9ae8, dex_pc=0x0, register_mask=0x0, stack_mask=0b)
  0x003f9ae8: f8180fe0	str x0, [sp, #-128]!
  0x003f9aec: a9035bf5	stp x21, x22, [sp, #48]
  0x003f9af0: a90463f7	stp x23, x24, [sp, #64]
  0x003f9af4: a9056bf9	stp x25, x26, [sp, #80]
  0x003f9af8: a90673fb	stp x27, x28, [sp, #96]
  0x003f9afc: a9077bfd	stp x29, lr, [sp, #112]
  0x003f9b00: b9008fe2	str w2, [sp, #140]
  0x003f9b04: 79400270	ldrh w16, [tr] ; state_and_flags
  0x003f9b08: 350016f0	cbnz w16, #+0x2dc (addr 0x3f9de4)   //如果state_and_flags不為0,則跳轉到0x3f9de4的位置
  ...
  0x003f9de4: 940e62c3	bl #+0x398b0c (addr 0x7928f0) ; pTestSuspend  //跳轉進入pTestSuspend

幾經跳轉,pTestSuspend最終會調用Thread::CheckSuspend函數。當checkpoint位被置上時,則執行相應的checkpoint函數(RunCheckpointFunction)。

inline void Thread::CheckSuspend() {
  DCHECK_EQ(Thread::Current(), this);
  for (;;) {
    if (ReadFlag(kCheckpointRequest)) {
      RunCheckpointFunction();
    } else if (ReadFlag(kSuspendRequest)) {
      FullSuspendCheck();
    } else if (ReadFlag(kEmptyCheckpointRequest)) {
      RunEmptyCheckpoint();
    } else {
      break;
    }
  }
}

下面舉一個Runnable線程自己收集調用棧的例子,2292行正好是writeNoException方法的第一行,與上述"在每個方法的起始位置插入檢查點"的描述相吻合。

"Binder:2278_C" prio=5 tid=97 Runnable
  | group="main" sCount=0 ucsCount=0 flags=0 obj=0x16104b20 self=0xb400007117c7afb0
  | sysTid=2890 nice=0 cgrp=foreground sched=0/0 handle=0x6eafe24cb0
  | state=R schedstat=( 47445156223 266433061959 175792 ) utm=1623 stm=3121 core=4 HZ=100
  | stack=0x6eafd2d000-0x6eafd2f000 stackSize=991KB
  | held mutexes= "mutator lock"(shared held)
  at android.os.Parcel.writeNoException(Parcel.java:2292)
  at android.os.IPowerManager$Stub.onTransact(IPowerManager.java:474)
  at android.os.Binder.execTransactInternal(Binder.java:1184)
  at android.os.Binder.execTransact(Binder.java:1143)
2291     public final void writeNoException() {
2292         AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);

3. Suspend標志位

對於那些非Runnable狀態的線程,收集的工作由"Signal Catcher"代為完成。這里我梳理了一下為單個線程”代工“的流程,總共分為4步。

thread->ModifySuspendCount(self, +1, nullptr, SuspendReason::kInternal);
checkpoint_function->Run(thread);
thread->ModifySuspendCount(self, -1, nullptr, SuspendReason::kInternal);
Thread::resume_cond_->Broadcast(self);
  1. 增加target thread的suspend count(+1),且給它置上suspend標志位。
  2. 運行相應的函數替target thread收集信息。
  3. 減少target thread的suspend count(-1),如果suspend count減為0,則清除suspend標志位。
  4. 調用resume_cond_條件變量的Broadcast函數,它會喚醒所有等待在它上面的線程。

流程的梳理總是簡單,難的是理解流程設計背后的原因,下面來條分縷析。

  1. 為什么在執行信息收集前需要給target thread置上suspend標志位?

    在回答這個問題前,我們需要補充些基礎知識。每個Java線程本質上都是一個pthread線程,而它在內核中又對應一個task_struct對象,該對象是CPU調度的基本單元。從CPU的視角來看,該線程可以是R態、S態、D態等,它們的含義如下所示。不過虛擬機中又為Java線程記錄了另一套狀態,它反映的是虛擬機視角下的狀態,具體分類如下。

    R  running or runnable (on run queue)
    D  uninterruptible sleep (usually IO)
    S  interruptible sleep (waiting for an event to complete)
```
enum ThreadState {
  //                                   Java
  //                                   Thread.State   JDWP state
  kTerminated = 66,                 // TERMINATED     TS_ZOMBIE    Thread.run has returned, but Thread* still around
  kRunnable,                        // RUNNABLE       TS_RUNNING   runnable
  kTimedWaiting,                    // TIMED_WAITING  TS_WAIT      in Object.wait() with a timeout
  kSleeping,                        // TIMED_WAITING  TS_SLEEPING  in Thread.sleep()
  kBlocked,                         // BLOCKED        TS_MONITOR   blocked on a monitor
  kWaiting,                         // WAITING        TS_WAIT      in Object.wait()
  kWaitingForLockInflation,         // WAITING        TS_WAIT      blocked inflating a thin-lock
  kWaitingForTaskProcessor,         // WAITING        TS_WAIT      blocked waiting for taskProcessor
  kWaitingForGcToComplete,          // WAITING        TS_WAIT      blocked waiting for GC
  kWaitingForCheckPointsToRun,      // WAITING        TS_WAIT      GC waiting for checkpoints to run
  kWaitingPerformingGc,             // WAITING        TS_WAIT      performing GC
  kWaitingForDebuggerSend,          // WAITING        TS_WAIT      blocked waiting for events to be sent
  kWaitingForDebuggerToAttach,      // WAITING        TS_WAIT      blocked waiting for debugger to attach
  kWaitingInMainDebuggerLoop,       // WAITING        TS_WAIT      blocking/reading/processing debugger events
  kWaitingForDebuggerSuspension,    // WAITING        TS_WAIT      waiting for debugger suspend all
  kWaitingForJniOnLoad,             // WAITING        TS_WAIT      waiting for execution of dlopen and JNI on load code
  kWaitingForSignalCatcherOutput,   // WAITING        TS_WAIT      waiting for signal catcher IO to complete
  kWaitingInMainSignalCatcherLoop,  // WAITING        TS_WAIT      blocking/reading/processing signals
  kWaitingForDeoptimization,        // WAITING        TS_WAIT      waiting for deoptimization suspend all
  kWaitingForMethodTracingStart,    // WAITING        TS_WAIT      waiting for method tracing to start
  kWaitingForVisitObjects,          // WAITING        TS_WAIT      waiting for visiting objects
  kWaitingForGetObjectsAllocated,   // WAITING        TS_WAIT      waiting for getting the number of allocated objects
  kWaitingWeakGcRootRead,           // WAITING        TS_WAIT      waiting on the GC to read a weak root
  kWaitingForGcThreadFlip,          // WAITING        TS_WAIT      waiting on the GC thread flip (CC collector) to finish
  kNativeForAbort,                  // WAITING        TS_WAIT      checking other threads are not run on abort.
  kStarting,                        // NEW            TS_WAIT      native thread started, not yet ready to run managed code
  kNative,                          // RUNNABLE       TS_RUNNING   running in a JNI native method
  kSuspended,                       // RUNNABLE       TS_RUNNING   suspended by GC or debugger
};

```

一個處於R態的線程表明它邏輯上正在運行(由於調度的關系,他可能暫時未被執行,但總會被執行[一定時間內]),而它運行的代碼可能位於kernel層,native層或java層。只有當它運行在java層時,虛擬機中記載的狀態才是Runnable。

如果target thread處於非Runnable狀態,也就意味它並未處於java層。可是不處在java層並不表示它不運行。在"Signal Catcher"代理target thread進行收集的過程中,target thread隨時可能返回java層(結束了native層的工作或是發起了對java方法的調用)。一旦返回java層,java層的調用棧形態就會被改變。這樣"Signal Catcher"和target thread之間對於調用棧整體形態就會存在競爭關系。

因此我們需要一種方案去解決這種競爭。

所有返回到java層的操作都需要進行線程狀態切換,也即調用TransitionFromSuspendedToRunnable函數。該函數內部會判斷suspend標志位,一旦它被置上,target thread就會等待在resume_cond_條件變量上。因此,置上suspend標志位可以保證target thread無法返回java層,也即無法改變java層的調用棧形態。(值得注意的是,網上有些言論認為置上suspend標志位是為了暫停線程,這其實是一種不嚴謹的認識。對於不想返回java層的線程而言,置上suspend標志位絲毫不影響它的運行。

  1. 為什么需要在信息收集結束后調用resume_cond_條件變量的Broadcast函數?

因為有些准備返回java層的線程此時正等待在resume_cond_條件變量上(處於S態),當執行完收集操作后,我們有必要喚醒它們讓它們繼續工作。

  1. 分析了這么多,舉個實際案例吧。通過native的#2可以知道,主線程已經結束了native層的工作,希望返回到java層。不過從堆棧中我們找不到TransitionFromSuspendedToRunnable的身影,原因是它被inline(內聯)到GoToRunnable函數內部了。而#1 WaitingHoldingLocks等待的就是resume_cond_條件變量。
    "main" prio=5 tid=1 Native
      | group="main" sCount=1 ucsCount=0 flags=1 obj=0x71a33c18 self=0xb400006f417a1380
      | sysTid=14756 nice=-10 cgrp=top-app sched=0/0 handle=0x71027344f8
      | state=S schedstat=( 603683604122 79803215759 1916541 ) utm=43513 stm=16854 core=6 HZ=100
      | stack=0x7fe8361000-0x7fe8363000 stackSize=8188KB
      | held mutexes=
      native: #00 pc 000000000004dff0  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+32)
      native: #01 pc 000000000028dc74  /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+152)
      native: #02 pc 000000000074c4ec  /apex/com.android.art/lib64/libart.so (art::GoToRunnable(art::Thread*)+412)
      native: #03 pc 000000000074c318  /apex/com.android.art/lib64/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+28)
      at android.os.BinderProxy.transactNative(Native method)
      at android.os.BinderProxy.transact(BinderProxy.java:571)
      at com.android.internal.telephony.ISub$Stub$Proxy.getAvailableSubscriptionInfoList(ISub.java:1543)
      at android.telephony.SubscriptionManager.getAvailableSubscriptionInfoList(SubscriptionManager.java:1640)

不過需要注意,這種trace只會在如下的時序條件下生成。如果運行在native層的函數沒有結束,那么也就不需要返回java層,同時也就不會調用GoToRunnable。

因此,當我們發生ANR時看到主線程的調用棧如上所示,千萬不要認為GoToRunnable才是ANR的元凶。它僅僅表明線程在執行過程中希望回到java層,而真正導致ANR的可能是一個消息的整體耗時。

4. Java調用棧收集

(這一小節較為粗略,如無興趣可跳過)

通過調用StackDumpVisitor::WalkStack函數,我們可以收集到java層的調用棧信息。這個函數的內部比較復雜,想要完整的了解還要補充ArtMethod及DexFile等一系列知識。本文不打算完整介紹,只是概括性地總結。

機器碼的每條指令都有編號,它在運行時表現為PC值。同樣,Dex字節碼的每條指令也有編號,它在Dex文件中表現為dex_pc(每個方法的dex_pc都從0開始編號)。譬如下方文件中的0x0003、0x0008等就是dex_pc。

DEX CODE:
  0x0000: 7010 5350 0100           	| invoke-direct {v1}, void android.media.IPlayer$Stub.<init>() // method@20563
  0x0003: 2200 791f                	| new-instance v0, java.lang.ref.WeakReference // type@TypeIndex[8057]
  0x0005: 7020 84fa 2000           	| invoke-direct {v0, v2}, void java.lang.ref.WeakReference.<init>(java.lang.Object) // method@64132
  0x0008: 5b10 582e                	| iput-object v0, v1, Ljava/lang/ref/WeakReference; android.media.PlayerBase$IPlayerWrapper.mWeakPB // field@11864
  0x000a: 0e00                     	| return-void

字節碼在實際運行時,可能被解釋執行,也可能被編譯成機器碼執行(AOT或JIT),而這兩種執行方式的調用棧回溯方法是不同的。原因是機器碼執行時,java方法在棧幀結構上表現得就像純native方法(S上引入了新的解釋器nterp,它的棧幀結構和機器碼執行也是一致的,因此性能比之前的mterp更好);而解釋執行(這里指mterp解釋器)會有專門的數據結構來記錄dex_pc值。

當我們希望回溯出一幀java調用棧信息時,其實是想得到三個信息:方法名,文件名以及行號(至於鎖信息,它並非每一幀都有,因此屬於另一個話題,這里不做闡述)。

at android.os.Looper.loop(Looper.java:174)

想要得到這三個信息,其實依賴的數據也有三個:ArtMethod對象、DexFile信息及dex_pc值。由於DexFile信息可以通過ArtMethod間接得到,因此我們在回溯的過程中,主要目的就是為每一幀尋找它的ArtMehtod對象和dex_pc值。

這個尋找對於解釋執行來說很簡單,因為解釋執行會有專門的數據結構來記錄它,這個特定的數據結構就是ShadowFrame。

可是對於機器碼執行來說,問題就變得復雜了很多。好在每一幀的機器碼執行都遵循一個規律:棧頂存放當前執行方法的ArtMethod指針。因此當一連串的方法調用發生時,我們可以僅憑最后一幀的sp值就解析出所有信息,原理如下:

  1. 通過sp值,我們兩次解引用可以獲得當前運行方法的ArtMethod對象。
  2. 通過ArtMethod進一步獲取FrameInfo,其中可知frame size。
  3. sp+frame size便可以得知上一幀的sp值。
  4. 通過上一幀的sp也可以獲知返回地址的值,通常存於x30寄存器,方法調用時會壓入棧中固定偏移的位置。

因此,我們可以獲取每一幀的ArtMethod對象及pc值(最頂上的一幀要么是native方法,要么是runtime方法,都不需要恢復行號)。通過如下方法,便可以進一步得到dex_pc值,這樣每一幀的詳細信息便可以解析出來。

uint32_t StackVisitor::GetDexPc(bool abort_on_failure) const {
  if (cur_shadow_frame_ != nullptr) {
    return cur_shadow_frame_->GetDexPC();
  } else if (cur_quick_frame_ != nullptr) {
    if (IsInInlinedFrame()) {
      return current_inline_frames_.back().GetDexPc();
    } else if (cur_oat_quick_method_header_ == nullptr) {
      return dex::kDexNoIndex;
    } else if ((*GetCurrentQuickFrame())->IsNative()) {
      return cur_oat_quick_method_header_->ToDexPc(
          GetCurrentQuickFrame(), cur_quick_frame_pc_, abort_on_failure);
    } else if (cur_oat_quick_method_header_->IsOptimized()) {
      StackMap* stack_map = GetCurrentStackMap();
      DCHECK(stack_map->IsValid());
      return stack_map->GetDexPc();
    } else {
      DCHECK(cur_oat_quick_method_header_->IsNterpMethodHeader());
      return NterpGetDexPC(cur_quick_frame_);
    }
  } else {
    return 0;
  }
}

不過我們上述的描述中還省略了一種情況,也即java inline的情況,它在unwind的過程中也是耗時的大戶。

5. Native調用棧收集

在正常的trace生成過程中,一個線程的native調用棧是否收集取決於以下函數的判斷,如下序號表示判斷優先級。

static bool ShouldShowNativeStack(const Thread* thread)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  ThreadState state = thread->GetState();

  // In native code somewhere in the VM (one of the kWaitingFor* states)? That's interesting.
  if (state > kWaiting && state < kStarting) {
    return true;
  }

  // In an Object.wait variant or Thread.sleep? That's not interesting.
  if (state == kTimedWaiting || state == kSleeping || state == kWaiting) {
    return false;
  }

  // Threads with no managed stack frames should be shown.
  if (!thread->HasManagedStack()) {
    return true;
  }

  // In some other native method? That's interesting.
  // We don't just check kNative because native methods will be in state kSuspended if they're
  // calling back into the VM, or kBlocked if they're blocked on a monitor, or one of the
  // thread-startup states if it's early enough in their life cycle (http://b/7432159).
  ArtMethod* current_method = thread->GetCurrentMethod(nullptr);
  return current_method != nullptr && current_method->IsNative();
}
  1. 當state是虛擬機相關的狀態時,需要收集native調用棧。那什么是虛擬機相關的狀態呢?譬如kWaitingForGcToComplete,它表示當前線程在等待GC結束。因此我們可以理解這些狀態是因為虛擬機自身工作而影響到本線程運行的狀態。

  2. 如果state是Waiting或Sleeping相關的狀態時,則省略native調用棧的收集。因為處於此狀態的線程,其native層的調用棧最終必然為futex系統調用,因此輸出這些調用棧並不會給調試帶來有價值的信息,故可省略。

  3. 如果該線程沒有java層調用棧信息,則需要收集native調用棧,否則沒有任何信息可輸出。

  4. 如果java層調用棧的最后一幀為native方法,則需要收集native調用棧,以便了解native層具體的動作。

接下來就要討論如何收集native調用棧,這個過程的專業術語叫做回溯或是展開(unwind),在Android中主要通過libunwindstack這個庫來完成。

收集native調用棧,本質上是尋找每一幀的pc值。當我們拿到最后一幀的sp值后,便可以通過尋找返回地址的方式,不斷回溯出其上的每一幀pc值。

因此,接下來的問題可以簡化為兩個:

  1. 如何尋找最后一幀的寄存器(sp/pc)值?
  2. 如何尋找每一幀的返回地址?

5.1 如何尋找最后一幀的寄存器值

寄存器信息本質上是線程相關的,因此這里分為兩種情況來討論。

  1. 本線程收集本線程的調用棧。
  2. "Signal Catcher"線程收集其他線程的調用棧。

本線程獲取寄存器值較為簡單,只需要一些基本的匯編指令便可以完成。譬如如下代碼,可以將32個通用寄存器的值存到用戶空間特定的數據結構中。

inline __attribute__((__always_inline__)) void AsmGetRegs(void* reg_data) {
  asm volatile(
      "1:\n"
      "stp x0, x1, [%[base], #0]\n"
      "stp x2, x3, [%[base], #16]\n"
      "stp x4, x5, [%[base], #32]\n"
      "stp x6, x7, [%[base], #48]\n"
      "stp x8, x9, [%[base], #64]\n"
      "stp x10, x11, [%[base], #80]\n"
      "stp x12, x13, [%[base], #96]\n"
      "stp x14, x15, [%[base], #112]\n"
      "stp x16, x17, [%[base], #128]\n"
      "stp x18, x19, [%[base], #144]\n"
      "stp x20, x21, [%[base], #160]\n"
      "stp x22, x23, [%[base], #176]\n"
      "stp x24, x25, [%[base], #192]\n"
      "stp x26, x27, [%[base], #208]\n"
      "stp x28, x29, [%[base], #224]\n"
      "str x30, [%[base], #240]\n"
      "mov x12, sp\n"
      "adr x13, 1b\n"
      "stp x12, x13, [%[base], #248]\n"
      : [base] "+r"(reg_data)
      :
      : "x12", "x13", "memory");
}

可如果是跨線程(不是跨進程)來獲取,該如何處理呢?

答案是通過信號。當目標線程運行在用戶空間時,寄存器值是不會有備份的。只有當它發生用戶態和內核態的切換時,信息才會被備份。此外,切換的過程也會檢測信號,進而觸發信號處理函數,因此剛剛備份的寄存器信息可以進一步傳遞給處理函數。而那里,才是我們跨線程獲取寄存器值的真正位置。

Android中采用信號33(THREAD_SIGNAL)來完成這項工作,它的處理函數也比較簡單,即將sigcontext中的寄存器信息拷貝到全局數據中,這樣其他線程便可以獲取它。

5.2 如何尋找每一幀的返回地址

當函數調用發生時,返回地址通常存在x30寄存器(AArch64)中。如果被調用者內部需要使用這個寄存器,那么它的起始片段肯定要將x30的值存到棧中,否則返回地址將丟失。可是x30的值到底存在棧中什么位置呢?

當開啟-fomit-frame-pointer編譯選項時,x30存儲的位置和x29(FP寄存器)相鄰,因此很容易找出。可是沒有此編譯選項時,x30的值就得依賴更多的信息來獲取。在64位庫中,這個信息稱為"Call Frame Information",它存儲在elf文件的.eh_frame段。微信技術團隊的一篇文章關於這點描述的比較清楚,引用如下:

當你的代碼執行到某一“行”時,根據此時的 pc 我們可以從"Call Frame Information"中查詢到退出當前函數棧時各個寄存器該怎么進行恢復,比如它可能描述了寄存器的值該從當前棧的哪個位置上讀回來。

除了可以unwind純native的幀,libunwindstack庫還支持AOT/JIT的幀以及解釋執行的幀。這也就表明,通過libunwindstack收集的調用棧除了可以反映native層調用信息,還可以反映java層的調用信息,如下示例。

#00 pc 000aa0f8  /system/lib/libart.so (void std::__1::__tree_balance_after_insert<std::__1::__tree_node_base<void*>*>(std::__1::__tree_node_base<void*>*, std::__1::__tree_balance_after_insert<std::__1::__tree_node_base<void*>*>)+32)
#01 pc 001a0a35  /system/lib/libart.so (art::gc::space::LargeObjectMapSpace::Alloc(art::Thread*, unsigned int, unsigned int*, unsigned int*, unsigned int*)+180)
#02 pc 003cd4f5  /system/lib/libart.so (art::mirror::Object* art::gc::Heap::AllocLargeObject<false, art::mirror::SetLengthVisitor>(art::Thread*, art::ObjPtr<art::mirror::Class>*, unsigned int, art::mirror::SetLengthVisitor const&)+108)
#03 pc 003cb659  /system/lib/libart.so (artAllocArrayFromCodeResolvedRegionTLAB+484)
#04 pc 00411613  /system/lib/libart.so (art_quick_alloc_array_resolved16_region_tlab+82)
#05 pc 0020cfe3  /system/framework/arm/boot-core-oj.oat (offset 0x10d000) (java.lang.AbstractStringBuilder.append+242)
#06 pc 002b809b  /system/framework/arm/boot-core-oj.oat (offset 0x10d000) (java.lang.StringBuilder.append+50)
#07 pc 001199b7  /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextString+214)
#08 pc 00119b73  /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextValue+162)
#09 pc 001195db  /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.readObject+314)
#10 pc 00119b47  /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextValue+118)
#11 pc 0040d775  /system/lib/libart.so (art_quick_invoke_stub_internal+68)
#12 pc 003e72c9  /system/lib/libart.so (art_quick_invoke_stub+224)
#13 pc 000a103d  /system/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+136)
#14 pc 001e60f1  /system/lib/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+236)
#15 pc 001e0bdf  /system/lib/libart.so (bool art::interpreter::DoCall<false, false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+814)
#16 pc 003e1f23  /system/lib/libart.so (MterpInvokeVirtual+442)
#17 pc 00400514  /system/lib/libart.so (ExecuteMterpImpl+14228)
#18 pc 002613ec  /system/priv-app/ReusLauncherDev/ReusLauncherDev.apk (offset 0x9c9000) (com.reus.launcher.AsusAnimationIconReceiver.a+80)
#19 pc 001c535b  /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEb.llvm.866626450+378)
#20 pc 001c9a41  /system/lib/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*, art::JValue*)+152)
#21 pc 001e0bc7  /system/lib/libart.so (bool art::interpreter::DoCall<false, false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+790)
#22 pc 003e2eff  /system/lib/libart.so (MterpInvokeStatic+130)
#23 pc 00400694  /system/lib/libart.so (ExecuteMterpImpl+14612)
#24 pc 0028ae7a  /system/priv-app/ReusLauncherDev/ReusLauncherDev.apk (offset 0x9c9000) (com.reus.launcher.d.run+1274)
#25 pc 001c535b  /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEb.llvm.866626450+378)
#26 pc 001c9987  /system/lib/libart.so (art::interpreter::EnterInterpreterFromEntryPoint(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*)+82)
#32 pc 0040d775  /system/lib/libart.so (art_quick_invoke_stub_internal+68)
#33 pc 003e72c9  /system/lib/libart.so (art_quick_invoke_stub+224)
#34 pc 000a103d  /system/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+136)
#36 pc 00348f6d  /system/lib/libart.so (art::InvokeVirtualOrInterfaceWithJValues(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, jvalue*)+320)
#37 pc 00369ee7  /system/lib/libart.so (art::Thread::CreateCallback(void*)+866)
#38 pc 00072131  /system/lib/libc.so (__pthread_start(void*)+22)
#39 pc 0001e005  /system/lib/libc.so (__start_thread+24)

24是虛擬幀,也即它並非存在於棧上,而是輔助調試所增加的信息,它反映的就是2325這幾幀解釋器正在解釋的java方法。0510反映的是機器碼執行(AOT編譯)的java方法。00~04反映的是純native層(so庫)的函數調用。

那么這時就有一個問題縈繞在我們心頭:libunwindstack可以收集到java層的調用信息,那為什么trace文件中的native調用棧僅僅顯示了native層的調用信息呢?

原因是trace文件在收集調用棧時做了截斷和省略。具體的策略如下:

  1. 在回溯的過程中,如果碰到所屬文件后綴為oat或odex的幀,則停止回溯。原因是JNI的跳板函數/AOT編譯的java方法通常都在oat/odex文件中,碰到它們停止回溯,可以略去后續java方法的回溯。
backtrace_map_->SetSuffixesToIgnore(std::vector<std::string> { "oat", "odex" });
  1. 回溯的棧幀中如果有落在"libunwindstack.so"和"libbacktrace.so"的幀,則不予顯示。原因是這些幀反映的是調用棧收集過程,而非線程原始的調用邏輯。
std::vector<std::string> skip_names{"libunwindstack.so", "libbacktrace.so"};

5.3 當前調用棧回溯的缺陷

仔細思考上述策略的第一條,其實可以發現它是有缺陷的。這個缺陷主要有兩點:

  1. JNI跳板函數一定在oat/odex文件中么?

其實並不是。在dex2oat階段,系統會為一個oat/odex文件中參數兼容(個數相同且類型相似)的native方法統一生成一個JNI跳板函數。這點可以展開舉個例子。

#05 pc 00000000000eeb24  /system/lib64/libandroid_runtime.so (android::nativeCreate(_JNIEnv*, _jclass*, _jstring*, int)+132)
    #06 pc 00000000003dff04  /system/framework/arm64/boot-framework.oat (offset 0x3d6000) (android.graphics.FontFamily.nInitBuilder [DEDUPED]+180)
    #07 pc 000000000091414c  /system/framework/arm64/boot-framework.oat (offset 0x3d6000) (android.database.CursorWindow.<init>+172)

如上調用棧是Android S以前tombstone文件的典型輸出。通過代碼我們可以得知,#7中的CursorWindow構造方法明明調用的是nativeCreate方法,可為什么回溯出來的#6卻是nInitBuilder?原因正是一個JNI跳板函數可以被多個native方法使用,而回溯時只是從眾多native方法中挑了一個名字顯示出來。因此后面的DEDUPED正是提醒我們,這一幀是不可信的。具體解釋如下:

    ## DEDUPED frames

    If the name of a Java method includes `[DEDUPED]`, this means that multiple
    methods share the same code. ART only stores the name of a single one in its
    metadata, which is displayed here. This is not necessarily the one that was
    called.

繼續查看nativeCreate和nInitBuilder的方法定義,可以發現它們的參數個數和類型均相同,因此在dex2oat后可以共用一個JNI跳板函數。

private static native long nativeCreate(String name, int cursorWindowSize);
private static native long nInitBuilder(String langs, int variant);

好在從Android S開始,這一幀不再顯示具體的方法名,而是統一的art_jni_trampoline,這樣可以減少對開發者的困擾。如下示例。

    #05 pc 00000000004a600c  /apex/com.android.art/lib64/libart.so (art::VMDebug_countInstancesOfClass(_JNIEnv*, _jclass*, _jclass*, unsigned char)+876) (BuildId: 2ede688a1cdde049a8439e413c1c41f8)
    #06 pc 0000000000010fb4  /apex/com.android.art/javalib/arm64/boot-core-libart.oat (art_jni_trampoline+180) (BuildId: a58ab7e35be2dda5ad3453c56bfefea6edf331bf)
    #07 pc 000000000064037c  /system/framework/arm64/boot-framework.oat (android.os.Debug.countInstancesOfClass+44) (BuildId: e47113da18d4f822af52023fa19893d55035facd)
    #08 pc 0000000000812930  /system/framework/arm64/boot-framework.oat (android.view.ViewDebug.getViewRootImplCount+48) (BuildId: e47113da18d4f822af52023fa19893d55035facd)

書歸正題,dex2oat生成的JNI跳板函數其實是位於oat/odex文件中的。但還有一種情況,dex2oat並不會為native方法生成JNI跳板函數,而是在運行時采用統一的art_quick_generic_jni_trampoline來動態執行參數傳遞和狀態切換。這時,art_quick_generic_jni_trampoline位於libart.so中,不符合oat/odex后綴的規律,因此調用棧回溯碰到這一幀時會繼續進行。而如果后續的java方法全都是解釋執行,那么解釋執行的幀也將全部回溯出來,如下示例。

    "Binder:1083_11" prio=5 tid=127 Native
      | group="main" sCount=1 dsCount=0 flags=1 obj=0x16002138 self=0xb40000715181e940
      | sysTid=6990 nice=0 cgrp=default sched=0/0 handle=0x6f5580fcc0
      | state=S schedstat=( 4739949803 13009985270 12510 ) utm=234 stm=239 core=3 HZ=100
      | stack=0x6f55718000-0x6f5571a000 stackSize=995KB
      | held mutexes=
      native: #00 pc 000000000009aa34  /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+4)
      native: #01 pc 0000000000057564  /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156)
      native: #02 pc 00000000000999d4  /system/lib64/libhidlbase.so (android::hardware::IPCThreadState::transact(int, unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int)+564)
      native: #03 pc 0000000000094e84  /system/lib64/libhidlbase.so (android::hardware::BpHwBinder::transact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function<void (android::hardware::Parcel&)>)+76)
      native: #04 pc 000000000000e538  /system/lib64/android.system.suspend@1.0.so (android::system::suspend::V1_0::BpHwSystemSuspend::_hidl_acquireWakeLock(android::hardware::IInterface*, android::hardware::details::HidlInstrumentor*, android::system::suspend::V1_0::WakeLockType, android::hardware::hidl_string const&)+324)
      native: #05 pc 0000000000003178  /system/lib64/libhardware_legacy.so (acquire_wake_lock+356)
      native: #06 pc 0000000000086648  /system/lib64/libandroid_servers.so (android::nativeAcquireSuspendBlocker(_JNIEnv*, _jclass*, _jstring*)+64)
      native: #07 pc 000000000013ced4  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+148)
      native: #08 pc 00000000001337e8  /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+568)
      native: #09 pc 00000000001a8a94  /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+228)
      native: #10 pc 0000000000318240  /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+376)
      native: #11 pc 000000000030e56c  /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall<false, false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+996)
      native: #12 pc 000000000067e098  /apex/com.android.art/lib64/libart.so (MterpInvokeStatic+548)
      native: #13 pc 000000000012d994  /apex/com.android.art/lib64/libart.so (mterp_op_invoke_static+20)
      native: #14 pc 0000000000617e00  /system/framework/services.jar (com.android.server.power.PowerManagerService.access$600)
      native: #15 pc 000000000067e33c  /apex/com.android.art/lib64/libart.so (MterpInvokeStatic+1224)
      native: #16 pc 000000000012d994  /apex/com.android.art/lib64/libart.so (mterp_op_invoke_static+20)
      native: #17 pc 0000000000614fec  /system/framework/services.jar (com.android.server.power.PowerManagerService$NativeWrapper.nativeAcquireSuspendBlocker)
      native: #18 pc 000000000067b3e0  /apex/com.android.art/lib64/libart.so (MterpInvokeVirtual+1520)
      native: #19 pc 000000000012d814  /apex/com.android.art/lib64/libart.so (mterp_op_invoke_virtual+20)
      native: #20 pc 00000000006152b0  /system/framework/services.jar (com.android.server.power.PowerManagerService$SuspendBlockerImpl.acquire+52)
      native: #21 pc 0000000000305b68  /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.llvm.10833873914857160001)+268)
      native: #22 pc 0000000000669e48  /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+780)
      native: #23 pc 000000000013cff8  /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88)
      native: #24 pc 00000000021f4bc4  /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.updateSuspendBlockerLocked+228)
      native: #25 pc 000000000201cf6c  /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.updatePowerStateLocked+988)
      native: #26 pc 00000000021a3800  /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.acquireWakeLockInternal+1712)
      native: #27 pc 000000000205640c  /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService$BinderService.acquireWakeLock+524)
      native: #28 pc 0000000002040b64  /memfd:jit-cache (deleted) (offset 2000000) (android.os.IPowerManager$Stub.onTransact+8340)
      native: #29 pc 00000000020c95a4  /memfd:jit-cache (deleted) (offset 2000000) (android.os.Binder.execTransactInternal+996)
      native: #30 pc 00000000020b9a0c  /memfd:jit-cache (deleted) (offset 2000000) (android.os.Binder.execTransact+284)
      native: #31 pc 0000000000133564  /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+548)
      native: #32 pc 00000000001a8a78  /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+200)
      native: #33 pc 0000000000553c70  /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeVirtualOrInterfaceWithVarArgs<art::ArtMethod*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, std::__va_list)+468)
      native: #34 pc 0000000000553e10  /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeVirtualOrInterfaceWithVarArgs<_jmethodID*>(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)+92)
      native: #35 pc 00000000003a0920  /apex/com.android.art/lib64/libart.so (art::JNI<false>::CallBooleanMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)+660)
      native: #36 pc 000000000009c698  /system/lib64/libandroid_runtime.so (_JNIEnv::CallBooleanMethod(_jobject*, _jmethodID*, ...)+124)
      native: #37 pc 0000000000124064  /system/lib64/libandroid_runtime.so (JavaBBinder::onTransact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+156)
      native: #38 pc 000000000004882c  /system/lib64/libbinder.so (android::BBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+232)
      native: #39 pc 0000000000051110  /system/lib64/libbinder.so (android::IPCThreadState::executeCommand(int)+1032)
      native: #40 pc 0000000000050c58  /system/lib64/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+156)
      native: #41 pc 0000000000051490  /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+60)
      native: #42 pc 00000000000773e0  /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24)
      native: #43 pc 000000000001549c  /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+260)
      native: #44 pc 00000000000a2590  /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+144)
      native: #45 pc 0000000000014d60  /system/lib64/libutils.so (thread_data_t::trampoline(thread_data_t const*)+412)
      native: #46 pc 00000000000af808  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64)
      native: #47 pc 000000000004fc88  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
      at com.android.server.power.PowerManagerService.nativeAcquireSuspendBlocker(Native method)
      at com.android.server.power.PowerManagerService.access$600(PowerManagerService.java:125)
      at com.android.server.power.PowerManagerService$NativeWrapper.nativeAcquireSuspendBlocker(PowerManagerService.java:713)
      at com.android.server.power.PowerManagerService$SuspendBlockerImpl.acquire(PowerManagerService.java:4643)
      - locked <0x073deae2> (a com.android.server.power.PowerManagerService$SuspendBlockerImpl)
      at com.android.server.power.PowerManagerService.updateSuspendBlockerLocked(PowerManagerService.java:3067)
      at com.android.server.power.PowerManagerService.updatePowerStateLocked(PowerManagerService.java:1956)
      at com.android.server.power.PowerManagerService.acquireWakeLockInternal(PowerManagerService.java:1320)
      - locked <0x03f99c8c> (a java.lang.Object)
      at com.android.server.power.PowerManagerService.access$4600(PowerManagerService.java:125)
      at com.android.server.power.PowerManagerService$BinderService.acquireWakeLock(PowerManagerService.java:4780)
      at android.os.IPowerManager$Stub.onTransact(IPowerManager.java:421)
      at android.os.Binder.execTransactInternal(Binder.java:1154)
      at android.os.Binder.execTransact(Binder.java:1123)

可以發現,native標記的調用棧里其實包含了java層的信息,因此java層的信息輸出了兩遍(信息冗余)。如果不了解棧回溯的具體原理,恐怕很多人都會好奇:為什么nativeAcquireSuspendBlocker的java方法會調用到#47的__start_thread?這其實並不是真實的調用路徑,而只是因為當下的trace調用棧收集方案中有些缺陷。

  1. 當調用棧回溯碰到位於oat/odex文件里的JNI跳板函數時,則停止回溯。這種方案適用於大多數場景。可是如果函數調用呈現如下的交錯情形,那么現行方案會丟失部分調用棧。
    Java method A
    ↓(call)
    Native method B
    ↓(call)
    Java method C
    ↓(call)
    Native method D

最終回溯出的整體調用棧信息中,Native method B將不見蹤影,因為native層的回溯在碰到C時就已經結束了。以下是一個實際的案例。

    "Binder:1540_2" prio=5 tid=9 Blocked
    | group="main" sCount=1 dsCount=0 flags=1 obj=0x13700580 self=0x7e0c139800
    | sysTid=1560 nice=-2 cgrp=default sched=0/0 handle=0x7df07474f0
    | state=S schedstat=( 126689305075 80266662086 342299 ) utm=8978 stm=3690 core=0 HZ=100
    | stack=0x7df064c000-0x7df064e000 stackSize=1009KB
    | held mutexes=
    at com.android.server.LocationManagerService.isProviderEnabledForUser(LocationManagerService.java:2813)
    - waiting to lock <0x07cdf9c8> (a java.lang.Object) held by thread 11
    at android.location.ILocationManager$Stub.onTransact(ILocationManager.java:488)
    at android.os.Binder.execTransact(Binder.java:726)
    (---丟失了這中間的native調用棧---)
    at android.os.BinderProxy.transactNative(Native method)
    at android.os.BinderProxy.transact(BinderProxy.java:473)
    at android.location.IGeocodeProvider$Stub$Proxy.getFromLocation(IGeocodeProvider.java:143)
    at com.android.server.location.GeocoderProxy$1.run(GeocoderProxy.java:79)
    at com.android.server.ServiceWatcher.runOnBinder(ServiceWatcher.java:425)
    - locked <0x0d7e7a61> (a java.lang.Object)
    at com.android.server.location.GeocoderProxy.getFromLocation(GeocoderProxy.java:74)
    at com.android.server.LocationManagerService.getFromLocation(LocationManagerService.java:3341)
    at android.location.ILocationManager$Stub.onTransact(ILocationManager.java:217)
    at android.os.Binder.execTransact(Binder.java:726)

該線程首先發起了一個通往對端進程的binder通信,對端進程在處理的過程中又給本進程發起了新的通信。基於binder transaction stack的設計,這個新的通信必然交給當初的線程。因此execTransact表明它正在處理這個通信。在transactNative和execTransact之間,其實漏掉了native層的幀。

這兩個缺陷其實都是小問題,無傷大雅。跟Google工程師反饋溝通以后,他們表示大概率在T上修復這些問題。

結語

當我們在解決大多數APP問題時,調用棧都是最重要的分析素材。如果它總能完美地反映線程的執行邏輯,那么是否了解細節其實不重要。可現實並非如此。ANR的某些場景下,線程可能卡在GoToRunnable中;交錯調用的情況下,中間的native方法可能丟失。等等。這些時候的調用棧會出現讓人迷惑的信息,而只有了解回溯的細節,才能真正解惑。

Android高級開發系統進階筆記、最新面試復習筆記PDF,我的GitHub

文末

您的點贊收藏就是對我最大的鼓勵!
歡迎關注我的簡書,分享Android干貨,交流Android技術。
對文章有何見解,或者有何技術問題,歡迎在評論區一起留言討論!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM