Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIPageViewController crashes when flipped too fast during low memory

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];
}
like image 305
Marty Avatar asked May 14 '12 01:05

Marty


2 Answers

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.

like image 123
sergio Avatar answered Nov 15 '22 23:11

sergio


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.

like image 41
jankins Avatar answered Nov 15 '22 21:11

jankins