Heavy SceneKit scene in UINavigationViewController

2019-09-13 21:24发布

问题:

My app uses standard UINavigationViewController.

At some point, user has navigated to a ViewController which shows a SceneKit Scene.

This scene is very heavy in memory, since it has around 6000 geometries and each geometry has its own Material (this needs to be this way).

When user clicks the back button at the top bar in order to return to the previous viewController, the app correctly pops the current View Controller and shows the one below.

However, for around 4 seconds, the UI of the new ViewController shown freezes.

I have used Instruments Time Profiler, and I can see that big part of those 4 seconds are taken by these SceneKit methods:

-[NSConcreteMapTable countByEnumeratingWithState:objects:count:]

-[SCNMetalResourceManager _geometryWillDie:]

-[SCNMetalResourceManager _materialWillDie:]

It makes sense because there are so many geometries and materials.

How can I solve this situation, so that UI is not blocked while the heavy Scene is being released, either from a SceneKit perspective (making deallocation quicker) or from a UINavigationViewController perspective (maybe forcing deallocation of the Scene to happen in a separate thread?).

回答1:

My understanding is that SceneKit uses it's own background thread to do work, so it shouldn't be blocking the main thread.

It is possible that the blocking of the main thread has something to do with UINavigationController animating the content away. I've had issues when trying to animate SCNViews/SCNScenes and it looks like SceneKit and CoreAnimation do not collaborate very well.

I don't have the full context here, but I can suggest couple of things for you to test:

  1. You can try setting the scene propery of the SCNView with the heavy scene scnView.scene = [SCNScene scene]; to a new empty scene just before the View Controller with the scene is popped off, so that the heavy scene is not involved in the animation, and SceneKit properly deallocates it in it's background threat.

  2. You can try keeping a strong reference to the heavy scene in the View Controller that prepares the problematic View Controller with the heavy scene.

    Then when the latter is popped off and you return to the one below it, the popped off View Controller and all of its content should have deallocated, except the heavy scene and you'll have a reference to it.

    Here you can try setting to nil (after the animation has completed) and possibly it will deallocate properly on SceneKit background thread.

    If not, you can first remove all heavyScene.rootNode child nodes, actions, etc and then deallocate.

    If this still does not work, you can first try using a recursive function to traverse all your nodes one by one and nil their geometry/material first and then deallocate the scene itself.

  3. Using the rendering loop -renderer:updateAtTime: method to remove the nodes over several frames (less than a second). Then you can be sure that SceneKit will deal with them the way it is designed to.

    You will have to detect when the user presses UINavigationController "Back button" and the VC with the heavy scene is being popped off. Then over, say 20 frames (1/3 of a second at 60fps), remove 5% of nodes at each frame, so there will still be a short delay but 1/3 of a second and hopefully unnoticeable.

    The detection of the VC being popped off can happen by overriding its didMoveToParentViewController: method and checking if nil is passed as the parent parameter.

E.g.

- (void)didMoveToParentViewController:(UIViewController *)parent {

    if (!parent) {
        // work that should be done when VC is being removed from parent
        // maybe set a counter in the VC being popped off to
        // count down from 20 to 0, -1 at each -update call
    }
}