Load images for collection view in background thread - ios

So I have a collection view in which I'm populating 141 images into it, but the problem I'm having is when scrolling through the view, it's not very smooth. I've already tried using low res images for the cells (which helped slightly) and decreasing the swipe speed (or should I say increasing the deceleration when scrolling).
So I figured my next step would be to try loading the cells on a background thread?
Firstly I have no clue how to do this, and Google isn't much help. Secondly, I'm not sure if the fact I'm re-using cells when they go out of view (so I don't create 141 cells, one cell per image) makes a difference.
Here's how I'm loading in the images:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("TreeCell", forIndexPath: indexPath) as? TreeCell {
cell.contentView.backgroundColor = UIColor.clearColor()
let tree = Tree(treeNumber: (indexPath.row + 1))
cell.configureCell(tree)
return cell
}
else {
return UICollectionViewCell()
}
}
TreeCell for whoever asked:
var tree: Tree!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
layer.cornerRadius = 10.0
}
func configureCell(tree: Tree) {
self.tree = tree
treeCellImage.image = UIImage(named: "\(self.tree.treeNumber)")
}
And the Tree class:
private var _treeNumber: Int!
init(treeNumber: Int) {
_treeNumber = treeNumber
}
var treeNumber: Int {
return _treeNumber
}

Don't try to create UICollectionViewCells on a background thread. The cells should be created only via a dequeueReusableCellWithReuseIdentifier variant and UI prefixed classes (UIKit) are not thread-safe unless explicitly stated as such. As you said, it will break cell reuse and potentially cause all sorts of memory issues.
Check out this SO question: Load UIImage in background Thread
If you aren't supporting iOS 8, you could load the images from disk on the background. Once they're loaded, I'd post a NSNotification on the main thread containing the image's unique id (filename?). The cells can all listen to the notification and – if the notification is observed for the cell's current image – display the new UIImage.
If you can use [UIImage imageNamed:], you'll get some nice low-memory-reactive caching benefits for free. Otherwise, you'll probably want to maintain a cache (NSCache or NSDictionary) of the images that you're already loaded.
If you do need to support iOS 8, you can still load image data from disk in the background via NSData, and feed the image data into a UIImage on the main thread. The expensive task is reading from disk. If your images are in Asset Catalogs, this will require a lot more work than imageNamed.
Now, assuming you find a method of safely loading the images on a background thread/queue that works in your specific situation, you have a few options to further optimize performance:
Cap concurrent tasks
Try to limit the number of concurrent image loads otherwise a quick scroll can saturate the system with blocking tasks and thread switching overhead.
Cancellable tasks
You should be able to cancel pending image load operations. If the user is scrolling just a bit faster than you can load the images, you want to be able to cancel loads for images that have already moved off-screen. Otherwise you can fill up the queue with unseen images.
Intelligent image loading
Don't load images that will be scrolled away in milliseconds (i.e. in response to a 'flick' or quick swipe). You can figure this out by hooking into the collection view UIScrollViewDelegate method
-scrollViewWillEndDragging:withVelocity:targetContentOffset:. This method lets you know how fast a user dragged the content and where the collection view will end up. You can even pre-load the images near the target content offset so that they're ready when scrolling ends!
A good for this sort of thing is NSOperationQueue because NSOperation instances support prioritization, concurrency caps, and concurrent execution. If you go with Grand Central Dispatch, you'll need to build in the ability to 'cancel' a task on your own.

Based on your previous comment on the image size, the first thing you should do is to resize your 200 x 200 images to match the 90 x 90 dimensions of your collectionViewCell.
UIKit will resize the 200 x 200 images as you populate your cells. Resizing images in general incur a non trivial cost, and this is done on the main thread. You should see drastic increase in smooth scrolling if you gave your cells 90 x 90 images instead of 200 x 200.

Related

Memory Allocations Profiler and Steadily Increasing Persistent Memory - Signs of Trouble?

I have an app I am developing and the stakeholder using it said that the app becomes slow and unusable/unresponsive after consistent usage all day. Killing it and starting over causes it to run fine.
I don't seem to have this trouble on my device, but I started looking at the memory usage in both simulator/phone in debugger, and observed my memory would steadily increase if I took the basic action of going between screen to screen. These are pretty involved screens, but if I just go forward to the 'add new item' screen, then back to the product listing screen, the memory jumps up 30mb. If I keep doing this same action, over and over and over, I can get it to 1.1gb of memory
I then took it a step further, hooked up my phone, and ran profiler (specifically memory leaks). I found one leak involving my usage of ads, so I just commented out all the code for a test and while the leaks are gone, the memory continues to go up steadily.
I then ran the allocations tool, and after a few min of going back and forth in the same manner, here is the output:
As you can see, it's 1.53GB and if I kept doing the same action I can get it to 2GB+. Oddly enough, my phone never seems to mind, and the screens are just slightly laggy at times otherwise not too bad. Certainly usable.
Before I start ripping out the floor boards, I wanted to confirm this is a likely sign of a problem. Any suggestions on where I can start looking? If persistent memory is the issue, what would be some typical gotchas or pitfalls? What is "anonymous vm?"
Thank you so much if you're reading this far, and appreciate any guidance!
UPDATE/EDIT
After some guidance here, I noticed, oddly enough, that on the "add product" page it causes the memory to jump ~10MB each time I visit it. After commenting out code, I narrowed it down to this section (and even the line of code) causing the jump. Removing this code causes it to remain stable and not increase.
//Render collection views
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath)
let member: MemberDto = groupMembers[indexPath.item]
let contactInitials = cell.viewWithTag(1) as! UILabel
let contactAvatar = cell.viewWithTag(2) as! UIImageView
contactAvatar.image = UIImage(named: "anonymous")
contactInitials.text = member.displayName
contactAvatar.layer.cornerRadius = contactAvatar.frame.size.width / 2
contactAvatar.clipsToBounds = true
contactAvatar.contentMode = UIViewContentMode.scaleAspectFill
contactAvatar.layer.borderWidth = 5.0
if (member.profileImage.trimmingCharacters(in: CharacterSet.whitespaces) != "") {
UserService.getProfilePicture(userId: member.userId) {
response in
contactAvatar.image = response.value
}
}
So, the offending line of code is here:
contactAvatar.image = response.value
Adding it in, and going back and forth to this tableviewcontroller causes the memory to go up and up and up all the way to 2gb. Removing that one line of code (where I set the image) keeps it stable at ~40-70mb, or it goes up but very very slowly (dozens of repeats only got it to 80mb)
I realized I was not caching this image
I decided to try caching this with my framework, and that immediately resolved the issue. I suppose the line of code was pulling the image into memory or something like that? It doesn't seem like the networking call is the actual issue, since I left that in (and even went so far to make additional calls to my API) and that doesn't seem to do much by way of memory increase.
Just a few pieces of info:
From the main screen, you tap on a + symbol in the navigation menu bar to come to this screen.
I am using a regular segue on my storyboard, associated with the navigationbutton, to take the user here
Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints
Making API calls from within my uitableviewcontroller doesn't seem to cause the image to load UNLESS I combine that with SETTING the image. If I make a network call, but don't set the image, it doesn't increase.
What mistake did I make? I feel like caching the image is a bandaid - I recall reading that you're not supposed to make calls to images within a UITableViewController but what is the alternative, to pull all user images from the collection in advance and cache them before the tableview loads?
EDIT 2
As #matt suggested, this was just a bandaid. The true problem still lingered as I knew deinit() was not being called. After pulling out major chunks of code, I found this
lblMessage.addTapGestureRecognizer {
self.txtMessage.becomeFirstResponder()
}
which maps to an extension class:
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
self.addGestureRecognizer(tapGestureRecognizer)
}
public func addLongPressGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.longPressGestureRecognizerAction = action
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
self.addGestureRecognizer(longPressGestureRecognizer)
}
// Every time the user taps on the View, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
So somewhere in here the problem must lie. I'm taking this to a new thread:
Deinit not calling - Cannot find why something is retaining (code provided)
Thanks! Hope this helps someone.
Yes it's a problem, and yes you need to fix it. The two usual causes of this sort of thing are:
You've got a retain cycle such that at least some of your view controllers are never able to go out of existence.
You've designed the storyboard (or manual segue) sequence incorrectly, so that (for example) you present from view controller A to view controller B, and then in order to get "back" you present from controller B to view controller A. Thus you are not actually going "back"; instead, you are piling up a second view controller A on top of the first one, and so on, forever.
Either way, you can rapidly test that that sort of thing is going on just by implementing deinit to print(self) in all your view controllers. Then play with the app. If you don't see the printout in the log every time you go "back", you've got a serious memory problem, because the view controller is not being released when it should be, and you need to fix it.

display downloaded images in tableview

I'm making an app in Swift 2.0 and I'm using a table view with an image view in the prototype cell. The app downloads some images from the facebook server and I want to display them. The images are all downloaded from a different url, so I am using a program to loop trough them and download, just like I did with the names to get them from the internet and display, but I'm a little bit stuck..
func fetchPicture(identifier: String, completion: (image: UIImage) -> Void)
{
let url2 = NSURL (string: "http://graph.facebook.com/" + identifier + "/picture?type=normal")
let urlRequest = NSURLRequest(URL: url2!)
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) {
(response, data, error) -> Void in
if error != nil
{
print(error)
}
else
{
if let pf = UIImage(data: data!)
{
dispatch_async(dispatch_get_main_queue())
{
completion(image: pf)
self.tableView.reloadData()
}
}
}
}}
var images = [UIImage]()
let queue2 = dispatch_queue_create("images", DISPATCH_QUEUE_SERIAL)
dispatch_apply(newArray.count, queue2) { index in
let identifier = newArray[index]
fetchPicture(identifier) {
image in dispatch_async(queue2)
{
}
}
}
I set the imageview in the cell equal to the variable 'image' so basically I will need to do something like self.image = pf but it needs to be different for each user. With names, I did this with an array, but this isn't working with images I assume..
Help is really appreciated guys!
Thanks!
You would generally want to use a framework that specializes on that kind of stuff. I would not recommend using SDWebImage, it's outdated (no NSURLSession), has a lot of open issues and doesn't work well with Swift (no nullability annotations).
Take a look at those two libraries that are up to date with iOS platform:
DFImageManager - advanced framework written in Objective-C but featuring nullability annotations (works great with Swift). Here's a list of things that make it better, than SDWebImage. Disclosure it's written by me, opinion might be biased.
Kingfisher - lightweight library written in Swift. Similar to SDWebImage, but has much less features that SDWebImage and DFImageManager.
You generally would not want to load all images like this. You definitely don't want to reload the whole table after every image (because that causes it to scroll back to the top of the tableview).
Generally it would be advised to fetch the images lazily (i.e. not until they're needed). You might call fetchPicture from cellForRowAtindexPath and in the completion handler update the cell's image view accordingly.
There are a bunch of details you have to worry about, though:
First, the cell may have been reused by the time the fetch is done, so you have to check to make sure the cell is still visible.
Second, rather than using an array, I'd suggest using a NSCache, and make sure to purge this cache upon memory pressure. You might want to also cache to persistent storage, though some people prefer to rely upon the NSURLCache mechanisms
Third, if the cell scrolls out of view, you might want to cancel the request (so that, for example, if you quickly scroll to the 100th row, that image request doesn't get backlogged behind 99 other image requests for cells that are no longer visible).
Personally, given all the work necessary to do this properly, I might suggest that you consider using a well established asynchronous image fetching mechanism, such as SDWebImage, DFImageManager or Kingfisher. Look at the UIImageView categories/extensions that offer asynchronous image loading. To do all of this properly yourself is a non-trivial exercise, so it's better to use some existing library that provides an asynchronous image view retrieval mechanism.

Long delays displaying UIImageView loaded from local file?

I see questions regarding long delays in displaying UIImageViews after downloading, but my question involves long delays when
reading from local storage.
After archiving my hierarchy of UIImageViews to a local file (as per narohi's answer in
How to output a view hierarchy & contents to file? ),
I find that if I want to reload them, it takes 5 to 20 seconds for the views to actually appear on screen,
despite my setting setNeedsDiplay() on the main view and all the subviews.
I can immediately query the data contained in the
custom subclasses of UIView that get loaded -- showing that NSKeyedUnarchiver and all the NS-decoding and all the init()'s have completed -- however
the images just don't appear on the screen for a long time. Surely the next redraw cycle is shorter than 5-20 seconds...?
It seems odd that images from PhotoLibrary appear instantly, but anything loaded from local file storage using NSKeyedUnarchiver takes "forever."
What's going on here, and how can I speed this up?
.
.
To be explicit, the relevant part of my Swift code looks like this:
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
myMainView.addSubview(view)
view.setNeedsDisplay()
// now do things with the data in view ...which all works fine
I find that, even if I add something like...
for subview in view.subviews {
subview.setNeedsDisplay()
}
...it doesn't speed up the operations.
We are not talking huge datasets either, it could be just a single imageview that's being reloaded.
Now, I do also notice these delays occurring when downloading from the internet using a downloader like the one shown in
https://stackoverflow.com/a/28221670/4259243
...but I have the downloader print a completion message after not only the download but when the (synchronous operation)
data.writeToFile() is complete (and before I try to load it using NSKeyedUnarchiver), so this indicates that the delay
in UIImageView redraws is NOT because the download is still commencing....and like I say, you can query the properties of the data and it's all in memory, just not displaying on the screen.
UPDATE: As per comments, I have enclosed the needsDisplay code in dispatch_async as per Leo Dabus's advice, and done some Time Profiling as per Paulw11's. Link to Time Profiling results is here: https://i.imgur.com/sa5qfRM.png I stopped the profiling immediately after the image appeared on the screen at around 1:00, but it was actually 'loaded' during the bump around 20s. During that period it seems like nothing's happening...? The code is literally just waiting around for a while?
Just to be clear how I'm implementing the dispatch_async, see here:
func addViewToMainView(path: String) {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
...Since posting this I've found a few posts where people are complaining about how slow NSKeyedUnarchiver is. Could it just be that? If so, :-(.
SECOND UPDATE: Ahh, the "let view = " needs to be in the dispatch_async. In fact, if you just put the whole thing in the dispatch_async, it works beautifully! so...
func addViewToMainView(path: String) {
dispatch_async(dispatch_get_main_queue(), {
let view = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as! UIView!
if (nil == view) {
return
}
self.myMainView.addSubview(view)
view.setNeedsDisplay()
self.myMainView.setNeedsDisplay()
})
}
This works instantly. Wow.. Credit to Leo Dabus. Leaving this here for others...

Specific UIImages and UIImageView help on theory?

I've been trying to figure out UIImage for some time now. I've been trying to figure out an approach to having one view the 'Main Game View' and showing either 2/3/4 different images depending on the 'level' variable. I'm just trying to be sure of logic. So for example level 1 would display 4 pictures and level 2 might display 3 different pictures. I don't want to hinder performance of the app but because the game is to be played offline and archiving won't make much of a difference all the images (several hundred optimised images) are being stored locally in the main app bundle.
I'm just wondering if my logic for trying to implement this so far is sound or not. For level 1 I would implement the 4 UIImageViews needed and initialise them with images, then display them on screen at set positions. I would then preload the next levels images using GCD. When a continue button is pressed I will set the UIImages and the UIImageViews to nil and display level 2's (or the next level) on screen.
I'm not confident in my approach and was wondering if there was something that would make it simpler or something I've missed or even if in practice it will work accordingly to the theory.
Thank you in advance for you time and any help.
Sorry if this is unclear.
I'm assuming you use a single view controller class for all your levels and load the levels from some .plist file or other format.
Don't bother using GCD. Loading 4 images once per level during loading costs practically nothing.
If all you need to do is have 2-4 images on the screen for each level (in addition to any other level elements), simply create 4 UIImageView instances and add them to your view. Then when you load a level, create new UIImage objects from the required image files and set your UIImageViews' image property to them. The UIImage objects of the old level will be released and deallocated at that moment, since you (and the UIImageViews) don't have a strong reference to them anymore.
Pseudo implementation:
- (void)loadLevel:(int)levelNumber {
// Assuming you have your image views in an array imageViews.
// Determine the image files required for this level.
// Put them into an array imageNames.
for (int i = 0; i < 4; i++) {
if (imageNames.count < i) {
UIImage *image = [UIImage imageNamed:imageNames[i]];
[self.imageViews[i] setImage:image];
} else {
[self.imageViews[i] setImage:nil];
}
}

Slow scroll on UITableView images

I'm displaying lots of images loaded directly from my app (not downloaded). My table view is slow when I scroll it the first time. It becomes smooth after all my cell has been displayed. I don't really know why.
I have an array of UIImage that I'm loading in the viewDidLoad. Then in my tableview delegate I just get the image at a given index path and set it to an UIImageView of my cell.
Do you know how I can improve performances ?
just to share I have fixed and it worked very well Steps I followed.
1) Set the performSelectorInBAckground function with Cell as parameter passed that holds the scroll view or uiview to put many iamges.
2) In the background function load the image stored from application bundle or local file using imagewithContents of file.
3) Set the image to the imageView using this code.
//// Start of optimisation - for iamges to load dynamically in cell with delay , make sure you call this function in performSelectorinBackground./////
//Setting nil if any for safety
imageViewItem.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = // Load from file or Bundle as you want
dispatch_sync(dispatch_get_main_queue(), ^{
//Set the image to image view not, wither cell.imageview or [cell add subviw:imageview later ]
[imageViewItem setImage:image];
[imageViewItem setNeedsLayout];
});
});
//// End of optimisation/////
This will load all images dynamically and also scroll the table view quite smoothly than previous slow and jerky behaviour.
All the best
You can read the answer I have just submitted here:
Loading image from CoreData at cellForRowAtIndexPath slows down scrolling
The basic idea is to use Grand Central Despatch to move your table-view-image-getting code to a separate thread, filling in your cells back on the main thread as the images become available. Your scrolling will be super-smooth even if there's a delay loading the images into memory from the filesystem.
What I understand from your question is that your images are all set to go and that they are loaded into RAM (stored in an array which is populated in viewDidLoad). In this case, I would want to see the exact code in cellForRowAtIndexPath in order to help out. My instinct tells me that something is being done there that shouldn't be done on the main thread (as He Was suggests). The thing is - if it's only a fetch from an NSArray (worst case O(log(n))), you shouldn't be seeing a performance hit.
I know you're not downloading the images but I would still recommend to do ANY non-UI operation on a background thread. I wrote something that might help you out.

Resources