内容简介:系统控件和系统堆栈的crash初看,总以为不好解决,本文通过一步步推导来分析定位,最终找到crash是应用堆栈触发的。最新线上新版本遇到了一个大规模的crash,也不太好复现,crash堆栈大概如下:
系统控件和系统堆栈的crash初看,总以为不好解决,本文通过一步步推导来分析定位,最终找到crash是应用堆栈触发的。
问题描述
最新线上新版本遇到了一个大规模的crash,也不太好复现,crash堆栈大概如下:
0 CoreFoundation 0x00000001819f6d8c ___exceptionPreprocess + 228 1 libobjc.A.dylib 0x0000000180bb05ec objc_exception_throw + 44 2 CoreFoundation 0x00000001819f6c6c -[NSException initWithCoder:] 3 UIKit 0x000000018bfe3134 -[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:] + 588 4 UIKit 0x000000018bfe3cbc -[UIPageViewController _setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:] + 568 5 UIKit 0x000000018bfe6da0 -[UIPageViewController _handlePanGesture:] + 292 6 UIKit 0x000000018b7e26e8 -[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 64 7 UIKit 0x000000018bd4f3b4 __UIGestureRecognizerSendTargetActions + 124 8 UIKit 0x000000018b944e38 __UIGestureRecognizerSendActions + 320 9 UIKit 0x000000018b7e1740 -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 764 10 UIKit 0x000000018bd40bd4 __UIGestureEnvironmentUpdate + 1096 11 UIKit 0x000000018b7db4d8 -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 404 12 UIKit 0x000000018b7db010 -[UIGestureEnvironment _updateGesturesForEvent:window:] + 276 13 UIKit 0x000000018b7da874 -[UIWindow sendEvent:] + 3132 23 UIKit 0x000000018b8d9758 UIApplicationMain + 228 24 mttlite 0x0000000102a843f4 main (main.mm:35) 25 libdyld.dylib 0x000000018134dfc0 _start + 4 The number of view controllers provided (0) doesn't match the number required (2) for the requested transition
问题分析
开始
咋一看,这个和UITableView等类似的the number of section after updated(xxx) does not match before(xxx) ... 类似,以为是不是命中了系统的什么bug,但是看了一圈代码也没找到有特殊的逻辑;再观察所有crash记录,发现出现问题时必有如下handlePanGesture的操作,难道这里有什么系统的bug吗?
而且由于这个问题很难复现到,所以一时就无从下手了。
5 UIKit 0x000000018bfe6da0 -[UIPageViewController _handlePanGesture:] + 292
分析问题
既然复现也不好复现,那就看从crash里找信息吧
3 UIKit 0x000000018bfe3134 -[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:] + 588
问题发送在[UIPageViewController _validataedViewControllersForTranstionWithViewController:animated:] 函数的段内偏移588的地方,添加符号断点,找到+588的代码
+588只做了一次赋值操作,所以应该是+584的一次函数调用触发了exception,但是正常路径里又不会走进去+584的代码;
bl 0x18c0452fc
这是一个有返回值的函数调用,返回值由x0寄存器存储;也就是在这个函数执行过程中发生了exception,继续搜索+584的符号地址0x18c0452fc,发现有若干个地方都会去调用这个方法;一步步修改寄存器值,尝试触发走到这个bl指令去回溯代码,发现修改+124代码里的w2寄存器值为0就能触发走到+248去,此时就能触发执行bl 0x18c0452fc,代码如下
0x18fac6f54 <+108>: mov x23, x0 0x18fac6f58 <+112>: orr w20, wzr, #0x1 0x18fac6f5c <+116>: cbnz x23, 0x18fac7138 ; <+592> 0x18fac6f60 <+120>: b 0x18fac70f4 ; <+524> 0x18fac6f64 <+124>: cbz w20, 0x18fac6fe0 ; <+248> 0x18fac6f68 <+128>: adrp x8, 158039 0x18fac6fe0 <+248>: adrp x8, 158039 0x18fac6fe4 <+252>: ldrsw x26, [x8, #0xc60] 0x18fac6fe8 <+256>: ldr x2, [x22, x26] 0x18fac6fec <+260>: adrp x8, 158011 0x18fac6ff0 <+264>: ldr x1, [x8, #0x360] 0x18fac6ff4 <+268>: mov x0, x22 0x18fac6ff8 <+272>: bl 0x18c0452fc
只要走进去了这个函数,那么就应该会必挂。
那接着看下这个函数干了什么?
step into进0x18c0452fc的函数调用,发现如下
-> 0x18c0452fc: b 0x1846a8900 ; objc_msgSend 0x18c045300: b 0x184684ba4 ; __cxa_allocate_exception 0x18c045304: b 0x184684cf4 ; __cxa_begin_catch 0x18c045308: b 0x184685ab8 ; __cxa_call_unexpected 0x18c04530c: b 0x184684d8c ; __cxa_end_catch 0x18c045310: b 0x1846a3284 ; objc_lookUpClass 0x18c045314: b 0x1846a8b00 ; objc_msgSendSuper2 0x18c045318: b 0x1846b0130 ; objc_autorelease
其中b 0x1846a8900函数调用是做了一次验证,接着就会走向抛exception流程
General Purpose Registers: x0 = 0x000000010d28d000 x1 = 0x000000018ff3a18d "_validRangeForPresentationOfViewControllersWithSpineLocation:"
从而触发了这个crash
2018-07-09 13:54:18.949648+0800 mttlite[55265:6636239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'The number of provided view controllers (2) doesn't match the number required (1) for the requested spine location (UIPageViewControllerSpineLocationMin)' *** First throw call stack: (0x1854dad8c 0x1846945ec 0x1854dac6c 0x18fac70a8 0x18fac7cbc 0x18facada0 0x18f2c66e8 0x18f8333b4 0x18f428e38 0x18f2c5740 0x18f824bd4 0x18f2bf4d8 0x18f2bf010 0x18f2be874 0x18f2bd1d0 0x18fa9ed1c 0x18faa12c8 0x18faa1628 0x18fa9a368 0x185483404 0x185482ce0 0x18548079c 0x1853a0da8 0x187385020 0x18f3bd758 0x1044ed90c 0x184e31fc0) libc++abi.dylib: terminating with uncaught exception of type NSException
好的思路已经有了,就是要避免 _validataedViewControllersForTranstionWithViewController:animated: 函数触发 bl 0x18c0452fc
指令的执行;
分析路径
那如何避免 bl 0x18c0452fc
的执行呢?
那就从头开始过一遍 validataedViewControllersForTranstionWithViewController:animated: 函数吧
UIKit`-[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:]: 0x18fac6ee8 <+0>: sub sp, sp, #0x70 ; =0x70 0x18fac6eec <+4>: stp x26, x25, [sp, #0x20] 0x18fac6ef0 <+8>: stp x24, x23, [sp, #0x30] 0x18fac6ef4 <+12>: stp x22, x21, [sp, #0x40] 0x18fac6ef8 <+16>: stp x20, x19, [sp, #0x50] 0x18fac6efc <+20>: stp x29, x30, [sp, #0x60] 0x18fac6f00 <+24>: add x29, sp, #0x60 ; =0x60 0x18fac6f04 <+28>: mov x20, x3 0x18fac6f08 <+32>: mov x22, x0 0x18fac6f0c <+36>: mov x0, x2 0x18fac6f10 <+40>: bl 0x18c04532c
前面的代码调用好像是为了栈入栈,后面3行,明显是为了进行新的objc_msgSend,像x0寄存器发消息0x18c04532c。
去除x0发现,x0是一个数组,返回了所有的viewcontroller,在仔细看下面的代码,都是在跳转,并最终尽量避免跳转到0x18c04532c异常处理去,而最后函数的返回值依旧是x0的原始值,也就是这个函数的本质就是再对x0的参数做校验,如果校验成功,返回x0,否则抛出exception。
从crash信息中可以看到,出现异常时x0的个数是0,而不是预期的2,那么x0的值是由谁修改的呢?回溯到上层调用,找到了源头。
0x18fac7ca4 <+544>: adrp x8, 158010 0x18fac7ca8 <+548>: ldr x1, [x8, #0x3e0] 0x18fac7cac <+552>: orr w3, wzr, #0x1 0x18fac7cb0 <+556>: mov x0, x25 0x18fac7cb4 <+560>: mov x2, x27 -> 0x18fac7cb8 <+564>: bl 0x18c0452fc 0x18fac7cbc <+568>: mov x29, x29 0x18fac7cc0 <+572>: bl 0x18c045338
+564是发消息执行validataedViewControllersForTranstionWithViewController:animated: 函数,x27是实际传入函数的数组,如果x27这个时候为0,则会必然导致crash的发生;那么x27是谁修改的?继续往上查找发现x27是由函数调用[UIPageViewController _setViewControllers:withCurlOfType: 调进来的
回溯到调用栈[UIPageViewController _handlePanGesture:]里,也就是这里调用[UIPageViewController _setViewControllers:withCurlOfType: 是传入的数组元素个数为0
0x18facad30 <+180>: mov x0, x20 -> 0x18facad34 <+184>: bl 0x18c0452fc 0x18facad38 <+188>: mov x29, x29 0x18facad3c <+192>: bl 0x18c045338 0x18facad40 <+196>: mov x19, x0
网上回溯,发现[UIPageViewController _setViewControllers:withCurlOfType: 的参数viewcontrollers是由0x18c0452fc函数调用修改的,找到调用发现,这里做了一个发消息
(lldb) re read -a General Purpose Registers: x0 = 0x000000010e27ec00 x1 = 0x000000018ff3a65a "_incomingViewControllersForGestureDrivenCurlInDirection:" (lldb) po 0x000000010e27ec00 <MttNovelPageViewController: 0x10e27ec00>
所以问题解决了,执行[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]返回了实际的viewcontrollers数组,如果这里返回了空数组,那进入[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]看看吧
定位原因
经过前面的操作,已经初步定位到了是[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:] 该函数最终返回了空数组,且在handlePanGestures:期间,改函数会被调用若干次,那么就看下这个函数到底调用了什么来返回了viewcontrollers数组了?
依旧从返回值开始分析
0x18faca1c0 <+1048>: mov x0, x21 0x18faca1c4 <+1052>: ldp x29, x30, [sp, #0x60] 0x18faca1c8 <+1056>: ldp x20, x19, [sp, #0x50] 0x18faca1cc <+1060>: ldp x22, x21, [sp, #0x40] 0x18faca1d0 <+1064>: ldp x24, x23, [sp, #0x30] 0x18faca1d4 <+1068>: ldp x26, x25, [sp, #0x20] 0x18faca1d8 <+1072>: ldp x28, x27, [sp, #0x10] 0x18faca1dc <+1076>: add sp, sp, #0x70 ; =0x70 0x18faca1e0 <+1080>: b 0x18c04531c
x0的值是由于x21传入的,也就是最终的函数返回值是由x21寄存器决定的,那就分析x21的相关代码,最终找到了如下疑似代码
0x18fac9ff0 <+584>: mov x0, x20 0x18fac9ff4 <+588>: mov x1, x23 0x18fac9ff8 <+592>: mov x2, x24 -> 0x18fac9ffc <+596>: bl 0x18c0452fc 0x18faca000 <+600>: mov x29, x29 0x18faca004 <+604>: bl 0x18c045338 0x18faca008 <+608>: mov x22, x0
执行完 bl 0x18c0452fc
后就获取到了数组
(lldb) re read -a General Purpose Registers: x0 = 0x000000010e27ec00 x1 = 0x000000018ff3a510 "_viewControllerAfterViewController:"
即实际的viewControllers是由[UIPageViewController _viewControllerAfterViewController:]函数返回的。好了,问题差不多找到了,最终[UIPageViewController _viewControllerAfterViewController:]是调用了UIPageViewControllerDatasource的
- (nullable UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController;
该方法返回了nil,问题也找到了,的确业务实现这个datasource时存在先返回nil再重设值的问题。
但是为什么没有必挂呢?
因为这个上层调用[UIPageViewController _incomingViewControllersForGestureDrivenCurlInDirection:]函数会在handlePanGesture时多次调用,而这里是多线程异步的会写setViewControllers:所以只有个别情况才能出现这个crash;
[self.pageCurlDelegate requestPageViewFor:novelLayerView toNextPage:bToNextPage complete:blockFunc]; bHaveReturnNil = YES; _countPageLoading--; if(!targetViewController&&bToNextPage) { //此处存在先返回nil,再异步回调[self.pageViewController setViewControllers:@[targetViewController] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];的问题,如果时序过久,则会触发crash;所以解决办法是永远不返回nil } return targetViewController;
总结
本质上是由于UIPageViewController的dataSource存在异步返回nil的情况,所以导致了该问题不会稳定必现;这种block设计嵌套的逻辑,搞的可能有时返回nil,但立即同步设置非nil的viewcontrollers;但有时因为线程切换导致需要延时调用,一旦超过UIPageViewController的某个条件,就触发了nil的crash了。
以上所述就是小编给大家介绍的《汇编分析一次系统控件系统栈的 crash》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- WindowsXamlHost:在 WPF 中使用 UWP 控件库中的控件
- WindowsXamlHost:在 WPF 中使用 UWP 控件库中的控件
- Flutter控件--Scaffold
- Flutter控件--AppBar
- Flutter 手势密码控件
- Flutter 设置控件是否可见
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。