什么可以使 UIViewController 暂时变得无响应?

What can make a UIViewController temporarily become unresponsive?

我的应用有四个视图控制器(VC):

1) Home: 在 SKView 上有一个 SpriteKit 动画。在此 VC 上滑动可让用户继续创作 VC(下一步)。

2) 创作:有一个菜单(UITableView)。该菜单允许用户访问 ViewGames 和商店(如下)。

3) ViewGames:包含一个 UICollectionView 和一个带关闭按钮的导航栏。这一次显示一个 UICollectionViewCell,并让用户滑动以继续到下一个单元格。每个cell都有一个SKView上的SpriteKit动画,还有三个按钮。

4) Store:有一个应用内购买商店,UI 实现为 UITableView。出于本次讨论的目的,我使用的唯一功能是 SKProductsRequest 以取回显示在 table 视图中的产品列表。

问题:在ViewGames中VC,在某些情况下,collection view中第二个及后面cell的UI操作非常慢。例如,比正常速度慢大约 10 倍。 SKView 中的动画非常慢。四个按钮(三个在集合视图单元格上,一个在导航栏中)操作非常缓慢。通常他们根本不会回应,你必须点击他们几次。滑动到下一个单元格的反应类似——而且很慢,如果有的话。 (如果我使用滑动返回到第一个单元格,第一个单元格也有类似的响应,但最初它没有出现这个问题)。

重现问题。好消息是,在我的应用程序中,重现此问题非常一致。生成它的方法如下:

Launch app > Swipe to go to Authoring > Use menu to go to the Store > exit Store to return to Authoring > exit Authoring back to Home > go to Authoring > go to View Games.

其他一些方面:

A) 如果我退出 View Games,返回 Authoring,然后重新进入 View Games,这个问题是一样的。

B) 应用程序的其他部分没有表现出这种 UI 响应缓慢的情况。

C) 如果在 View Games VC 中出现此行为后,我现在退出 ViewGames 返回 Authoring,重新进入 Store,返回 Authoring,然后返回 ViewGames,问题走了。

D) 此问题仅在 iOS9、iOS9.1、iOS9.2(测试版)上出现。它不会发生在 iOS8.4。 (所有 运行ning 在物理设备上;我还没有尝试过模拟器)。我最初使用 Xcode 7.0.1,但现在使用 Xcode 7.2 beta,问题仍然存在。我的应用程序针对 iOS8 及更高版本。

E) 如果我启动该应用程序,然后转到“创作”和“查看游戏”,则不会出现此问题。


问题:什么东西可以使UI运行的部分缓慢,但只是暂时的?


目前探索过的道路:

(i) 我已经在 Time Profiling Instrument 中查看了这个应用程序,但看不到任何看起来像吸收时间的东西。

(ii) 应用程序只有一部分在进行网络交互,那就是商店。并且产品提取成功,并显示该信息。

(iii) 我现在最好的猜测是这与内存使用有关。当出现这些症状时,从 Authoring UICollectionView 的单元格 1 到单元格 2 似乎至少使用了更多的 RAM(在出现问题的情况下为 0.4 到 0.9MB;0.3MB在没有出现问题的情况下)。

(iv) 在应用程序的开发历史中,当我准备将 v1.0 提交给 Apple 时,我遇到了内存泄漏并表现出其中一些症状。然而,根据我的回忆,内存泄漏只影响了 SpriteKit 动画,影响了 所有 SpriteKit 动画(在主页和创作 VC 上),并且是 不是临时的。您必须重新启动应用程序才能绕过它。

(v) 我已经使用 Instruments/Leaks/Allocations 仔细查看了该应用程序。有一些泄漏,但它们似乎来自 Apple 框架,而不是我的。

(vi) 我在 dealloc/deinit 方法中放置了断点和日志消息,所有主要的 classes 似乎都在释放(例如,VC 的,以及集合视图及其单元格)。


更新 1:2015 年 11 月 4 日; 3:47pm MST:问题与 ViewGames SpriteKit 动画无关。我刚刚禁用了 ViewGames UICollectionViewCell 中的动画,但问题仍然存在。滑动和按钮按下响应仍然会出现迟缓。当然,单元格仍然有一个 SKView/SKScene.

更新 2:11/4/15; 3:55pm MST:我刚刚禁用了商店中的产品提取(使用 SKProductsFetch)。 问题消失了!!问题范围明显缩小!

更新3:11/4/15; 6:10pm MST:有了产品提取,但 SKProductsFetch 对象的委托设置为 nil,问题不会发生! 还需要注意的是作为我的 class 构造的一部分的完成处理程序(称为 fetchProductsCompletion)也被设置为 nil。

更新 4:11/4/15; 6:10pm MST:产品提取到位,SKProductsFetch 有一个非 nil 委托,但 fetchProductsCompletion 设置为 nil,问题不会发生!

如果你想完全冻结应用程序,你可以使用 sleep(amountTime)。
或使用 runAfterDelay(amountTime){} 进行延迟。

我找到了解决该问题的方法。我不知道为什么会这样。但是,我会 post 一些代码来提供上下文。

这是我在商店中用来获取产品的方法:

// Not calling this from init, so that we can make sure there is a network connection, and only give a single error/alert if there is no network connection.
private func initiateProductFetch(done:(success:Bool)->()) {
    let productIds = self.productsInfo!.productIds()
    if nil == productIds {
        Assert.badMojo(alwaysPrintThisString: "No product Ids!")
    }

    /* Bug #C34, #C39.
    10/30/15; I was having a memory retention issue in regards to the fetchProducts completion handler. The SMIAPStore instance wasn't getting deallocated until the *next* time the store was presented because the store delegate was retaining a reference to the completion handler, which had a strong reference to self (SMIAPStore).
    At first I tried to resolve this by having [unowned self] in the following, but that fails with an exception. Not sure why. And having [weak self] also causes self! to fail-- "fatal error: unexpectedly found nil while unwrapping an Optional value". Why?
    The only way I've found to work around this is to have a selfCopy as below, which is nil'ed out in the completion handler. Seems really clumsy. For consistency sake, and safety sake, I'm also using this technique in the other self.storeDelegate usages in this class.
    */

    var selfCopy:SMIAPStore? = self

    self.storeDelegate?.fetchProducts(productIds!) {
        (products:[SKProduct]?, error:NSError?) in

        Log.msg("\(products)")

        if (products != nil && products!.count > 0 && nil == error) {
            // No error.
            selfCopy!.productsInfo!.products = products
            Log.msg("Done fetching products")
            done(success:true)
        }
        else {
            // Show an error. The VC will not be displayed by now. Returning the error with the done call back will not allow it to be displayed.

            // It seems possible the products are empty and error is nil (at least this occurs in my debug case above).
            var message = ""
            if error != nil {
                message = error!.localizedDescription
            }

            let alert = UIAlertView(title: "Couldn't get product information from Apple!", message: message, delegate: nil, cancelButtonTitle: SMUIMessages.session().OkMsg())
            selfCopy!.userMessageDetails = UserMessage.session().showAlert(alert, ofType: UserMessageType.Error) { buttonNumber in
                done(success:false)
            }
        }

        selfCopy = nil
    }
}

存储委托方法(我的构造)如下所示:

// Call this first to initiate the fetch of the SKProduct info from Apple
public func fetchProducts(productIdentifiers:[String], done: (products:[SKProduct]?, error:NSError?) ->()) {
    self.requestDelegateUseType = .ProductsRequest
    self.productsRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
    self.productsRequest!.delegate = self
    self.fetchProductsCompletion = done
    self.productsRequest!.start()
}

这里是 SKProductsRequest 的委托方法:

// Seems like this delegate method gets called *before* the requestDidFinish method. Don't really want to rely on that behavior though.
public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
    Assert.If(.ProductsRequest != self.requestDelegateUseType, thenPrintThisString: "Didn't have a product request")

    if 0 == response.invalidProductIdentifiers.count {

        /*
        #if DEBUG
            // Debugging-- simulate an error fetching products.
            self.fetchProductsCompletion!(products: nil, error: nil)
            return
        #endif
        */

        self.fetchProductsCompletion?(products: response.products, error: nil)
        //self.fetchProductsCompletion = nil
    }
    else {
        let message = "Some products were invalid: \(response.invalidProductIdentifiers)"
        self.fetchProductsCompletion!(products: nil, error: NSError.create(message))
    }
}

上面给出的代码确实存在问题。当我在使用 fetchProductsCompletion 处理程序后将其设置为 nil 时,问题就消失了。关于原因有什么想法吗?