So I've seen this question asked before and in fact I asked it last night but I thought I'd give it another go just to see if I could get any other unique views on the problem.
The Problem — I have an app with a large number of uiimageviews (with image downloaded to disk) in a scrollview which of course has two large problems facing it: Memory use, and performance. In my app memory use isn't so much a problem because I am employing techniques such as dequeuing and reusing imageviews and such. But performance is another thing entirely. Right now, as a memory saving procedure, I only store image filepaths in memory because it would be ridiculous to store images in memory. But the problem with this is that reading from the disk takes more time than from memory and slows down scrolling on the scrollview immensely.
So, what kind of techniques do any of you suggest for something like this? I've seen three20 but don't want to use it because I need high customizability in my view and that just won't do. Image files are not large, but just thumbnail size so there is no scaling or excess size. There's got to be an intuitive way to handle this. The built in photos app handles up to thousands of photos perfectly with low memory and slick and smooth scrolling performance.
Fundamentally, the problem is that you're probably doing a bunch of disk I/O on your UI thread, which is basically guaranteed to cause performance problems.
You should consider loading your images on a background thread and updating the image views on the main thread when the images are loaded. Depending on your use case you can get more or less clever about how far you preload in advance, etc, so you can have images ready. (There might be some usable source code or even Apple sample code out there that does something like this, but I don't know of it off the top of my head.)
You may notice that some applications (not sure about the Photos app) have an intermediate stage where they load a very small thumb size image for all images, and scale it up to the render size, which acts as a placeholder until the full size version is loaded-- if the user scrolls past that image before the full size is loaded, the visible effect is nearly the same as if the image was there all along.
Related
I have an iPhone app that, among other things, allows users to store photos. When a new photo is added to the app's data store, I cache a thumbnail version of the image so that the photo thumbnail grids load in a reasonable amount of time.
The problem is that these thumbnails look great on a pre-Retina Display screen, but they look a little blurry on RD displays. It's not so bad that the images are unusable, but I would really like to be able to get the full benefit of Retina Display for images users saved with older versions of my app.
The problem is that re-creating all these thumbnails takes way too long. In my tests, it took about a minute and a half to re-encode a sample database to high-res thumbnails (admittedly a large one) on my iPhone 4. It will be even worse on older hardware.
How can I get around this? Doing a one-time migration seems out of the question, given the performance results above. Other options are shrinking the thumbnails lazily (i.e. as they're displayed on-screen) and then saving them to the database at that point. Screens full of old images will be sluggish the first time they're viewed, and then snappier after that.
Are there other approaches to consider? Anyone else faced this problem?
I dont like the idea that you try and convert the images.
User will quickly get impatient and say you app is buggy and takes ages to load.
I think you solve the situation without any re-processing of full sized images.
On older hardware you would not have a retina display (so no need to upsize the images). If they have a retina display then they have a fast iPhone iPod.
I would suggest you graphically solve the problem by how you display the thumbnail images. so instead of fullscreen put a border around this image and show it at its true resolution (dont upscale it). Or show 4 images where you normally show 1 (since iPhone screen is 4x the resolution).
Instead of resampling the original massive image, you could do a bicubic upsample of the thumbnail making it 4x the size. This will make it slightly blurry but it should look better than the iPhone scaling which will look really bad. The upsample would be ultra fast as its working with a small image.
I cannot help you out on upsampling but there will be some code somewhere.
Cheers, John.
Screens full of old images will be sluggish the first time they're viewed, and then snappier after that.
It doesn't have to be sluggish.
It's a bit of a pain, but you can do most of your processing in a background thread. Set the thread priority to something low (like 0.1) to avoid making the UI too slow. The easiest way to do this is to set up an NSOperation for each image you need to convert and add them to a NSOperationQueue with maxConcurrentOperationCount=1.
If writes are not atomic, in -applicationDidEnterBackground: or -applicationWillTerminate: (or in something listening for the corresponding notifications notifications), do something like [queue cancelAllOperations]; for (NSOperation * operation in queue) { [operation setThreadPriority:1]; } [queue waitUntilAllOperationsAreFinished];; you get about 10 seconds or so which should be enough for the image conversion to finish writing to disk (and thus avoid half-written files). For added protection, check [operation isCancelled] immediately before the write if it might take longer than 10 seconds. Obviously, in -applicationWillEnterForeground:, you should restart the conversion (remembering that some of the images have already been converted).
Concurrency issues are fun to track down...
(Note that [data writeToFile:path atomically:YES] isn't sufficient — it's likely to leave temporary files lying around if the app is killed during the write. I'd recommend storing thumbnails in Core Data if you can, but that might be out of the question for existing apps.)
I'm seeing my app being killed by iOS with an out of memory message, however, while tracing the progress of the app in the Allocations Instrument, I see lots of mallocs that seem to be occurring outside of the code I've written.
I'm not seeing any leaks being caught, so I assume these allocations are supposed to be there. Thing is, because I'm not sure about why they have been allocated, I'm not sure what I can do to optimize the app and prevent the OS from jettisoning my app.
Does anyone know why the memory is being allocated, or is there any way for me to find out?
Here are a couple of shots from Instruments showing the mallocs. In the second shot, all of the allocations have the same stack trace.
EDIT
I' displaying a single large image as the UIView background (1024x768), then overlaying a smaller (600px square) UIView with some custom drawing and a third UIView (550px square) over the top of those that contains two 550px square images overlayed.
I'm guessing that this is not appropriate, and there is probably a better way of achieving the composition of views I need for the app to work.
Should this be possible on the iPad?
I think there's not really much information to go on here - if you add a bit more information about what this view in your app is doing you might get some more informed suggestions.
From the screenshot, it would appears large blocks are being allocated to display an image.
Given that I'd hazard a guess that either you're trying to display some very large images, or you UIView is large, or you have more UIViews in memory that you need to display the current screen.
I guess the easiest way to track down exactly where they're coming from would be to disable the part of the application you suspect then run again and see if the allocations still occur.
EDIT
Are all the images the same size as you're displaying them? (ie. are you trying to display a 5M photo as the 1024x768 background?) If not you probably need to scale them down to the size you are display them, or at least closer.
If you're not needing transparency, make sure to make all the views opaque.
I figured out the source of the problem - I was using
[UIImage imageNamed:#'Someimage']
to load in my images. This, as I'm sure many people are aware, caches the image data. I had enough images of sufficient size to cause my app to be jettisoned.
The problem was apparent not because of the size of the image but because of both the size and number of images I was using. The lesson here is be careful with [UIImage imageNamed:].
Thanks for all of the help, chaps!
Mallocs can occur inside of other API's that your app calls (such as loading images, views, playing long sounds, etc.) You can try changing the size of your images, views, sounds and other objects by various amounts as a test, and see if the size of the malloc'd memory changes track one of the changes that you've made.
In my application I use lots of images based in interface builder. The problem with this is that it uses large amounts of memory because interface builder caches it much liked "imageNamed" so I've begun removing the image from imageViews in interface builder and instead adding them once the view starts using "imageWithContentsOfFile". After several hours I have made little progress because I have literally hundreds of images. I'm just wondering if there is a more straightforward way to do this?
Yes, don't do it. UIImage and the whole xib business pretty much delay loading until things are needed, as well as drop cached images where possible and needed. You can even see this happening in Instruments. It helps to split your design over several xibs, so they can be loaded when needed.
What you can do however, is to make sure that you don't scale images but display them 1:1, and that you save them in the lowest acceptable quality. For photo's, take JPEG. For other images, take PNG.
I've got an app I'm working on where we handle a LOT of images at once in a scrollview. (Here's how it looks, each blue block being in image on a scrollview expanding to the right: http://i.stack.imgur.com/o7lFx.png) So to be able to handle the large strain doing this puts on memory. So I've implemented a bunch of techniques such as reusing imageviews etc which have all worked quite successfully in keeping my memory usage down. Another thing I do is instead of keeping the actual image in memory (which I of course couldn't do for all of them because that would run out of memory very quickly) I only keep the image's filepath in memory and then read the image when the user scrolls to an area of the scroll view near that image. However, although this is memory efficient, it's causing a LOT of lag in the scrollview because of the fact that it has to constantly read images from the disk. I can't think of a good solution on how to fix this. Basically right now the app draws to the screen only the visible uiimageviews and while the user scrolls the app will look to see if it can dequeue another imageview so it doesn't have to allocate another one and at that point it reads the image into memory, but as I said it's causing the scrolling action to be very slow. Any ideas on a strategy to use to fix this? Does anyone know what the native photos app does to handle this kind of thing? Thanks so much!
I can suggest you a simple solution to balance both the memory and the computer processing. You only keep small images like thumbnails in memory and only keep about 20 of them. One project that I am doing, I keep 20 thumbnail images (100 x 100) recently accessed, which doesn't cost a lot of memory. I believe that it costs about 200 kb all the time but comparing to a general available memory. I think it is good enough.
It also depends on your use case : if user scroll really fast and you don't know when will they go. You can have even smaller images than the thumnail and when you show it on the UIImageView, you resize it to fit. When user stops scrolling for a while. You can start loading bigger images and then you have a nicer images. User may not even notice about the process
I don't think there is a solution that can be fast and using as less memory as possible. Because we have memory, maybe not big but have enough if we use it smartly.
Slow scrolling performance might mean that you're blocking the main thread while loading images. In that case, the scrolling animation won't continue until the images are loaded, which would indeed cause pretty choppy scrolling performance.
It would be better to lazily load requested images in the background, while the main thread continues to handle the scrolling animation. A library that provides this functionality (among other things) is the 'three20' library. See the Tidbits document, and scroll down to the bottom where the 'TTImageView' class is described.
I had a similar issue with a PDF viewer, The recommended way to do this is to have as low a res image as you can get away with and if you are allowing the user to blow the image up/zoom, then have two versions or three versions of that image increasing the res as you go.
Put as much code as you can get away with in the didDecelerate method (like loading in higher res images like vodkhang talks about), rather than processing loads in didScroll. Recycle Views out of scope as you have said. and beware of autoreleased Context based Image Creation functions.
Load images in on background threads intelligently (based on the scrollView Offset position and zoom level), and think about using CALayer/Tiled Layer drawing for larger images.
Three20 (an open source iOs lib) has a great Photo Viewer that can be subclassed, it has thumbnail navigation, large image paging, caching and gestures right out of the box.
I have around 20 tableview cells that each contain a number (2-5) thumbnail sized pictures (they are VERY small Facebook profile pictures, ex. http://profile.ak.fbcdn.net/hprofile-ak-sf2p/hs254.snc3/23133_201668_2989_q.jpg). Each picture is an UIImageView added to the cell's contentview.
Scrolling performance is poor, and measuring the draw time I've found the UIImage rendering is the bottleneck.
I've researched/thought of some solutions but as I am new to iphone development I am not sure which strategy to pursue:
preload all the images and retrieve
them from disk instead of URL when
drawing cells (I'm not sure if cell
drawing will still be slow, so I want
to hold off on the time investment
here)
Have the cells display a placeholder
image from disk, while the picture is
asynchronously loaded (this seems to
be the best solution, but I'm
currently not sure exactly how to do
best do this)
There's the fast drawing
recommendation from Tweetie, but I
don't know that will have much affect
if it turns out my overhead is in network loading
(http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/)
Thoughts/implementation advice? Thanks!
Suggest you do a search in the XCode help docs for LazyTableImages. It's a sample app provided by Apple that asynchronously loads images into a table cell. It should be a good starting point.
You'll probably want to add a local cache to save the images so you don't have to keep downloading them each time, and a way to prune out old images out of the cache.