闲鱼Flutter技术的基础设施已基本趋于稳定,就在我们准备松口气的时候,一个Crash却异军突起冲击着我们的稳定性防线!闲鱼技术火速成立侦探小组执行嫌犯侦查行动,经理重重磨难终于在一个隐蔽的角落将其绳之以法!

幽灵Crash

问题要从闲鱼Flutter基础设施上一次大规模升级说起。2018年我们对闲鱼的Flutter基建作了比较大的重构,目标在于提高基建的稳定性和可扩展性。这个过程当然是挑战重重,在上一次大规模的重构集成发版后,我们虽然没有发现非常明显的异常问题,但是Crash率却出现了一个比较明显的增长。虽然总体数值还在可控范围之内,但这一个Crash却占据了几乎一大半。这个问题引起了我们警觉,我们立刻成立专项小组重点进行排查。

一般Crash Log能够为我们定位Crash提供主要信息,我们一起看看这个Crash的Log:

Thread 0 Crashed:
0 libobjc.A.dylib 0x00000001c1b42b00 objc_object::release() :16 (in libobjc.A.dylib)
1 libobjc.A.dylib 0x00000001c1b4338c (anonymous namespace)::AutoreleasePoolPage::pop(void*) :676 (in libobjc.A.dylib)
2 CoreFoundation 0x00000001c28e8804 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ :28 (in CoreFoundation)
3 CoreFoundation 0x00000001c28e8534 __CFRunLoopDoTimer :864 (in CoreFoundation)
4 CoreFoundation 0x00000001c28e7d68 __CFRunLoopDoTimers :248 (in CoreFoundation)
5 CoreFoundation 0x00000001c28e2c44 __CFRunLoopRun :1880 (in CoreFoundation)
6 CoreFoundation 0x00000001c28e21cc _CFRunLoopRunSpecific :436 (in CoreFoundation)
7 GraphicsServices 0x00000001c4b59584 _GSEventRunModal :100 (in GraphicsServices)
8 UIKitCore 0x00000001efb59054 _UIApplicationMain :212 (in UIKitCore)
9 Runner 0x0000000102df4eb4 main main.m:49 (in Runner)
10 libdyld.dylib 0x00000001c23a2bb4 _start :4 (in libdyld.dylib)

这是一个很典型的野指针Crash Log,是其中一种俗称的Over released问题。但是具体是哪个对象和方法,很难直接从Log上面得知,况且ARC下面的野指针更令人费解。

一些推测

Crash理因由变更引入的,我们直觉地从最近发版引入的主要变更去推测。考虑到我们开始出现问题的版本有几个比较大的改造,我们让相关的同学重新review了一下自己的代码,主要关注内存方面的问题。虽然没有找到非常确切的问题,我们还是进行了一次可疑代码优化,进行技术灰度却没有任何效果。在庞大的代码库数不清的提交中去找寻毫无头绪的野指针问题看起来不是一件容易的事情,

机型 iOS版本 闲鱼版本

我们详细的分析了Crash的数据以及用户操作日志,然后得出结论这个Crash与机型,系统版本都没明显联系。但是我们可以发现用户基本上都是在Flutter容器的详情页容易崩溃。Flutter不可避免成为了被怀疑对象,包括我们自己实现的基础设施,以及Flutter底层的库。

但是Flutter已经在闲鱼应用比较长的一段时间,Flutter底层我们几乎确定是稳定的,不然早就出问题了。这个时候主要怀疑点转移到了我们自己实现的组件,主要包括混合栈组件以及一些监控埋点设施。但是我们随后将这些怀疑对象通过技术灰度手段一一排除了嫌疑。

版本走势

从版本的Crash率的走势看,我们还发现这个问题有一个缓慢增长放量的过程,这不免让我们开始怀疑App是否存在类似的慢慢放量的功能需求。然而事实证明,这个方向没有任何收获。

无法复现的问题

不断有用户向我们反馈容易遇到闪退,但是我们自己的设备经过大量尝试却没有复现这个问题。这是最为头疼的,从用户的操作路径来看并无特殊的地方。无论是测试还是开发同学都无法在自己设备上面复现出来,无法复现的野指针问题非常难以定位。

线上监控技术

从变更和问题特征排除没有实质性的进展,我们开始尝试线上的一些监控方法来协助排查。希望可以拿到更加详细的相关信息。

GCD线程跟踪技术

从Crash Log我们可以到这应该是一个autorelease对象野指针导致的问题,本来应该autorelease进行释放的对象,在其被AutoReleasePool释放前就因为某种原因提前释放。我们怀疑是否存在多线程导致的问题,所以我们采用GCD线程跟踪技术进行监控。

这个技术的基本原理是hook住GCD的dispatch方法,将block的返回地址通过 __builtin_return_address函数拿到,然后编码写入到当前的线程名中,崩溃的时候,从线程名字中解码得出dispather的返回地址即可定位到是谁dispatch的这个block,然后随同Crash Log的扩展字段将其上传到后台。

GCD是一套C接口,所以我们采用fishhook去hook,此类底层hook对性能会有一定影响,所以我们只在专门的技术验证灰度中采用此项技术。fishhook的大致原理是重新绑定一些C的符号,因为很多共享的库的符号比如GCD在iOS中是动态绑定到App的可执行文件中的。而目前这部分符号表所在的内存没有签名,所以可以通过MachO提供的接口去进行重新绑定。感兴趣的同学可以参考Facebook fishhook项目。

我们准备了一个技术灰度版本来监控这个问题。可能由于样本比较小,我们收集到的返回地址数量非常有限。通过符号解析,得出来的都是一些NSFoundation对象,没有太多有价值的东西。之前怀疑这问题可能发生在GCD执行的block中,只是收集崩溃的时候GCD上一次调用的返回地址本身也缺乏针对性。

期望是美好的,现实是骨感觉,最终我们没有拿到有用的信息。

线上Zombie的野指针监控

在Debug模式下,Xcode有用强大的工具去帮助你定位野指针。最为通用的野指针监控工具莫过于NSZombie,如果我们能在线上开启Zombie应该能够很容易的抓到野指针对象。淘系基础设施里面有线上Zombie的实现。

线上的Zombie实现主要原理hook对象的dealloc方法在dealloc的时候通过runtime的动态性将其转变成一个Zombie类,当有其它消息发给Zombie对象的时候我们就可以根据存储下来的类型定位到Zombie的对象类型。详细可以参考Mike Ash的Let's build NSZombie。不过需要注意的是,这里面的实现是基于MRC,ARC实现上可能会有差异,基本原理是大致相同的。

我们在闲鱼App中根据基础提供的文档将线上Zombie打开进行灰度监控,所幸的是我们拿到了一些野指针对象。量也不是很多,只有个位数的类型。

可能是由于样本不够大,没有覆盖到典型的用户。或许是我们的监控组件无法抓到这个特定类型的Crash。最终在排查完所有收集到的野指针对象后,依然没有解决这个Crash。

线上监控似乎没能为我们打开突破口。

UI自动化

我们还是期望与能够将问题重现出来,这样可以迅速通过Xcode定位到问题。从概率上确实不算太高,基于前面手动复现困难的问题,我们尝试利用自动化工具去做自动复现尝试。

SwiftMonkey + 引擎DEBUG

SwiftMonkey是一个比较好的UI自动化工具,集成简单,而且可以在Debug模式下面进行自动UI测试。也就是说我们可以在保持Xcode各种强大工具有效的前提下进行自动化测试。

我们采用Local Debug Flutter引擎进行测试以便拿到相关的符号,经过一段时间的自动化测试我们在模拟器上面抓到了一摸一样的Crash Log!

这不得不说是一个令人振奋的消息,Xcode抓到的Zombie对象是一个NSMutableArray,这是一个通用对象,似乎也没有特别的地方。这个时候我们需要用到Xcode提供的malloc log和Address sanitizer去跟踪是谁创建的这个对象。

我们在模拟器上面打开malloc log以及Address sanitizer复现问题导出MemGraph然后使用

memory history 地址
malloc log MemGraph 地址

最终定位到问题出现在Flutter引擎内部文件 accessibility_bridge.mm 533行:

    NSMutableArray* newChildren =
[[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
for (NSUInteger i = 0; i < newChildCount; ++i) {
SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
child.parent = object;
[newChildren addObject:child];
}
object.children = newChildren;

这个问题把我们带到了Flutter的Accessibility(通用->辅助功能)支持模块,我们跟用户经过了交流,并没有发现用户有打开相关的辅助功能。

虽然Log是一摸一样的,我们有点不相信我们追寻的Crash是由于这个原因导致的。这的确是Flutter在Accessibility的一个坑,但是跟我们用户交流的情形不一致。而且模拟器上面容易出现,我们将测试包装到手机上却无法在复现这问题。很显然,用户都是真机,模拟器或许不能说明问题。此时我们还没有信心确认这个问题,开辅助功能的人应该是不多的。

这感觉好像在黑暗中看到光亮,一瞬间又被黑暗淹没了,我们似乎又来到了一个死胡同。到底是哪里出问题了?

用户面对面

线上交流

在问题排查的过程中我们一直跟用户保持良好的交流。工程师们主动联系用户,很多用户也热心响应我们的访问,给我们录制了不少崩溃现场的视频。我们可以看到那些反馈问题的用户很容易出现,但是不出现的用户基本上没有这个问题。我们开始怀疑跟账号的关系,可能有一些ABTest的参数所有影响。线上的交流虽然给了我们不少有用的信息,但是依然没有实质性突破。

线下面对面

我们开始寻找愿意协助我们现场排查问题用户,我们重点找了几个非常容易出现问题的杭州用户打算上门现场Debug。在和用户进行了深入交流以后,其中一个用户愿意已访问园区方式来现场协助工程师排查问题。

我们选了用户有时间的一个周末然后拿到用户的手机进行了调试,果然在用户的手机上非常容易复现。而且就是我们前面提到的accessibility_bridge.mm处的崩溃,为什么之前再模拟器上那么容易出现呢?

原来在引擎的代码中如果是模拟器的话是默认打开Accessibility的,而真机是取决于系统的设置。

#if TARGET_OS_SIMULATOR
// There doesn't appear to be any way to determine whether the accessibility
// inspector is enabled on the simulator. We conservatively always turn on the
// accessibility bridge in the simulator, but never assistive technology.
platformView->SetSemanticsEnabled(true);
platformView->SetAccessibilityFeatures(flags);
#else
bool enabled = UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning();
if (enabled)
flags |= static_cast<int32_t>(blink::AccessibilityFeatureFlag::kAccessibleNavigation);
platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled());
platformView->SetAccessibilityFeatures(flags);
#endif

原来这名用户打开了iOS的阅读屏幕功能: UIAccessibilityIsSpeakScreenEnabled, 这导致Flutter辅助支持模块被打开。我们马上联系其它用户确认,基本上用户都打开了“阅读屏幕”功能。至此,我们基本确认就是这个问题所致。我们随后进行了一个小范围禁用Accessibility的灰度实验确认就是这问题导致的Crash。

在经过止血修复以后,我们继续寻找野指针的源头。问题出在这个autorelease的NSMutableArray对象,这个代码看起来也没什么明显问题。FLutter引擎的iOS使用MRC进行内存管理。我们继续review相关的代码, 终于在SemanticsObject类发现了一段奇怪的代码:

- (void)dealloc {
for (SemanticsObject* child in _children) {
child.parent = nil;
}
[_children removeAllObjects];
[_children dealloc];
_parent = nil;
[_container release];
_container = nil;
[super dealloc];
}

注意其中的[_children dealloc];,这里不应该直接调用dealloc,而只需要release,这或许就是MRC难以避免的误写吧。问题定位到,修复也就是分分钟钟的事情。

后来我们发现其实这个问题最近已经在Flutter官方master分支上修复了,只是我们自己维护的引擎尚未同步对应的代码。

至此,问题得到圆满解决,Crash率恢复到正常水平。

总结

为了排查这个问题,我们从多个方向同时进行了不同的尝试。具体来说从代码变更跟踪,线上监控技术,UI自动化以及深入阅读相关源码等方式同时去推进问题的解决。需要特别强调的是,跟用户的紧密交流也是解决问题的关键,俗话说知彼知己方能百战不殆,只有充分理解需要解决的问题才能更有效的将其解决。

问题的复现与否通常对于解决方案至关重要,一个能够复现的问题基本能够在现代的IDE提供的强大工具的帮助下方便定位到。一开始我们也是苦于没能找到复现的路径,原来这个Crash却被掩盖在一个并不常见的系统设置下面,同时深藏于Flutter复杂的引擎深部。好在有热心用户愿意协助我们排查问题为我们提供精确的问题现场,才得以最终成功将其确认并解决。


本文作者:闲鱼技术-福居

原文链接

本文为云栖社区原创内容,未经允许不得转载。

大侦探福老师——幽灵Crash谜踪案的更多相关文章

  1. 助教总结 -【福大软工实践-2017-2018-K班】

    助教总结 -[福大软工实践-2017-2018-K班] 非常抱歉这么晚才来写总结! 助教工作 助教共发表博客39篇. 助教共点评约500条. 起步 对于常规课程的起步,通常都是在第一次课堂上由老师对课 ...

  2. 2018年,Java程序员转型大数据开发,是不是一个好选择?

    近日网上有一篇关于Java程序员职场生存现状的文章“2017年 Java 程序员,风光背后的危机”,在Java程序员圈子里引起了广泛关注和热议. 2017年,Java 程序员面临更加激烈的竞争. 不得 ...

  3. P1039 侦探推理(洛谷)

    昨天做了一个非常神奇的题,告诉我们做题之前一定要好好检测评测姬! 明明同学最近迷上了侦探漫画<柯南>并沉醉于推理游戏之中,于是他召集了一群同学玩推理游戏.游戏的内容是这样的,明明的同学们先 ...

  4. Python爬虫之解析网页

    常用的类库为lxml, BeautifulSoup, re(正则) 以获取豆瓣电影正在热映的电影名为例,url='https://movie.douban.com/cinema/nowplaying/ ...

  5. 做中学(Learning by Doing)之背单词-扇贝网推荐

    做中学(Learning by Doing)之背单词-扇贝网推荐 看完杨贵福老师(博客,知乎专栏,豆瓣)的「继续背单词,8个月过去了」,我就有写这篇文章的冲动了,杨老师说: 有时候我会感觉非常后悔,如 ...

  6. 分而治之(Work Breakdown Structure, WBS)

    不知道大家有没有和我一样的情况,就是想写一篇博客,不知道从何写起,如何组织语言,如何安排这篇博客的要交待的事情的前因后果:如果在写作过程中被打断,又不知道如何重新拾起键盘,从哪里写起."就如 ...

  7. 铜齿铁牙UP计划

    铜齿铁牙UP计划 我在""做教练"之好声音训练"给出了老师.播音主持学习者,声乐学习者科学用声三要点: 用气发声 共鸣发声 虚实结合 用气发声首先要学会腹式呼吸 ...

  8. 软工+C(9): 助教指南,持续更新...

    上一篇:提问与回复 下一篇:从命令行开始逐步培养编程能力(Java) 目录: ** 0x00 Handshake ** 0x01 点评 ** 0x02 评分 ** 0x03 知识储备 ** 0x04 ...

  9. PAT乙级考前总结(五)

    字符串处理 1003 我要通过! (20 分) “答案正确”是自动判题系统给出的最令人欢喜的回复.本题属于 PAT 的“答案正确”大派送 —— 只要读入的字符串满足下列条件,系统就输出“答案正确”,否 ...

随机推荐

  1. TZ_13_微服务场景Eureka

    1.搭建Eureka的注册中心 1.1Eureka几个时间间隔配置详解 1 >客户端信息上报到eureka服务的时间周期,配置的值越小,上报越频繁,eureka服务器应用状态管理一致性越高 #客 ...

  2. Yii 网站上线不需手动配置

    参考: http://www.cnblogs.com/x3d/p/php_auto_prepend_file.html

  3. Ubuntu 16.04 配置 L2tp 客户端

    #install lib -dev libsecret--dev libgtk--dev libglib2.-dev xl2tpd strongswan #install network-manage ...

  4. nginx日志字段解析

    许包含的变量注释如下: $remote_addr, $http_x_forwarded_for 记录客户端IP地址 $remote_user 记录客户端用户名称 $request 记录请求的URL和H ...

  5. NOIP模拟 7.01

    水灾(sliker.cpp/c/pas) 1000MS  64MB 大雨应经下了几天雨,却还是没有停的样子.土豪CCY刚从外地赚完1e元回来,知道不久除了自己别墅,其他的地方都将会被洪水淹没. CCY ...

  6. PHP 学习1.3

    1.展示类的继承和静态的方法 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "htt ...

  7. Visual studio加载项目时报错 尚未配置为Web项目XXXX指定的本地IIS,需要配置虚拟目录。解决办法。

    在SVN上下载工程项目.使用visual studio打开时,出现如下提示: 查找相关资料,解决办法如下: 使用记事本打开工程目录下的.csproj文件.把<UseIIS>False< ...

  8. vue 数组遍历方法forEach和map的原理解析和实际应用

    一.前言 forEach和map是数组的两个方法,作用都是遍历数组.在vue项目的处理数据中经常会用到,这里介绍一下两者的区别和具体用法示例. 二.代码 1. 相同点 都是数组的方法 都用来遍历数组 ...

  9. 备考2019年6月份PMP考试-分享一些考试笔记(二)

    最新比较经典的100道试题,有备考的小伙伴可以练练手,文章末尾附答案. 1     一个项目经理在运作一个数据中心安装项目.他发现相关方很恼火,因为他超出了预算,原因是人员费用要高于原先的计划.另外项 ...

  10. vue页面跳转传参

    跳转页 this.$router.push({name:'路由命名',params:{参数名:参数值,参数名:参数值}}) 接收页 this.$route.params.参数名