5
votes

Developing an iPad PDF-Reader we decided to prepare high-res images of rendering intensive pages (lots of paths in them) and use those instead of the pdf pages to avoid performance issues. We decided that 3*768 by 3*1024 is a good compromise between readability and rendering performance which results in ~1.5 MB jpegs.

However we tested two implementations for displaying the image pages. One that uses a CATiledLayer subclass which is also responsible for handling the "normal" PDF pages (drawing with CGContextDrawImage) and another which uses UIImageView. The latter has the advantage that displaying and zooming is very quick, but memory usages is really bad - it takes about 30 MB in memory (which is consistent with the image's bitmap size). The other approach (CATiledLayer) needs more time to first display the page and needs another two seconds to re-render after zooming (similar to pdf pages, but much faster) but doesn't grab more memory than it needs to display a much smaller image or a PDF page.

Does anyone know what's going on behind the scenes and if it's possible to combine low memory usage of CGContextDrawImage with high performance of UIImageView by using the Quartz Framework.

1

1 Answers

4
votes

Not sure if this question is still relevant, but if so, maybe this will help:

I've fought the battle of efficient display of large views in my tiled image view as well. There are a bunch of underlying parts to the problem:

  • A normal UIView, including UIImageView, seems to always be completely backed by memory for its image. Even if you implement the drawRect: method, it always seems to pass the entire bounds of the view, not only the area visible within a scroll view. As you've discovered, this quickly takes up a lot of memory as each pixel takes 4 bytes.
  • The CATiledLayer does only request the contents for visible tiles. It also discards tiles that are no longer visible - this is where the memory savings come from. However, it does the notification and the drawing on a background thread, while animating from white to the contents. It seems to do this via a private API, I haven't found a way to re-implement CATiledLayer's functionality as my own subclass of CALayer, as there simply seems to be no notification mechanism that we can use as mere mortals.
  • If you have multiple views within the scroll view, they receive drawRect: messages appropriately as they are paged in. UIKit seems to struggle with too many subviews below a view, though.

For you, I can see a couple of possible options:

  • It might be possible to salvage the CATiledLayer-based implementation. The fadeDuration defaults to 0.25 seconds, which may be too long if your load times are short. You can drop this right down to something like 1.0/60.0, i.e. one frame. One thing that's not clear from your description is whether your images cover the whole page size or just each 256x256 pixel tile. Decoding the whole JPEG over and over again for each tile will be much slower than decoding individual tile files.
  • If the latency from doing everything from the CATiledLayer thread is too high, you can manually create a bunch of tiles as UIImageView subviews to a blank UIView which is the main subview to the scroll view. Each of these subviews is assigned its own tile image. If you're lucky, UIKit will be smart enough to drop the contents of these views and re-load the corresponding JPEGs on demand.
  • If UIImageViews aren't smart enough, or you have too many views for UIKit to cope, you can build your own views which load their JPEGs in drawRect:. If it's still too jerky, try with your own CALayers, which will be sublayers of your single view's blank layer and again load their image on demand. This is what I eventually went with for my tiled image view.

That should cover scrolling, but it could still be really slow if you allow the user to zoom right out (minification). In that case I recommend you store appropriate lower-resolution versions and load/show those at those outermost zoom levels.