Delay On IPad When Calling From A Background Thread - ios

I am converting my IPhone app to IPad and I'm having an issue with the conversion. I have a background thread that creates some thumbnails.
The problem seems to be that the app loops though the array and outputs all the items but only the last item seems to be loading with the UIButton text present. There is a 5-15 second delay before all the previous buttons have there text displayed.
UIImage *tmp = [self thumbnailImage:[effect objectAtIndex:0] thumbnailType:#"category"];
for (id effect in effectArray) {
UIImage *tmp = [self thumbnailImage:[effect objectAtIndex:0] thumbnailType:#"category"];
dispatch_async(dispatch_get_main_queue(), ^{
UIView *effectView = [[UIView alloc] initWithFrame:CGRectMake(item_x_axis, current_row_height, item_width, item_height)];
UIImageView *imagepreview = [[UIImageView alloc] init];
[imagepreview setFrame:CGRectMake(20, 20, 100, 100)];
[imagepreview setImage:tmp];
imagepreview.contentMode = UIViewContentModeScaleAspectFit;
imagepreview.layer.shadowColor = [UIColor blackColor].CGColor;
imagepreview.layer.shadowOffset = CGSizeMake(0, 1);
imagepreview.layer.shadowOpacity = 1;
imagepreview.layer.shadowRadius = 2.0;
imagepreview.clipsToBounds = NO;
[effectView addSubview:imagepreview];
if([[effect objectAtIndex:3] isEqualToString:#"iap"]){
UIImageView *buttonPlus = [[UIImageView alloc] init];
[buttonPlus setFrame:CGRectMake(54, 66, 23, 23)];
[buttonPlus setImage:[UIImage imageNamed:#"ButtonPlus.png"]];
[effectView addSubview:buttonPlus];
}
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self action:NSSelectorFromString([effect objectAtIndex:1]) forControlEvents:UIControlEventTouchDown];
[button setTitle:[effect objectAtIndex:2] forState:UIControlStateNormal];
button.frame = CGRectMake(0, 0, item_width, item_height);
button.titleLabel.font = [UIFont systemFontOfSize:11];
[button setTitleShadowColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setShadowOffset:CGSizeMake(1.0f, 1.0f)];
[button setTitleEdgeInsets:UIEdgeInsetsMake(120, 0, 0.0, 0.0)];
[effectView addSubview:button];
[self.viewScrollCategories addSubview:effectView];
button = nil;
});
if(current_items_per_row < 3){
current_items_per_row++;
item_x_axis = item_x_axis + item_width;
}else {
current_row_height = current_row_height + item_height;
current_items_per_row = 1;
item_x_axis = 0;
}
}
Any idea how to solve this issue? it seemed to work fine on the IPhone.
EDIT
This is the code that is calling the background thread (shortned)
- (void)imagePickerController:( UIImagePickerController *)picker didFinishPickingMediaWithInfo: (NSDictionary *)info {
//[self generatingThumbnailMessageShow];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self loadAllEffects];
dispatch_async(dispatch_get_main_queue(), ^{
[self generatingThumbnailMessageHide];
});
});
}

Well, you are asking for a delay with dispatch_async(dispatch_get_main_queue().... That code means "Please do this whenever you have time, sometime after my current code finishes running." Once you say that, you are basically saying you don't care when this code runs. You hope it will be some time pretty soon, but you've left the runtime to take care of the details.
So perhaps it is this "hope" that is the issue. The architecture of the device processors is different and the threading model for the device might change the way the device responds to having a bunch of these delayed requests piling up.
I'm not at all clear on what you're trying to accomplish in this code (sorry, tl:dr) but the use of delayed performance in the middle of it and your complaint that there is a " 5-15 second delay" seem to go together somehow...
EDIT: OK, I see now that your code is running in the background and you are using dispatch_async to step out to the main thread in order to modify the interface. So perhaps the problem is not in the code you quote, but in the code you are using to manage your background threading in the first place.
ANOTHER EDIT: Just a wild and crazy idea here. If what takes time is the image processing, why don't you do all the image processing and then do your interface updating on the main thread once? What I mean is, you're doing this:
for (id effect in effectArray) {
UIImage *tmp = // ...
dispatch_async(dispatch_get_main_queue(), ^{
// ...
[imagepreview setImage:tmp];
So you're getting back on the main thread repeatedly each time through the loop. Why not try this:
dispatch_async(dispatch_get_main_queue(), ^{
for (id effect in effectArray) {
UIImage *tmp = // ...
// ...
[imagepreview setImage:tmp];
Now you're just getting back on the main thread once. There will be a delay while the images process, but then interface should just update, badda bing badda boom (technical programming term).

Related

Is there a memory impact from not using returned function values?

Just a general question about the best practices for functions which return values. Say for example I have the following function (pseudo code):
- (UIImageView *)createImageViewAndAddWithImageName:(NSString *)sName
{
UIImageView *iv = nil;
if (sName)
{
UIImage *image = [UIImage imageNamed:sName];
if (image)
{
iv = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[iv setImage:image];
[iv setContentMode:UIViewContentModeScaleAspectFit];
[self.view addSubview:iv];
}
}
return iv;
}
Now in some instances I want to perform further modifications like below:
UIImageView *iv = [self createImageViewAndAddWithImageName:#"Blah"];
[iv setAlpha:0.5f];
Where-as other times I want to just add an image:
[self createImageViewAndAddWithImageName:#"Blah"];
In the second instance, I presume that the memory will just be autoreleased? In the second instance, if this was running in a loop would it make sense to use an auto release block like below:
for (int i = 0; i < 100; i++)
{
#autoreleasepool {
[self createImageViewAndAddWithImageName:#"Blah"];
}
}
Just to free up the memory sooner? And is there any memory impact in having an autorelease pool in the main function like below too?
- (UIImageView *)createImageViewAndAddWithImageName:(NSString *)sName
{
UIImageView *iv = nil;
#autoreleasepool {
if (sName)
{
UIImage *image = [UIImage imageNamed:sName];
if (image)
{
iv = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[iv setImage:image];
[iv setContentMode:UIViewContentModeScaleAspectFit];
[self.view addSubview:iv];
}
}
}
return iv;
}
I just want to make sure that not using a returned value doesn't have a negative impact on my application, and that over-using autorelease pools doesn't either.
Thanks for any advice.
Every time the run loop calls out, it creates an autorelease pool. So all you have to do is to not allocate too much memory while doing anything caused by the run loop. For example if a user presses a button, your code handling this shouldn't allocate 100 MB of memory, even if autoreleased. Otherwise, autoreleased is reasonably cheap.

Why does loading images asynchronously take forever?

I have a UICollectionView displaying a bunch of images. If I don't load the images asynchronously the scrolling is very choppy and provides a poor user experience. When I load the images asynchronously the scrolling is smooth but it takes a good 5 to 10 seconds to load each image.
Why does it take so long for images to appear when loaded in the background? Here is my code for the background thread which is inside of the cellForItemAtIndexPath delegate:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImageView *bg = (id)self.backgroundView;
UIImageView *selbg = (id)self.selectedBackgroundView;
if (![bg isKindOfClass:[UIImageView class]])
bg = [[UIImageView alloc] initWithImage:thumb];
else
[bg setImage:thumb];
if (![selbg isKindOfClass:[UIImageView class]]){
selbg = [[UIImageView alloc] initWithImage:thumb];
coloroverlay = [[UIView alloc] initWithFrame:selbg.bounds];
[coloroverlay setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[selbg addSubview:coloroverlay];
} else
[selbg setImage:thumb];
[bg setContentMode:UIViewContentModeScaleAspectFill];
[bg setTag: 1];
[coloroverlay setBackgroundColor:[col colorWithAlphaComponent:0.33f]];
[selbg setContentMode:UIViewContentModeScaleAspectFill];
dispatch_sync(dispatch_get_main_queue(), ^{
[self setBackgroundView:bg];
[self setSelectedBackgroundView:selbg];
});
});
EDIT: As #geraldWilliam pointed out, I shouldn't be accessing views from the secondary thread. Here is what I have updated my code to and fixed the issue of images getting set to the wrong cell:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImageView *bg = (id)self.backgroundView;
UIImageView *selbg = (id)self.selectedBackgroundView;
if (![bg isKindOfClass:[UIImageView class]]) bg = [[UIImageView alloc] initWithImage:thumb];
if (![selbg isKindOfClass:[UIImageView class]]){
selbg = [[UIImageView alloc] initWithImage:thumb];
coloroverlay = [[UIView alloc] initWithFrame:selbg.bounds];
[coloroverlay setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
[selbg addSubview:coloroverlay];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[bg setImage:thumb];
[selbg setImage:thumb];
[bg setContentMode:UIViewContentModeScaleAspectFill];
[bg setTag: 1];
[coloroverlay setBackgroundColor:[col colorWithAlphaComponent:0.33f]];
[selbg setContentMode:UIViewContentModeScaleAspectFill];
[self setBackgroundView:bg];
[self setSelectedBackgroundView:selbg];
});
});
Most of the code you have here is fine for the main queue. The loading of the image should be on a global queue, but the rest, especially setting the image view's image, should be on the main queue. What's going on in your code is that you're dispatching back to the main queue to set the background view but leaving the assignment of the image property in the background. So, try something like:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:myImageURL]];
dispatch_sync(dispatch_get_main_queue(), ^{
imageView.image = image;
[self setBackgroundView:imageView];
});
});
I strongly recommend watching WWDC 2012 Session 211: Building Concurrent User Interfaces on iOS, which is the best WWDC session ever. It’s full of clearly presented, practical advice.
Doing stuff with UIImageView off the main queue is worrying and should be fixed, but is probably not the cause of slowness. You haven’t showed us where thumb comes from, which is likely the slow bit.

EXC_BAD_ACCESS on selector in objective C

I've got a UIBarButtonItem category where I build UIBarButtonItems with custom UIButtons, since I've found UIButtons easier to customize then UIBarButtonItems.
Now, I'd like to continue to use the BarButtonItem's target and action properties instead of using those in the button so that the BarButtonItem can continue to be customized externally without anyone having to know the implementation details (i.e., that it is using a button internally).
Now, in order to do that, I'm written up this code in my category:
+ (UIBarButtonItem *)backBarButtonItemWithColor:(UIColor *)color
{
UIImage *closeIcon = [MyImageUtility navBarBackArrow];
if (color) closeIcon = [closeIcon imageWithColorOverlay:color];
UIButton *close = [[UIButton alloc] initWithFrame:CGRectMake(0.0f, 0.0f, closeIcon.size.width+10.0f, closeIcon.size.height+10.0f)];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:close];
[close setImage:closeIcon forState:UIControlStateNormal];
[close addTarget:item action:#selector(SD_executeBarButtonItemAction) forControlEvents:UIControlEventTouchUpInside];
return item;
}
- (void)SD_executeBarButtonItemAction
{
[self.target performSelector:self.action];
}
Whenever the SD_executeBarButtonItemAction is called, I get a exc_bad_access on the selector, though I am not sure why. Any ideas? Is there a way around this?
Thanks!
EDIT:
here is the code being called by that selector that is crashing:
void (^transition)(void) = ^(void) {
[self.rightContainer setFrame:[self offscreenContainerFrame]];
[self.centerContainer setAlpha:1.0f]; //TODO: this is unreliable in iOS6 -- we should add a view to the top of it to darken
[self.centerContainer setTransform:CGAffineTransformIdentity];
};
[self notifyWillShowPrimaryViewController];
[self performBlock:transition animated:YES completion:^(BOOL finished) {
[self notifyDidShowPrimaryViewController];
[self setForegroundController:self.primaryNavigationController];
if (block != NULL) block(finished);
}];
Your code is a recursive call.
- (void)SD_executeBarButtonItemAction
{
[self.target performSelector:self.action];
}
You set like:
[close addTarget:item action:#selector(SD_executeBarButtonItemAction) forControlEvents:UIControlEventTouchUpInside];
Where item is a UIBarButtonItem.

Image not showing on preloaded UIImageView

When app launches I add UIImageViews to screen with no image. Then I load image one by one using NSThread and set it for those views. The UiImageViews remain blank until after all images have been loaded and the thread finishes. Why doesn't the image show as soon as I set it for the UIImageView, why does it wait for NSThread to end?
To add image views :
for(i = value; i < val; i++){
UIButton *newbutton = [UIButton buttonWithType:UIButtonTypeCustom];
[newbutton setBackgroundColor:[UIColor whiteColor]];
UIImageView *newback = [[UIImageView alloc] init];
newback.contentMode = UIViewContentModeScaleAspectFit;
[newback setFrame:CGRectMake(0, 0, 140, 140)];
[newbutton addSubview:newback];
height = MIN(300, newSize.height);
[newbutton setFrame:CGRectMake(currentX, currentY, width, height)];
newbutton.layer.cornerRadius = 10;
newbutton.clipsToBounds = YES;
[newbutton addTarget:self action:#selector(processButtonClick:) forControlEvents:UIControlEventTouchUpInside];
}
To add images in a thread I call a function which does following :
for(i = value; i < val; i++){
UIImage *newimage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#“http://test.com/a.jpg”, hash]]]];
UIButton *newbutton = (UIButton*)[self.subviews objectAtIndex:i];
[((UIImageView*)[newbutton.subviews objectAtIndex:0]) newimage];
}
Generally speaking, UIKit is not thread-safe. Fetch data from URL in a background thread and set the image in the main thread.
EDIT:
In your main thread do something like
dispatch_async(someDispatchQ, ^{
[self functionToGetData];
});
and inside functionToGetData do
for(i = value; i < val; i++){
NSData *data = //get Data
UIImage *newImage = //make image from data
dispatch_async(dispatch_get_main_queue(), ^{
[imageView setImage:newImage];
});
}
This will ensure that you are using a background thread to fetch data/make image and using the main thread to set the image. This also eliminates the need to use a timer to constantly poll the background thread since the background thread automatically dispatches image-setting to the main thread as soon as an image is received.
use-
import class ImageDownloader.h & .m
link-> https://github.com/psychs/imagestore/tree/master/Classes/Libraries/ImageStore
//Now use this method to download
-(void)downloadImageWithURL:(NSString *)url{
if(self.downloder)return; //If already downloading please stay away
url=[NSString stringWithFormat:#"%#%0.0fx%0.0f/%#/%#",IMAGE_ENDPOINT,self.imgViewExhibitors.frame.size.width,self.imgViewExhibitors.frame.size.height,#"fit",[ImagePath forURLString:url]];
self.downloder=[[ImageDownloader alloc] init];
self.downloder.delegate=self;
self.downloder.indicator=self.indicator;
[self.downloder downloadImageWithURL:url];
}
-(void)imageDownloaded:(UIImage *)image forIndexPath:(NSIndexPath *)indexPath{
if(image){
//Put the downloaded image in dictionary to update while scrolling
//[self.speakerData setObject:image forKey:#"image"];
self.imgViewExhibitors.image=image;
}
}
A simple fix would be
for(i = value; i < val; i++){
UIImage *newimage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#“http://test.com/a.jpg”, hash]]]];
UIButton *newbutton = (UIButton*)[self.subviews objectAtIndex:i];
// this ensures that the UI update (setting the image) is done on the main thread.
// This is important for the UI to update properly.
dispatch_async(dispatch_get_main_queue(), ^{
[((UIImageView*)[newbutton.subviews objectAtIndex:0]) setImage:newimage];
}
}

Performing many UIKit operations blocking main thread

when the app is launched, I'm programmatically creating many complex UIViews with transparent background colour, each being a 'screen' in the app (has to be this way since there's animated background underneath). This causes the app to stay on the static launch image for quite long (about 4 seconds).
I would like to create all the UIViews while an animation is shown. I can't create UIViews on background thread. How can I accomplish this?
Attempt 1:
-(void)viewDidLoad {
[super viewDidLoad];
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIView *baseView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 200, 100)];
[baseView setBackgroundColor:[UIColor blueColor]];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 180, 80)];
[button setBackgroundColor:[UIColor redColor]];
[button setTitle:#"button" forState:UIControlStateNormal]; //doesn't work
[baseView addSubview:button];
dispatch_async( dispatch_get_main_queue(), ^{ //Finished
[self.view addSubview:baseView];
});
});
}
The button is added, but the title is never set. It appears after tapped.
UIViews are not thread safe in normal usage, and all interaction should occur on the main thread. That said, you should be able to, on some background thread (dispatch queue), create the view, modify its parameters, set complex background colors, etc - as long as the view is not connected to anything (and thus receiving events from iOS). Once the view is completely setup and wired, then dispatch a block with it to the main queue and only then add it as a subview to some other view.
What I would do is dispatch all this work to the normal priority queue after having set up my initial video clip. When all the views have been posted back to the main queue (and thus this work is complete), you can stop the video and start interacting with the user.
Thanks for help. Here's the final solution that I tested and works perfectly.
I created a task manager class, FCTaskManager:
//Header File
typedef void (^TaskBlock)(void);
typedef void (^TaskCompletedBlock)(void);
#interface FCTaskManager : NSObject
-(void)performUITask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock;
-(void)performBackgroundTask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock;
#end
//Implementation
#import "FCTaskManager.h"
#implementation FCTaskManager
-(void)performUITask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock {
dispatch_async( dispatch_get_main_queue(), ^{
#autoreleasepool {
taskBlock();
dispatch_async( dispatch_get_main_queue(), ^{
taskCompletedBlock();
});
}
});
}
-(void)performBackgroundTask:(TaskBlock)taskBlock completionBlock:(TaskCompletedBlock)taskCompletedBlock {
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
#autoreleasepool {
taskBlock();
dispatch_async( dispatch_get_main_queue(), ^{
taskCompletedBlock();
});
}
});
}
#end
Then I can use the task manager like so:
-(void)viewDidLoad {
[super viewDidLoad];
//Create taskManager
taskManager = [[FCTaskManager alloc] init];
//Create UIViews, etc WITHOUT blocking main queue
[taskManager performUITask:^{
button = [[UIButton alloc] initWithFrame:CGRectMake(30, 30, 300, 300)];
[button setBackgroundColor:[UIColor redColor]];
[button setTitle:#"Working" forState:UIControlStateNormal];
[button addTarget:self action:#selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
} completionBlock:^{
NSLog(#"CREATED BUTTON");
[self.view addSubview:button];
}];
}

Resources