iOS UIPageViewController系统方法崩溃修复
一切始于这个神奇的报错
Last Exception Backtrace:
0 CoreFoundation 0x1a3692a48 __exceptionPreprocess + 220 (NSException.m:199)
1 libobjc.A.dylib 0x1a33b9fa4 objc_exception_throw + 56 (objc-exception.mm:565)
2 CoreFoundation 0x1a35881c0 +[NSException raise:format:] + 108 (NSException.m:155)
3 UIKitCore 0x1a7078bbc -[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:] + 512 (UIPageViewController.m:1223)
4 UIKitCore 0x1a70796e0 -[UIPageViewController _setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:] + 560 (UIPageViewController.m:1397)
5 UIKitCore 0x1a707c4d8 -[UIPageViewController _handlePanGesture:] + 292 (UIPageViewController.m:0)
6 UIKitCore 0x1a72f3630 -[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 48 (UIGestureRecognizer.m:129)
7 UIKitCore 0x1a72fbd10 _UIGestureRecognizerSendTargetActions + 124 (UIGestureRecognizer.m:1282)
8 UIKitCore 0x1a72f94a4 _UIGestureRecognizerSendActions + 352 (UIGestureRecognizer.m:1321)
9 UIKitCore 0x1a72f8a04 -[UIGestureRecognizer _updateGestureForActiveEvents] + 652 (UIGestureRecognizer.m:0)
10 UIKitCore 0x1a72ec998 _UIGestureEnvironmentUpdate + 2108 (UIGestureEnvironment.m:160)
11 UIKitCore 0x1a72ec110 -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 372 (UIGestureEnvironment.m:1143)
12 UIKitCore 0x1a72ebed4 -[UIGestureEnvironment _updateForEvent:window:] + 204 (UIGestureEnvironment.m:1112)
13 UIKitCore 0x1a77416f8 -[UIWindow sendEvent:] + 3324 (UIWindow.m:2876)
14 UIKitCore 0x1a771de2c -[UIApplication sendEvent:] + 344 (UIApplication.m:11171)
作为系统控件而言, UIPageViewController
着实不让人省心, 坑坑洼洼就罢, 还能通过外部曲线救国, 这次直接在内部方法崩溃了, 一时半会还没有复现的方法
当然了, 要是没解决此问题的话, 也不会有这篇文章
节约时间, 先上代码
@implementation UIPageViewController (fix)
+ (void)fix {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method dstMethod = class_getInstanceMethod([UIPageViewController class], NSSelectorFromString(@"_setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:"));
Method srcMethod = class_getInstanceMethod(self, @selector(fix_setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:));
method_exchangeImplementations(dstMethod, srcMethod);
});
}
- (void)fix_setViewControllers:(nullable NSArray<UIViewController *> *)viewControllers
withCurlOfType:(UIPageViewControllerTransitionStyle)type
fromLocation:(CGPoint)location
direction:(UIPageViewControllerNavigationDirection)direction
animated:(BOOL)animated
notifyDelegate:(BOOL)notifyDelegate
completion:(void (^ __nullable)(BOOL finished))completion {
if (!viewControllers.count) return;
[self fix_setViewControllers:viewControllers withCurlOfType:type fromLocation:location direction:direction animated:animated notifyDelegate:notifyDelegate completion:completion];
}
@end
复现
要不是没地方copy代码, 我怎么会考虑复现自己修bug呢?
使用UIPageViewController
的属性设置是UIPageViewControllerTransitionStylePageCurl
和UIPageViewControllerNavigationOrientationHorizontal
已知这个问题是在翻页中出现的, 但等待翻页触发崩溃过于漫长, 太过费时, 甚至考虑准备弄个自动化测试
毕竟我懒, 既然是翻页中出现的问题, 那么先排查翻页的过程
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(nonnull UIViewController *)viewController
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(nonnull UIViewController *)viewController
页面前后翻时的Delegate, 添加代码, 直接输出控制台
若干次尝试后...
出现了一个诡异的事, 在我乱翻之下, 短时间内可能触发2次向前翻页
再次尝试若干次...
期望的行为是左右滑翻页, 但上下滑动也可能瞬间触发2次向前翻页且界面没有发生可见的变化, 触发2次前翻时大概率发生崩溃
搞事
根据苹果爸爸的崩溃日志, 最终方法发生在这里
[UIPageViewController _validatedViewControllersForTransitionWithViewControllers:animated:]
原因是空数组, 那就明显是ViewControllers:
的参数出了问题
既然都复现了, 也找到了方法, 剩下的就是复刻基本套路了
for (hook系统方法; !搞定; hook上一层) {
直接return || 换参数
}
最后在上一层[UIPageViewController _setViewControllers:withCurlOfType:fromLocation:direction:animated:notifyDelegate:completion:]
这里直接return
异常清空就行了
后记
理论上比较完备的测试是需要考虑Swift
的情况, 想了想...还是留给勤快的有缘人好了