iOS应用程序在PushViewController时冻结

26
我的导航控制器偶尔在推送时会冻结。它似乎将新的视图控制器添加到堆栈上,但动画从未发生。我还有两个容器在屏幕上容纳视图控制器,在导航控制器冻结后,我可以与它们互动得很好。真正有趣的是,如果我尝试将另一个视图控制器推入导航控制器的堆栈中,我注意到堆栈顶部多了一个额外的视图控制器(最初推动导致导航控制器冻结的视图控制器)。因此,如果我在主屏幕(我们将其称为VC-Home)上并尝试推出新视图(VC-1),然后它冻结,然后我尝试推出新视图(VC-2),这是在推出之前当前堆栈中看到的:

{ [VC-Home,VC-1] }

而在调用pushViewController之后,它仍然保持不变。 VC-2没有添加到堆栈中。

据我所知,导航控制器通过在动画开始之前使先前的视图控制器无效来启动动画,但然后动画从未发生,使导航控制器处于冻结状态。

我通过调用UIStoryboard(name:“Main”,bundle:nil).instantiateViewControllerWithIdentifier(“ViewController”)从故事板创建新的视图控制器,因此我认为那里没有任何问题。我也没有覆盖导航栏上的pushViewController。我的应用程序的一些独特之处是它具有非常高分辨率的图像(使用SDWebImage来管理),并且一次始终在屏幕上有三个容器(一个导航控制器,一个搜索视图控制器和一个交互式的装饰/侧面/滑出菜单)。

CPU使用率低,内存使用率正常(设备冻结时稳定在60-70MB左右)。

是否有任何可能导致此问题的想法或任何可以帮助我发现真正问题的调试提示?

更新

由于我只是使用pushViewController()进行推送,因此没有唯一的UINavigationController代码。这是调用它的代码:

func didSelectItem(profile: SimpleProfile) {
     let vc = UIStoryboard.profileViewController()
     vc.profile = profile
     navigationController?.pushViewController(vc, animated: true)
}

我推出的 ViewController 在 viewDidLoad 中有以下代码:

override func viewDidLoad() {
    super.viewDidLoad()
    button.roundView()

    if let type = profile?.profileType {
        //load multiple view controllers into a view pager based on type 
        let viewControllers = ProfileTypeTabAdapter.produceViewControllersBasedOnType(type)
        loadViewPagerViews(viewControllers)

        let topInset = headerView.bounds.height + tabScrollView.contentSize.height
        if let viewPager = viewPager {
            for view in viewPager.views {
                if let tempView = view as? PagingChildViewController {
                    tempView.profile = fullProfile
                    tempView.parentVCDelegate = self
                    tempView.topInset = topInset
                }
            }
        }
    }
}

func loadViewPagerViews(viewControllers: [UIViewController]) {
    viewPager?.views = viewControllers
    viewPager?.delegate = self

    //loading views into paging scroll view (using PureLayout to create constraints)
    let _ = subviews.map { $0.removeFromSuperview() }
    var i = 0
    for item in views {
        addSubview(item.view)
        item.view.autoSetDimensionsToSize(CGSize(width: tabWidth, height: tabHeight))
        if i == 0 {
            item.view.autoPinEdgeToSuperviewEdge(.Leading)
        } else if let previousView = views[i-1].view {
            item.view.autoPinEdge(.Leading, toEdge: .Trailing, ofView: previousView)
        }
        if i == views.count {
            item.view.autoPinEdgeToSuperviewEdge(.Trailing)
        }
        i += 1
    }

    contentSize = CGSize(width: Double(i)*Double(tabWidth), height: Double(tabHeight))
}

更新2

我终于让它再次冻结了。应用程序在后台,我把它带回来并尝试将视图控制器推到堆栈上时它就冻结了。我注意到有一个动画正在进行中。页面顶部有一个滚动视图,每10秒钟浏览其内容(类似于应用商店的顶部横幅)。在这次冻结中,我注意到横幅正处于动画中。

这是我UIScrollView的滚动函数,每10秒钟调用一次:

func moveToNextItem() {
    let pageWidth: CGFloat = CGRectGetWidth(frame)
    let maxWidth: CGFloat = pageWidth * CGFloat(max(images.count, profileImages.count))
    let contentOffset: CGFloat = self.contentOffset.x
    let slideToX = contentOffset + pageWidth

    //if this is the end of the line, stop the timer
    if contentOffset + pageWidth == maxWidth {
        timer?.invalidate()
        timer = nil
        return
    }

    scrollRectToVisible(CGRectMake(slideToX, 0, pageWidth, CGRectGetHeight(frame)), animated: true)
}

我不记得动画/滚动会导致推送停止,但我可能错了。

我还重新检查了堆栈,情况与上述描述相同,其中 [VC-Home, VC-1] 是堆栈,而 VC-2 没有被推入。我还查看了 VC-1 的变量,所有的数据调用和图像加载都已完成。

更新3

这变得越来越奇怪了。我重写了 pushViewController,以便在 Alessandro Ornano 的回复基础上进行一些调试。如果我尝试无法推送视图控制器,然后将我的应用程序发送到后台,并在 pushViewController 调用中设置断点,然后将应用程序带回前台,那么断点会立即被触发多次。如果我继续通过所有命中点,那么下一个视图控制器会突然出现,并且我尝试推送的最后一个视图控制器现在成为堆栈上的最后一个视图控制器。这意味着我看到的仍然是禁用的,这本质上使我处于之前的位置。


你能详细说明VC-1的方法,例如viewWillAppear、viewDidAppear或viewDidDisappear吗?在主线程中是否有指令会影响布局构建? - Alessandro Ornano
我同意 @AlessandroOrnano,你在同一个线程里吗? - bourvill
我只覆盖了viewDidAppear方法,但只是设置了一个布尔值并将子视图置于前台。没有后台线程。如果发现任何可疑的后台线程,我会继续寻找并更新我的问题。 - Maxwell
稳定地在80-100 MB左右...我觉得不正常,除非它是一个游戏。 - Andrea
请问您能否展示一些相关的代码? - Sulthan
添加了代码@Sulthan。我已经放置了我认为相关的内容,但如果您想要看到任何具体信息,请告诉我,我会发布它。 - Maxwell
6个回答

51

我们几个星期前也遇到了相同的问题。针对我们的问题,我们将其缩小到 左边缘弹出手势识别器。您可以尝试以下步骤来检查是否能够重现此问题:

  • 在没有视图控制器的情况下尝试使用左边缘弹出手势(即在根视图控制器上,您的VC-Home控制器)
  • 在此之后尝试单击任何UI元素。

如果您能够复现此问题,请尝试在视图控制器堆栈仅包含一个视图控制器时禁用interactivePopGestureRecognizer

有关更多详细信息,请参考问题。以下是链接中的代码以供参考。

- (void)navigationController:(UINavigationController *)navigationController
   didShowViewController:(UIViewController *)viewController
                animated:(BOOL)animate
{
    if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
{
        if (self.viewControllers.count > 1)
        {
            self.interactivePopGestureRecognizer.enabled = YES;
        }
        else
        {
            self.interactivePopGestureRecognizer.enabled = NO;
        }
    }
}

1
那不是很有趣吗!我明天要运行几个测试,但这使它非常一致地崩溃。 - Maxwell
@Maxwell 是的。我也在 App Store 上一些流行应用上注意到了这个问题。 - Penkey Suresh
不错的发现!谢谢分享。 - Maxwell
1
这个救了我的一天。 - user626776

10

很棒的答案,@Penkey Suresh拯救了我的一天!这是一个带有小改动的SWIFT 3版本,对我非常有帮助:

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {


    if (navigationController.viewControllers.count > 1)
    {
         self.navigationController?.interactivePopGestureRecognizer?.delegate = self
        navigationController.interactivePopGestureRecognizer?.isEnabled = true;
    }
    else
    {
         self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
        navigationController.interactivePopGestureRecognizer?.isEnabled = false;
    }
}

只要不要忘记添加UINavigationControllerDelegate并设置navigationController?.delegate = self。另一个重要的部分是根据需要将interactivePopGestureRecognizer分配给self或nil。


我不得不将 self 强制转换为 UIGestureRecognizerDelegate。例如:self.navigationController?.interactivePopGestureRecognizer?.delegate = self as? UIGestureRecognizerDelegate - lenooh
这样做会从vc1到vc2中移除_UIParallaxView浅灰色过渡背景吗?因为它对我不起作用。 - Marlhex

2
我在思考间歇性冻结主线程SDWebImage。假设您正在使用从downloadImageWithURL:options:progress:completed:的完成块下载的图像,如果是这样,请确保在使用图像之前将其调度到主队列。
如果直接使用SDWebImageDownloader,则完成块(如您所指出的)将在后台队列上调用,您可以使用来自完成块的dispatch_async在主队列上修复它。
否则,您可以使用:SDWebImageManager downloadImageWithURL:options:progress:completed:(调用完成块的方法在主队列上)。
如果问题仍然存在(只是因为您谈到了“..我的应用程序有些独特的地方,它非常依赖于图片..”),请查看常见问题,特别是处理图像刷新已知问题。
还可以添加此漂亮的代码片段进行检查:
import UIKit.UINavigationController

public typealias VoidBlock = (Void -> Void)

public extension UINavigationController  
{
    public func pushViewController(viewController: UIViewController, animated: Bool, completion: VoidBlock) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}

或许可以帮助理解pushViewController是否完成,如果完成了所有预期的viewControllers..

我尝试进行另一个测试,使用iOS 8.x和iPhone 6+启动应用程序,因为在pureLayout项目的iOS 9周围存在一些问题。您能否就此测试发送反馈?

在推送视图之前,我还对实际滚动视图尺寸有所怀疑,您能通过检查视图层次结构来分析当前视图吗?


我正在使用imageView.sd_setImageWithURL(url)来加载所有的图片,它会在主线程上运行完成块,因此我不希望出现任何冻结。 - Maxwell
更新:我已经加入了你的代码,完成块会在控制台上打印出转换结束的信息。我成功让导航控制器冻结了,但我仍然可以尝试从我的其他容器之一向堆栈中推送新的视图控制器。当我这样做时,完成块被执行,但是打印语句从未在控制台上显示。 - Maxwell

0
请检查您的BaseNavigationViewController中是否有像下面这样不必要的代码:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

这里我有一个样例代码,可以复现你遇到的问题(在推送VC时冻结应用程序)。您需要按照以下步骤进行复现:

  • 尝试在下面没有视图控制器的情况下使用(左|右)边缘弹出手势(即在根视图控制器上,您的VC-Home控制器)
  • 在此之后尝试单击任何UI元素(将您推到下一个ViewController)。

样例代码:https://github.com/aliuncoBamilo/TestNavigationPushBug


0

感谢@Penkey Suresh!和@Tim Friedland的帮助,他们帮了我很多。对于我的用例,我需要额外做一些事情,这可能也会帮助其他人。

用例: 我有一个选项卡控制器,我想在屏幕上使用滑动返回手势,比如我在tabA上打开VC1、VC2。我的滑动手势在VC1和VC2上正常工作,但由于某种原因,当回到tabA时它没有被禁用,这就是为什么在尝试从那里向左滑动并尝试点击某个地方时会冻结(如@Pankey Suresh所述)。

SWIFT 5中的解决方案 我已经实现了这个自定义类:

class BaseSwipeBack: UIViewController, UIGestureRecognizerDelegate {
    func swipeToPop(enable: Bool) {
        if enable && (navigationController?.viewControllers.count ?? 0 > 1){
            self.navigationController?.interactivePopGestureRecognizer?.delegate = self
            navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
        }
        else {
            self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
            navigationController?.interactivePopGestureRecognizer?.isEnabled = false;
        }
    }
}

使用方法

  • VC1类 - 将其作为BaseSwipeBack的子类,以便您可以访问函数

     class VC1: BaseSwipeBack {
     override func viewDidLoad() {
    
     }
    
     override func viewDidAppear(_ animated: Bool) {
         self.swipeToPop(enable: true)
     }
     deinit {
         self.swipeToPop(enable: false)
     }
     }
    
  • tabAController类 - 虽然我已经在deiniting VC1中禁用了手势,但它并不正常工作,所以我不得不再次禁用它tabAController

     class tabAController: BaseSwipeBack {
     override func viewDidLoad() {
    
     }
     override func viewDidAppear(_ animated: Bool) {
         self.swipeToPop(enable: false)
     }
     }
    

0
我的问题通过重新启动Xcode和模拟器,然后从模拟器列表中选择不同的设备来解决。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接