I have a basic implementation for iCarousel where I am pulling images from a core-data store. I am experiencing memory related crashes when you get a lot of images loaded, as I guess is expected. The problem is I don't know of any way to use the AsyncImageView library since there is no NSURL reverence for a core-data object. Anyone know another way to manage this memory issue?
Here is my iCarousel viewForItemAtIndex
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
{
JournalEntry *journalEntry = (JournalEntry *)[fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
if (view == nil)
{
view = [[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 240.0f, 240.0f)] autorelease];
view.contentMode = UIViewContentModeScaleAspectFit;
}
[((UIImageView *)view) setImage:[journalEntry.image valueForKey:#"image"]];
NSLog(#"%i", index);
return view;
}
For reference here is code for adding images to my core-data store straight out of the iPhoneCoreDataRecipes sample code.
#implementation ImageToDataTransformer
+ (BOOL)allowsReverseTransformation {
return YES;
}
+ (Class)transformedValueClass {
return [NSData class];
}
- (id)transformedValue:(id)value {
NSData *data = UIImagePNGRepresentation(value);
return data;
}
- (id)reverseTransformedValue:(id)value {
UIImage *uiImage = [[UIImage alloc] initWithData:value];
return [uiImage autorelease];
}
#end
After studying this for a while it turns out the large memory growth is due to the coredata cache and not iCarousel. This can be cleared by calling reset on the managedObjectContext.
Related
I have implemented iCarousel for my Banner View on iPad with this relevant codes:
- (NSInteger)numberOfItemsInCarousel:(__unused iCarousel *)carousel
{
return (NSInteger)[self.bannerDatas count];
}
- (UIView *)carousel:(__unused iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
if (view == nil)
{
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 215.0f, 155.0f)];
((UIImageView *)view).image = [UIImage imageNamed:[self.netImages objectAtIndex:index]];
view.contentMode = UIViewContentModeCenter;
}
return view;
}
But the problem lies here at this line:
((UIImageView *)view).image = [UIImage imageNamed:[self.netImages objectAtIndex:index]];
netImages need to be image's name in order to get this works but my netImages contain array of images' URLs. So my question is how to use image URL instead of name for Carousel?
Here below is how netImages get data:
- (NSArray *)setUpNetImages {
self.netImages = [NSMutableArray new];
for (BannerData *data in self.bannerDatas) {
if (data.imageUrl != nil) {
[self.netImages addObject:data.imageUrl]; //original
}
}
return self.netImages;
}
Please Use SDwebImage that provide you functionality to download your image in Asynchronous orderer from url and update your image once downloading done.and your UI never get halt.
((UIImageView *)view)sd_setImageWithURL:[NSURL URLWithString:[self.netImages objectAtIndex:index]]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]]
You just need to add it and import
#import <SDWebImage/UIImageView+WebCache.h>
I am using iCarousel custom control to show image from web that consumed with JSON data.
Here is my codes to show image in iCarousel
to Load JSON Data in ViewDidLoad
JSONLoader *jsonLoader = [[JSONLoader alloc]init];
self.items = [[NSMutableArray alloc]init];
[self.items removeAllObjects];
self.items = (NSMutableArray *) [jsonLoader loadJSONDataFromURL:[NSURL URLWithString:#"https://public-api.wordpress.com/rest/v1/sites/www.myWebsite.com/posts?category=blog&page=1"]];
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(AsyncImageView *)view
{
MMSPLoader *mmObject = [self.items objectAtIndex:index];
view = [[AsyncImageView alloc]initWithFrame:CGRectMake(0, 0, 250.0f, 250.0f)];
view.layer.borderColor = [UIColor whiteColor].CGColor;
view.layer.borderWidth = 0.3f;
view.image=[UIImage imageNamed:#"page.png"];
view.imageURL = [NSURL URLWithString:[mmObject featureImageUrl]];
return view;
}
That can show image correctly. My case is when i tap on that image , i want to show that image in FULL SCREEN. So i used GGFullScreenImageViewController.
However when i tap on that Image to show FULL SCREEN , i retrieved Image URL and show in GGFullScreenImageViewController. It's fine but , i don't want to retrieve from that URL because it downloading image from web again and slowing to show.
In my idea , i saved that image when tap on image in iCarousel and show it in GGFullScreenImageViewController.
So i don't need to download image again.
- (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
dispatch_queue_t myqueue = dispatch_queue_create("com.i.longrunningfunctionMain", NULL);
dispatch_async(myqueue, ^{
UIApplication *apps = [UIApplication sharedApplication];
apps.networkActivityIndicatorVisible = YES;
MMLoader *mmObject = [self.items objectAtIndex:index];
NSData *data = [[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:mmObject.featureImageUrl]];
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageWithData:data]];
GGFullscreenImageViewController *vc = [[GGFullscreenImageViewController alloc] init];
vc.liftedImageView = imageView;
dispatch_async(dispatch_get_main_queue(), ^{
apps.networkActivityIndicatorVisible = NO;
[self presentViewController:vc animated:YES completion:nil];
});
});
NSLog(#"%i",index);
}
So should i save to local file or is there any others nice idea?
Really you should use a library to save the image when you initially download it. AsyncImageView isn't necessarily the best choice as it just caches in memory.
That said, at the moment you can just get the image from the view. This isn't ideal, and you should save it to disk - just sooner rather than later. Look at, perhaps, SDWebImage for that.
To get the image from the view (typed in browser so verify syntax and API usage...):
- (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
AsyncImageView *view = (AsyncImageView *)[carousel itemViewAtIndex:index];
UIImageView *imageView = [[UIImageView alloc] initWithImage:view.image];
GGFullscreenImageViewController *vc = [[GGFullscreenImageViewController alloc] init];
vc.liftedImageView = imageView;
[self presentViewController:vc animated:YES completion:nil];
}
I'm using Nick Lockwood's iCarousel for an iPad app. Right now it's just a carousel of 15 full screen images.
I setup a single view project, add the iCarousel view in storyboard, add it's view controller as a data source and put this code for the datasource methods:
- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel {
return 15;
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view {
UIImage* img = [UIImage imageNamed:[NSString stringWithFormat:#"image%d.jpg", index]];
UIImageView* imgView = [[UIImageView alloc] initWithImage:img];
return imgView;
}
This works, but the first time I scroll through all the items you can notice a little performance hit when a new item is being added to the carousel. This does not happen the second time I go through all the items.
You can see what I mean in this profiler screenshot. The peaks in the first half are for the first time I scroll through all the images, then I do it again and there are no peaks and no performance hit.
How can I fix this?
EDIT
I isolated one of the peaks on instruments and this is the call tree
EDIT
The code with jcesar suggestion
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray* urls_aux = [[NSMutableArray alloc] init];
for (int i = 0; i<15; i++) {
NSString *urlPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"captura%d", i] ofType:#"jpg"];
NSURL *url = [NSURL fileURLWithPath:urlPath];
[urls_aux addObject:url];
}
self.urls = urls_aux.copy;
self.carousel.dataSource = self;
self.carousel.type = iCarouselTypeLinear;
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view {
if (view == nil) {
view = [[[AsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 1024, 768)] autorelease];
view.contentMode = UIViewContentModeScaleAspectFit;
}
[[AsyncImageLoader sharedLoader] cancelLoadingImagesForTarget:view];
((AsyncImageView *)view).imageURL = [self.urls objectAtIndex:index];
return view;
}
I don't think this is a very orthodox solution, but it works.
What I do is use iCarousel's method insertItemAtIndex:animated: to add the images one at a time on setup. Performance gets hit then, but after that everything runs smoothly.
- (void)viewDidLoad
{
[super viewDidLoad];
self.images = [[NSMutableArray alloc] init];
self.carousel.dataSource = self;
self.carousel.type = iCarouselTypeLinear;
for (int i = 0; i<15; i++) {
UIImage* img = [UIImage imageNamed:[NSString stringWithFormat:#"captura%d.jpg", i]];
[self.images addObject:img];
[self.carousel insertItemAtIndex:i animated:NO];
}
}
- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel {
return self.images.count;
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view {
UIImage* img = [self.images objectAtIndex:index];
UIImageView* imgView = [[UIImageView alloc] initWithImage:img];
return imgView;
}
It's hard to be certain, but I'd guess that the first time through it's loading the images from the file; whereas the second time it's using the cached copies - that's something that +[UIImage imageNamed:] does automatically for you; without it, you'd see the performance hit every time. You could load the images earlier, either in your app delegate or in the initialiser for your controller.
Another thing you could do would be to make use of iCarousel's view reuse logic. I don't think it's responsible for your problem, but you may as well do it now:
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view {
UIImage* img = [UIImage imageNamed:[NSString stringWithFormat:#"image%d.jpg", index]];
UIImageView* imgView = nil;
if ([view isKindOfClass:[UIImageView class]])
{
imgView = (UIImageView *)view;
imgView.image = img;
[imgView sizeToFit];
}
else
{
[[UIImageView alloc] initWithImage:img];
}
return imgView;
}
UPDATE: It would be great to get a bit more information on where your app is spending its time. If you use the Instruments Time Profiler you can get a breakdown of what percentage of the time is spent in each method.
Look at his example: Dynamic Downloads. He uses AsyncImageView instead UIImageView, that loads the images in an async way. So it won't try to load all the 15 images at the same time, and I think it shouldn't have performance issues
I have a system which loads alot of large images from the web and displays them in custom table cells. On older devices the memory warnings happen pretty quickly so I implemented a system of deleting some from the table to try to combat this but it didn't work well enough (lots of images were deleted affecting the UI).
So I thought I could load all the images into the device's cache and then load them from there - I've implemented SDWebImage. This is great but I still havent solved the problem of memory allocation as the images are still being displayed all the time and therefore kept in memory - causing crashes.
I think I need to implement a system which shows the images (from the cache) if the cell is being displayed and hide it if the cell is not showing - I'm just stuck at how to build such a system.
Or is this not going to work? Can you really keep the apps memory low (and stop it having memory warnings / crashing) by removing images from its table cells? Or do I just need to carry on with my earlier solution and just delete images/cells until the memory warnings stop?
Updated with code
TableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0)
{
currentIndexPath = indexPath;
ImageTableCell *cell = (ImageTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
ImageDownloader *download = [totalDownloads objectAtIndex:[indexPath row]];
if (cell == nil)
{
cell = [[[ImageTableCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: CellIdentifier] autorelease];
}
cell.imageView.image = download.image;
return cell;
}
return nil;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
int t = [totalDownloads count];
return t;
}
ImageTableCell.m - Custom cell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
self.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.contentView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 0.0f);
self.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentMode = UIViewContentModeScaleToFill;
self.autoresizesSubviews = YES;
self.contentView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.contentView.contentMode = UIViewContentModeScaleToFill;
self.contentView.autoresizesSubviews = YES;
[self.imageView drawRect:CGRectMake(0.0f, 0.0f, 320.0f, 0.0f)];
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
self.imageView.autoresizingMask = (UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth);
self.imageView.opaque = YES;
}
return self;
}
ImageDownloader (implements SDWebImageManagerDelegate)
-(void) downloadImage // Comes from Model class
{
if (image == nil)
{
NSURL *url = [NSURL URLWithString:self.urlString];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[manager cancelForDelegate:self];
if (url)
{
[manager downloadWithURL:url delegate:self retryFailed:YES];
}
}
}
- (void)cancelCurrentImageLoad
{
[[SDWebImageManager sharedManager] cancelForDelegate:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)_image
{
self.image = _image;
if ([self.delegate respondsToSelector:#selector(addImageToModel:)]) [self.delegate addImageToModel:self];
}
- (void)webImageManager:(SDWebImageManager *)imageManager didFailWithError:(NSError *)error;
{
if ([self.delegate respondsToSelector:#selector(badImage)]) [self.delegate badImage];
}
After you download the images, dont keep the large images in memory. just create a small size of image(thumbnail) to display in the tableview and write the larger image to some directory.
you can create a thumbnail of your image using the following code.
CGSize size = CGSizeMake(32, 32);
UIGraphicsBeginImageContext(size);
[yourImage drawInRect:CGRectMake(0, 0, 32, 32)];
yourImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Instead of using [UIImage imageNamed:#""] , try [[[UIImage alloc] initWithContentsOfFile:#""] autorelease];
Edit:
Fine. I have gone through the SDWebImage.
Use NSAutoreleasePool wherever you find that a new thread has been spawned.
And one more solution would be, resize the image before saving to cache.
So basically once the image is downloaded it stays in it's SDWebImage instance and never gets released. You should save your image to iPhone's disk with:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *imagePath = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:#"image%d.jpg", rowNumber]; // you should have this set somewhere
[UIImageJPEGRepresentation(_image, 1.0) writeToFile:imagePath atomically:YES];
and keep just the path to it in your SDWebImage instance. Then in -cellForRowAtIndexPath: method instead of doing:
cell.imageView.image = download.image;
you should do something like:
UIImage *image = [[UIImage alloc] initwithContentsOfFile:download.imagePath];
cell.imageView.image = image;
[image release];
This will always load the image from the disk and since cell.imageView.image is a retained property, once it get's niled or reused, it will clean up the image from memory.
I would like to know how you are loading the images, are you using custom cells? If so please go through the Apple's UITableView Programming guide They are clearly saying us how to load the images. In that they are saying we should need to draw the images top avoid the memory issues.
Best example on how to load images are given in Apple's Sample Code LazyTableImages Please go through this too.
I have used kingfisher SDK and resized the server image to my custom cell size. It helps me a lot.
extension NewsCollectionViewCell {
func configure(with news: Articles) {
KF.url(URL(string:news?.urlToImage ?? ""), cacheKey: "\(news?.urlToImage ?? "")-").downsampling(size: imgNews.frame.size).cacheOriginalImage().set(to: imgNews)
}
}
I've long been trying to make an image viewer, but really I do not what does not work.
Here's a picture like you should get!
This is UIScrollView. The UIImage add to UIScrollView. When user scroll - image must download to UIImageView. I think we need download image in a separate thread. For this I am using - NSOperationqueue and NSInvocationOperation.
This is my code.
In delegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
MyThumbnailsBook * myThumbnailsBook = [[MyThumbnailsBook alloc] initWithFrame:CGRectMake(0, 20, 768, 1004)];
[myThumbnailsBook createThumbnails];
[myThumbnailsBook setContentSize:CGSizeMake(768, 1004 * 10)];
[myThumbnailsBook setBackgroundColor:[UIColor blackColor]];
[self.window addSubview:myThumbnailsBook];
[self.window makeKeyAndVisible];
return YES;
}
In class inherit UIScrollView MyThumbnailsBook.m
#define COUNT 100
#import "MyThumbnailsBook.h"
#import "MyImageDownload.h"
#implementation MyThumbnailsBook
#pragma mark -
#pragma mark Initialization & Create Thumbnails
- (id)initWithFrame:(CGRect)frame{
if((self = [super initWithFrame:frame])){
self.delegate = self;
}
return self;
}
- (void)createThumbnails{
float point_x = 20;
float point_y = 20;
for(NSInteger i = 0; i < COUNT; i++){
if(i%3==0 && i != 0){
point_x = 20;
point_y += 220;
}
// Create new image view.
NSURL * url = [[NSURL alloc] initWithString:#"http://storage.casinotv.com/videos/SOYCL/pages/P68.jpg"];
MyImageDownload * imageDownload = [[MyImageDownload alloc] initWithImageURL:url];
[imageDownload setFrame:CGRectMake(point_x, point_y, 200, 200)];
[self addSubview:imageDownload];
[imageDownload release];
[url release];
point_x += 220;
}
}
#pragma mark -
#pragma mark Scroll View Protocol
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
// If scroll view no decelerating.
if(!decelerate){
[self asyncImageDownload];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
[self asyncImageDownload];
}
#pragma mark -
#pragma mark Download Image
// If scroll stopped - download image to thumbnails.
- (void)asyncImageDownload{
}
#end
In class inherit UIScrollView MyImageDownload.m
#import "MyImageDownload.h"
#implementation MyImageDownload
// This is init method. When class init - we add new operation for download image.
- (id)initWithImageURL:(NSURL*)url{
if((self == [super init])){
[self setBackgroundColor:[UIColor yellowColor]];
NSArray * params = [NSArray arrayWithObjects:url, nil];
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(asyncDownload:) object:params];
[queue addOperation:operation];
[params release];
}
return self;
}
// Download image to data and after to self (UIImageView).
- (void)asyncDownload:(NSArray *)params{
NSData * data = [NSData dataWithContentsOfURL:[params objectAtIndex:0]]; // Get image data with url.
if(data) [self setImage:[UIImage imageWithData:data]];
[data release];
}
#end
In this example, the images are not loaded and I do not understand why?
I have always used these classes in which the images were loaded from the internet, but I also had problems with memory and I do not know too how to solve it. For example, when to scroll images I get exception - Received memory warning. Level=1 & 2 and after app crash. And I have no idea what to do. I understand what you need as a separate download of images over time and to remove non visible objects but I have found I need the algorithm.
For example - when i go to scrollDidScroll method i get all object when i not see them and remove image:
NSArray *views = [self subviews];
for (UIImageView *v in views) {
if(v.frame.origin.y >= self.contentOffset.y && v.frame.origin.y <= self.contentOffset.y + self.frame.size.height){
// If image of imageview is equal nil - call new operation
if(v.image == nil){
[self requestImageForIndexPath:[NSNumber numberWithInt:v.tag]];
}
else
{
v.image = nil;
}
}
}
self - this is class inherit UIScrollView.
I was confused and ask for help in resolving this issue. TNX all!
The Three20 library includes this functionality in the form of TTPhotoViewController.