8

I have developed an app which shows "full screen cards" in a UICollectionView like Tinder. The card contains an image and some text. I was loading the image using SDWebImage's sd_setImageWithURL method in the UICollectionView's cell.

However this was not giving me good performance since the images were mostly loaded when the user was on a card. I therefore used the SDWebImagePrefetcher prefetch queue to do this as follows:-

func startImagePreloadingOperationForIndex(index:Int)
{
    let numberOfImagesToBePreloadedOnEachSide = 20
    var previousIndex = index - numberOfImagesToBePreloadedOnEachSide
    var nextIndex = index + numberOfImagesToBePreloadedOnEachSide

    if previousIndex < 0
    {
        previousIndex = 0
    }
    if nextIndex >= currentNewsCollection.count
    {
        nextIndex = currentNewsCollection.count - 1
    }

    if previousIndex >= nextIndex || currentNewsCollection.isEmpty
    {
        return
    }
    let arrayOfNewsStories = currentNewsCollection[previousIndex...nextIndex]

    let arrayOfImageURLs = arrayOfNewsStories.map( { ImageUtility.getImageStringForNewsStory($0) } )

    SDWebImagePrefetcher.sharedImagePrefetcher().prefetchURLs(arrayOfImageURLs, progress: { (x, y) -> Void in
        }) { (x, y) -> Void in
    }
}

This function is called when the user lands on a particular card. The prefetching queue automatically manages not downloading images in cache so I don't have to worry about that. Along with this I am also using the sd_setImageWithURL in cellForItemAtIndexPath to pick up the downloaded image from the cache.

This is a better solution since I am now preloading images when the user is on a card. However this is a parallel queue. Which means image no. index + 20 could be loaded before the current image and the user could have to wait for the image. Also the app becomes a bit laggy if the user scrolls through over 50 cards and the memory usage also keeps on going up.

Can anyone please suggest improvements to this or a better algorithm?

Thanks

Rajeev Bhatia
  • 2,974
  • 1
  • 21
  • 29

3 Answers3

6

Download the image on the current card with sd_setImageWithURL and in the completion handler start the downloading of the prefetching queue. In this way the current image always enjoys the highest priority and does not lag.

currentCardImageView.sd_setImageWithURL(url) { (image, error, imageCacheType, url) in
   // start prefetching here
}

If the current image is already downloaded SDWebImage will recognize this and fetch it from the cache.

The other issue with the lag and the high memory consumption can maybe be solved be reducing the number of prefetched image. 20 is quite high, 5 should be enough, but this depends on the image size, network speed, etc...

Another thing: SDWebImage has a maxConcurrentOperationCount property. Start with 1, test, increase to 2, test, increase... and so on. Until you find the point it lags.

Darko
  • 9,655
  • 9
  • 36
  • 48
  • Thanks @Darko . This would only solve the problem for the current image, won't it? I tried prefetching only 5 images but that was also causing memory and lag issues. – Rajeev Bhatia Apr 06 '16 at 05:11
  • Try prefetching just one. How big are the images? And what is the usual network speed? – Darko Apr 06 '16 at 05:22
  • The images are less than 100KB in size and expected network speed is 3G/4G/Wi-Fi – Rajeev Bhatia Apr 06 '16 at 05:24
  • I assume that somewhere the processing of the images happens on the main queue instead on a background queue. But without more code this is hard to find. Can you post this part of the project? – Darko Apr 06 '16 at 07:00
  • Another thing: SDWebImage has a maxConcurrentOperationCount property. Start with 1, test, increase to 2, test, increase... and so on. Until you find the point it lags. – Darko Apr 06 '16 at 07:08
  • Hey Darko. What particular part of the code are you looking for? I don't think the lag has anything to do with the number of concurrent operations since when I start 20 operations on the first launch, it doesn't lag but lags after about 150 swipes when the memory size reaches 20 MB. after I clear half the memory, it works without any lag – Rajeev Bhatia Apr 08 '16 at 04:13
  • Clear the memory as soon as possible, not when it reaches a certain threshold. The images are cached on disk anyway if you didn't turn of the disk cache. As a rule of thumb your disk cache can be 5x the size of your memory cache. The image loading from disk cache is hyper-fast, almost realtime (subjectively). – Darko Apr 08 '16 at 04:52
  • Thanks @Drako. I'm clearing the memory everytime the app is opened from the icon or from background. The maximum size of the cache is 20MB. Is that too much? – Rajeev Bhatia Apr 11 '16 at 05:01
  • No, don't clear the cache at all. The cache is self-regulating. Clear the images, set the image references in your code to nil. 20MB is enough for in memory cache. I would use 100MB disk cache, this is more important. – Darko Apr 11 '16 at 05:42
  • If I don't clear the cache manually, the size keeps on growing as I use the app and even reaches over 100MB. Can you set the sizes of the disk and image caches separately in SDWebImage? Also how would I set the UIImageView's images to nil in a UICollectionView? I set the image property in dequeReusableCell. – Rajeev Bhatia Apr 11 '16 at 05:48
  • The cache size keeps growing until the configured limit is reached. Then the oldest images are released. If iOS issues a memory warning the cache automatically cleared. You don't have to bother with this strategy, that's the point of using a cache. Just set your desired sizes and forget the cache. – Darko Apr 11 '16 at 08:27
  • The cache is not cleared immediately. I have to call the SDImageCache.sharedImageCache().cleanDisk() method to clear the cache and this method might have some overhead since it queries the disk for all the files. Should I call it more frequently? – Rajeev Bhatia Apr 11 '16 at 11:12
  • Please look at the first init method, you can see that SDWebImage registers itself for memory warnings and in the selector clears all objects. https://github.com/rs/SDWebImage/blob/master/SDWebImage/SDImageCache.m – Darko Apr 11 '16 at 11:15
  • Does this mean that I never need to call the cleanDisk method? In that case, the app should never lag and the system should throw a memory warning in such a case which should clear the cache but curiously enough this is not happening – Rajeev Bhatia Apr 11 '16 at 11:31
1

I'd suggest handling your own queue, saving images to disk, then loading them from disk.

Instead of an parallel queue, use a serial queue, or at least turn down the number of parallel connections

Michael
  • 419
  • 3
  • 11
  • Thanks Michael. How would adding my own queue solve the problem since SDWebImage does provide an optimized queue. I tried using a serial queue but that was turning out to be too slow. I also tried playing around with the number of images being loaded but that was not helping – Rajeev Bhatia Apr 01 '16 at 10:52
1

I solved an analogous problem on the App I'm developing. On my App there are thousands of users, and each one may have an avatar.

To improve performance I developed a 3 levels controller to manage the avatar request.

At first I check the NSCache (all the most recently used avatars I save to NSCache - which is handled by iOS so I do not have to care about releasing cache memory whenever needed). You just need to define a couple of parameters to configure NSCache.

If the avatar is not found on NSCache, I check on my local Db (Sqlite), where the most recently used avatars are saved as Blob fields. Just remember that sometimes you will need to perform a clean up on local Db, releasing the old data to save the device memory.

If still not found, I save the request on a queue using LIFO method (last in first out). It means that whenever the local user is scrolling the user list, the current users' avatars on the screen should be obtained before the ones that have already been out of the table view screen.

Once again, to increase again the performance (decide it on your choice), while the user is fast scrolling the screen I do not request any download. I just start requesting downloads whenever the user slows down the table view scroll. Beyond this, I limit the number of paralel downloads. A new download is started just whenever a download slot opens (on succeed or fail).

These optimizations work very well for me, my App is running light.

I hope it may help you.

Jorg B Jorge
  • 1,109
  • 9
  • 17
  • Thanks Jorg. My solution seems to be a two level solution. The SDImageCache is an alternate to the NSCache and I have also implemented a queue as the second level. How does adding a SQLite layer here improve performance? – Rajeev Bhatia Apr 11 '16 at 05:20
  • Hi Rajeev. I've added SQLite for cases like rebooting device, restarting App or even whether the iOS released it from NSCache. On that cases, without SQLite the App should download the missed avatars again from web services / AWS. To avoid this, if the avatar is already on local db (SQLite) the App will not need to download it again. It will just get the avatar from local db, save on NSCache and return the image to App. – Jorg B Jorge Apr 11 '16 at 20:49