Skip to content
DreamPiggy edited this page Aug 19, 2022 · 18 revisions

Using dynamic image size with UITableViewCell

UITableView determines the size of the image by the first image set for a cell. If your remote images don't have the same size as your placeholder image, you may experience strange anamorphic scaling issue. The following article gives a way to workaround this issue:

http://www.wrichards.com/blog/2011/11/sdwebimage-fixed-width-cell-images/

Optimization for large images

Configuration to reduce memory pressure (aggressive)

You may not need all these configurations, pick what you need base on the use case.

  • Objective-C
SDImageCache.sharedImageCache.config.maxDiskAge = 3600 * 24 * 7; // 1 Week 
SDImageCache.sharedImageCache.config.maxMemoryCost = 1024 * 1024 * 4 * 20; // 20 images (1024 * 1024 pixels)
SDImageCache.sharedImageCache.config.shouldCacheImagesInMemory = NO; // Disable memory cache, may cause cell-reusing flash because disk query is async
SDImageCache.sharedImageCache.config.shouldUseWeakMemoryCache = NO; // Disable weak cache, may see blank when return from background because memory cache is purged under pressure
SDImageCache.sharedImageCache.config.diskCacheReadingOptions = NSDataReadingMappedIfSafe; // Use mmap for disk cache query
SDWebImageManager.sharedManager.optionsProcessor = [SDWebImageOptionsProcessor optionsProcessorWithBlock:^SDWebImageOptionsResult * _Nullable(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
     // Disable Force Decoding in global, may reduce the frame rate
     options |= SDWebImageAvoidDecodeImage;
     return [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
}];
  • Swift
SDImageCache.shared.config.maxDiskAge = 3600 * 24 * 7 // 1 Week
SDImageCache.shared.config.maxMemoryCost = 1024 * 1024 * 4 * 20 // 20 images (1024 * 1024 pixels)
SDImageCache.shared.config.shouldCacheImagesInMemory = false // Disable memory cache, may cause cell-reusing flash because disk query is async
SDImageCache.shared.config.shouldUseWeakMemoryCache = false // Disable weak cache, may see blank when return from background because memory cache is purged under pressure
SDImageCache.shared.config.diskCacheReadingOptions = .mappedIfSafe // Use mmap for disk cache query
SDWebImageManager.shared.optionsProcessor = SDWebImageOptionsProcessor() { url, options, context in
    // Disable Force Decoding in global, may reduce the frame rate
    var mutableOptions = options
    mutableOptions.insert(.avoidDecodeImage)
    return SDWebImageOptionsResult(options: mutableOptions, context: context)
}

Reduce image memory usage by resolution and channel

For bitmap image, the memory usage is simple to follows the formula:

RAM = pixel count * bytes per pixel (4 for RGBA)

So, try to reduce both resolution and channel as much as you can.

  • Channel

    1. Use non-alpha channel image if possible (25% reduce)
    2. Use greyscale image if possible (75% reduce)
  • Resolution

    1. Best: Optimize image resolution on server, before sending to client
    2. Use SDWebImageScaleDownLargeImages option
    3. Use Resize Image Transformer to scale down
    4. Use .imageThumbnailPixelSize context option to use Thumbnail Decoding, which does not allocate original image buffer size, but only with your thumbnail size. For example, if you want to load an image which have 10000*10000 pixels, use thumbnail at 1000*1000 pixels can save RAM from 381 MB to 3.81 MB.

Use SDAnimatedImageView to play animated image

SDAnimatedImageView provide the Decoding Just in Time feature for animated image. It also provides a flexible buffer size and based on current CPU/RAM pressure.

Compared to UIImageView (which keep all frames in memory), it can help you to reduce the memory usage. Especially you have large and long frames animated images.

For all these details see #1544

Handle cell reusing for view category

The View Category including UIImage+WebCache, UIView+WebCache or some other categories supports cell reusing from the scratch design. Actually, for most of use cases. All you need to do to work with cell reusing in UITableView, UICollectionView on iOS (NSTableView, NSCollectionViewon macOS) is quite simple, just use:

  • Objective-C:
[cell.imageView sd_setImageWithURL:url placeholderImage:[UIImage imageNamed:@"placeholder"]];
  • Swift:
cell.imageView?.sd_setImage(with: url, placeholderImage: UIImage(named: “placeholder"))

However, there are some options to control the detailed behavior using for View Category such as SDWebImageDelayPlaceholder, SDWebImageAvoidAutoSetImage. You can try these options if the default behavior cause some issues with your use cases.

Detailed behavior when using view category
  1. When you call sd_setImageWithURL:, whatever you provide a valid url or nil, we will firstly set the placeholder(Or nil, which means will clear the current rendered image) to the current imageView's image before any cache or network request. Then the placeholder argument is totally unused, unless you specify SDWebImageDelayPlaceholder.

  2. If the image fetched in memory cache, the callback from manager is synchronously and is dispatched to the main queue if need(If you already call sd_setImageWithURL: in the main queue, there is no queue disaptch and just like a normal function call). So it will immediately set the result image to override the placeholder. In the set image process, we will call setNeedsLayout to mark the view it's ready for rendering. UIKit draw all the views during the same runloop(No asynchronous or queue dispatch) depended on view's first state and last state but not the intermediate state. The state we set the placeholder(or nil) at the beginning will be totally ignored and not even rendering to cause a flashing. If you do not understand this behavior, see Understanding UIKit Rendering

  3. If the image fetched in disk cache, the callback is asynchronously and dispatched from the cache queue to the main queue. So the placeholder (or nil) will be rendered with a little short time(depend on the IO speed and decoding time. This may looks like a flashing). And then replaced by the final image. (Note if you really do not want any flashing, try Image Transition to reduce this visual effect. Or using SDWebImageQueryDiskSync, which may decrease the frame rate)

  4. If the image fetched from network, the callback is asynchronously and dispatched from the downloader session queue to the main queue. So the placeholder (or nil) will be rendered with a long time(depend on the network speed and decoding time). And then replcaed by the final image. This behavior is nearlly the same as disk cache except the duration for placeholder rendering.(network speed is far more slower than disk cache)

  5. If you use SDWebImageDelayPlaceholder, we will use the placeholder as target image after the fetch finished if the image is nil.

Handle image refresh

SDWebImage does very aggressive caching by default. It ignores all kind of caching control header returned by the HTTP server and cache the returned images with no time restriction. It implies your images URLs are static URLs pointing to images that never change. If the pointed image happen to change, some parts of the URL should change accordingly.

If you don't control the image server you're using, you may not be able to change the URL when its content is updated. This is the case for Facebook avatar URLs for instance. In such case, you may use the SDWebImageRefreshCached flag. This will slightly degrade the performance but will respect the HTTP caching control headers:

  • Objective-C
[imageView sd_setImageWithURL:[NSURL URLWithString:@"https://graph.facebook.com/olivier.poitrey/picture"]
             placeholderImage:[UIImage imageNamed:@"avatar-placeholder.png"]
                      options:SDWebImageRefreshCached];
  • Swift
imageView.sd_setImage(with: URL(string: "https://graph.facebook.com/olivier.poitrey/picture"), placeholderImage: UIImage(named: "avatar-placeholder.png"), options: .refreshCached)

Add a progress indicator

Add these before you call sd_setImageWithURL

  • Objective-C
[imageView sd_setShowActivityIndicatorView:YES];
[imageView sd_setIndicatorStyle:UIActivityIndicatorViewStyleGray];
  • Swift
imageView.sd_setShowActivityIndicatorView(true)
imageView.sd_setIndicatorStyle(.Gray)

Handle self capture in completion block

When you try to use sd_setImageWithURL:completed and write some code in the completion block which contains self. You should take care that this may cause Strong Reference Cycles for Closures . This because the completion block you provided will be retained by SDWebImageManager's running operations. So the self in the completionBlock will also be captured until the load progress finished (Cache fetched or network finished).

If you do not want to keep self instance alive during load progress, just mark it to weak.

  • For Objective-C, use a weak reference to self:
__weak typeof(self) wself = self;
[self.imageView sd_setImageWithURL:url completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
    wself.imageView.image = image;
}];
  • For Swift, use weak self in capture lists:
self.imageView.sd_setImage(with: url, completed: { [weak self] (image, error, cacheType, imageURL) in
    self?.imageView.image = image
})

Behavior about async/sync control for cache

SDWebImage supports to load images into your view site, but however, UIKit have the behavior to only render the last state in the same runloop. See more about this behavior: Understanding UIKit Rendering

Because of this, there may be something complicated use case when query synchronously or asynchronously. SDWebImage provide 3 options can control all the behavior you need, though it's a little confused. Here I list the result by combining these 3 options. More clear.

  • SDWebImageQueryMemoryData: means we want image data, but not care about it's async or sync
  • SDWebImageQueryMemoryDataSync: means we want image data, and it must be sync when data from memory, must be combined with SDWebImageQueryMemoryData
  • SDWebImageQueryDiskDataSync: means we want image data, and it must be sync when data from disk
Options Memory hit data Memory miss data
0 nil async
QueryMemoryDataSync (invalid) nil async
QueryDiskDataSync nil sync
QueryMemoryData async async
QueryMemoryData + QueryMemoryDataSync sync async
QueryMemoryData + QueryDiskDataSync async sync
QueryMemoryData + QueryMemoryDataSync + QueryDiskDataSync sync sync

Retrieve HTTP response/metrics from Top Level API

In general, SDWebImage's loading system, do not care whether the image is downloaded from network (via SDWebImageDownloader), or even from extra resource like Photos (via SDImagePhotosLoader), Firebase (via FUIStorageImageLoader). So there is no something like NSURLResponse or NSURLSessionTaskMetrics inside the current 5.x completion block. (Note this design may be changed in the future)

But some users need to grab the underlaying information of HTTP response, or monitor the HTTP loading metrics. Here we provide one possible solution for this task:

Firstly, you need to get the SDWebImageCombinedOperation. You can do this via operation key, or keep the return value from View Category API.

Next, there is one loaderOperation property. If you use SDWebImageDownloader as loader to load HTTP URL, then you can cast that into the actual SDWebImageDownloadToken class. (For other loader like Firebase, check their API documentation)

Finally you can just grab the response and metrics.

  • Objective-C
[imageView sd_setImageWithURL:url
                    completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
    // Grab the combined operation
    SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey];
    if (![operation isKindOfClass:[SDWebImageCombinedOperation class]]) {
        return;
    }
    // Cast to actual class
    SDWebImageDownloadToken *token = operation.loaderOperation;
    if (![token isKindOfClass:[SDWebImageDownloadToken class]]) {
        return;
    }
    // Use the response and metrics
    NSURLResponse *response = token.response;
    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSLog(@"Response Headers: %@", ((NSHTTPURLResponse *)response).allHeaderFields);
        NSLog(@"Response Status: %ld", (long)((NSHTTPURLResponse *)response).statusCode);
    }
    if (@available(iOS 10.0, *)) {
        NSURLSessionTaskMetrics *metrics = token.metrics;
        if (metrics) {
            NSLog(@"Metrics: %@ download in (%f) seconds", imageURL.absoluteString , metrics.taskInterval.duration);
        }
    }
}];
  • Swift
imageView.sd_setImage(with: url) { (image, error, _, _) in
    // Grab the combined operation
    guard let operation = imageView.sd_imageLoadOperation(forKey: imageView.sd_latestOperationKey) as? SDWebImageCombinedOperation else {
        return
    }
    // Cast to actual class
    guard let token = operation.loaderOperation as? SDWebImageDownloadToken else {
        return
    }
    // Use the response and metrics
    if let response = token.response as? HTTPURLResponse {
        print("Response Headers: \(response.allHeaderFields)")
        print("Response Status: \(response.statusCode)")
    }
    if #available(iOS 10.0, *) {
        if let metrics = token.metrics {
            print("Metrics: \(url.absoluteString) download in (\(metrics.taskInterval.duration)) seconds")
        }
    }
}