UIPageViewController crashes when flipped too fast

2019-02-03 02:19发布

问题:

I had some memory problems due to Xcode's template for a UIPageViewController caching all the page data, so I changed it to load the pages dynamically, so now when my app receives a low memory warning, it releases the memory for page's not showing, but if the user is flipping through the pages real fast by tapping the edge of the screen, it still crashes. I'm guessing this is because it can't free the memory fast enough when didReceiveMemoryWarning gets called. If the user is flipping slowly, it works fine. I limited the speed at which the user can flip pages, but it still happens. I want to be able to free up the memory every time the page is turned, and not have to wait for a low memory warning. I'm using ARC. Is there a way to do this? Or what else can I do to prevent this? Thanks.

EDIT:

(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
    NSUInteger index = [self indexOfViewController:(SinglePageViewControllerSuperclass *)viewController];
    if ((index == 0) || (index == NSNotFound)) {
        return nil;
    }

    index--;
    return [self viewControllerAtIndex:index];
} 

(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
    NSUInteger index = [self indexOfViewController:(SinglePageViewControllerSuperclass *)viewController];
    if (index == NSNotFound || index == MAX_PAGE_INDEX) {
        return nil;
    }

    return [self viewControllerAtIndex:++index];
}

回答1:

I think you hypothesis is correct, since I also experienced a similar behavior: when you flip to the next page, also for the sake of animating things nicely, the new page is allocated before the old one is deallocated and it takes some times for the old one to be deallocated. So, when you flip fast enough, objects are allocated faster than they are deallocated and eventually (actually, pretty soon), your app is killed due to memory usage. The deallocation delay when flipping pages becomes pretty obvious if you follow the allocation/deallocation of memory in Instruments.

You have three approaches to this, IMO:

  1. implement a "light" viewDidLoad method (actually, the whole initialization/initial display sequence): in some app, it makes sense, e.g., to load a low resolution image instead of the high resolution image that will be displayed; or, slightly delaying the allocation of additional resources your page need (db access, sound, etc.);

  2. use a pool of pages, say an array of three pages (or 5, it depends on your app), that you keep "reusing" so that the memory profile of your app remains stable and avoid spikes;

  3. careful review the way you allocate and release memory; in this sense, you often read that autorelease add some "inertia" to the release/deallocation mechanism, and this is pretty easy to understand: if you have an autoreleased object, it will be released by its release pool only when you cycle through the main loop (this is true for the main release pool); so, if you have a long sequence of methods that are called when flipping page, this will make the release/dealloc happen later.

There is no magical bullet when it comes to memory usage optimization, it's a pretty detailed and hard work, but IME you will be able to reduce the memory profile of your app if you review your code and apply those 3 guidelines. Especially, inspecting the spikes of memory allocation in Instruments and trying to understand to what they relate is extremely powerful.



回答2:

Here is an additional change I made, which someone might find helpful:

Basically, I only allow a new page turn to begin if the previous one has finished.

I'm using apple's default PageViewController project as a template so I'll use terms defined in that project.

Whenever a page VC is requested through viewControllerAtIndex:, I set a boolean value on ModelController called 'shouldDenyVC' to YES.

In my EbookViewController which is the UIPageViewController's delegate, I capture the gesture recognizers, and assign EbookViewController as their delegate:

self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
for (UIGestureRecognizer *gr in self.view.gestureRecognizers) {
    gr.delegate = self;
}

Then, I'm able to deny a page turn by denying the gesture recognizers:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:    (UITouch *)touch
{
    if (_modelController.shouldDenyPageTurn == YES) {
        return FALSE;
    }
    return TRUE;
}

And finally, I set _modelController.shouldDenyPageTurn = NO at the end of the UIPageViewController delegate method pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:

I also had to set _modelController.shouldDenyPageTurn = NO at the end of any preloading so that page turns are allowed off the bat.



回答3:

There is a bug in iOS5 at the moment that causes a scroll view to leak a small amount of memory.

Have you tried profiling your application in instruments checking for allocations and memory leaks?

You can simulate a low memory warning either in the simulator (hardware -> simulate low memory warning). Or you can do it via code, (just remember to remove after debugging because this will get your app rejected!)

[[UIApplication sharedApplication] performSelector:@selector(_performMemoryWarning)];

If you are using strong or retain properties then set them to nil after you are done with them and ARC will release the memory they are pointing to behind the scenes.

If you are creating lots of temporary objects (objects that are not properties or not alloc'd) then insert an autorelease pool:

@autoreleasepool {

}

And finally, show some code and we can better help you.



回答4:

It might be caused by Rendering. When flipper too fast, the memory and CPU used by redrawing the "Page" will increase rapidly. If the views you used in UIPageViewController is based on CALayer and have too many pages, flipping too fast will definitely crash the App.

One solution is to customize the layer and cache the rendering result. Re-render the content only when must. But the cache might increase memory usage.



回答5:

Since you didn't post any code it's hard to guess where exactly lies your problem.

  1. To force unloading a view you can override viewDidDisappear: method of those viewcontroller classes that are appearing in UIPageViewController.

    Code would look like:

    - (void)viewDidDisappear:(BOOL)animated {
        [self didReceiveMemoryWarning];
    }
    

    If you also have didReceiveMemoryWarning overriden don't forget to call [super didReceiveMemoryWarning]; from it.

  2. There can also be some confusion on how UIPageViewControllerDataSource methods work - you might have some 'mixed wires there'. Check accepted answer here.