I am using a tableview which loads images from the documents directory, creates a thumbnail and shows it in the tableview. However, I have a problem: it becomes slow and crashes as the pictures are large, taken using the camera.
I have explored several solution including GCD to do the work in a background thread but the result is the same thing. So, I thought to look into SDWebImage but I don't know if it will also work for local files, not web images in this case. Can someone advise me please? If not, how is this problem solved? Is there an API that can help to resolve this issue?
That question is not easy to answer as the Question is asked fairly broad but I will do my best.
First, I usually dispatch a Background Thread if I have expensive processing to do as to not block the Main Thread, which is fairly important.
I don't really know why you are not using the normal UIImageView for what you are doing but try to implement following :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"YourCell";
MyCellClass *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[MyCellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
/*
Whatever Code you want
*/
NSArray* params =#[cell.myImageView, #"http://myfancyimages.com/image.png"];
[self performSelectorInBackground:#selector(loadEventImageWithParameters:) withObject:params];
return cell;
}
And now add the function :
- (void) loadEventImageWithParameters:(id) parameters {
NSArray* params = [[NSArray alloc] initWithArray:(NSArray*)parameters];
NSURL *url = [NSURL URLWithString:[params objectAtIndex:0]];
UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:url]];
UIImageView* theImageView = (UIImageView*) [params objectAtIndex:0];
[theImageView setImage:image];
}
If you got a lot of Pictures to load you are well advised to queue your Processes so you don;t "steal" all resources with Grand Central Dispatch.
Have a read through this excellent post http://www.raywenderlich.com/4295/multithreading-and-grand-central-dispatch-on-ios-for-beginners-tutorial for further details.
Hope that helped
Related
A tablview cell is being setup using the following code...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableviewcell *cell = [tableView dequeueReusableCellWithIdentifier:#"mytableviewcell" forIndexPath:indexPath];
cell.titleLabel.text = [self.data[indexPath.row] objectForKey:#"node_title"];
cell.taxonomy1Label.text = [self.data[indexPath.row] objectForKey:#"group"];
#try {
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.data[indexPath.row] objectForKey:#"image"]]];
cell.thumbnailImageView.image = [UIImage imageWithData:imageData];
}
#catch (NSException * e) {
NSLog(#"Exception: %#", e);
}
return cell;
}
The try/catch are just because it may or may not have an image but it was happening even before I put that in. It seems like there is some sort of issue when it goes to dequeue the cell. Any ideas?
If you are familiar with using third party libraries or CocoaPods, but this is a common problem, and I recommend the use of https://github.com/rs/SDWebImage or https://github.com/AFNetworking/AFNetworking, which have UIImageView category methods to handle loading images from a URL in the background, and not in a main thread.
For example using SDWebImage:
[cell.thumbnailImageView sd_setImageWithURL:yourImageURL];
This method will fetch the image in the background, not blocking the main thread and will not make your UITableView jitter.
As already mentioned this call blocks the main thread:
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.data[indexPath.row] objectForKey:#"image"]]];
What wasn't already mentioned is the right way to handle this kind of work.
You can't just wrap the above in a GCD call and expect everything to be OK.
You need to lazy load the images and populate them on the tableView when appropriate.
The most elegant way to handle this is to write your own NSOperation.
Here is a tutorial, any further questions let me know.
You are obstructing the main thread by trying to fetch image. Put your image call in a separate thread.
I have a uitableview where I use a custom cell. However, when I scroll the table view there is some serious lag. It happens when I set the UIImaveView's image property with an image. I am accessing an image from the directory. But since file IO is slow I am using the dispatch_async to load the image into a UIImage object on a separate thread.
However there is still some lag. When I scroll up and down the rows without any images, the scrolling is very smooth. However when the row actually has an image, there is lag. the momentum scrolling will halt, then the app becomes unresponsive, then when the image finally loads the momentum continues where it left off.
I am not sure what is causing the lag. At first I thought it had to do with the image being too large so i tried scaling it down. Still lags. Again, if I don't set the image in the custom cell there is no lag. But when I do set it there is lag. I am not sure how to fix this.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
DHTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReuseIdentifierGoalCell forIndexPath:indexPath];
[self configureCell:cell forIndexPath:indexPath isForOffscreenUse:NO];
return cell;
}
- (void)configureCell:(DHTableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath isForOffscreenUse:(BOOL)offscreenUse {
if (cell == nil) {
return;
}
[cell setDelegate:self];
PATH_TO_FILE = SQLITE_QUERY_TO_GET_PATH; //some pseudo codes
__weak typeof(sSelf)wSelf = sSelf;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__strong typeof(wSelf)sSelf = wSelf;
UIImage *unscaled_image = [UIImage imageWithContentsOfFile:PATH_TO_FILE];
UIImage *image = [unscaled_image imageScaledToFitInSize:kCellUIImageSize];
__weak typeof(sSelf)wSelf = sSelf;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(wSelf)sSelf = wSelf;
DHTableViewCell *cell = (id)[sSelf.tableView cellForRowAtIndexPath:indexPath];
if (cell) {
[cell.imageStored setImage:image]; //Commenting this out relieves all lag
}
});
});
}
#Calimari328 I hate if to put this as an answer because it does not really answer your question but the truth is what you should really do is use a library to achieve this. for example SDWebImage
Example:
#import <SDWebImage/UIImageView+WebCache.h>
...
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
You have to rethink this, cells get reused, that means each time you scroll your app will try to download images again.What about performance? memory ? cache ? processing?
As you think more about it is a lot more complex then a simple async task. Fortunately there are opensource projects to achieve this. No need to reinvent the wheel.
Please note I am not advertising any library if you want to write your own code you can do this as well. You can also search on the web for easy ones to implement.
Currently I am loading all of my UITableViewControllers with images and text. I'm not sure if there is a way of shortening my loading times. I'm thinking that GCD might be the best route to go, however, I'm not too sure that I'm using this correctly:
dispatch_async(dispatch_get_main_queue(), ^{
[some method];
});
This is being loaded in the ViewDidLoad, and I'm unsure if this is the correct place to use GCD. Also, is this correct way of asynchronously loading information?
use this, it downloads multiple files in different threads.And its asynchronous.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//load image here
});
If you want to load all images ,even the images which might be very down in tableView (say image at 20th row).Then viewDidLoad is perfect approach, or if you want to only load images that are currently to be shown, then download images in cellForRow:atIndexPath method.
OR,you can use AFNetworking to download multiple files much efficiently.
Hope this helps
For loading images faster in table view cells you can use SDWebImage.
It is very simple to use. Just import the SDWebImage folder into your Xcode project. It contains a category on UIImageVIew. So on any imageView object just call the method setImageWithURL as illustrated below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
cell.textLabel.text = #"My Text";
return cell;
}
So I've been searching for an answer to this and still am unable to figure it out. I have an array that contains 15 images or so I'm trying to display using a subview in a UITableViewCell. Code below - everything I've read mentions using autorelease/release to fix the issue, but I simply get ARC errors when attempting to do that. Any help would be much appreciated.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
int countthis = indexPath.row;
NSString *image = imgarr[countthis];
UIImageView *iv = [[UIImageView alloc] initWithFrame:(CGRect){.size={320, tableView.rowHeight}}];
iv.image = [UIImage imageNamed:image];
cell.imageView.image = iv.image;
return cell;
}
Large files tend to cause problems, no matter what your domain. Specifically, Apple says:
You should avoid creating UIImage objects that are greater than 1024 x 1024 in size.
It looks like you're trying to resize the image, but UIImages are immutable. So your allocated UIImageView serves only to waste processor cycles.
If you're stuck with big images that need to be scaled down, consider scaling before you assign them to the cell. You might find these routines useful: The simplest way to resize an UIImage?
Re autorelease/release: those have been deprecated since ARC. Your code doesn't appear to be leaking memory. I wouldn't sweat it. But you should edit your question to include details about the crash.
Your code can be cleaned up to this, which may help slightly with performance. You don't need to convert the indexPath.row to an int, since it's already a NSInteger, which is an architecture dependent type (int for 32-bit, long for 64-bit). You also probably want to use self.imgarr since its probably a property in your class. The image changes are as Neal mentioned.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSString *image = self.imgarr[indexPath.row];
cell.imageView.image = [UIImage imageNamed:image];
return cell;
}
As for autorelease/release, you mentioned you're getting ARC errors using them, which indicates you're on the iOS 5 or higher SDK. They are no longer needed in your code.
You can try to resize images using CGImageSourceCreateThumbnailAtIndexfirst before displaying them in the tableview.
If you have the path to the image you want to resize, you can use this:
- (void)resizeImageAtPath:(NSString *)imagePath {
// Create the image source (from path)
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:imagePath], NULL);
// To create image source from UIImage, use this
// NSData* pngData = UIImagePNGRepresentation(image);
// CGImageSourceRef src = CGImageSourceCreateWithData((CFDataRef)pngData, NULL);
// Create thumbnail options
CFDictionaryRef options = (__bridge CFDictionaryRef) #{
(id) kCGImageSourceCreateThumbnailWithTransform : #YES,
(id) kCGImageSourceCreateThumbnailFromImageAlways : #YES,
(id) kCGImageSourceThumbnailMaxPixelSize : #(640)
};
// Generate the thumbnail
CGImageRef thumbnail = CGImageSourceCreateThumbnailAtIndex(src, 0, options);
CFRelease(src);
// Write the thumbnail at path
CGImageWriteToFile(thumbnail, imagePath);
}
More details here.
Whenever I scroll my tableview it is very laggy. I think it has to do with how I am loading up my cells. I use UINib (5.0+) whenever I can while still providing backwards compatibility. Then I load my custom cell's labels and images with items from a NSDictionary from a NSArray which is loaded from NSUserDefaults in the ViewDidLoad.
Is there any way to improve the efficiency of this cellForRowAtIndexPath?
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomCell *cell = (CustomCell *)[aTableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
if ([self labelCellNib]) {
[[self labelCellNib] instantiateWithOwner:self options:nil];
} else {
[[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
}
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
}
NSDictionary *dictionary = [myArray objectAtIndex:indexPath.row];
NSData *data = [dictionary objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];
cell.titleLabel.text = [dictionary objectForKey:#"Title"];
cell.titleLabel.delegate = self;
cell.dateLabel.text = [dictionary objectForKey:#"Date"];
if (indexPath.row%2) {
cell.backgroundImage.image = firstImage;
}
else {
cell.backgroundImage.image = secondImage;
}
return cell;
}
Edit:
- (UIImage*)roundCorneredImage: (UIImage*)orig radius:(CGFloat) r {
UIGraphicsBeginImageContextWithOptions(orig.size, NO, 0);
[[UIBezierPath bezierPathWithRoundedRect:(CGRect){CGPointZero, orig.size}
cornerRadius:r] addClip];
[orig drawInRect:(CGRect){CGPointZero, orig.size}];
UIImage* result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
Edit2: These are the lines that are causing the lag:
NSData *data = [dictionary objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];
As #Till said in a comment, you should launch your app in Instruments (Product -> Profile in Xcode), and select the CPU -> Time Profiler instrument.
Then, scroll around over the place for a few seconds, then hit the Record toolbar icon in instruments to close your app. You will be able to see the scrolling section because CPU usage will probably be pinned at 100% (unless it's slow because of network activity problem).
Click on the timeline after the start of the high CPU activity area, and click the "start inspection range" toolbar button, then click before the end of the high CPU activity area and click the "stop inspection range" toolbar button.
You can now drill down into the call tree view at the bottom of the window to figure out exactly where all your CPU usage is. In my experience it's usually easier to find the problem if you turn off "invert call tree" option on the left.
Performance bugs can be very hard to find, and sometimes a line of code that is obviously slow actually isn't causing any problems at all. The only way to fix performance issues without wasting time is to use Instruments.
Make sure that you've set the reuse identifier for your cell to the same thing that you've specified in your code, i.e. #"Cell". If they don't match, then you won't be reusing cells properly, and probably spending a lot more time creating cells than necessary.
If you are properly recycling cells, then you should take a look at the code after the if (cell == nil) {...} block. You'll be skipping that entire block once the table has created enough cells to fill the screen (and maybe one or two more), so most of the time attributable to this method while scrolling will be due to the following code. It'd be interesting to know what myArray is, and if it's actually an array, what the objectForKey: method does. Nothing else there looks like it should take a long time, but the best way to find out where the cycles are going is to profile your code in Instruments.
Some of my notes after looking at your code:
Is roundCorneredImage:radius: caching the result? If not, executing CG calls for every cell would surely present a bottleneck. Updated: Use instruments to be sure, but it might be faster (memory allowing) to store the processed UIImage in a collection so that you can pull it out again the next time that method is called with the same parameters.
All of your UIImages could be declared elsewhere and then presented in this method. Your current code instantiates a new UIImage for each cell which can also bottleneck your scrolling. Updated: Since Image1.png and Image2.png are basically static, you could declare them in your interface or as a static ivar and then just assign them to the background image rather than instantiating UIImage each time.
It may be faster to subclass UITableViewCell and instantiate that instead of reaching into UINib. Also, you'd then be able to separate your layout/data logic from the delegate method. Here's a gist of what I did in my UITableViewCell subclass. Basically, I store the entity with the cell and the cell knows about it's labels and such. This keeps the cell layout logic out of my data source code.
It looks like you're using an NSDictionary as your data source. If you have a lot of objects in that dictionary, it may be considerable faster to use CoreData and an NSFetchedResultsController. Here's a good post on the matter. Updated: Ok, that shouldn't be an issue.
-
Edit
So if you removed all of this:
NSDictionary *dictionary = [myArray objectForKey:#"OCRImage"];
cell.previewPicture.image = [self roundCorneredImage:[UIImage imageWithData:data] radius:60];
if (indexPath.row%2) {
cell.backgroundImage.image = firstImage;
}
else {
cell.backgroundImage.image = secondImage;
}
and it still lags, let's look at your constructors...what do these lines do?
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
Also, you're not using any transparent images or anything in your table cell are you? Those have been known to cause drawing lag...
-
Edit #2
If you strip down to this, what happens?
CustomCell *cell = (CustomCell *)[aTableView dequeueReusableCellWithIdentifier:#"Cell"];
if (cell == nil) {
if ([self labelCellNib]) {
[[self labelCellNib] instantiateWithOwner:self options:nil];
} else {
[[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
}
cell = [self CustomTableCell];
[self setCustomTableCell:nil];
}
cell.titleLabel.text = [dictionary objectForKey:#"Title"];