大侦探福老师——幽灵Crash谜踪案

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

幽灵Crash

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

通常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下面的野指针更使人费解。app

一些推测

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

机型 iOS版本 闲鱼版本

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

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

版本走势

从版本的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复杂的引擎深部。好在有热心用户愿意协助咱们排查问题为咱们提供精确的问题现场,才得以最终成功将其确认并解决。

 

原文连接

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