Frage

I'm currently working on an iPad app that loads ~250 UIImages into UIButtons (then changes the colors), creating a map of the world - each country having its own button and corresponding image - when loading the game. The problem I'm having is that, on a retina iPad, the app is using ~650MB of RAM while loading the images, which is insane.

When the app initially loads the game, it uses the following code to set the images to the buttons (Territory is a subclass of UIButton).

//Initialize the arrays of each territory and add an action to the territory
    for (int i = (int)territoryArray.count - 1; i >= 0; i--) {

        @autoreleasepool {

            //Cast the object into a territory object
            Territory *ter = (Territory *)[territoryArray objectAtIndex:i];

            //Set the territory's image
            [ter setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@T%i.png", [defaults objectForKey:@"Current_Map"], i + 1] ofType:nil]] forState:UIControlStateNormal];
        }
    }

This results in the following memory usage. The spike occurs as the loop is running, but doesn't go away even though the block is surrounded by an @autoreleasepool. Note that ImageIO is using the memory:

Without coloring

That screenshot was taken when the territories had the original color in their image files. I'm using the UIImage+Tint library from MGImageUtilities to color the images. When using that library, the following code is used:

[ter setImage:[[ter imageForState:UIControlStateNormal] imageTintedWithColor:[UIColor colorWithRed:[[colors objectAtIndex:0] floatValue]/255.0 green:[[colors objectAtIndex:1] floatValue]/255.0 blue:[[colors objectAtIndex:2] floatValue]/255.0 alpha:1.0]] forState:UIControlStateNormal];

When using this code in another loop (that is enclosed with an @autoreleasepool in another function) after loading all the images, the following memory usage occurs. Note that CG raster data is using the memory.

With coloring

I don't know why the things using memory are different.

I posted a thread on the Apple Developer Forums which preludes this issue as well.

I have also contacted Apple Developer TSI, and they recommended using "lazy image loading". I looked into that, but I haven't found a way to do this on a non-page-based UIScrollView.

At this point I'm pretty much lost as to how to solve the problem. I've tried everything I can think of over hours and hours of trying to fix this but I can't seem to come up with any viable solutions. If anyone can help me, I'd really appreciate it. If you want to see more information or code, let me know and I'd be glad to post it. Thanks in advance for your help.

EDIT:

I've been experimenting, and I'm working with the following code in scrollViewDidScroll:. It appears to be working, but the memory isn't being released, it continues going up. Ideas?

CGRect currentF = [scrollView convertRect:scrollView.bounds toView:scrollView.contentView];

    //Loop through all territories and determine which need to be rendered
    for (int i = (int)territoryArray.count - 1; i >= 0; i--) {

        @autoreleasepool {

            //See if the territory is on-screen
            if (CGRectIntersectsRect(currentF, [[territoryArray objectAtIndex:i] frame])) {

                //See if the territory needs an image
                if ([[territoryArray objectAtIndex:i] image] == nil) {

                    //Set the territory's image
                    [[territoryArray objectAtIndex:i] setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@T%i.png", [defaults objectForKey:@"Current_Map"], i + 1] ofType:nil]] forState:UIControlStateNormal];
                }
            } else {
                //Remove the territory's image, it is off-screen
                [[territoryArray objectAtIndex:i] setImage:nil forState:UIControlStateNormal];
            }
        }
    }
War es hilfreich?

Lösung 2

Firstly, thank you user3339357 for all your help. You gave me the basis to my final solution.

Solution:

I adjusted the scrollViewDidScroll: function as follows, in order to create missing images and release images that were no longer needed:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {

    //Get the frame that the scroll view is currently showing
    CGRect visibleF = [scrollView convertRect:scrollView.bounds toView:scrollView.contentView];

    //Loop through all territories and determine which need to be rendered
    for (int i = (int)territoryArray.count - 1; i >= 0; i--) {

        //Put the following in an autoreleasepool to conserve memory
        @autoreleasepool {

            //See if the territory is on-screen
            if (CGRectIntersectsRect(visibleF, [[territoryArray objectAtIndex:i] frame])) {

                //See if the territory needs an image
                if ([[territoryArray objectAtIndex:i] image] == nil) {

                    //Set the territory's image
                    [[territoryArray objectAtIndex:i] setImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@T%i.png", [defaults objectForKey:@"Current_Map"], i + 1] ofType:nil]]];

                    //Update the territory's color
                    [self updateColorOnTerritory:[territoryArray objectAtIndex:i]];
                }
            } else {

                //Remove the territory's image, it is off-screen
                [[territoryArray objectAtIndex:i] setImage:nil];
            }
        }
    }
}

Essentially, the code gets the visible frame of the scroll view's content view (the line of code that creates visibleF allows for scaling/zooming on the scroll view as well). The program then loops through an array that holds a reference to each Territory object in the scroll view and determines which territories should be visible, and sets their images if they don't exist. It also resets image views that are not currently visible on the screen.

It should be noted that I adjusted the OBShapedButton library and changed it so that it's a subclass of UIImageView since I was having issues forcing buttons to release memory that they were no longer using. I used UITapGestureRecognizers in place of IBActions.

EDIT:

The code above was causing lag in the scroll view (a lot of it). I solved this problem by adding the following code in the MapScrollView subclass I made for the UIScrollView:

- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
    return NO;
}

Hope this helps anyone else with a similar problem!

Andere Tipps

I find that when I have to load images into a scroll view, to avoid memory issues and for general responsiveness of the app you load the images incrementally. Meaning, although you may have 250 images in the scroll view, you won't be able to see them all at the same time. So load he visible tiles, and a few rows below it. You can load the rest as you scroll.

EDIT: Here's an example.

-(void)scrollViewDidScroll:(UIScrollView *)myScrollView {

int currentPage = (1 + myScrollView.contentOffset.x / kXItemSpacingIphone);
for (ItemView* itemView in [self.itemRow subviews]){
    if (itemView.tag >= currentPage-1 && itemView.tag <= currentPage+1)
    {
        //keep it visible
        if (!itemView.isLoaded) {
            [itemView layoutWithData:[self.items objectAtIndex:itemView.tag-1]];
        }
    }
    else
    {
        //hide it
        if (itemView.isLoaded) {
            [itemView unloadData];
        }

    }
}

}

The code above will load images that are a page above and a page below of the visible page. I found that the page scrolling is a bit choppy. But I'm loading images from the network, so you may find that this works just fine.

Good luck!

EDIT 2:

If your scroll view isn't set up for paging, try this.

  1. Make my view controller the delegate of the scroll view (if you do this in code, you have to modify your view controller's .h to say that it conforms to UIScrollViewDelegate).

  2. Define a scrollViewDidScroll method that (a) determines the frame of the visible portion of the scroll view; (b) determine which of the subviews intersect with that visible portion; (c) load the items that are visible, and unload the ones that aren't.

It will end up looking something like this.

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    // Determine the frame of the visible portion of the scrollview.

    CGRect visibleScrollViewFrame = scrollView.bounds;
    visibleScrollViewFrame.origin = scrollView.contentOffset;

    // Now iterate through the various items, remove the ones that are not visible,
    // and show the ones that are.

    for (Item *itemObject in self.itemCollection)
    {
        // Determine the frame within the scrollview that the object does (or 
        // should) occupy.

        CGRect itemObjectFrame = [self getItemObjectFrame:itemObject];

        // see if those two frames intersect

        if (CGRectIntersectsRect(visibleScrollViewFrame, itemObjectFrame))
        {
            // If it's visible, then load it (if it's not already).
            // Personally, I have my object have a boolean property that
            // tells me whether it's loaded or not. You can do this any
            // way you want.

            if (!itemObject.loaded)
                [itemObject loadItem];
        }
        else
        {
            // If not, go ahead and unload it (if it's loaded) to conserve memory.

            if (itemObject.loaded)
                [itemObject unloadItem];
        }
    }
}

Reference

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top