Android 平臺 Native 代碼的崩潰捕獲機制及實現 原

本文來自於騰訊Bugly公衆號(weixinBugly),未經作者同意,請勿轉載,原文地址:https://mp.weixin.qq.com/s/g-WzYF3wWAljok1XjPoo7w

一、背景

在Android平臺,native crash一直是crash裏的大頭。native crash具有上下文不全、出錯信息模糊、難以捕捉等特點,比java crash更難修復。所以一個合格的異常捕獲組件也要能達到以下目的:

  • 支持在crash時進行更多擴展操作
  • 打印logcat和應用日誌
  • 上報crash次數
  • 對不同的crash做不同的恢復措施
  • 可以針對業務不斷改進和適應

二、現有的方案

方案優點缺點
Google Breakpad權威,跨平臺代碼體量較大
利用LogCat日誌利用安卓系統實現需要在crash時啓動新進程過濾logcat日誌,不可靠
coffeecatch實現簡潔,改動容易存在兼容性問題

其實3個方案在Android平臺的實現原理都是基本一致的,綜合考慮,可以基於coffeecatch改進。

三、信號機制

1.程序奔潰

  • 在Unix-like系統中,所有的崩潰都是編程錯誤或者硬件錯誤相關的,系統遇到不可恢復的錯誤時會觸發崩潰機制讓程序退出,如除零、段地址錯誤等。

  • 異常發生時,CPU通過異常中斷的方式,觸發異常處理流程。不同的處理器,有不同的異常中斷類型和中斷處理方式。

  • linux把這些中斷處理,統一爲信號量,可以註冊信號量向量進行處理。

  • 信號機制是進程之間相互傳遞消息的一種方法,信號全稱爲軟中斷信號。

2.信號機制

函數運行在用戶態,當遇到系統調用、中斷或是異常的情況時,程序會進入內核態。信號涉及到了這兩種狀態之間的轉換。

(1) 信號的接收

接收信號的任務是由內核代理的,當內核接收到信號後,會將其放到對應進程的信號隊列中,同時向進程發送一箇中斷,使其陷入內核態。注意,此時信號還只是在隊列中,對進程來說暫時是不知道有信號到來的。

(2) 信號的檢測

進程陷入內核態後,有兩種場景會對信號進行檢測:

  • 進程從內核態返回到用戶態前進行信號檢測
  • 進程在內核態中,從睡眠狀態被喚醒的時候進行信號檢測

當發現有新信號時,便會進入下一步,信號的處理。

(3) 信號的處理

信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,並且修改指令寄存器(eip)將其指向信號處理函數。

接下來進程返回到用戶態中,執行相應的信號處理函數。

信號處理函數執行完成後,還需要返回內核態,檢查是否還有其它信號未處理。如果所有信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最後回到用戶態繼續執行進程。

至此,一個完整的信號處理流程便結束了,如果同時有多個信號到達,上面的處理流程會在第2步和第3步驟間重複進行。

(4) 常見信號量類型

四、捕捉native crash

1.註冊信號處理函數

第一步就是要用信號處理函數捕獲到native crash(SIGSEGV, SIGBUS等)。在posix系統,可以用sigaction():

#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)); 
  • signum:代表信號編碼,可以是除SIGKILL及SIGSTOP外的任何一個特定有效的信號,如果爲這兩個信號定義自己的處理函數,將導致信號安裝錯誤。

  • act:指向結構體sigaction的一個實例的指針,該實例指定了對特定信號的處理,如果設置爲空,進程會執行默認處理。

  • oldact:和參數act類似,只不過保存的是原來對相應信號的處理,也可設置爲NULL。

struct sigaction sa_old;  
memset(&sa, 0, sizeof(sa));  
sigemptyset(&sa.sa_mask);  
sa.sa_sigaction = my_handler;  
sa.sa_flags = SA_SIGINFO;  
if (sigaction(sig, &sa, &sa_old) == 0) {  
  ...  
}

2.設置額外棧空間

#include <signal.h>
int sigaltstack(const stack_t *ss, stack_t *oss);
  • SIGSEGV很有可能是棧溢出引起的,如果在默認的棧上運行很有可能會破壞程序運行的現場,無法獲取到正確的上下文。而且當棧滿了(太多次遞歸,棧上太多對象),系統會在同一個已經滿了的棧上調用SIGSEGV的信號處理函數,又再一次引起同樣的信號。

  • 我們應該開闢一塊新的空間作爲運行信號處理函數的棧。可以使用sigaltstack在任意線程註冊一個可選的棧,保留一下在緊急情況下使用的空間。(系統會在危險情況下把棧指針指向這個地方,使得可以在一個新的棧上運行信號處理函數)

stack_t stack;  
memset(&stack, 0, sizeof(stack));  
/* Reserver the system default stack size. We don't need that much by the way. */  
stack.ss_size = SIGSTKSZ;  
stack.ss_sp = malloc(stack.ss_size);  
stack.ss_flags = 0;  
/* Install alternate stack size. Be sure the memory region is valid until you revert it. */  
if (stack.ss_sp != NULL && sigaltstack(&stack, NULL) == 0) {  
  ...  
}

3.兼容其他signal處理

static void my_handler(const int code, siginfo_t *const si, void *const sc) {
...  
  /* Call previous handler. */  
  old_handler.sa_sigaction(code, si, sc);  
}
  • 某些信號可能在之前已經被安裝過信號處理函數,而sigaction一個信號量只能註冊一個處理函數,這意味着我們的處理函數會覆蓋其他人的處理信號

  • 保存舊的處理函數,在處理完我們的信號處理函數後,在重新運行老的處理函數就能完成兼容。

五、注意事項

1.防止死鎖或者死循環

首先我們要了解async-signal-safe和可重入函數概念:

  • A signal handler function must be very careful, since processing elsewhere may be interrupted at some arbitrary point in the execution of the program.
  • POSIX has the concept of "safe function". If a signal interrupts the execution of an unsafe function, and handler either calls an unsafe function or handler terminates via a call to longjmp() or siglongjmp() and the program subsequently calls an unsafe function, then the behavior of the program is undefined.

回想下在“信號機制”一節中的圖示,進程捕捉到信號並對其進行處理時,進程正在執行的正常指令序列就被信號處理程序臨時中斷,它首先執行該信號處理程序中的指令(類似發生硬件中斷)。但在信號處理程序中,不能判斷捕捉到信號時進程執行到何處。如果進程正在執行malloc,在其堆中分配另外的存儲空間,而此時由於捕捉到信號而插入執行該信號處理程序,其中又調用malloc,這時會發生什麼?這可能會對進程造成破壞,因爲malloc通常爲它所分配的存儲區維護一個鏈表,而插入執行信號處理程序時,進程可能正在更改此鏈表。(參考《UNIX環境高級編程》)

Single UNIX Specification說明了在信號處理程序中保證調用安全的函數。這些函數是可重入的並被稱爲是異步信號安全(async-signal-safe)。除了可重入以外,在信號處理操作期間,它會阻塞任何會引起不一致的信號發送。下面是這些異步信號安全函數:

但即使我們自己在信號處理程序中不使用不可重入的函數,也無法保證保存的舊的信號處理程序中不會有非異步信號安全的函數。所以要使用alarm保證信號處理程序不會陷入死鎖或者死循環的狀態。

static void signal_handler(const int code, siginfo_t *const si,
                                    void *const sc) {

    /* Ensure we do not deadlock. Default of ALRM is to die.
    * (signal() and alarm() are signal-safe) */
    signal(code, SIG_DFL);
    signal(SIGALRM, SIG_DFL);

    /* Ensure we do not deadlock. Default of ALRM is to die.
	  * (signal() and alarm() are signal-safe) */
    (void) alarm(8);
    ....
}

2.在哪裏打印堆棧

(1) 子進程

考慮到信號處理程序中的諸多限制,一般會clone一個新的進程,在其中完成解析堆棧等任務。

下面是Google Breakpad的流程圖,在新的進程中DoDump,使用ptrace解析crash進程的堆棧,同時信號處理程序等待子進程完成任務後,再調用舊的信號處理函數。父子進程使用管道通信。

(2) 子線程

在我的實驗中,在子進程或者信號處理函數中,經常無法回調給java層。於是我選擇了在初始化的時候就建立了子線程並一直等待,等到捕捉到crash信號時,喚醒這條線程dump出crash堆棧,並把crash堆棧回調給java。

static void nativeInit(JNIEnv* env, jclass javaClass, jstring packageNameStr, jstring tombstoneFilePathStr, jobject obj) {
	...
    initCondition();
    
    pthread_t thd;
    int ret = pthread_create(&thd, NULL, DumpThreadEntry, NULL);
    if(ret) {
        qmlog("%s", "pthread_create error");
    }
}

void* DumpThreadEntry(void *argv) {
    JNIEnv* env = NULL;
     if((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK)
    {
        LOGE("AttachCurrentThread() failed");
        estatus = 0;
        return &estatus;
    }

    while (true) {
    	//等待信號處理函數喚醒
        waitForSignal();
        
        //回調native異常堆棧給java層
        throw_exception(env);
        
        //告訴信號處理函數已經處理完了
        notifyThrowException();
    }

    if((*g_jvm)->DetachCurrentThread(g_jvm) != JNI_OK)
    {
        LOGE("DetachCurrentThread() failed");
        estatus = 0;
        return &estatus;
    }

    return &estatus;
}

六、收集native crash原因

信號處理函數的入參中有豐富的錯誤信息,下面我們來一一分析。

/*信號處理函數*/
void (*sa_sigaction)(const int code, siginfo_t *const si, void * const sc) 

siginfo_t {
   int      si_signo;     /* Signal number 信號量 */
   int      si_errno;     /* An errno value */
   int      si_code;      /* Signal code 錯誤碼 */
   }

1.code

發生native crash之後,logcat中會打出如下一句信息:

signal 11 (SIGSEGV), code 0 (SI_USER), fault addr 0x0

根據code去查表,其實就可以知道發生native crash的大致原因:

代碼的一部分如下,其實就是根據不同的code,輸出不同信息,這些都是固定的。

case SIGFPE:
    switch(code) {
    case FPE_INTDIV:
      return "Integer divide by zero";
    case FPE_INTOVF:
      return "Integer overflow";
    case FPE_FLTDIV:
      return "Floating-point divide by zero";
    case FPE_FLTOVF:
      return "Floating-point overflow";
    case FPE_FLTUND:
      return "Floating-point underflow";
    case FPE_FLTRES:
      return "Floating-point inexact result";
    case FPE_FLTINV:
      return "Invalid floating-point operation";
    case FPE_FLTSUB:
      return "Subscript out of range";
    default:
      return "Floating-point";
    }
    break;
  case SIGSEGV:
    switch(code) {
    case SEGV_MAPERR:
      return "Address not mapped to object";
    case SEGV_ACCERR:
      return "Invalid permissions for mapped object";
    default:
      return "Segmentation violation";
    }
    break;

2.pc值

信號處理函數中的第三個入參sc是uc_mcontext的結構體,是cpu相關的上下文,包括當前線程的寄存器信息和奔潰時的pc值。能夠知道崩潰時的pc,就能知道崩潰時執行的是那條指令。

不過這個結構體的定義是平臺相關,不同平臺、不同cpu架構中的定義都不一樣:

  • x86-64架構:uc_mcontext.gregs[REG_RIP]
  • arm架構:uc_mcontext.arm_pc

3.共享庫名字和相對偏移地址

(1) dladdr()

pc值是程序加載到內存中的絕對地址,我們需要拿到奔潰代碼相對於共享庫的相對偏移地址,才能使用addr2line分析出是哪一行代碼。通過dladdr()可以獲得共享庫加載到內存的起始地址,和pc值相減就可以獲得相對偏移地址,並且可以獲得共享庫的名字。

Dl_info info;  
if (dladdr(addr, &info) != 0 && info.dli_fname != NULL) {  
  void * const nearest = info.dli_saddr;  
  //相對偏移地址
  const uintptr_t addr_relative =  
    ((uintptr_t) addr - (uintptr_t) info.dli_fbase);  
  ...  
}

作爲有追求的我們,肯定不滿足於僅僅通過一個函數就獲得答案。我們嘗試下如何手工分析出相對地址。首先要了解下進程的地址空間佈局。

(2) Linux下進程的地址空間佈局

任何一個程序通常都包括代碼段和數據段,這些代碼和數據本身都是靜態的。程序要想運行,首先要由操作系統負責爲其創建進程,並在進程的虛擬地址空間中爲其代碼段和數據段建立映射。光有代碼段和數據段是不夠的,進程在運行過程中還要有其動態環境,其中最重要的就是堆棧。

上圖中Random stack offset和Random mmap offset等隨機值意在防止惡意程序。Linux通過對棧、內存映射段、堆的起始地址加上隨機偏移量來打亂佈局,以免惡意程序通過計算訪問棧、庫函數等地址。

棧(stack),作爲進程的臨時數據區,增長方向是從高地址到低地址。

(3) /proc/self/maps:檢查各個模塊加載在內存的地址範圍

在Linux系統中,/proc/self/maps保存了各個程序段在內存中的加載地址範圍,grep出共享庫的名字,就可以知道共享庫的加載基值是多少。

得到相對偏移地址之後,使用readelf查看共享庫的符號表,就可以知道是哪個函數crash了。

七、獲取堆棧

1.原理

在前一步,我們獲取了奔潰時的pc值和各個寄存器的內容,通過SP和FP所限定的stack frame,就可以得到母函數的SP和FP,從而得到母函數的stack frame(PC,LR,SP,FP會在函數調用的第一時間壓棧),以此追溯,即可得到所有函數的調用順序。

2.實現

  • 在4.1.1以上,5.0以下:使用安卓系統自帶的libcorkscrew.so
  • 5.0以上:安卓系統中沒有了libcorkscrew.so,使用自己編譯的libunwind
#ifdef USE_UNWIND
    /* Frame buffer initial position. */
    t->frames_size = 0;

    /* Skip us and the caller. */
    t->frames_skip = 0;

    /* 使用libcorkscrew解堆棧 */
#ifdef USE_CORKSCREW
    t->frames_size = backtrace_signal(si, sc, t->frames, 0, BACKTRACE_FRAMES_MAX);
#else
    /* Unwind frames (equivalent to backtrace()) */
    _Unwind_Backtrace(coffeecatch_unwind_callback, t);
#endif

/* 如果無法加載libcorkscrew,則使用自己編譯的libunwind解堆棧 */
#ifdef USE_LIBUNWIND
    if (t->frames_size == 0) {
        size_t i;
        t->frames_size = unwind_signal(si, sc, t->uframes, 0,BACKTRACE_FRAMES_MAX);
        for(i = 0 ; i < t->frames_size ; i++) {
            t->frames[i].absolute_pc = (uintptr_t) t->uframes[i];
            t->frames[i].stack_top = 0;
            t->frames[i].stack_size = 0;
            __android_log_print(ANDROID_LOG_DEBUG, TAG, "absolute_pc:%x", t->frames[i].absolute_pc);
        }
    }
#endif

libunwind是一個獨立的開源庫,高版本的安卓源碼中也使用了libunwind作爲解堆棧的工具,並針對安卓做了一些適配。下面是使用libunwind解堆棧的主循環,每次循環解一層堆棧。

static ALWAYS_INLINE int
slow_backtrace (void **buffer, int size, unw_context_t *uc)
{
  unw_cursor_t cursor;
  unw_word_t ip;
  int n = 0;

  if (unlikely (unw_init_local (&cursor, uc) < 0))
    return 0;

  while (unw_step (&cursor) > 0)
    {
      if (n >= size)
          return n;

      if (unw_get_reg (&cursor, UNW_REG_IP, &ip) < 0)
          return n;
      buffer[n++] = (void *) (uintptr_t) ip;
    }
  return n;
}

八、獲取函數符號

(1) libcorkscrew

可以通過libcorkscrew中的get_backtrace_symbols函數獲得函數符號。

/*
* Describes the symbols associated with a backtrace frame.
*/
typedef struct {
    uintptr_t relative_pc;
    uintptr_t relative_symbol_addr;
    char* map_name;
    char* symbol_name;
    char* demangled_name;
} backtrace_symbol_t;

/*
* Gets the symbols for each frame of a backtrace.
* The symbols array must be big enough to hold one symbol record per frame.
* The symbols must later be freed using free_backtrace_symbols.
*/
void get_backtrace_symbols(const backtrace_frame_t* backtrace, size_t frames,
        backtrace_symbol_t* backtrace_symbols);
(2) dladdr

更通用的方法是通過dladdr獲得函數名字。

int dladdr(void *addr, Dl_info *info);

typedef struct {
   const char *dli_fname;  /* Pathname of shared object that
                              contains address */
   void       *dli_fbase;  /* Base address at which shared
                              object is loaded */
   const char *dli_sname;  /* Name of symbol whose definition
                              overlaps addr */
   void       *dli_saddr;  /* Exact address of symbol named
                              in dli_sname */
} Dl_info;

傳入每一層堆棧的相對偏移地址,就可以從dli_fname中獲得函數名字。

九、獲得java堆棧

如何獲得native crash所對應的java層堆棧,這個問題曾經困擾了我一段時間。這裏有一個前提:我們認爲crash線程就是捕獲到信號的線程,雖然這在SIGABRT下不一定可靠。有了這個認知,接下來就好辦了。在信號處理函數中獲得當前線程的名字,然後把crash線程的名字傳給java層,在java裏dump出這個線程的堆棧,就是crash所對應的java層堆棧了。

在c中獲得線程名字:

char* getThreadName(pid_t tid) {
    if (tid <= 1) {
        return NULL;
    }
    char* path = (char *) calloc(1, 80);
    char* line = (char *) calloc(1, THREAD_NAME_LENGTH);
    
    snprintf(path, PATH_MAX, "proc/%d/comm", tid);
    FILE* commFile = NULL;
    if (commFile = fopen(path, "r")) {
        fgets(line, THREAD_NAME_LENGTH, commFile);
        fclose(commFile);
    }
    free(path);
    if (line) {
        int length = strlen(line);
        if (line[length - 1] == '\n') {
            line[length - 1] = '\0';
        }
    }
    return line;
}

然後傳給java層:

    /**
     * 根據線程名獲得線程對象,native層會調用該方法,不能混淆
     * @param threadName
     * @return
     */
    @Keep
    public static Thread getThreadByName(String threadName) {
        if (TextUtils.isEmpty(threadName)) {
            return null;
        }

        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);

        Thread theThread = null;
        for(Thread thread : threadArray) {
            if (thread.getName().equals(threadName)) {
                theThread =  thread;
            }
        }

        Log.d(TAG, "threadName: " + threadName + ", thread: " + theThread);
        return theThread;
    }

十、 結果展示

經過諸多探索,終於得到了完美的堆棧:

java.lang.Error: signal 11 (Address not mapped to object) at address 0x0
  at dalvik.system.NativeStart.run(Native Method)
Caused by: java.lang.Error: signal 11 (Address not mapped to object) at address 0x0
  at /data/app-lib/com.tencent.moai.crashcatcher.demo-1/libQMCrashGenerator.so.0xd8e(dangerousFunction:0x5:0)
  at /data/app-lib/com.tencent.moai.crashcatcher.demo-1/libQMCrashGenerator.so.0xd95(wrapDangerousFunction:0x2:0)
  at /data/app-lib/com.tencent.moai.crashcatcher.demo-1/libQMCrashGenerator.so.0xd9d(nativeInvalidAddressCrash:0x2:0)
  at /system/lib/libdvm.so.0x1ee8c(dvmPlatformInvoke:0x70:0)
  at /system/lib/libdvm.so.0x503b7(dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*):0x1ee:0)
  at /system/lib/libdvm.so.0x28268(Native Method)
  at /system/lib/libdvm.so.0x2f738(dvmMterpStd(Thread*):0x44:0)
  at /system/lib/libdvm.so.0x2cda8(dvmInterpret(Thread*, Method const*, JValue*):0xb8:0)
  at /system/lib/libdvm.so.0x648e3(dvmInvokeMethod(Object*, Method const*, ArrayObject*, ArrayObject*, ClassObject*, bool):0x1aa:0)
  at /system/lib/libdvm.so.0x6cff9(Native Method)
  at /system/lib/libdvm.so.0x28268(Native Method)
  at /system/lib/libdvm.so.0x2f738(dvmMterpStd(Thread*):0x44:0)
  at /system/lib/libdvm.so.0x2cda8(dvmInterpret(Thread*, Method const*, JValue*):0xb8:0)
  at /system/lib/libdvm.so.0x643d9(dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list):0x14c:0)
  at /system/lib/libdvm.so.0x4bca1(Native Method)
  at /system/lib/libandroid_runtime.so.0x50ac3(Native Method)
  at /system/lib/libandroid_runtime.so.0x518e7(android::AndroidRuntime::start(char const*, char const*):0x206:0)
  at /system/bin/app_process.0xf33(Native Method)
  at /system/lib/libc.so.0xf584(__libc_init:0x64:0)
  at /system/bin/app_process.0x107c(Native Method)
Caused by: java.lang.Error: java stack
  at com.tencent.crashcatcher.CrashCatcher.nativeInvalidAddressCrash(Native Method)
  at com.tencent.crashcatcher.CrashCatcher.invalidAddressCrash(CrashCatcher.java:33)
  at com.tencent.moai.crashcatcher.demo.MainActivity$4.onClick(MainActivity.java:56)
  at android.view.View.performClick(View.java:4488)
  at android.view.View$PerformClick.run(View.java:18860)
  at android.os.Handler.handleCallback(Handler.java:808)
  at android.os.Handler.dispatchMessage(Handler.java:103)
  at android.os.Looper.loop(Looper.java:222)
  at android.app.ActivityThread.main(ActivityThread.java:5484)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:515)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:676)
  at dalvik.system.NativeStart.main(Native Method)

在native層構造了一個Error傳給java,所以在java層可以很輕鬆地根據堆棧進行業務上的處理。

public interface CrashHandleListener {
    @Keep
    void onCrash(int id, Error e);
}

另外初始化時就建立等待回調線程的方式,提供了穩定的給java層的回調。在回調中我們打印了app的狀態信息,包括activity的堆棧、app是否在前臺等,以及打印crash前的logcat日誌和把應用日誌flush進文件。針對某些具體的native crash還做了業務上的處理,例如遇到熱補丁框架相關的crash時就回滾補丁。

在用戶環境中的很多native crash單靠堆棧是解決不了的,logcat是非常重要的補充。好幾例webview crash都是通過發生crash時的logcat定位的。比如我們曾經遇到過的一個的webview crash:

#00 pc 00039874  /system/lib/libc.so (tgkill+12)
#01 pc 00013b5d  /system/lib/libc.so (pthread_kill+52)
#02 pc 0001477b  /system/lib/libc.so (raise+10)
#03 pc 00010ff5  /system/lib/libc.so (__libc_android_abort+36)
#04 pc 0000f554  /system/lib/libc.so (abort+4)
#05 pc 00239885  /system/lib/libwebviewchromium.so
#06 pc 00219da3  /system/lib/libwebviewchromium.so
#07 pc 00206459  /system/lib/libwebviewchromium.so
#08 pc 001fb6c7  /system/lib/libwebviewchromium.so
#09 pc 001edc97  /system/lib/libwebviewchromium.so
#10 pc 001ec5ad  /system/lib/libwebviewchromium.so
#11 pc 001ec617  /system/lib/libwebviewchromium.so
#12 pc 001ec5e5  /system/lib/libwebviewchromium.so
#13 pc 001ec5bf  /system/lib/libwebviewchromium.so
#14 pc 0022c941  /system/lib/libwebviewchromium.so
#15 pc 0022c92b  /system/lib/libwebviewchromium.so
#16 pc 0022e6a1  /system/lib/libwebviewchromium.so
#17 pc 0022ebcd  /system/lib/libwebviewchromium.so
#18 pc 0022ee1d  /system/lib/libwebviewchromium.so
#19 pc 0022c511  /system/lib/libwebviewchromium.so
#20 pc 00013347  /system/lib/libc.so (_ZL15__pthread_startPv+30)
#21 pc 0001135f  /system/lib/libc.so (__start_thread+6)

單憑堆棧根本看不出來是什麼問題,但是在logcat中卻看到這樣一個warning log:

05-21 15:09:28.423 W/System.err(16811): java.lang.NullPointerException: Attempt to get length of null array
05-21 15:09:28.424 W/System.err(16811): 	at java.io.ByteArrayInputStream.<init>(ByteArrayInputStream.java:60)
05-21 15:09:28.424 W/System.err(16811): 	at com.tencent.*.InlineImage.fetcher.HttpImageFetcher.fetchFromNetwork(HttpImageFetcher.java:86)
05-21 15:09:28.424 W/System.err(16811): 	at com.tencent.*.InlineImage.fetcher.BaseFetcher.fetch(BaseFetcher.java:24)
05-21 15:09:28.424 W/System.err(16811): 	at com.tencent.*.InlineImage.delaystream.DelayInputStream.read(DelayInputStream.java:36)
05-21 15:09:28.424 W/System.err(16811): 	at com.tencent.*.InlineImage.delaystream.DelayHttpInputStream.read(DelayHttpInputStream.java:12)
05-21 15:09:28.424 W/System.err(16811): 	at java.io.InputStream.read(InputStream.java:181)
05-21 15:09:28.424 W/System.err(16811): 	at org.chromium.android_webview.InputStreamUtil.read(InputStreamUtil.java:54)

查代碼發現是我們在WebViewClient的shouldInterceptRequest接口中的業務代碼發生了NullPointerException, 傳進去WebView內部變成了natvie crash,問題解決。

注:目前此組件尚未對外開放


更多精彩內容歡迎關注騰訊 Bugly的微信公衆賬號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發佈後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章