开发中您时常会不会应为开发了一个很流弊的功能而自豪,然后当测试反馈你的应用”崩溃“系统性的灾难时却时常苦恼泄气,今天带你了解下“奔溃”的哪些事儿

Python91

今天也就给大家简单分享下Android崩溃信息,时常在开发中,不管是手机端,前端,后台偶发行的应用闪退后台严重错误服务器挂了,各种鬼问题,引发了一些崩溃的状态,那我也不废话,以Android为列简单聊聊崩溃的那些事儿。。。。。。

崩溃现场

崩溃现场是我们的"第一案发现场",它保留着很多有价值的线索。在这里我们挖掘到的信息越多,下一步分析的方向就越清晰,而不是去靠盲目猜测。

操作系统是整个崩溃过程的"旁观者",也是我们最重要的"证人"。一个好的崩溃捕获工具知道应该采集哪些系统信息,也知道在什么场景要深入挖掘哪些内容,从而可以更好地帮助我们解决问题。

接下来我们具体来看看在崩溃现场应该采集哪些信息。

1.崩溃信息

从崩溃的基本信息,我们可以对崩溃有初步的判断。

  • 进程名、线程名。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在UI线程。
  • 崩溃堆栈和类型。崩溃是属于Java崩溃、Native崩溃,还是ANR,对于不同类型的崩溃我们关注的点也不太一样。特别需要看崩溃堆栈的栈顶,看具体崩溃在系统的代码,还是我们自己的代码里面。
Process Name: 'com.sample.crash'
Thread Name: 'MyThread'

java.lang.NullPointerException
    at ...TestsActivity.crashInJava(TestsActivity.java:275)

有时候我们除了崩溃的线程,还希望拿到其他关键的线程的日志。就像上面的例子,虽然是MyThread线程崩溃,但是我也希望可以知道主线程当前的调用栈。

2.系统信息

系统的信息有时候会带有一些关键的线索,对我们解决问题有非常大的帮助。

  • Logcat。这里包括应用、系统的运行日志。由于系统权限问题,获取到的Logcat可能只包含与当前App相关的。其中系统的event logcat会记录App运行的一些基本情况,记录在文件/system/etc/event-log-tags中。
system logcat:
10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ...

event logcat:
10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
10-25 17:13:47.788 21430 21430 I am_low_memory: 系统内存不足
10-25 17:13:47.788 21430 21430 I am_destroy_activity: 销毁 Activty
10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及原因
10-25 17:13:47.888 21430 21430 I am_kill: APP 被杀以及原因
  • 机型、系统、厂商、CPU、ABI、Linux版本等。我们会采集多达几十个维度,这对后面讲到寻找共性问题会很有帮助。
  • 设备状态:是否root、是否是模拟器。一些问题是由Xposed或多开软件造成,对这部分问题我们要区别对待。

3.内存信息

OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。如果我们把用户的手机内存分为"2GB以下"和"2GB以上"两个桶,会发现"2GB以下"用户的崩溃率是"2GB以上"用户的几倍。

  • 系统剩余内存。关于系统内存状态,可以直接读取文件/proc/meminfo。当系统可用内存很小(低于MemTotal的 10%)时,OOM、大量GC、系统频繁自杀拉起等问题都非常容易出现。
  • 应用使用内存。包括Java内存、RSS(Resident Set Size)、PSS(Proportional Set Size),我们可以得出应用本身内存的占用大小和分布。PSS和RSS通过/proc/self/smap计算,可以进一步得到例如apk、dex、so等更加详细的分类统计。
  • 虚拟内存。虚拟内存可以通过/proc/self/status得到,通过/proc/self/maps文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但是很多类似OOM、tgkill等问题都是虚拟内存不足导致的。
Name:     com.sample.name   // 进程名
FDSize:   800               // 当前进程申请的文件句柄个数
VmPeak:   3004628 kB        // 当前进程的虚拟内存峰值大小
VmSize:   2997032 kB        // 当前进程的虚拟内存大小
Threads:  600               // 当前进程包含的线程个数

一般来说,对于32位进程,如果是32位的CPU,虚拟内存达到3GB就可能会引起内存申请失败的问题。如果是64位的CPU,虚拟内存一般在3~4GB之间。当然如果我们支持64位进程,虚拟内存就不会成为问题。Google Play要求 2019年8月一定要支持64位,在国内虽然支持64位的设备已经在90%以上了,但是商店都不支持区分CPU架构类型发布,普及起来需要更长的时间。

4.资源信息

有的时候我们会发现应用堆内存和设备内存都非常充足,还是会出现内存分配失败的情况,这跟资源泄漏可能有比较大的关系。

  • 文件句柄fd。文件句柄的限制可以通过/proc/self/limits获得,一般单个进程允许打开的最大文件句柄个数为1024。但是如果文件句柄超过800个就比较危险,需要将所有的fd以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。
opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4
2 -> /dev/binder
3 -> /data/data/com.crash.sample/files/test.config
...

  • 线程数。当前线程数大小可以通过上面的status文件得到,一个线程可能就占2MB的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过400个就比较危险。需要将所有的线程id以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。
 threads count 412:
 1820 com.sample.crashsdk
 1844 ReferenceQueueD
 1869 FinalizerDaemon
 ...

  • JNI。使用JNI时,如果不注意很容易出现引用失效、引用爆表等一些崩溃。我们可以通过DumpReferenceTables统计JNI的引用表,进一步分析是否出现了JNI泄漏等问题。

5.应用信息

除了系统,其实我们的应用更懂自己,可以留下很多相关的信息。

  • 崩溃场景。崩溃发生在哪个Activity或Fragment,发生在哪个业务中。
  • 关键操作路径。不同于开发过程详细的打点日志,我们可以记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助。
  • 其他自定义信息。不同的应用关心的重点可能不太一样,比如网易云音乐会关注当前播放的音乐,QQ浏览器会关注当前打开的网址或视频。此外例如运行时间、是否加载了补丁、是否是全新安装或升级等信息也非常重要。

除了上面这些通用的信息外,针对特定的一些崩溃,我们可能还需要获取类似磁盘空间、电量、网络使用等特定信息。所以说一个好的崩溃捕获工具,会根据场景为我们采集足够多的信息,让我们有更多的线索去分析和定位问题。当然数据的采集需要注意用户隐私,做到足够强度的加密和脱敏。

崩溃分析

有了这么多现场信息之后,我们可以开始真正的"破案"之旅了。绝大部分的"案件"只要我们肯花功夫,最后都能真相大白。不要畏惧问题,经过耐心和细心地分析,总能敏锐地发现一些异常或关键点,并且还要敢于怀疑和验证。下面我重点给你介绍崩溃分析"三部曲"。

第一步:确定重点

确认和分析重点,关键在于在日志中找到重要的信息,对问题有一个大致判断。一般来说,我建议在确定重点这一步可以关注以下几点。

1. 确认严重程度。解决崩溃也要看性价比,我们优先解决Top崩溃或者对业务有重大影响,例如启动、支付过程的崩溃。我曾经有一次辛苦了几天解决了一个大的崩溃,但下个版本产品就把整个功能都删除了,这令我很崩溃。

2. 崩溃基本信息。确定崩溃的类型以及异常描述,对崩溃有大致的判断。一般来说,大部分的简单崩溃经过这一步已经可以得到结论。

  • 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导致卡死。

3. Logcat。Logcat一般会存在一些有价值的线索,日志级别是Warning、Error的需要特别注意。从Logcat中我们可以看到当时系统的一些行为跟手机的状态,例如出现ANR时,会有"am_anr";App被杀时,会有"am_kill"。不同的系统、厂商输出的日志有所差别, 当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。

4. 各个资源情况。结合崩溃的基本信息,我们接着看看是不是跟 "内存信息" 有关,是不是跟"资源信息"有关。比如是物理内存不足、虚拟内存不足,还是文件句柄fd泄漏了。

无论是资源文件还是Logcat,内存与线程相关的信息都需要特别注意,很多崩溃都是由于它们使用不当造成的。

第二步:查找共性

如果使用了上面的方法还是不能有效定位问题,我们可以尝试查找这类崩溃有没有什么共性。找到了共性,也就可以进一步找到差异,离解决问题也就更进一步。

机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,共性问题例如是不是因为安装了Xposed,是不是只出现在x86的手机,是不是只有三星这款机型,是不是只在Android 5.0的系统上。应用信息也可以作为维度来聚合,比如正在打开的链接、正在播放的视频、国家、地区等。

找到了共性,可以对你下一步复现问题有更明确的指引。

第三步:尝试复现

如果我们已经大概知道了崩溃的原因,为了进一步确认更多信息,就需要尝试复现崩溃。如果我们对崩溃完全没有头绪,也希望通过用户操作路径来尝试重现,然后再去分析崩溃原因。

"只要能本地复现,我就能解",相信这是很多开发跟测试说过的话。有这样的底气主要是因为在稳定的复现路径上面,我们可以采用增加日志或使用Debugger、GDB等各种各样的手段或工具做进一步分析。

回想当时在开发Tinker的时候,我们遇到了各种各样的奇葩问题。比如某个厂商改了底层实现、新的Android系统实现有所更改,都需要去Google、翻源码,有时候还需要去抠厂商的ROM或手动刷ROM。这个痛苦的经历告诉我,很多疑难问题需要我们耐得住寂寞,反复猜测、反复发灰度、反复验证。

疑难问题:系统崩溃

系统崩溃常常令我们感到非常无助,它可能是某个Android版本的bug,也可能是某个厂商修改ROM导致。这种情况下的崩溃堆栈可能完全没有我们自己的代码,很难直接定位问题。针对这种疑难问题,我来谈谈我的解决思路。

1. 查找可能的原因。通过上面的共性归类,我们先看看是某个系统版本的问题,还是某个厂商特定ROM的问题。虽然崩溃日志可能没有我们自己的代码,但通过操作路径和日志,我们可以找到一些怀疑的点。

2. 尝试规避。查看可疑的代码调用,是否使用了不恰当的API,是否可以更换其他的实现方式规避。

3. Hook解决。这里分为Java Hook和Native Hook。以我最近解决的一个系统崩溃为例,我们发现线上出现一个Toast相关的系统崩溃,它只出现在Android 7.0的系统中,看起来是在Toast显示的时候窗口的token已经无效了。这有可能出现在Toast需要显示时,窗口已经销毁了。

android.view.WindowManager$BadTokenException:
    at android.view.ViewRootImpl.setView(ViewRootImpl.java)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4)
    at android.widget.Toast$TN.handleShow(Toast.java)

为什么Android 8.0的系统不会有这个问题?在查看Android 8.0的源码后我们发现有以下修改:

try {
  mWM.addView(mView, mParams);
  trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
  /* ignore */
}

考虑再三,我们决定参考Android 8.0的做法,直接catch住这个异常。这里的关键在于寻找Hook点,这个案例算是相对比较简单的。Toast里面有一个变量叫mTN,它的类型为handler,我们只需要代理它就可以实现捕获。

如果你做到了我上面说的这些, 95%以上的崩溃都能解决或者规避,大部分的系统崩溃也是如此。当然总有一些疑难问题需要依赖到用户的真实环境,我们希望具备类似动态跟踪和调试的能力。专栏后面还会讲到xlog日志、远程诊断、动态分析等高级手段,可以帮助我们进一步调试线上疑难问题,敬请期待。

崩溃攻防是一个长期的过程,我们希望尽可能地提前预防崩溃的发生,将它消灭在萌芽阶段。这可能涉及我们应用的整个流程,包括人员的培训、编译检查、静态扫描工作,还有规范的测试、灰度、发布流程等。

而崩溃优化也不是孤立的,它跟我们后面讲到的内存、卡顿、I/O等内容都有关。可能等你学完整个课程后,再回头来看会有不同的理解。

今天我们介绍了崩溃问题的一些分析方法、特殊技巧、以及疑难和常见问题的解决方法。当然崩溃分析要具体问题具体分析,不同类型的应用侧重点可能也有所不同,我们不能只局限在上面所说的一些方法。

讲讲自己的一些心得体会,在解决崩溃特别是一些疑难问题时,总会觉得患得患失。有时候解了一个问题,发现其他问题也跟"开心消消乐"一样消失了。有时候有些问题"解不出来郁闷,解出来更郁闷",可能只是一个小的代码疏忽,换来了一个月的青春和很多根白头发。

Original: https://www.cnblogs.com/mysweetAngleBaby/p/16191332.html
Author: 一眼万年的星空
Title: 开发中您时常会不会应为开发了一个很流弊的功能而自豪,然后当测试反馈你的应用”崩溃“系统性的灾难时却时常苦恼泄气,今天带你了解下“奔溃”的哪些事儿



相关阅读

Title: pandas子集选取的三种方法:[]、.loc[]、.iloc[]

pandas读取Excel、csv文件中的数据时,得到的大多是表格型的二维数据,在pandas中对应的即为 DataFrame数据结构。在处理这类数据时,往往要根据据需求先获取数据中的子集,如某些列、某些行、行列交叉的部分等。可以说 子集选取是一个非常基础、频繁使用的操作,而 DataFrame的子集选取看似简单却有一定复杂性。本文聚焦 DataFrame的子集选取操作逻辑,力求在实战中遇到子集选取操作的需求时"不迷路"。

一、图解 DataFrame

DataFrame是一种二维的表格型数据结构,每一行/列都有对应的 标签位置序号。行列标签、位置序号的对应关系如下图所示:

  • 列标签(也叫列名:columns)
  • 行标签(也叫行索引:index)默认为(0, 1, 2, ..., n)。这里与位置序号恰好一致。

针对 DataFrame的数据结构,pandas提供了三种获取子集的索引器: [].loc[].iloc[]

  • df[]:快捷的整行整列选取
  • df.loc[]:按 标签的行列交叉选取
  • df.iloc[]:按 位置序号的行列交叉选取

二、整行整列选取: df[]

<br>df[<span class="hljs-string">'&#x65E5;&#x671F;'</span>]
<br>df[[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>,<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>,<span class="hljs-string">'&#x98CE;&#x529B;&#x98CE;&#x5411;'</span>]]
<br>df[<span class="hljs-number">1</span>:<span class="hljs-number">4</span>]

切片语法也支持字符串的索引标签值,如将"日期"列修改为行索引(index)

df1&#xA0;=&#xA0;df.set_index(<span class="hljs-string">"&#x65E5;&#x671F;"</span>)
<br>df1[<span class="hljs-number">1</span>:<span class="hljs-number">4</span>]&#xA0;&#xA0;<br>df1[<span class="hljs-string">'2021-12-02&#xA0;&#x5468;&#x56DB;'</span>:<span class="hljs-string">'2021-12-04&#xA0;&#x5468;&#x516D;'</span>]&#xA0;&#xA0;

df[]语法小结:

  • df[] 语法中,方括号内输入 &#x6807;&#x7B7E;&#x540D;&#x5217;&#x8868; 选取的是列;而方括号内输入 &#x5207;&#x7247;&#x6761;&#x4EF6; 选取的是行(条件筛选在下文单独介绍)。
  • df[] 输入切片选取整行时,如果是按照位置序号的切片,左闭右开;按行标签的切片,左闭右闭。

三、行列交叉选取

行列交叉选择,可以通过 df.loc[]df.iloc[]两个索引器来实现,两者都需要输入两组参数,先行选择,后列选择。行、列选择都可以是单个标签(序号)、列表和切片。根据需求组合使用,威力强大!

df.loc[&#x884C;&#x9009;&#x62E9;,&#x5217;&#x9009;&#x62E9;]。参数面向的是 &#x6807;&#x7B7E;

df.iloc[&#x884C;&#x4F4D;&#x7F6E;&#x5E8F;&#x53F7;,&#x5217;&#x4F4D;&#x7F6E;&#x5E8F;&#x53F7;]。参数面向的是 &#x4F4D;&#x7F6E;&#x5E8F;&#x53F7;

  • &#x884C; :单个数值, &#x5217; :单个数值
df1.loc[<span class="hljs-string">'2021-12-05&#xA0;&#x5468;&#x65E5;'</span>,<span class="hljs-string">'&#x7A7A;&#x6C14;&#x8D28;&#x91CF;&#x6307;&#x6570;'</span>]<br>df1.iloc[<span class="hljs-number">4</span>,<span class="hljs-number">4</span>]
  • &#x884C; :列表, &#x5217; :列表
df1.loc[[<span class="hljs-string">'2021-12-05&#xA0;&#x5468;&#x65E5;'</span>,<span class="hljs-string">'2021-12-07&#xA0;&#x5468;&#x4E8C;'</span>],[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>,<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>,<span class="hljs-string">'&#x98CE;&#x529B;&#x98CE;&#x5411;'</span>]]<br>df1.iloc[[<span class="hljs-number">4</span>,<span class="hljs-number">6</span>],[<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">3</span>]]
  • &#x884C; :切片, &#x5217; :切片
df1.loc[<span class="hljs-string">'2021-12-01&#xA0;&#x5468;&#x4E09;'</span>:<span class="hljs-string">'2021-12-03&#xA0;&#x5468;&#x4E94;'</span>,<span class="hljs-string">'&#x5929;&#x6C14;'</span>:<span class="hljs-string">'&#x7A7A;&#x6C14;&#x8D28;&#x91CF;&#x6307;&#x6570;'</span>]<br>df1.iloc[:<span class="hljs-number">3</span>,<span class="hljs-number">2</span>:<span class="hljs-number">5</span>]
  • &#x884C; :切片(全选), &#x5217; :列表
df1.loc[:,[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>,<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]]<br>df1.iloc[:,[<span class="hljs-number">0</span>,<span class="hljs-number">1</span>]]

四、按条件筛选子集

df.[]df.loc[]df.iloc[]除了按照行列的标签和位置序号选取子集,还可以使用条件(布尔表达式)筛选子集。

将最高温、最低温处理成数值型:

df1.loc[:,<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>]&#xA0;=&#xA0;df1[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>].str.replace(<span class="hljs-string">'&#xB0;'</span>,<span class="hljs-string">''</span>).astype(<span class="hljs-string">'float32'</span>)<br>df1.loc[:,<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]&#xA0;=&#xA0;df1[<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>].str.replace(<span class="hljs-string">'&#xB0;'</span>,<span class="hljs-string">''</span>).astype(<span class="hljs-string">'float32'</span>)

获取最高温大于10度,最低温小于6度的数据

<br>df1[(df1[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>]><span class="hljs-number">10</span>)&#xA0;&&#xA0;(df1[<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]<<span class="hljs-number">6</span>)]<br><br>df1.loc[(df1[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>]><span class="hljs-number">10</span>)&#xA0;&&#xA0;(df1[<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]<<span class="hljs-number">6</span>),:]<br><br>df1.loc[(df1[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>]><span class="hljs-number">10</span>)&#xA0;&&#xA0;~(df1[<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]>=<span class="hljs-number">6</span>),:]

五、函数筛选子集

<br>df1.loc[<span class="hljs-keyword">lambda</span>&#xA0;df&#xA0;:&#xA0;(df[<span class="hljs-string">'&#x6700;&#x9AD8;&#x6E29;'</span>]><span class="hljs-number">10</span>)&#xA0;&&#xA0;(df[<span class="hljs-string">'&#x6700;&#x4F4E;&#x6E29;'</span>]<<span class="hljs-number">6</span>)]
<br><span class="hljs-function"><span class="hljs-keyword">def</span>&#xA0;<span class="hljs-title">queryData</span><span class="hljs-params">(df)</span>:</span><br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;df.index.str.startswith(<span class="hljs-string">'2021-12-0'</span>)&#xA0;&&#xA0;df[<span class="hljs-string">'&#x7A7A;&#x6C14;&#x8D28;&#x91CF;&#x6307;&#x6570;'</span>].str.endswith(<span class="hljs-string">'&#x4F18;'</span>)<br><br>df1.loc[queryData&#xA0;,&#xA0;:]

在pandast提供的 df[]df.loc[]df.iloc[]这个三种索引器,前两个更为常用。 df[]在整行或者整列获取时更为方便。整行整列选取可以看作是行列交叉选取的一个特例,故 df.loc[]是更为通用的方法,它支持单个标签值、列表多选、切片区间、条件(布尔)表达式、函数调用五种方式索引子集,功能强大。

Original: https://www.cnblogs.com/wansq/p/16219298.html
Author: 字节杂谈
Title: pandas子集选取的三种方法:[]、.loc[]、.iloc[]