Quartz Drawing While Scrolling Issue - uiview

I have a subclass of UIView which I draw a graph on it.
GraphingView.h
#interface GraphingView : UIView
...
#end
I draw things here:
GraphingView.m
- (void)drawRect:(CGRect)rect
{
...
}
Then, from another controller, I call this graph by just initializing and add that subview
graphView = [[GraphingView alloc] init];
...
[self.view addSubview:graphView];
But I actually call this subview multiple times with UIScrollView in different pages. Let's say 3 pages for 3 different graphs. Everything works fine if I just need 3 static graphs. Because of my scroller is infinite, every time I swipe the scroller left or right, the scroller will generate an older or newer graph which sits previous or next to the graph of direction. For that reason, I use scrollViewDidScroll to detect and insert new graph. However, scrollViewDidScroll makes too many calls to the subviews since it has not been stopped. So, that means I am re-drawing too much with Quartz 2D and it causes the app to crash. Does that sounds memory leak of Quartz?
For that matter, I then decided to call the graph subview in scrollViewDidEndDecelerating instead. But after a while scrolling, the app crashes just like above since I still call the subview which has drawRect method too many times.
My question is, how do I prevent this Quartz from crashing because drawRect is being called more than it should be?

The memory leak is caused by sqlite3 actually because I query data in every graph view.
There is an outstanding article for those who deals with memory leak which I highly recommend When is a Leak not a Leak? Using Heapshot Analysis to Find Undesirable Memory Growth

Related

How to draw parts of a UIView as it becomes visible inside UICollectionView?

On Android I have item views with potentially large widths on a horizontal scrolling list. The view loads and draws chunks "images" as parts of the view become visible on the list. This is an optimization to avoid drawing all images at once as it would be wasteful and slow. The content drawn is essentially an audio waveform. Do to the ways things need to work I can't split the chunks as individual view items on a list. This strategy works perfect because of how android drawing architecture works.
Now I'm trying to use a similar pattern in iOS but I'm having some trouble and I'm not too sure how to come up with a solution.
In iOS I'm are using a UICollectionView which draws large width Cells and we need that same optimization of loading and drawing only the visible chunks.
Solution 1:
Check what parts of the UIView is visible and draw only those visible chunks. The problem with this solution is that as the UICollectionView scrolls the UIView doesn't draw the next chucks that are becoming visible. Here are examples of what I'm talking about.
UIView loads initial chunks
These can be seen by their different chunk colors:
Scroll a bit
Black is shown and no content is loaded because there is no hint that the view needs to draw again so we can't load the next visible chunk.
Solution 2:
Use custom UIView backed by a CATiledLayer. This works perfect because it draws the tiles as they become visible while scrolling the UICollectionView.
The problem is that drawing happens on the background thread or on the next drawing cycle if shouldDrawOnMainThread is defined. This brings issues when the UIView is resized or my internal zoom logic kicks. Things shift because the drawing cycle is not synchronized to the view resizing.
So how could I get notified like CATiledLayer that a part is becoming visible and I could draw normally like a CALayer backed UIView?
UPDATE 1
I'm looking into using preferredLayoutAttributesFittingAttributes as a way to check if a new chunk need to be drawn. This is called every time the cell is moved into a new position because of scrolling. Hopefully, this isn't a bad idea. :)
- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
UPDATE 2
After much testing and playing around. Using solution 1 is not an option. When a UIView reaches a huge width memory usage goes off the roof while using CATiledLayer memory usage is at minimal as I guess one would expect. :/
I don't know if you can completely turn off the effect. You can soften it by setting the fadeDuration to 0. To do this, you must create a subclass of CATiledLayer and overwrite the class method fadeDuration.
You can also try to implement shouldDrawOnMainThread class method, but this is a private method, and you're risking a rejection on the App Store:
#implementation MyTiledLayer
+(CFTimeInterval)fadeDuration {
return 0.0;
}
+ (BOOL)shouldDrawOnMainThread {
return YES;
}
#end
or in Swift:
class MyTiledLayer: CATiledLayer {
override class func fadeDuration() -> CFTimeInterval {
return 0.0
}
class func shouldDrawOnMainThread() -> Bool {
return true;
}
}
If you want to implement your own tiled layer instead, you should waive of the tiles and try to compute the visible part (section and zoom level) of the view. Than you can draw only this part as layer whole content.
Apparently, there is no easy way to re-draw the view when the scrollview is scrolling. But you may implement scrollViewDidScroll:, and trigger the redrawing manually. The visibleRect method always returns the complete area of the layer, but you can use
CGRect theVisibleRect = CGRectIntersection(self.frame, self.superlayer.bounds);
or in Swift
let theVisibleRect = frame.intersection(superlayer.bounds)
to determine the visible area of the layer.

Prepare UIImage with lots of layers on background thread

I have the problem that I have some really big UIScrollView and tons of images loaded on it as user scrolls. Images are stored on the device, however I receive information from server what to display on particular part of UIScrollView. When user scrolls a bit I need to show images at new position as I cannot afford to draw whole UIScrollView with images at startup. For the background I had one relatively small image which I move throughout the View. But the problem is that on top of that background I should draw a lot of UIImage objects(about 300-400) which are not particulary bih however are separeted on layers(one image on top of other on top of other etc.). Blocking scrolling while drawing is NOT an option.
Now I'm trying to decide which approach will suite my best:
Add all needed images to UIView on background thread and then just add UIView to ScrollView on main thread(which hopefully wont take long). Here when scroll somewhere I will need to calculate and create new UIView with objects and position it next to existing and eventualy to remove first UIView with all objects and layers when user continues to scroll in some direction.
Combine all layers in image with CoreGraphics and present them as objects with already decided layers. In this way I can remove specific object(image) from scroll view. When user scrolls I just create new objects and add them to view as full objects, and can remove objects when user scrolls enough instead of removing whole view. The problem here is adding multiple objects to UIScrollView on main thread, however when they are combined they won't be more than 15-20 objects.
My biggest concerns are performance and threading. As I said I cannot block main thread(or let's say cannot do this for a time that user will notice) and cannot combine images at my graphics department as they have tons of variatons which are decided at runtime. That's why I'm thinking of a way to prepare data on background thread and just really fast add it on main thread instead of preparing and adding it on main thread(which will block UI).
Every help will be greatly appriciated!
Regards,
hris.to
Look at using CATiledLayer for a UIView backing. It is was designed for this.
I have a map that has one UIView in a UIScrollView and the UIView is sized to the full size of the entire map. The CATiledLayer handles when to draw each tile of the view.
Ok, so I'm writing here just to let you know how I fix my issue.
Main problem was that I was moving a background picture while scrolling(so I don't load an enormous file) and while doing that I was fetching information from server and try to draw on the same tiles which makes system crash with well known crash:
CALayer was muted while enumerated
I was depending on performSelector method along with #synchronized but it turns out that this is not effective and sometimes two threads(main UI thread and background thread) were trying to change same tiles on screen. So basically what I did is to change background fetching and drawing from:
[self performSelectorOnBackgroundThread]
to using concrete background thread, to which I store reference:
#property(nonatomic, strong) NSThread* backgroundThread;
And now each time I need to load new tiles or I need to move background, I'm cancelling this thread(and make sure it's cancelled) before start any other operation. Also there was a little problem when switching between view with this thread and other views, as my thread hangs, and I needed to set a timeout:
[_backgroundThread cancel];
int currentAttempts = 0;
while( [_backgroundThread isExecuting] )
{
if( currentAttempts == MAX_ATTEMPTS_TO_CANCEL_THREAD )
{
//make sure we dont hang and force background
_backgroundThread = nil;
return;
}
[_backgroundThread cancel];
currentAttempts++;
}
In my 'scrollViewDidScroll' however, I didn't use the recursion as this results in slight UI blocks while scrolling on older devices(such as iPhone 4) which is unacceptable. So there I basically just cancel the thread and hope to get cancelled quick enough(which with dozens of tests appears to be working):
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
[_backgroundThread cancel];
//move background around
[self moveBackground];
}
Downside of this approach is that you need lots of check in your background thread as calling 'cancel' to it won't actually cancel anything. As per apple docs it'll only change isCancelled state of your thread and you are responsible to make this thread quit in basically the same way as it'll quit normally(so the system has a chance to cleanup after your thread):
if( [_backgroundThread isCancelled] )
{
return;
}
Regards,
hris.to

iOS slow to release images while scrolling (no tableview)

Here is the problem :
I am writing an app which displays some pictures, with a treemap layout (for an example, see https://raw.github.com/beckchr/ithaka-treemap/master/Core-API.png)
This layout is displayed in a UIScrollView. Since many pictures can be added to that scrollview, I want to release the ones which are not on currently on screen. I am not using ARC.
At my point, I know which pictures I should release, and how to do it while scrolling (calling some "unload" method). There is no useless call of that method. The problem is that, when pictures are released, the scrolling stops for a little moment (a few ms, but this is enough to be bad looking, making the scroll kind of "jumping" and slow, not smooth at all).
What I've tried (put in the body of my "unload" method) :
imageview.image = nil
performSelectorInBackground:#selector(effectiveUnload) withObject:nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0,^(void){
dispatch_sync(dispatch_get_main_queue(), ^(void){
imageview.image=nil}
}
I think this problem is weird, since there is absolutly no slowing effect with memory allocation, but only with memory release.
Thanks for help, don't hesitate to ask for more information.
did you try removeFromSuperview to remove the imageview from scrollview
Don't add more and more UIImageViews - recycle them!
To save as much memory as possible you should follow the UITableView way of recycling views:
1. Once a view has left the visible area, add it to a "views pool" (and remove it from its superview. It's not an expansive operation!)
2. When a new view becomes visible, first check if there's a view in the pool. If not, then create a new view.
I know my answer doesn't answer your question directly, but that's the way to go. Otherwise you'll run out of memory eventually.

iOS: Lazy-loading in UIScrollView with ARC

I'm working on an iPad-App with ARC which have to display thousands of UIImageViews in a UIScrollView...
When I load them all at once (or more accurately in a queue with GCD), I run out of memory after a while of loading..
Now, I thought i have to use lazy-loading and load only those UIImages which are necessary and a kind of release those which are no longer visible, but I don't know if this is possible with ARC..
Anybody have an idea to do this, or a better idea to handle this case..?
Thanks, tonistair
Make something that implements UIScrollViewDelegate, and in its viewDidScroll method, calculate the currently visible rect from contentOffset and bounds. Then remove things that are no longer visible, and add things that have just become visible (or some other appropriate algorithm). ARC has nothing to do with this.

A CATiledLayer-enabled UIView with drawRect-defined subviews crashes due to abnormal memory usage

We have an out-of-memory crash on the iPad 3 which we traced to the following scenario:
A UIView which uses a CATiledLayer and draws content (say, a PDF) has subviews with their own drawRect methods (which, for example, highlight search results). This makes Core Animation consume tons of memory (100+ MBs in the VM Tracker instrument), and can easily lead to a crash. While this issue exists on all devices, only on the iPad's Retina display does the cache size grow too large.
This can be reproduced with Apple's PhotoScroller example: subclass UIView, uncomment drawRect, and add an instance to the TilingView. The app will crash on iPad 3. Commenting drawRect resolves the memory usage.
Now, we can drop the subviews and do the drawing in the top-most UIView. However, working with subviews is convenient (since we're representing different, independent layers on top of the PDF). Two questions:
What is a good work-around? Preferably one that allows us to continue working with multiple views.
Why is this happening, exactly? I guess the cache mechanism is working overtime, but it would be great to understand the technical details behind it.
Thanks!
EDIT:
I want to elaborate on Kai's answer. The problem was indeed unrelated to CATiledLayer, but to the usage of UIViews that implemented drawRect.
In the case of PhotoScroller, I created a UIView which was of a size of the image - 2000x2000 and more, which creates a huge backing store if drawRect is present.
In the case of our app, the overlay views are full-screen (=~11 MBs on iPad 3) and we have about 5 of them per page. We keep up to three pages in memory while scrolling, and that means more than 150 MBs extra memory. Not fun.
So the solution is to optimize drawRect away, or use less such views. Back to the drawing board it is :-)
To 2.: Whenever you implement drawRect in a UIView subclass and have lots of instances of that class, your memory usage will grow dramatically. Reason is that a lot of optimization tricks in UIKit view/subview handling (e.g. when zooming or scrolling) don't work with such objects, because the framework doesn't know what you're doing/what you are drawing.
So - independent of retina or not - avoid implementing drawRect, especially when having many objects or many layers of subviews.
To 1.: I didn't exactly get what you are trying, but I implemented an PDF-Viewer which is also able to show additional content on top of the PDF. I did it all with normal UIView hierarchies, images etc. and I fear that's the only reliable work around you'll get
My experience:
Never add subviews to a UIView that's backed by a CATiledLayer
Never add sublayers to a CATiledLayer
Unfortunately, that seems to be the only practical answer - Apple's implementation goes horribly wrong in many different ways (not just performance - the rendering itself starts to exhibit visual artifact bugs, some of Apple's rendering code goes weird, etc).
In practice, I always do this:
UIView : view
+-- UIView w/ CATiledLayer : tiledLayerView
+-- UIview : subViewsView
...and safely add views and subviews to "subViewsView". Works fine.

Resources