美文网首页
Android 如何打造高质量的应用?( 二)

Android 如何打造高质量的应用?( 二)

作者: 唐小鹏 | 来源:发表于2022-03-01 09:36 被阅读0次

崩溃率只是一个数字,我们的出发点应该是让用户有更好的体验。

Android 崩溃分为 Java 崩溃和 Native 崩溃
简单来说,Java 崩溃就是在 Java 代码中,出现了未捕获异常,导致程序异常退出。那 Native 崩溃又是怎么产生的呢?一般都是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 abort,这些都会产生相应的 signal 信号,导致程序异常退出。
1.Native 崩溃的捕获流程
Android 平台 Native 代码的崩溃捕获机制及实现
Native 崩溃从捕获到解析要经历哪些流程。
编译端。编译 C/C++ 代码时,需要将带符号信息的文件保留下来。
客户端。捕获到崩溃时候,将收集到尽可能多的有用信息写入日志文件,然后选择合适的时机上传到服务器。
服务端。读取客户端上报的日志文件,寻找适合的符号文件,生成可读的 C/C++ 调用栈。

Chromium 的Breakpad是目前 Native 崩溃捕获中最成熟的方案,但很多人都觉得 Breakpad 过于复杂。其实我认为 Native 崩溃捕获这个事情本来就不容易,跟当初设计 Tinker 的时候一样,如果只想在 90% 的情况可靠,那大部分的代码的确可以砍掉;但如果想达到 99%,在各种恶劣条件下依然可靠,后面付出的努力会远远高于前期。

生成崩溃日志时会有哪些比较棘手的情况呢?
情况一:文件句柄泄漏,导致创建日志文件失败,怎么办?
应对方式:我们需要提前申请文件句柄 fd 预留,防止出现这种情况。

情况二:因为栈溢出了,导致日志生成失败,怎么办?
应对方式:为了防止栈溢出导致进程没有空间创建调用栈执行处理函数,我们通常会使用常见的 signalstack。在一些特殊情况,我们可能还需要直接替换当前栈,所以这里也需要在堆中预留部分空间。

情况三:整个堆的内存都耗尽了,导致日志生成失败,怎么办?
应对方式:这个时候我们无法安全地分配内存,也不敢使用 stl 或者 libc 的函数,因为它们内部实现会分配堆内存。这个时候如果继续分配内存,会导致出现堆破坏或者二次崩溃的情况。Breakpad 做的比较彻底,重新封装了Linux Syscall Support,来避免直接调用 libc。

情况四:堆破坏或二次崩溃导致日志生成失败,怎么办?
应对方式:Breakpad 会从原进程 fork 出子进程去收集崩溃现场,此外涉及与 Java 相关的,一般也会用子进程去操作。这样即使出现二次崩溃,只是这部分的信息丢失,我们的父进程后面还可以继续获取其他的信息。在一些特殊的情况,我们还可能需要从子进程 fork 出孙进程。

对于很多中小型公司来说,我并不建议自己去实现一套如此复杂的系统,可以选择一些第三方的服务。目前各种平台也是百花齐放,包括腾讯的Bugly、阿里的啄木鸟平台、网易云捕、Google 的 Firebase,云测 等等。

Breakpad 来捕获一个 Native 崩溃上传到后台

作为技术人员,我们不应该盲目追求崩溃率这一个数字,应该以用户体验为先,如果强行去掩盖一些问题往往更加适得其反。我们不应该随意使用 try catch 去隐藏真正的问题,要从源头入手,了解崩溃的本质原因,保证后面的运行流程。

崩溃基本信息。确定崩溃的类型以及异常描述,对崩溃有大致的判断。一般来说,大部分的简单崩溃经过这一步已经可以得到结论。
Java 崩溃。 Java 崩溃类型比较明显,比如 NullPointerException 是空指针,OutOfMemoryError 是资源不足,这个时候需要去进一步查看日志中的 “内存信息”和“资源信息”。
Native 崩溃。需要观察 signal、code、fault addr 等内容,以及崩溃时 Java 的堆栈。关于各 signal 含义的介绍,你可以查看崩溃信号介绍。比较常见的是有 SIGSEGV 和 SIGABRT,前者一般是由于空指针、非法指针造成,后者主要因为 ANR 和调用 abort() 退出所导致。
ANR。我的经验是,先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是由于大量 GC 导致卡死。
Logcat。Logcat 一般会存在一些有价值的线索,日志级别是 Warning、Error 的需要特别注意。从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,例如出现 ANR 时,会有“am_anr”;App 被杀时,会有“am_kill”。不同的系统、厂商输出的日志有所差别,当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。

如果想向崩溃发起挑战,那么 Top 20 崩溃就是我们无法避免的对手。在这里面会有不少疑难的系统崩溃问题,TimeoutException 就是其中比较经典的一个。

java.util.concurrent.TimeoutException: 
         android.os.BinderProxy.finalize() timed out after 10 seconds
at android.os.BinderProxy.destroy(Native Method)
at android.os.BinderProxy.finalize(Binder.java:459)

今天的Sample提供了一种“完全解决”TimeoutException 的方法,主要是希望你可以更好地学习解决系统崩溃的套路。

  1. 通过源码分析。我们发现 TimeoutException 是由系统的 FinalizerWatchdogDaemon 抛出来的。
  2. 寻找可以规避的方法。尝试调用了它的 Stop() 方法,但是线上发现在 Android 6.0 之前会有线程同步问题。
  3. 寻找其他可以 Hook 的点。通过代码的依赖关系,发现一个取巧的 Hook 点。最终代码你可以参考 Sample 的实现,但是建议只在灰度中使用。这里需要提的是,虽然有一些黑科技可以帮助我们解决某些问题,但对于黑科技的使用我们需要慎重,比如有的黑科技对保活进程频率没有做限制,可能会导致系统卡死。

try catch 被滥用的问题处理方案:
一般做法有

  1. 在线程池直接拦截所有的java异常,但是只在正式版本使用,保留灰度包不拦截
  2. 一般crash sdk都提供虽然try catch,但依然会上报到后台的方法。

相关文章

网友评论

      本文标题:Android 如何打造高质量的应用?( 二)

      本文链接:https://www.haomeiwen.com/subject/hfljfrtx.html