iOS Can't add self as subview崩溃解决

近期新版本发布后,发现线上新版本的APP在bugly中出现一些Can't add self as subview 的崩溃日志。

崩溃日志如图:


bugly崩溃.png

根据日志分析可能有两种原因造成崩溃:
1.视图添加自身;
2.导航控制器的跳转动画引起。

通过对上一版本的新增代码分析,并无明显的造成上述两个问题的代码漏洞。所以只能通过模拟这两种情况查看是否会造成崩溃,崩溃的日志是否相同来确认。

视图添加自身

1.png
2.png
bugly日志.png
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对外部进行了动画隔离.

A push to B (transition)
A push to B (complete)
B pop to A (transition)
崩溃分析

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 

NS_ASSUME_NONNULL_BEGIN

@interface UINavigationController (CLSafeTransition)
@property (nonatomic, assign) BOOL viewTransitionInProgress;
@end

NS_ASSUME_NONNULL_END



#import "UINavigationController+CLSafeTransition.h"
#import 

@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 

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (CLSafeTransitionLock)

@end

NS_ASSUME_NONNULL_END



#import "UIViewController+CLSafeTransitionLock.h"
#import "UINavigationController+CLSafeTransition.h"
#import 

@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

经线上测试,无新的崩溃日志上报。

  • 参考资料:
    关于项目中崩溃问题处理:Can't add self as subview
    Can't Add Self as Subview 崩溃解决办法
    Can't add self as subview解析

你可能感兴趣的:(iOS Can't add self as subview崩溃解决)