乐趣区

关于前端:一群人的战斗

1. Bug 来了

一个平静的周日午后,正悠闲地在公园里遛娃。忽然来了一条音讯,关上企业微信认真看了下,竟大吃一惊:客户胜利在群内反馈了 Android A/B Testing SDK 的一个 crash,须要紧急解决。

得悉问题后我立即和客户胜利进行了语音沟通:理解到客户的利用明天早晨会上线,放心该 crash 带到线上之后会产生井喷,须要咱们这边尽快予以修复。得悉这个状况之后,一场轰轰烈烈的 Bug 反抗之旅就此拉开序幕。

2. 紧急动员

17 点 09 分

刚刚和客户胜利语音沟通结束,须要立刻动员相干的同学解决此问题。

在介绍如何动员之前,先和大家简略阐明下 SDK 的发版流程:

研发修复 Bug 并实现自测;
提供给组员进行 Code Review 并同步影响范畴;
QA 进行测试,如果测试通过则进行公布。
因而,这里要求研发和 QA 同学的深度参加。如果放在工作日,可能会比较简单。然而此时恰逢周日,并且事发忽然,客户给予的工夫紧迫。

基于此背景,开始兵分两路:

第一工夫找到 A/B Testing SDK 的项目经理,请他来帮忙协调版本公布的相干人员;
与此同时,我开始进行问题的定位剖析。

3. 独立剖析

17 点 52 分

飞奔到家里,关上了客户提供的 crash 堆栈。该堆栈是客户从本人的剖析平台里提取进去的,本地无奈复现,堆栈如下:

main(1)

java.util.UnknownFormatConversionException

Conversion = ‘_’

还原失败(未找到符号表)(404_1_0_2_0_0_0_0_9_0)

解析原始
1 java.util.Formatter$FormatSpecifier.conversion(Formatter.java:2782)
2 java.util.Formatter$FormatSpecifier.<init>(Formatter.java:2812)
3 java.util.Formatter$FormatSpecifierParser.<init>(Formatter.java:2625)
4 java.util.Formatter.parse(Formatter.java:2558)
5 java.util.Formatter.format(Formatter.java:2505)
6 java.util.Formatter.format(Formatter.java:2459)
7 java.lang.String.format(String.java:2870)
8 com.sensorsdata.abtest.a.c.a(SABErrorDispatcher.java:29)
9 com.sensorsdata.abtest.a.d$3.a(SensorsABTestApiRequestHelper.java:236)
10 com.sensorsdata.abtest.a.d$2.onFailure(SensorsABTestApiRequestHelper.java:185)
11 com.sensorsdata.analytics.android.sdk.network.HttpCallback$1.run(HttpCallback.java:46)
12 android.os.Handler.handleCallback(Handler.java:900)
13 android.os.Handler.dispatchMessage(Handler.java:103)
14 android.os.Looper.loop(Looper.java:219)
15 android.app.ActivityThread.main(ActivityThread.java:8387)
16 java.lang.reflect.Method.invoke(Native Method)
17 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
18 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
联合上下文剖析发现:接口申请失败时,SDK 会打印谬误日志。在打印的过程中调用 format 办法导致 UnknownFormatConversionException 异样。

通过上述剖析,问题排查范畴放大到:调用 format 办法导致的 crash,并将初步排查论断同步给客户和客户胜利。

正在进一步定位为什么会导致 crash 时,忽然发现我曾经被拉进「紧急解决 Android A/B Testing SDK 解体问题」企业微信群。

4. 抽丝剥茧

18 点 04 分

进入到专项问题探讨群后,技术顾问向客户确认了更多的信息,例如 crash 的机型、次数、版本等,进一步欠缺了问题的周边信息。

此时,群内各位大佬纷纷出谋划策,尝试复现 crash。

思路一:format 办法的第一个参数存在特殊字符 ‘% ‘

String.format(“where name like % %s”,”Zhang san”);
format 办法传入特殊字符,尝试复现,crash 堆栈如下:

Exception in thread “main” java.util.IllegalFormatFlagsException: Flags = ‘ ‘

at java.util.Formatter$FormatSpecifier.checkText(Formatter.java:3037)
at java.util.Formatter$FormatSpecifier.<init>(Formatter.java:2733)
at java.util.Formatter.parse(Formatter.java:2560)
at java.util.Formatter.format(Formatter.java:2501)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
at com.zxwei.cf.lib.MyClass.main(MyClass.java:22)

可见,这个堆栈和理论的 crash 堆栈不统一。

思路二:format 办法的第一个参数为 null

String.format(null,”Zhang san”);
format 办法传入 null,此时 crash 堆栈如下:

Exception in thread “main” java.lang.NullPointerException

at java.util.regex.Matcher.getTextLength(Matcher.java:1283)
at java.util.regex.Matcher.reset(Matcher.java:309)
at java.util.regex.Matcher.<init>(Matcher.java:229)
at java.util.regex.Pattern.matcher(Pattern.java:1093)
at java.util.Formatter.parse(Formatter.java:2547)
at java.util.Formatter.format(Formatter.java:2501)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
at com.zxwei.cf.lib.MyClass.main(MyClass.java:24)

比对堆栈信息,发现和理论的 crash 堆栈还是不统一。

思路三:format 办法的第一个参数存在特殊字符 ‘%_’

String.format(“where name like %_”,”Zhang san”);
通过这种形式的模仿,堆栈如下:

Exception in thread “main” java.util.UnknownFormatConversionException: Conversion = ‘_’

at java.util.Formatter.checkText(Formatter.java:2579)
at java.util.Formatter.parse(Formatter.java:2565)
at java.util.Formatter.format(Formatter.java:2501)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
at com.zxwei.cf.lib.MyClass.main(MyClass.java:22)

和理论的 crash 堆栈是一样的,但理论我的项目中 format 办法的第一个参数是不变的,不可能会呈现特殊字符。

思路四:从源码剖析产生 UnknownFormatConversionException 异样的起因

private char conversion(String s) {

        c = s.charAt(0);
        if (!dt) {if (!Conversion.isValid(c))
                throw new UnknownFormatConversionException(String.valueOf(c));
            if (Character.isUpperCase(c))
                f.add(Flags.UPPERCASE);
            c = Character.toLowerCase(c);
            if (Conversion.isText(c))
                index = -2;
        }
        return c;
    }

从源码来看,的确是存在非法字符才会抛出异样,但为什么就是复现不了呢?

5. 斗争还是保持

18 点 41 分

工夫在一点点的流逝,然而问题始终都没有复现。就在大家都束手无策之际,我忽然灵光一现:因为该问题的呈现只是产生在日志打印期间,是否能够绕过 format 办法,通过字符串间接打印日志?这样相当于间接绕过问题,也能进行解决。

很多时候咱们都会面临相似的抉择:绕过去还是坚持到底?此时,书记给了一个倡议:目前是偶现,不焦急修复,还是尽量找到根本原因。

不轻易放过一个问题,将问题跟进到底,咱们一起抉择了保持。

6. 拨开迷雾

19 点 09 分

这时候项目经理给了一个很要害的假如:间断屡次调用 format 办法,并且将援用传递到 format 办法的第一个参数,这样会不会有问题呢?

沿着这个思路,联合我的项目中的理论代码,的确存在特殊字符且间断调用的场景。基于此假如,咱们胜利复现出了问题并予以修复。

这一刻让我感到了一个人的力量是无限的,团队的力量是无穷的。

随后 QA 立刻近程投入问题验证以及版本公布,在大家的共同努力下于 20 点 40 分顺利交付给客户。

客户拿到新版本后立刻集成、自测,并于当天早晨胜利上线。

至此,这个问题算是解决结束。

7. 总结

问题产生在周日,从发现到解决,前后总共 4 个小时不到。

在这短短的 4 个小时内,波及到项目经理、测试、研发、技术顾问共 7 人。感激大家的通力合作,使得问题可能在如此短的工夫内予以解决。

最初,我想和大家分享的是:

于本人而言,遇到问题,要有一颗坚守的心。放弃很容易,但有了一次放弃就会有第二次、第三次。寻求假相的过程必然崎岖,当失去后果的那一刻所有都值得,但求无愧于心;
于团队而言,遇到问题,不要一个人去战斗,在必要的时候能够寻求帮忙,总会有一群人给咱们出谋划策。大家有着同样的指标,相互配合往往能够碰撞出不一样的火花。如果没有大家的帮忙,因为集体思路的局限性肯定会消耗大量的工夫,更别提后续的测试、发版。这次经验也让我更加领会到团队单干的重要性。

退出移动版