崩溃优化(1):关于“崩溃”

Android 平台Native代码崩溃捕获机制及实现

Android 平台Native代码崩溃捕获机制及实现(来自于bugly团队,原文链接: https://mp.weixin.qq.com/s/g-WzYF3wWAljok1XjPoo7w。作为学习笔记摘抄到了这里,仅用于学习交流。如有侵权,请联系我立即删除)

一、现有的方案

方案 优点 缺点
Google Breakpad 权威,跨平台 代码体量较大
利用LogCat日志 利用Android系统实现 需要在Crash时启动新进程过滤logcat日志,不可靠
coffeecatch 实现简洁,改动容易 存在兼容性问题
  • 其实3个方案在Android平台的实现原理都是基本一致的,综合考虑,可以基于coffeecatch改进。

二、信号机制

  1. 程序奔溃
  • 在Unix-like系统中,所有的奔溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发奔溃机制让程序退出,如除零、段地址错误等
  • 异常发生时,CPU通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式
  • linux把这些中断处理,统一为信号量,可以注册信号量向量进行处理
  • 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号
  1. 信号机制
    函数运行在用户态,当遇到系统调用、中断或者异常的情况,程序会进入内核态。信号涉及了这两种状态之间的转换。
    1. 信号的接收
      接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中
    2. 信号的检测
      进程陷入内核态后,有两种场景会对信号进行检测:
    • 进程从内核态返回到用户态之前进行信号检测
    • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测

当发现有新信号时,便会进入下一步,信号的处理

  1. 信号的处理流程

    1. 信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数
    2. 进程返回到用户态中,执行相应的信号处理函数
    3. 信号处理函数执行完成后,还需要返回内核态,检查是否还有其他信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程

至此,一个完整的信号处理流程结束。如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行

  1. 常见的信号量类型
信号量 Value 描述 例子
SIGABRT 6 进程发现错误或者调用了abort() 很多C的库函数中,如果发现异常会调用abort,如strlen
SIGBUS 10,7,10 不存在的无力地址,硬件有误 更多的是因为硬件或者系统引起的
SIGFPE 8 浮点数运算错误 如除0操作,余0,整形溢出
SIGILL 4 非法指令 损坏的可执行文件或者代码区损坏
SIGSEGV 11 段地址错误 空指针,访问不存在的地址空间,访问内核区,写只读空间。栈溢出,数组越界,野指针
SIGSTKFLT 16 //未使用
SIGPIPE 13 管道错误,往没有reader的管道中写 Linux中的socket,如果断掉了继续写、signal(SIGPIPE,SIG_IGN)

三、捕捉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) {  
  ...  
}
  1. 设置额外栈空间
#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) {  
  ...  
}
  1. 兼容其他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. 防止死锁或者死循环

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);
    ....
}
  1. 在哪里打印堆栈

    1. 子进程

    考虑到信号处理程序中的诸多限制,一般会clone一个新的进程,在其完成解析堆栈等任务

    1. 子线程

    在子进程或者信号处理函数中,经常无法回调给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
信号量 错误码 描述
SIGABRT
SIGBUS BUS_ADRALN
,BUS_ADRERR
,BUS_OBJERR
,BUS_MCEERR_AR
,BUS_MCEERR_AO
地址无法对齐
物理地址有误



SIGFPE FPE_INIDIV
FPE_INTOVF
FPE_FLTDIV
FPE_FLTOVF
FPE_FLTUND
FPE_FLTRES
FPE_FLTINV
FPE_FLTSUB
int除0
int溢出
浮点数除0
浮点数溢出
浮点数下溢
浮点数不精确
非法浮点数操作
下表越界
SIGILL ILL_ILLOPC
ILL_ILLOPN
ILL_ILLADR
ILL_ILLTRP
ILL_PRVOPC
ILL_PRVREG
ILL_COPROC
ILL_BADSTK
Illegal opcode
Illegal operand
Illegal addressing mode
Illegal trap
Prtivileged opcode
Prtivileged register
Coprocessor error
Internal stack error
SIGSEGV SEGV_MAPERR
SEGV_ACCERR
地址无效
地址无访问权限
SIGSTKFLT
SIGPIPE

代码的一部分如下,其实就是根据不同的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;
  1. pc值

    信号处理函数中的第三个入参uc_mcontext的结构体,是cpu相关的上下文,包括当前线程的寄存器信息和奔溃时的pc值。能够知道奔溃时的pc,就能知道奔溃时执行的那条指令。

  • x86-64架构:uc_mcontext.gregs[REG_RIP]
  • arm架构: uc_mcontext.arm_pc
  1. 共享库名字和相对偏移地址
  2. 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);  
  ...  
}
  1. Linux下进程的地址空间布局
    [图片上传失败...(image-70da4b-1572705830421)]
  2. /proc/self/maps:检查各个模块加载在内存的地址范围
    在Linux系统中,/proc/self/maps保存了各个程序段在内存中的加载地址范围,grep出共享库的名字,就可以知道共享库的加载基值是多少。
    [图片上传失败...(image-fb10e9-1572705830421)]
    得到相对偏移地址之后,使用readelf查看共享库的符号表,就可以知道是哪个函数crash了。
    [图片上传失败...(image-bfc81e-1572705830421)]

六获取堆栈

  1. 原理

在前一步,我们获取了奔溃时的pc值和各个寄存器的内容,通过SP和FP所限定的stack frame,就可以得到母函数的SP和FP,从而得到母函数的stack frame(PC,LR,SP,FP会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序。
[图片上传失败...(image-e48fe3-1572705830421)]

  1. 实现
  • 在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);
  1. 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)

这样根据错误就可以轻松的解决了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容