近期新版本发布后,发现线上新版本的APP在bugly中出现一些Can't add self as subview 的崩溃日志。
崩溃日志如图:
根据日志分析可能有两种原因造成崩溃:
1.视图添加自身;
2.导航控制器的跳转动画引起。
通过对上一版本的新增代码分析,并无明显的造成上述两个问题的代码漏洞。所以只能通过模拟这两种情况查看是否会造成崩溃,崩溃的日志是否相同来确认。
视图添加自身
Test[11942:84749] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Can't add self as subview'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23e3de6e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff512539b2 objc_exception_throw + 48
2 CoreFoundation 0x00007fff23e3dcac +[NSException raise:format:] + 188
3 UIKitCore 0x00007fff498260f4 -[UIView(Internal) _addSubview:positioned:relativeTo:] + 122
4 Test 0x0000000102bb1c9f -[AViewController viewDidLoad] + 367
5 UIKitCore 0x00007fff48c7598e -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
6 UIKitCore 0x00007fff48c7a8ac -[UIViewController loadViewIfRequired] + 1084
7 UIKitCore 0x00007fff48c7acc9 -[UIViewController view] + 27
8 UIKitCore 0x00007fff48bca589 -[UINavigationController _startCustomTransition:] + 1047
9 UIKitCore 0x00007fff48be0431 -[UINavigationController _startDeferredTransitionIfNeeded:] + 698
10 UIKitCore 0x00007fff48be1820 -[UINavigationController __viewWillLayoutSubviews] + 150
11 UIKitCore 0x00007fff48bc27f0 -[UILayoutContainerView layoutSubviews] + 217
12 UIKitCore 0x00007fff4982d5f4 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2478
13 QuartzCore 0x00007fff2b4e9260 -[CALayer layoutSublayers] + 255
14 QuartzCore 0x00007fff2b4ef3eb _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 523
15 QuartzCore 0x00007fff2b4faa8a _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 80
16 QuartzCore 0x00007fff2b443a7c _ZN2CA7Context18commit_transactionEPNS_11TransactionEd + 324
17 QuartzCore 0x00007fff2b477467 _ZN2CA11Transaction6commitEv + 649
18 UIKitCore 0x00007fff4931ef44 _UIApplicationFlushRunLoopCATransactionIfTooLate + 104
19 UIKitCore 0x00007fff493cca2c __handleEventQueueInternal + 7506
20 UIKitCore 0x00007fff493c2f35 __handleHIDEventFetcherDrain + 88
21 CoreFoundation 0x00007fff23da1c91 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
22 CoreFoundation 0x00007fff23da1bbc __CFRunLoopDoSource0 + 76
23 CoreFoundation 0x00007fff23da1394 __CFRunLoopDoSources0 + 180
24 CoreFoundation 0x00007fff23d9bf8e __CFRunLoopRun + 974
25 CoreFoundation 0x00007fff23d9b8a4 CFRunLoopRunSpecific + 404
26 GraphicsServices 0x00007fff38c39bbe GSEventRunModal + 139
27 UIKitCore 0x00007fff49325968 UIApplicationMain + 1605
28 Test 0x0000000102bb2362 main + 114
29 libdyld.dylib 0x00007fff520ce1fd start + 1
30 ??? 0x0000000000000001 0x0 + 1
)
通过测试发现视图添加自身以及在视图子视图添加子视图自身都会引起视图崩溃,但奔溃堆栈信息并不相同。
跳转动画
参考关于项目中崩溃问题处理:Can't add self as subview中视图跳转测试,同时push多个视图时,pop时程序将崩溃报错。
AViewController *vc = [[AViewController alloc] init];
BViewController *vc2 = [[BViewController alloc] init];
CViewController *vc3 = [[CViewController alloc] init];
[self.navigationController pushViewController:vc animated:NO];
[self.navigationController pushViewController:vc2 animated:YES];
[self.navigationController pushViewController:vc3 animated:YES];
通过测试发现pop操作时,程序崩溃,查看bugly日志发现崩溃堆栈相同。
崩溃原因分析
转场动画解析
在了解具体崩溃原因之前我们需要先了解转场动画的过程(此处内容摘至 Archerlly文章Can't add self as subview解析)。
UINavigationController对于translation动画做了一定的封装, 同时持有fromAnimateView与toAnimateView, 在进行translation动画时将对应的VC的view挂载到对应的AnimateView上, 动画视图AnimateView又挂载到容器视图wrapperView, UINavigationController只需控制容器中的AnimateView实现相应translation动画, translation动画完成后, 移除动画视图并挂载栈顶的视图, 实现navigationController对外部进行了动画隔离.
崩溃分析
push CViewController,C成功入栈,但是视图没有加载到容器中,实际显示的还是B的vc与view,但是栈顶是C的vc。
第一次点返回时(实际应该C的vc出栈), 当前视图(B的view)被先后加载到fromAnimateView与toAnimateView上, 原本视图在出栈完成后应该被释放, 但是容器栈内还存在B的vc, 故保留了。
第二次点返回时(实际应该B的vc出栈), A的view加载到toAnimateView上, 随后toAnimateView需要加载到wrapperView进行transition动画, 但wrapperView通过栈顶元素view.superview取值(即C.view.superview(To Animation View)), 而栈顶元素B的view由于上一次错误的转场, 并未在transition动画完成后挂载到wrapperView, 还保留在的临时的动画视图toAnimateView上, 所以使toAnimateView加载到WrapperView的操作变成了动画视图toAnimateView加载到自己上。
解决办法
通过Runtime的Method Swizzling技术分类实现修改navigationControlle的pop和push方法,拦截控制器进入栈\出栈操作的方法调用,通过安全的方式,确保当有控制器嵌套入栈\出栈操作时,没有其他入栈\出栈操作。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UINavigationController (CLSafeTransition)
@property (nonatomic, assign) BOOL viewTransitionInProgress;
@end
NS_ASSUME_NONNULL_END
#import "UINavigationController+CLSafeTransition.h"
#import <objc/runtime.h>
@implementation UINavigationController (CLSafeTransition)
+ (void)load {
method_exchangeImplementations(class_getInstanceMethod(self, @selector(pushViewController:animated:)),
class_getInstanceMethod(self, @selector(safePushViewController:animated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popViewControllerAnimated:)),
class_getInstanceMethod(self, @selector(safePopViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popToRootViewControllerAnimated:)),
class_getInstanceMethod(self, @selector(safePopToRootViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(popToViewController:animated:)),
class_getInstanceMethod(self, @selector(safePopToViewController:animated:)));
}
#pragma mark - setter & getter
- (void)setViewTransitionInProgress:(BOOL)property {
NSNumber *number = [NSNumber numberWithBool:property];
objc_setAssociatedObject(self, @selector(viewTransitionInProgress), number, OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)viewTransitionInProgress {
NSNumber *number = objc_getAssociatedObject(self, @selector(viewTransitionInProgress));
return [number boolValue];
}
#pragma mark - Intercept Pop, Push, PopToRootVC
- (NSArray *)safePopToRootViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
self.viewTransitionInProgress = YES;
}
NSArray *viewControllers = [self safePopToRootViewControllerAnimated:animated];
if (viewControllers.count == 0) {
self.viewTransitionInProgress = NO;
}
return viewControllers;
}
- (NSArray *)safePopToViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated){
self.viewTransitionInProgress = YES;
}
NSArray *viewControllers = [self safePopToViewController:viewController animated:animated];
if (viewControllers.count == 0) {
self.viewTransitionInProgress = NO;
}
return viewControllers;
}
- (UIViewController *)safePopViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
self.viewTransitionInProgress = YES;
}
UIViewController *viewController = [self safePopViewControllerAnimated:animated];
if (viewController == nil) {
self.viewTransitionInProgress = NO;
}
return viewController;
}
- (void)safePushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewTransitionInProgress == NO) {
[self safePushViewController:viewController animated:animated];
if (animated) {
self.viewTransitionInProgress = YES;
}
}
}
@end
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (CLSafeTransitionLock)
@end
NS_ASSUME_NONNULL_END
#import "UIViewController+CLSafeTransitionLock.h"
#import "UINavigationController+CLSafeTransition.h"
#import <objc/runtime.h>
@implementation UIViewController (CLSafeTransitionLock)
+ (void)load {
Method m1;
Method m2;
m1 = class_getInstanceMethod(self, @selector(safeViewDidAppear:));
m2 = class_getInstanceMethod(self, @selector(viewDidAppear:));
method_exchangeImplementations(m1, m2);
}
- (void)safeViewDidAppear:(BOOL)animated {
self.navigationController.viewTransitionInProgress = NO;
[self safeViewDidAppear:animated];
}
@end
经线上测试,无新的崩溃日志上报。