Android Runtime | Trace文件的生成機制

本文分析基于Android S(12)

當App發(fā)生ANR或是System觸發(fā)watchdog時掸掸,系統(tǒng)都希望生成一份trace文件采盒,用來記錄各個線程的調(diào)用棧信息背伴,以及一些進程/線程的狀態(tài)信息树姨。這份文件通常存放在/data/anr目錄下蹈集,APP開發(fā)者拿不到。不過從Android R(11)開始犀暑,App便可以通過AMS的getHistoricalProcessExitReasons接口讀取該文件的詳細信息驯击。以下是一份典型trace文件中的內(nèi)容。

----- 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
...
(進程整體的一些狀態(tài)耐亏,譬如GC的統(tǒng)計數(shù)據(jù))

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的觸發(fā)類型徊都,也無意于流水賬式地展示每塊內(nèi)容的生成順序,因為這些已經(jīng)有不少文章寫過广辰,而且其中不乏精品暇矫。鑒于此,本文將重點分析調(diào)用棧的生成流程择吊,而它將幫助我們更好地理解trace信息李根。

前言

不論是ANR還是Watchdog,trace的生成過程都放在target process中進行几睛。以ANR為例房轿,它的判定過程發(fā)生在system_server(AMS)中,而trace的生成過程卻發(fā)生在APP中所森。那么如何讓APP開始這個過程呢囱持?答案是給它發(fā)送SIGQUIT(信號3)。之所以這樣處理必峰,是因為跨進程的信息收集通常采用ptrace方案洪唐,它要求收集方要么擁有特殊權(quán)限钻蹬,要么滿足進程間父子關(guān)系吼蚁,而這些都沒有進程內(nèi)收集方便。

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

1. Signal Catcher線程

"Signal Catcher"線程在每個Java進程中都會存在。正常運行時顺献,它將掛起等待信號3(以及信號10)的到來旗国。當該進程接收到信號3時,將會交由"Signal Catcher"線程處理注整,處理的函數(shù)為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());
} 

中間的跳轉(zhuǎn)過程就不展示了度硝,直接進入我們關(guān)心的議題:調(diào)用棧的收集過程。通過ThreadList::Dump函數(shù)寿冕,我們可以收集所有線程的調(diào)用棧信息蕊程。

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);
  }
}

其中關(guān)鍵的環(huán)節(jié)是執(zhí)行RunCheckpoint函數(shù)。它將每個線程的信息收集分為單獨的任務(wù):**如果該線程正處在Runnable狀態(tài)(運行java代碼)驼唱,則將收集的任務(wù)派發(fā)給線程自己處理藻茂;如果該線程處于其他狀態(tài),則由"Signal Catcher"線程代為完成玫恳。**請記住這句話辨赐,因為下面2、3小節(jié)分析的就是它的兩種不同情況京办。

2. Checkpoint機制

將任務(wù)派發(fā)給Runnable狀態(tài)的線程采用的是checkpoint機制掀序,它分為兩個部分:

  1. "Signal Catcher"線程調(diào)用RequestCheckpoint去改變目標線程的art::Thread對象的內(nèi)部數(shù)據(jù),具體而言改變的是以下兩個字段惭婿。

    tls32_.state_and_flags.as_struct.flags |= kCheckpointRequest;
    tlsPtr_.checkpoint_function = function;
    (tls32_和tlsPtr_均是art::Thread對象的內(nèi)部數(shù)據(jù))
    
    
  2. 對ART虛擬機而言森枪,目標線程在每個方法的起始位置和循環(huán)語句的跳轉(zhuǎn)位置都會去檢查state_and_flags字段,如果checkpoint位被置上审孽,則執(zhí)行相應(yīng)的checkpoint函數(shù)县袱。這樣安插檢查點可以保證線程”及時“地處理checkpoint任務(wù):因為所有向前執(zhí)行(直線型、帶條件分支都算)的代碼都會在有限時間內(nèi)執(zhí)行完佑力,而可能導(dǎo)致長時間執(zhí)行的代碼式散,要么是循環(huán),要么是方法調(diào)用打颤,所以只要在這兩個地方插入檢查點就可以保證及時性了暴拄。(參考R大知乎回答

關(guān)于目標線程的檢查點,這里我還想舉個例子编饺,讓大家真切地感受到它的存在乖篷。

字節(jié)碼在ART虛擬機中可以解釋執(zhí)行,也可以編譯成機器碼執(zhí)行透且。當一個方法被編譯為機器碼后(如下所示)撕蔼,我們可以在函數(shù)的入口處看到有檢測state_and_flags的操作。當有標志位被置上時秽誊,則執(zhí)行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,則跳轉(zhuǎn)到0x3f9de4的位置
  ...
  0x003f9de4: 940e62c3  bl #+0x398b0c (addr 0x7928f0) ; pTestSuspend  //跳轉(zhuǎn)進入pTestSuspend

幾經(jīng)跳轉(zhuǎn)锅论,pTestSuspend最終會調(diào)用Thread::CheckSuspend函數(shù)讼溺。當checkpoint位被置上時,則執(zhí)行相應(yīng)的checkpoint函數(shù)(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線程自己收集調(diào)用棧的例子怒坯,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狀態(tài)的線程剔猿,收集的工作由"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. 運行相應(yīng)的函數(shù)替target thread收集信息弄慰。
  3. 減少target thread的suspend count(-1)第美,如果suspend count減為0,則清除suspend標志位陆爽。
  4. 調(diào)用resume_cond_條件變量的Broadcast函數(shù)什往,它會喚醒所有等待在它上面的線程。

流程的梳理總是簡單慌闭,難的是理解流程設(shè)計背后的原因别威,下面來條分縷析。

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

    在回答這個問題前省古,我們需要補充些基礎(chǔ)知識。每個Java線程本質(zhì)上都是一個pthread線程丧失,而它在內(nèi)核中又對應(yīng)一個task_struct對象豺妓,該對象是CPU調(diào)度的基本單元。從CPU的視角來看布讹,該線程可以是R態(tài)琳拭、S態(tài)、D態(tài)等描验,它們的含義如下所示白嘁。不過虛擬機中又為Java線程記錄了另一套狀態(tài),它反映的是虛擬機視角下的狀態(tài)膘流,具體分類如下絮缅。

    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態(tài)的線程表明它邏輯上正在運行(由于調(diào)度的關(guān)系,他可能暫時未被執(zhí)行睡扬,但總會被執(zhí)行[一定時間內(nèi)])盟蚣,而它運行的代碼可能位于kernel層黍析,native層或java層卖怜。只有當它運行在java層時,虛擬機中記載的狀態(tài)才是Runnable阐枣。

    如果target thread處于非Runnable狀態(tài)马靠,也就意味它并未處于java層奄抽。可是不處在java層并不表示它不運行甩鳄。在"Signal Catcher"代理target thread進行收集的過程中逞度,target thread隨時可能返回java層(結(jié)束了native層的工作或是發(fā)起了對java方法的調(diào)用)。一旦返回java層妙啃,java層的調(diào)用棧形態(tài)就會被改變档泽。這樣"Signal Catcher"和target thread之間對于調(diào)用棧整體形態(tài)就會存在競爭關(guān)系。

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

    所有返回到j(luò)ava層的操作都需要進行線程狀態(tài)切換馆匿,也即調(diào)用TransitionFromSuspendedToRunnable函數(shù)。該函數(shù)內(nèi)部會判斷suspend標志位燥滑,一旦它被置上渐北,target thread就會等待在resume_cond_條件變量上。因此铭拧,置上suspend標志位可以保證target thread無法返回java層赃蛛,也即無法改變java層的調(diào)用棧形態(tài)。(值得注意的是搀菩,網(wǎng)上有些言論認為置上suspend標志位是為了暫停線程呕臂,這其實是一種不嚴謹?shù)恼J識。對于不想返回java層的線程而言肪跋,置上suspend標志位絲毫不影響它的運行诵闭。

  2. 為什么需要在信息收集結(jié)束后調(diào)用resume_cond_條件變量的Broadcast函數(shù)?

    因為有些準備返回java層的線程此時正等待在resume_cond_條件變量上(處于S態(tài))澎嚣,當執(zhí)行完收集操作后疏尿,我們有必要喚醒它們讓它們繼續(xù)工作。

  3. 分析了這么多易桃,舉個實際案例吧褥琐。通過native的#2可以知道,主線程已經(jīng)結(jié)束了native層的工作晤郑,希望返回到j(luò)ava層敌呈。不過從堆棧中我們找不到TransitionFromSuspendedToRunnable的身影,原因是它被inline(內(nèi)聯(lián))到GoToRunnable函數(shù)內(nèi)部了造寝。而#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層的函數(shù)沒有結(jié)束析显,那么也就不需要返回java層,同時也就不會調(diào)用GoToRunnable签赃。

因此谷异,當我們發(fā)生ANR時看到主線程的調(diào)用棧如上所示分尸,千萬不要認為GoToRunnable才是ANR的元兇。它僅僅表明線程在執(zhí)行過程中希望回到j(luò)ava層歹嘹,而真正導(dǎo)致ANR的可能是一個消息的整體耗時箩绍。

4. Java調(diào)用棧收集

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

通過調(diào)用StackDumpVisitor::WalkStack函數(shù)尺上,我們可以收集到j(luò)ava層的調(diào)用棧信息材蛛。這個函數(shù)的內(nèi)部比較復(fù)雜,想要完整的了解還要補充ArtMethod及DexFile等一系列知識怎抛。本文不打算完整介紹仰税,只是概括性地總結(jié)。

機器碼的每條指令都有編號抽诉,它在運行時表現(xiàn)為PC值陨簇。同樣,Dex字節(jié)碼的每條指令也有編號迹淌,它在Dex文件中表現(xiàn)為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

字節(jié)碼在實際運行時耙饰,可能被解釋執(zhí)行,也可能被編譯成機器碼執(zhí)行(AOT或JIT)纹份,而這兩種執(zhí)行方式的調(diào)用椆豆颍回溯方法是不同的。原因是機器碼執(zhí)行時蔓涧,java方法在棧幀結(jié)構(gòu)上表現(xiàn)得就像純native方法(S上引入了新的解釋器nterp件已,它的棧幀結(jié)構(gòu)和機器碼執(zhí)行也是一致的,因此性能比之前的mterp更好)元暴;而解釋執(zhí)行(這里指mterp解釋器)會有專門的數(shù)據(jù)結(jié)構(gòu)來記錄dex_pc值篷扩。

當我們希望回溯出一幀java調(diào)用棧信息時,其實是想得到三個信息:方法名茉盏,文件名以及行號(至于鎖信息鉴未,它并非每一幀都有,因此屬于另一個話題鸠姨,這里不做闡述)铜秆。

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

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

這個尋找對于解釋執(zhí)行來說很簡單值纱,因為解釋執(zhí)行會有專門的數(shù)據(jù)結(jié)構(gòu)來記錄它鳞贷,這個特定的數(shù)據(jù)結(jié)構(gòu)就是ShadowFrame坯汤。

可是對于機器碼執(zhí)行來說,問題就變得復(fù)雜了很多搀愧。好在每一幀的機器碼執(zhí)行都遵循一個規(guī)律:棧頂存放當前執(zhí)行方法的ArtMethod指針惰聂。因此當一連串的方法調(diào)用發(fā)生時,我們可以僅憑最后一幀的sp值就解析出所有信息咱筛,原理如下:

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

因此奕塑,我們可以獲取每一幀的ArtMethod對象及pc值(最頂上的一幀要么是native方法堂污,要么是runtime方法,都不需要恢復(fù)行號)龄砰。通過如下方法盟猖,便可以進一步得到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調(diào)用棧收集

在正常的trace生成過程中碟案,一個線程的native調(diào)用棧是否收集取決于以下函數(shù)的判斷,如下序號表示判斷優(yōu)先級颇蜡。

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是虛擬機相關(guān)的狀態(tài)時价说,需要收集native調(diào)用棧。那什么是虛擬機相關(guān)的狀態(tài)呢风秤?譬如kWaitingForGcToComplete鳖目,它表示當前線程在等待GC結(jié)束。因此我們可以理解這些狀態(tài)是因為虛擬機自身工作而影響到本線程運行的狀態(tài)缤弦。

  2. 如果state是Waiting或Sleeping相關(guān)的狀態(tài)時领迈,則省略native調(diào)用棧的收集。因為處于此狀態(tài)的線程,其native層的調(diào)用棧最終必然為futex系統(tǒng)調(diào)用狸捅,因此輸出這些調(diào)用棧并不會給調(diào)試帶來有價值的信息衷蜓,故可省略。

  3. 如果該線程沒有java層調(diào)用棧信息尘喝,則需要收集native調(diào)用棧磁浇,否則沒有任何信息可輸出。

  4. 如果java層調(diào)用棧的最后一幀為native方法朽褪,則需要收集native調(diào)用棧置吓,以便了解native層具體的動作。

接下來就要討論如何收集native調(diào)用棧缔赠,這個過程的專業(yè)術(shù)語叫做回溯或是展開(unwind)故黑,在Android中主要通過libunwindstack這個庫來完成果善。

收集native調(diào)用棧逝撬,本質(zhì)上是尋找每一幀的pc值宠蚂。當我們拿到最后一幀的sp值后,便可以通過尋找返回地址的方式踢匣,不斷回溯出其上的每一幀pc值告匠。

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

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

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

寄存器信息本質(zhì)上是線程相關(guān)的,因此這里分為兩種情況來討論男娄。

  1. 本線程收集本線程的調(diào)用棧行贪。
  2. "Signal Catcher"線程收集其他線程的調(diào)用棧。

本線程獲取寄存器值較為簡單模闲,只需要一些基本的匯編指令便可以完成建瘫。譬如如下代碼,可以將32個通用寄存器的值存到用戶空間特定的數(shù)據(jù)結(jié)構(gòu)中尸折。

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");
}

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

答案是通過信號实夹。當目標線程運行在用戶空間時橄浓,寄存器值是不會有備份的。只有當它發(fā)生用戶態(tài)和內(nèi)核態(tài)的切換時亮航,信息才會被備份荸实。此外,切換的過程也會檢測信號缴淋,進而觸發(fā)信號處理函數(shù)准给,因此剛剛備份的寄存器信息可以進一步傳遞給處理函數(shù)泄朴。而那里,才是我們跨線程獲取寄存器值的真正位置露氮。

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

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

當函數(shù)調(diào)用發(fā)生時,返回地址通常存在x30寄存器(AArch64)中油讯。如果被調(diào)用者內(nèi)部需要使用這個寄存器详民,那么它的起始片段肯定要將x30的值存到棧中延欠,否則返回地址將丟失陌兑。可是x30的值到底存在棧中什么位置呢由捎?

當開啟-fno-omit-frame-pointer編譯選項時兔综,x30存儲的位置和x29(FP寄存器)相鄰,因此很容易找出狞玛∪沓郏可是沒有此編譯選項時,x30的值就得依賴更多的信息來獲取心肪。在64位庫中锭亏,這個信息稱為"Call Frame Information",它存儲在elf文件的.eh_frame段硬鞍。微信技術(shù)團隊的一篇文章關(guān)于這點描述的比較清楚慧瘤,引用如下:

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

除了可以unwind純native的幀,libunwindstack庫還支持AOT/JIT的幀以及解釋執(zhí)行的幀伐坏。這也就表明怔匣,通過libunwindstack收集的調(diào)用棧除了可以反映native層調(diào)用信息,還可以反映java層的調(diào)用信息桦沉,如下示例每瞒。

#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是虛擬幀,也即它并非存在于棧上纯露,而是輔助調(diào)試所增加的信息剿骨,它反映的就是#23#25這幾幀解釋器正在解釋的java方法。#05#10反映的是機器碼執(zhí)行(AOT編譯)的java方法苔埋。#00~#04反映的是純native層(so庫)的函數(shù)調(diào)用懦砂。

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

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

  1. 在回溯的過程中罚随,如果碰到所屬文件后綴為oat或odex的幀,則停止回溯羽资。原因是JNI的跳板函數(shù)/AOT編譯的java方法通常都在oat/odex文件中淘菩,碰到它們停止回溯,可以略去后續(xù)java方法的回溯屠升。

    backtrace_map_->SetSuffixesToIgnore(std::vector<std::string> { "oat", "odex" });
    
    
  2. 回溯的棧幀中如果有落在"libunwindstack.so"和"libbacktrace.so"的幀潮改,則不予顯示。原因是這些幀反映的是調(diào)用棧收集過程腹暖,而非線程原始的調(diào)用邏輯汇在。

    std::vector<std::string> skip_names{"libunwindstack.so", "libbacktrace.so"};
    
    

5.3 當前調(diào)用棧回溯的缺陷

仔細思考上述策略的第一條脏答,其實可以發(fā)現(xiàn)它是有缺陷的糕殉。這個缺陷主要有兩點:

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

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

    #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)
    
    

    如上調(diào)用棧是Android S以前tombstone文件的典型輸出羡洁。通過代碼我們可以得知,#7中的CursorWindow構(gòu)造方法明明調(diào)用的是nativeCreate方法爽丹,可為什么回溯出來的#6卻是nInitBuilder筑煮?原因正是一個JNI跳板函數(shù)可以被多個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.
    
    

    繼續(xù)查看nativeCreate和nInitBuilder的方法定義诽里,可以發(fā)現(xiàn)它們的參數(shù)個數(shù)和類型均相同袒餐,因此在dex2oat后可以共用一個JNI跳板函數(shù)。

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

    好在從Android S開始谤狡,這一幀不再顯示具體的方法名灸眼,而是統(tǒng)一的art_jni_trampoline,這樣可以減少對開發(fā)者的困擾墓懂。如下示例焰宣。

    #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跳板函數(shù)其實是位于oat/odex文件中的捕仔。但還有一種情況匕积,dex2oat并不會為native方法生成JNI跳板函數(shù)盈罐,而是在運行時采用統(tǒng)一的art_quick_generic_jni_trampoline來動態(tài)執(zhí)行參數(shù)傳遞和狀態(tài)切換。這時闪唆,art_quick_generic_jni_trampoline位于libart.so中盅粪,不符合oat/odex后綴的規(guī)律,因此調(diào)用椙睦伲回溯碰到這一幀時會繼續(xù)進行票顾。而如果后續(xù)的java方法全都是解釋執(zhí)行,那么解釋執(zhí)行的幀也將全部回溯出來帆调,如下示例奠骄。

    "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)
    
    

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

  2. 當調(diào)用棧回溯碰到位于oat/odex文件里的JNI跳板函數(shù)時靴姿,則停止回溯沃但。這種方案適用于大多數(shù)場景》鹣牛可是如果函數(shù)調(diào)用呈現(xiàn)如下的交錯情形宵晚,那么現(xiàn)行方案會丟失部分調(diào)用棧。

    Java method A
    ↓(call)
    Native method B
    ↓(call)
    Java method C
    ↓(call)
    Native method D
    
    

    最終回溯出的整體調(diào)用棧信息中维雇,Native method B將不見蹤影淤刃,因為native層的回溯在碰到C時就已經(jīng)結(jié)束了。以下是一個實際的案例吱型。

    "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調(diào)用棧---)
    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)
    
    

    該線程首先發(fā)起了一個通往對端進程的binder通信逸贾,對端進程在處理的過程中又給本進程發(fā)起了新的通信〗蛑停基于binder transaction stack的設(shè)計铝侵,這個新的通信必然交給當初的線程。因此execTransact表明它正在處理這個通信触徐。在transactNative和execTransact之間咪鲜,其實漏掉了native層的幀。

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

結(jié)語

當我們在解決大多數(shù)APP問題時享郊,調(diào)用棧都是最重要的分析素材发皿。如果它總能完美地反映線程的執(zhí)行邏輯,那么是否了解細節(jié)其實不重要拂蝎⊙ㄊ可現(xiàn)實并非如此。ANR的某些場景下温自,線程可能卡在GoToRunnable中玄货;交錯調(diào)用的情況下,中間的native方法可能丟失悼泌。等等松捉。這些時候的調(diào)用棧會出現(xiàn)讓人迷惑的信息,而只有了解回溯的細節(jié)馆里,才能真正解惑隘世。

最后

您的點贊收藏就是對我最大的鼓勵! 歡迎關(guān)注我鸠踪,分享Android干貨丙者,交流Android技術(shù)。 對文章有何見解营密,或者有何技術(shù)問題械媒,歡迎在評論區(qū)一起留言討論!最后給大家分享一些Android相關(guān)的視頻教程评汰,感興趣的朋友可以去看看纷捞。

本文轉(zhuǎn)自 https://juejin.cn/post/7041834777229393934,如有侵權(quán)被去,請聯(lián)系刪除主儡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惨缆,隨后出現(xiàn)的幾起案子糜值,更是在濱河造成了極大的恐慌,老刑警劉巖踪央,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臀玄,死亡現(xiàn)場離奇詭異,居然都是意外死亡畅蹂,警方通過查閱死者的電腦和手機健无,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來液斜,“玉大人累贤,你說我怎么就攤上這事叠穆。” “怎么了臼膏?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵硼被,是天一觀的道長。 經(jīng)常有香客問我渗磅,道長嚷硫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任始鱼,我火速辦了婚禮仔掸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘医清。我一直安慰自己起暮,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布会烙。 她就那樣靜靜地躺著负懦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柏腻。 梳的紋絲不亂的頭發(fā)上纸厉,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音葫盼,去河邊找鬼残腌。 笑死,一個胖子當著我的面吹牛贫导,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蟆盹,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼孩灯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了逾滥?” 一聲冷哼從身側(cè)響起峰档,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寨昙,沒想到半個月后讥巡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡舔哪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年欢顷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捉蚤。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抬驴,死狀恐怖炼七,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情布持,我是刑警寧澤豌拙,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站题暖,受9級特大地震影響按傅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胧卤,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一逞敷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧灌侣,春花似錦推捐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至痊乾,卻和暖如春皮壁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哪审。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工蛾魄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人湿滓。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓滴须,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叽奥。 傳聞我的和親對象是個殘疾皇子扔水,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容