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 <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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342