无法将自己添加为子视图

Posted

技术标签:

【中文标题】无法将自己添加为子视图【英文标题】:Can't add self as subview 【发布时间】:2013-11-19 22:19:12 【问题描述】:

我们使用Crashlytics,已有 30 多个用户看到此崩溃。此崩溃日志来自该领域的用户。我们从未能够复制它。这是在ios7 上运行的。不知道是什么原因造成的,因为您可以看到调用堆栈中没有与我们的应用程序相关的任何内容。其他人看到这个或解决了这个问题吗?谢谢!请不要让我发布代码(参见上面的 cmets)。

【问题讨论】:

你在崩溃附近的任何地方使用UIControlEventAllTouchEvents吗? 我有几个用户遇到完全相同的崩溃的案例。我也不清楚是怎么引起的。它似乎只发生在 iPhone 而不是 iPad 上。 我有同样的崩溃。我能找到的唯一似乎将 self 添加为子视图的代码是 SVSegmentedControl。有人在这方面取得任何进展吗? 我们不在任何地方使用 UIControlEventAllTouchEvents。谢谢。 -rrh 我刚刚在这里发布了详细答案:***.com/a/21875395/721460。希望对您有所帮助。 【参考方案1】:

tl;dr - 请参阅底部的结论。

我也偶尔从用户那里收到有关此的崩溃报告,然后今天为自己遭受了侮辱。这是在我的表格视图上选择一行尝试推动新的导航控制器,然后我按下后退按钮时引起的。奇怪的是,我推送的导航控制器不包含任何数据。按下后,导航栏下的视图变黑,应用崩溃了。

环顾四周,我只能找到其他用户(here、here 和 here),这表明这是由于调用 segue 或快速连续两次推动视图控制器。但是,我只在我的tableView:didSelectRowAtIndexPath: 中调用pushViewController: 一次。没有用户双击一个单元格,我不明白为什么这会发生在我身上。通过双击表格视图单元格进行的测试未能重现观察到的崩溃。

但是,如果用户双击了,因为当时主线程恰好被阻塞了怎么办? (我知道,我知道,永远不要阻塞主线程!)

为了测试这一点,我创建了一些(相当可怕,抱歉,从其他代码快速剪切和粘贴)类方法(在虚构类 MyDebugUtilities 中)来反复阻塞和解除阻塞主线程,在我打开之前启动它包含导致崩溃的表视图的视图控制器。这是任何想要自己快速检查的人的代码(对[MyDebugUtilities repeatedlyBlockTheMainThread]; 的调用将使用信号量阻塞主线程 3 秒,然后在 3 秒后排队另一个对重复BlockTheMainThread 的调用,无限期。顺便说一句,你不要'不想重复启动这个方法,否则它会一直阻塞):

+ (void)lowCostSemaphoreWait:(NSTimeInterval)seconds

    // Use a semaphore to set up a low cost (non-polling) delay on whatever thread we are currently running
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC);

    dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
        dispatch_semaphore_signal(semaphore);
    );

    NSLog(@"DELAYING...");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"END of delay\n\n");



+ (void)repeatedlyBlockTheMainThread
    
    dispatch_async(dispatch_get_main_queue(), ^
        // Block the main thread temporarily using a semaphore
        [MyDebugUtilities lowCostSemaphoreWait:3.0];

        // Queue up another blocking attempt to happen shortly
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
        dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
            [MyDebugUtilities repeatedlyBlockTheMainThread];
        );
    );

在我的tableView:didSelectRowAtIndexPath: 中进行 NSLog 调用,然后尝试在主线程被阻塞的某个时间段内双击确实重现了上面观察到的错误 - 按下使应用程序崩溃,并从 NSLog很明显tableView:didSelectRowAtIndexPath: 被调用了两次。我认为这里发生的情况是,当主线程被阻塞时,触摸正在排队,然后在它被释放后立即交付。在主线程未被阻塞时双击不会产生两次对tableView:didSelectRowAtIndexPath:的调用,大概是因为它已经处理了第一次触摸并处理了推送。

因为这需要临时阻塞主线程,而在精心设计的应用程序中,这应该很少发生(如果有的话),这可以解释这样一个事实,即这真的很难重现 - 我有崩溃报告这来自一小部分用户,而且由于崩溃报告甚至没有指出我的哪个视图控制器触发了该场景,所以在我亲身体验之前,我不知道从哪里开始寻找。

要解决这个问题,真正简单的解决方案是创建一个 BOOL 属性(例如selectionAlreadyChosen),您在viewWillAppear: 中将其设置为 NO(因此当您返回此屏幕时它会被重置),然后实现tableView:willSelectRowAtIndexPath: 做类似的事情:

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath

    if (self.selectionAlreadyChosen) 
        NSLog(@"BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:");
        return nil;
    

    self.selectionAlreadyChosen = YES;
    return indexPath;

完成后不要忘记删除任何 NSLog 和对重复 BlockTheMainThead 的调用。

这个确切的场景可能不是其他人都遇到的问题,但我认为 Apple 处理同时多个表格视图选择的方式存在问题 - 它不仅仅是双击 - 通过疯狂点击来测试上述解决方案当主线程被阻塞时,同一个单元格生成BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:消息流。

实际上,在 iOS 6 上对此进行测试 - 多次调用 tableView:didSelectRowAtIndexPath: 并没有被阻止,但处理方式不同(这当然是反复调用 pushViewController: 的结果!) - 在 iOS 6 上多次点击,而主线程被阻塞会给你消息:

警告:尝试从视图控制器中关闭 在演示或解雇时 正在进行中!

就在被毫不客气地甩出 UINavigationController 之前。因此,这可能不是 iOS 7 的错误,而是处理方式发生了变化,并且一直存在的问题现在表现为崩溃。

更新:

通过我的项目寻找其他存在此问题的 tableView,我发现了一个没有。我能看到的唯一区别是这个工作是可编辑的(因此支持切换编辑模式、移动行和滑动删除),并且更接近我的视图层次结构的根。上面失败的那个是不可编辑的,并且位于视图层次结构的下方——出现在许多模态序列之后。我看不出它们之间有任何其他主要区别——它们都有一个共同的超类——但是一个人在没有上述修复的情况下反复尝试pushViewController:,而另一个人则没有。

有趣的是,没有出现问题的那个在编辑模式下也会调用performSegueWithIdentifier:而不是pushViewController:,在这种情况下,它会重复调用tableView:didSelectRowAtIndexPath:(因此@987654343 @) - 但是调用pushViewController: 似乎取消了对tableView:didSelectRowAtIndexPath: 的重复调用。

困惑?我知道我是。试图通过使其不可编辑来破坏“工作”视图控制器什么都不做。将非编辑模式pushViewController: 替换为performSegueWithIdentifier: 之一会导致segue 被多次调用,因此与tableView 中的编辑模式无关。

将层次结构中的工作视图控制器替换为不工作的视图控制器(以便它通过 rootViewController 关系而不是模式 segues 链接)修复了以前损坏的视图控制器(使用以前的修复从中取出)。

结论 - 关于您的视图层次结构是如何连接的,可能是使用故事板或模态序列,或者我的视图层次结构损坏导致这种情况 - 导致多个如果您的主线程恰好在用户双击时被阻塞,则点击要立即发送的表格视图单元格。如果您在tableView:didSelectRowAtIndexPath: 中调用pushViewController:,它将最终被多次调用。在 iOS 6 上,这会将你从有问题的视图控制器中删除。在 iOS 7 上,这将导致崩溃。

【讨论】:

感谢您进行所有这些研究。一种替代方法可能是让视图控制器检查self == self.navigationController.viewControllers.lastObject。 (即,如果已经发生了一次推送,则此测试将失败。)这似乎比在每个表控制器中使用 BOOL 乱扔您的应用程序更容易维护。你知道这种方法是否可行吗? 任何防止重复印刷的方法都应该有效 - 任何最适合您的方法。对我来说,BOOL 检查很简单,每个 VC 只添加几行代码。我上面描述的只是我遇到的失败场景。我已经详细介绍了一些细节,以防其中有什么东西可以给其他人那种灯泡时刻——完全有可能还有其他我还没有遇到过的情况。这确实需要先进行模态推送,因此理论上您只需将修复应用于可能出现在模态推送之后的 VC。 有一个类似的问题,因为我们在等待下一个屏幕的数据加载时没有禁用集合视图上的按下。经过重构,现在新屏幕会继续运行,并且该控制器管理自己的数据加载,而不是让之前的屏幕预先填充它。 我在 HockeyApp 中收到了这些崩溃报告,但是由于阻塞了主线程,我仍然无法重现它们。在主线程被阻塞时快速点击将打开新 VC 的行不会导致多个实例相互叠加。我用 iPhone 4s、iOS 7.1.2 进行了测试。 dispatch_release 也会在 ARC 下引发警告。 @hris.to dispatch_release on semaphores 在早期版本的 iOS 上仍然需要即使使用 ARC,但不再需要,所以我将删除它。不确定为什么您无法复制,也许您看到的东西略有不同?除了在主线程被阻塞时推送多个 VC 之外,还有几个不同的问题可能导致此崩溃报告,这只是由于底层更改而在 iOS 7 上意外出现的“非常奇怪、无法解释”的问题在行为上。【参考方案2】:

我在iOS7 上调用pushViewController/popViewController 导致同样的崩溃,而在viewDidLoadviewWillAppear 方法中。

当对同一个 _block_invoke 事物的现有调用正在运行时,它似乎与使用 YES 为 animateTransition 调用上面的 #5 有关。为我工作,你的里程可能会有所不同。

我的堆栈看起来非常相似,但并不完全相同。不确定这是由于我们的代码不同还是什么。我通过重构加载序列解决了我的问题,当我不能时,控制“动画:”的值,所以它只在一个并发事件上是 YES。

【讨论】:

感谢您提供的信息。我检查了我们的代码,我不相信在 viewDidLoad 或 viewWillAppear 中调用了 pushViewController 或 popViewController。我会继续寻找。谢谢。 -rrh【参考方案3】:

我们在项目中遇到了同样的问题。

我们的场景是: ViewController A 有 2 个按钮。按钮 1 正在推动 ViewController B,而按钮 2 正在推动 ViewController C 到一个导航控制器。

如果用户同时按下按钮 1 和 2,然后试图返回 ViewController A(通过 popViewController),则应用程序会因您发布的日志而崩溃。 奇怪的是,这只发生在 iOS 7 上。

我们的临时解决方案是覆盖 navigationController 的 pushViewController 以具有自定义行为,从而避免这种猴子场景。希望这可以帮助。

【讨论】:

【参考方案4】:

如果您使用动画(动画:YES)推送(或弹出)视图控制器,它不会立即完成,如果您在动画完成之前再次推送或弹出,则会发生不好的事情。

要重现此错误,请尝试同时推送或弹出两个视图控制器。示例:

- (void)viewWillAppear:(BOOL)animated

    [super viewWillAppear:animated];
    UIViewController *vc = [[UIViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];

您将收到此错误:

2014-07-03 11:54:25.051 Demo[2840:60b] 嵌套推送动画可以 导致导航栏损坏 2014-07-03 11:54:25.406 演示[2840:60b] 意外完成导航转换 状态。导航栏子视图树可能已损坏。

有一个完美的解决方案: https://github.com/macjam/SafeTransition

只需将代码文件添加到您的项目中,并将您的导航控制器作为 APBaseNavigationController 的子类,就可以了。

【讨论】:

您的项目包括对 iOS 8 的检查,这在 iOS 8 中是否已修复? Apple 已在 iOS 8 中修复此问题。 首先存储库已关闭。其次,我能够通过推动两个带有动画的控制器来重现 iOS 8 上的崩溃。第二个控制器不显示,推回->黑屏,回退又崩溃。【参考方案5】:

看起来嵌套/重叠推送是造成这种情况的原因。 我为 UINavigationController 制作了一个简单的包装 pod,它可以防止嵌套的弹出和推送(https://github.com/Itheme/SmartPopNavigationController — 应该用于应用程序中的每个导航控制器才能正常工作) 在我在所有导航控制器上使用后无法重现此崩溃。虽然不能 100% 确定这不是巧合。

【讨论】:

以上是关于无法将自己添加为子视图的主要内容,如果未能解决你的问题,请参考以下文章

将 UIImagePickerController 添加为子视图

按下时将 UIbutton 添加为子视图无法调用 Selector

UIView:将 UIViewController 的视图添加为子视图并将其删除

WKWebView 添加为子视图

将viewcontroller的视图添加为子视图后UIButton点击事件崩溃

将 UIImageView 添加为子视图