Android 版微信的「两位数字+15个句号」 bug 的原理是怎样的?
300 个回答
哈哈,之前写过一系列调试Android Framework的文章,唯独没写过动态调试;借此机会补上。
这么多答案,到底谁对谁错?来来来,手把手教你分析一波。
_________________________________________________
首先这是个ANR,说白了就是主线程由于某些原因停滞不前导致无法响应界面操作,我们先拉取ANR的trace文件分析,在手机的 /data/anr/traces/txt 目录,直接 `adb pull /data/anr/traces.txt` 拉出来看即可,只需要关注主线程的堆栈。
复现了好几次发现这个ANR的堆栈每次都有细微的不一样:
trace1:
"main" prio=5 tid=1 Runnable
| group="main" sCount=0 dsCount=0 obj=0x74dee000 self=0xf3e85400
| sysTid=7194 nice=-8 cgrp=default sched=0/0 handle=0xf6ac8534
| state=R schedstat=( 8038679820 736972966 3703 ) utm=746 stm=57 core=4 HZ=100
| stack=0xff39d000-0xff39f000 stackSize=8MB
| held mutexes= "mutator lock"(shared held)
at java.util.regex.Matcher.<init>(Matcher.java:172)
at java.util.regex.Pattern.matcher(Pattern.java:1006)
at com.tencent.mm.ui.widget.celltextview.g.a.o(SourceFile:95)
at com.tencent.mm.ui.widget.celltextview.g.a.dc(SourceFile:55)
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
at android.view.View.measure(View.java:19733)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6120)
trace2:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x74dee000 self=0xf3e85400
| sysTid=28710 nice=0 cgrp=default sched=0/0 handle=0xf6ac8534
| state=S schedstat=( 9092111599 382548336 2994 ) utm=840 stm=68 core=5 HZ=100
| stack=0xff39d000-0xff39f000 stackSize=8MB
| held mutexes=
kernel: __switch_to+0x8c/0x98
kernel: futex_wait_queue_me+0xd4/0x130
kernel: futex_wait+0xfc/0x210
kernel: do_futex+0xe0/0x920
kernel: compat_SyS_futex+0xe8/0x17c
kernel: cpu_switch_to+0x48/0x4c
native: #00 pc 000173e4 /system/lib/libc.so (syscall+28)
native: #01 pc 000b62bd /system/lib/libart.so (_ZN3art17ConditionVariable16WaitHoldingLocksEPNS_6ThreadE+92)
native: #02 pc 003efd1b /system/lib/libart.so (_ZN3artL12GoToRunnableEPNS_6ThreadE+230)
native: #03 pc 003efc0b /system/lib/libart.so (_ZN3art12JniMethodEndEjPNS_6ThreadE+8)
native: #04 pc 0006de8f /data/app/com.tencent.mm-1/oat/arm/base.odex (Java_com_tencent_mars_xlog_Xlog_logWrite2__ILjava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2IIJJLjava_lang_String_2+242)
at com.tencent.mars.xlog.Xlog.logWrite2(Native method)
at com.tencent.mars.xlog.Xlog.logI(SourceFile:61)
at com.tencent.mm.sdk.platformtools.w.i(SourceFile:260)
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
at android.view.View.measure(View.java:19733)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6120)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1464)
trace3:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x74dee000 self=0xf3e85400
| sysTid=29538 nice=-8 cgrp=default sched=0/0 handle=0xf6ac8534
| state=S schedstat=( 30247025742 1240920862 6268 ) utm=2836 stm=187 core=4 HZ=100
| stack=0xff39d000-0xff39f000 stackSize=8MB
| held mutexes=
kernel: __switch_to+0x8c/0x98
kernel: futex_wait_queue_me+0xd4/0x130
kernel: futex_wait+0xfc/0x210
kernel: do_futex+0xe0/0x920
kernel: compat_SyS_futex+0xe8/0x17c
kernel: cpu_switch_to+0x48/0x4c
native: #00 pc 000173e4 /system/lib/libc.so (syscall+28)
native: #01 pc 000b62bd /system/lib/libart.so (_ZN3art17ConditionVariable16WaitHoldingLocksEPNS_6ThreadE+92)
native: #02 pc 00279749 /system/lib/libart.so (_ZN3art3JNI18ReleaseStringCharsEP7_JNIEnvP8_jstringPKt+204)
native: #03 pc 0001329d /system/lib/libjavacore.so (???)
native: #04 pc 00012e73 /system/lib/libjavacore.so (???)
native: #05 pc 001fbc83 /system/framework/arm/boot.oat (Java_java_util_regex_Matcher_findNextImpl__JLjava_lang_String_2_3I+142)
at java.util.regex.Matcher.findNextImpl(Native method)
at java.util.regex.Matcher.find(Matcher.java:437)
- locked <0x0f4fd7f4> (a java.util.regex.Matcher)
at com.tencent.mm.ui.widget.celltextview.g.a.o(SourceFile:96)
at com.tencent.mm.ui.widget.celltextview.g.a.dc(SourceFile:55)
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
at android.view.View.measure(View.java:19733)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6120)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1464)
at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1117)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:642)
at android.view.View.measure(View.java:19733)
trace4:
"main" prio=5 tid=1 Runnable
| group="main" sCount=0 dsCount=0 obj=0x74dee000 self=0xf3e85400
| sysTid=31062 nice=-8 cgrp=default sched=0/0 handle=0xf6ac8534
| state=R schedstat=( 7752111273 152798863 1498 ) utm=728 stm=46 core=5 HZ=100
| stack=0xff39d000-0xff39f000 stackSize=8MB
| held mutexes= "mutator lock"(shared held)
at com.tencent.mm.ui.widget.celltextview.g.a.V(SourceFile:-1)
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
at android.view.View.measure(View.java:19733)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6120)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1464)
at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1117)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:642)
at android.view.View.measure(View.java:19733)
trace1和trace3看起来都是正则表达式的问题,但是trace2堆栈落在了输出日志上,trace4则是另一个函数的函数。由于堆栈并不是固定的,因此可以排除是某一个函数耗时超长的情况(比如正则匹配);不过这几个堆栈有相似之处,就是他们有一些相同的父调用链:
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
因此可以初步下结论:`com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)` 这个函数内部有循环,由于某未知原因导致循环无法退出,或者循环内某个调用较为耗时导致整个循环体超级耗时,进而出现用户点击界面之后主线程由于一直在循环内无法响应用户操作,出现ANR;ANR的时候(注意这时候主线程并没有死掉,而是一直在执行代码,只不过在死循环里面出不来)系统抓主线程的堆栈,不同的时间会抓到循环体内不同的行数上,所以会发现调用栈不同的情况。
有了基本方向之后就可以直接调试微信并验证了。
工具:Root设备(准确滴说co.debuggable = 1的设备)或者模拟器,Android Studio,baksmali,SmaliIdea插件,某信最新版apk
步骤:
- 安装SmaliIDEA调试插件
- 下载SmaliIDEA插件到本地:https://bitbucket.org/JesusFreke/smali/downloads/smalidea-0.05.zip
- 打开AndroidStudio的设置面板,左侧选择Plugin:
点`Install plugin from disk`,然后选择你刚刚下载的那个文件,然后按步骤重启Android Studio即可完成安装。
2. 反编译为smali
接下来可以把某信反编译为smali指令,然后进行调试。
1)首先下载https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.2.1.jar 和 某信的最新apk文件
2)然后用上述下载的baksmali反编译某信:`java -jar baksmali-2.2.1.jar d mouxin.apk`,即可得到一个名为`out`的输出目录,里面是反编译好的smali文件。(这一步也可以使用apktool来进行,更为简单)
3)在符合要求的设备上装上某信安装包,然后登陆。
3. 建立动态调试环境
1)首先导入并创建项目。打开Android Studio,选择菜单File-New Project ->Import Project:
然后选择刚刚baksmali的输出目录:
点确定,Next:
然后一路Next直到Finish完成。
2)设置smali源码目录。首先把目录视图从‘Android’切换为‘project’:
这样就能看到你导入的smali源码目录,右键此目录:
这一步完成。
3)打开调试器。首先用usb连上手机,在有Android SDK的前提下,在命令行执行:`$ANDROID_HOME/tools/monitor`即可打开Android Monitor;如果你的设备满足条件(所有进程可调试)那么左边会出现进程列表:
直接鼠标点击你要调试的进程,会出现一个如上图的端口号,这个是adb进行调试用来端口转发的socket号,记住这个数字,这里是8700。
4)连上调试器。点击Android Studio菜单,Run->Edit Configration:
点击+号,新建Remote类型的配置:
然后把要连接的端口号修改为上面的8700即可:
点击确定之后,Android Studio的工具栏右上角会出现Debug按钮,点击此按钮:
下方的控制台就会输出:
这样,整个调试环境已经准备好了;接下来可以进行动态调试。
4. 开始动态调试。
通过第一步的anr trace文件分析,我们知道大概出问题的地方是 `com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)` ,我们在Android Studio中打开这个文件,找到这个函数调用的地方,直接下个断点:
然后打开某信复现此bug,可以看到Android Studio中断点触发:
接下来就可以正常地进行调试;如果要查看变量值,可以用Alt + F8 或者在下方的watch窗口中添加,如下图(点➕添加):
断点下好之后,可以不停滴单步执行,观察程序的流程;不停滴跟踪之后发现,这个函数会不断滴被重入,因此上面下的基本结论“此函数内有循环”是错误的,应该是次函数被循环调用。于是我们把断点下在调用此函数的地方:
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.CD(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cu(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
继续跟踪,就可以知道为什么会出现死循环的情况。
关于更多的调试原理与调试姿势,可以移步我的博客:
正好群里大佬在自己分析,目前国内的那几个厂商的很多出现了这个问题,公司内三星的手机貌似都幸免于难了,不过据部分网友反应,三星一些机型也有这问题。
原文链接:http://androidwing.net/index.php/243 (转侵删)
首先,微信发生ANR以后,会生成traces.txt文件。通过adb 导出
adb pull /data/anr/traces.txt ~/
其中有这么一段:
native: #05 pc 0043a419 /data/dalvik-cache/arm/system@framework@boot.oat (Java_java_util_regex_Matcher_setInputImpl__JLjava_lang_String_2II+132)
at java.util.regex.Matcher.setInputImpl(Native method)
at java.util.regex.Matcher.resetForInput(Matcher.java:252)
- locked <0x0ecefa84> (a java.util.regex.Matcher)
at java.util.regex.Matcher.reset(Matcher.java:208)
at java.util.regex.Matcher.reset(Matcher.java:177)
at java.util.regex.Matcher.<init>(Matcher.java:90)
at java.util.regex.Pattern.matcher(Pattern.java:297)
at com.tencent.mm.ui.widget.celltextview.g.a.o(SourceFile:95)
at com.tencent.mm.ui.widget.celltextview.g.a.dc(SourceFile:55)
at com.tencent.mm.ui.widget.celltextview.f.b.a(SourceFile:76)
at com.tencent.mm.ui.widget.celltextview.d.a.Cw(SourceFile:466)
at com.tencent.mm.ui.widget.celltextview.d.a.Cp(SourceFile:92)
at com.tencent.mm.ui.widget.celltextview.CellTextView.onMeasure(SourceFile:102)
at android.view.View.measure(View.java:18794)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1465)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:748)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:630)
at android.view.View.measure(View.java:18794)
发现是cellTextView锁在了celltextView正则的时候。
于是乎debug celltextview包的a类的o方法,
发现一段超级复杂的正则(部分位置打码),所以初步断定为可能是正则时间太长导致。于是写了一个单元测试,来测试该正则是否有问题。
实验发现,这个正则根本不会导致耗时过长,平均耗时0-1ms。
那也就是说明,其实不是这里的原因。
于是将断点打靠上层,到 com.tencent.mm.ui.widget.celltextview.f.b.a() 方法上
点击放过按钮发现程序无限次落到这个断点上,由此可知,是造成了死循环,无限调用a()方法导致的。
继续深究,为什么会导致死循环。
线索1:
发现a()方法上面有一个判断,会导致跳到cond_6最终会继续跳到goto_4调用a()方法。
这里有个
add-int/lit8 v4, v4, -0x1
其实他相当于
i-1
线索2
观察a()方法后面,有wwk,width等属性调用。
结合线索
接下来,打开jadx,将class文件反编译为java文件,利用线索快速定位代码。发现这些逻辑代码片段如下:
有了java代码,一下子就和蔼可亲了,来屡一下这段的逻辑。
可以看到有两个while循环,这里不关心外部while,因为可以看出真正卡死的是在内部while循环。
内部while循环首先判断了dVar2 是否为空,以及dVar2的text是否为空。
debug发现,dVar2是一个TextPaint类,用于绘制文本信息(包括字号,大小,颜色,超链接样式之类的)。
也就是说,只要dVar2不为空,这个循环就不会退出,根据代码可以看出,只有在i4>0的时候才可能把dVar2置为空:
那么i4是什么呢,在红框上面可以看到,i4是a的wwk属性。这个值暂时不知道是什么。
不过通过debug发现,这个wwk是始终等于0的,也就是不满足while内部的dVar2的置空条件,也就造成了while死循环。
于是乎,造成anr的最根本原因就是在这个while里了。
(至于上面的代码怎么得到的,有个东西叫做动态反编译【为更好地学习研究Android技术,请勿用于抄袭等操作】,不过有大佬找出原因了,我就不自己动手去重复做一遍了。)
顺便,试过卡死的小伙伴,直接用对方机子回复,直到这消息顶出当前界面就好。(目前公司一个小伙伴已经打进了医院,因为每次群里发红包之前在群里发这个,导致很多安卓小伙伴无法正常抢红包)