I did not experience any crashing in testing, but I've gotten a few crash reports from iTunesConnect that look like this:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x45d319f8
Crashed Thread: 0
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libobjc.A.dylib 0x3a6595be objc_msgSend + 30
1 UIKit 0x34796e30 -[UIImageView setImage:] + 116
2 My App 0x000c40b2 -[AsyncImageView setImage:] (AsyncImageView.m:224)
3 My App 0x000c3950 __47-[AsyncImageView loadImageWithURL:animated:]_block_invoke_2 (AsyncImageView.m:147)
AsyncImageView is a typical UIImageView subclass that loads images asynchronously from a URL.
Here is the asset loading code with the offending line number indicated:
- (void)loadImageWithURL:(NSURL *)url animated:(BOOL)animated {
if (url == nil) {
[self setImage:nil];
return;
}
self.imageAsset = [[Asset alloc] init];
self.imageAsset.assetURL = url;
AssetRequest *request = [[AssetRequest alloc] init];
request.assetURL = url;
__weak AsyncImageView *weakSelf = self;
self.assetLoader = [AssetLoader AssetLoaderWithRequest:request
completion:^(Asset *asset){
dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.imageAsset.assetURL == asset.assetURL) {
weakSelf.imageAsset = asset;
if (animated) {
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
transition.duration = 0.20;
[weakSelf.layer addAnimation:transition forKey:nil];
}
[weakSelf setImage:weakSelf.imageAsset.assetImage]; //THIS IS LINE 147
[weakSelf setDisplayLoadingIndicator:NO];
[weakSelf stopAnimating];
}
});
}
error:^(NSError *err){
if (weakSelf.failedToLoad)
weakSelf.failedToLoad(url);
}];
[self.assetLoader load];
}
And here is where is sets the image, with the offending line number indicated:
- (void)setImage:(UIImage *)image {
if (image) {
[super setImage:image]; //THIS IS LINE 224
[self hidePlaceholderView];
if (self.imageLoadedBlock)
self.imageLoadedBlock();
}
else {
[self showPlaceholderView];
}
}
The crash report indicates that the crash occurs when setting the image. Is there any obvious reason why this might happen? Or any further error checking I can do (I'm already checking that image isn't null)? And again, this doesn't happen all the time, only once in a while.
The problem is you are using a walk around for retain cycle:
__weak AsyncImageView *weakSelf = self;
With that approach it's not retained by a block - so if someone leaves fast enough before block is executed - selfBlock gets deallocated.
The best is to cancel all operations on dealloc or at least set completion block to "nil".
- (void) dealloc
{
[self.assetLoader cancelAllOperations]; //of course you need to implement that
[super dealloc];
}
I think you will be able to reproduce that if you stress test your app and play with it on edge connection and leave a view really quickly before response comes.
Another simpler solution is to not keep "AssetLoader" as property and remote this __block variable letting block to retain "self". But it may lead to unexpected behavior when view is not on screen and will gets updated.
I looked into the SDWebImage source code and they do something like this when the asset loader completes:
if (!weakSelf) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.imageAsset.assetURL == asset.assetURL) {
weakSelf.imageAsset = asset;
[weakSelf setImage:weakSelf.imageAsset.assetImage];
...
}
});
So basically if weakSelf no longer exists don't do anything. Seems a little weird to ask itself if it still exists but would this fix the problem?
Related
Getting an exception when using Skobbler Maps in iOS. Seems like a concurrency exception resulting in an invalid array access, but since it's a problem with the provided code from Skobbler, not sure how the best way to fix their broken code.
See the line marked "HERE!!!" in the code below:
File: SKTNavigationManager.m
- (void)routingService:(SKRoutingService *)routingService didFinishRouteCalculationWithInfo:(SKRouteInformation *)routeInformation {
dispatch_async(dispatch_get_main_queue(), ^{
__block BOOL routeExists = NO;
[_calculatedRoutes enumerateObjectsUsingBlock:^(SKRouteInformation *obj, NSUInteger idx, BOOL *stop) {
if (routeInformation.routeID == obj.routeID) {
routeExists = YES;
*stop = YES;
}
}];
if (!routeInformation.corridorIsDownloaded || routeExists || _calculatedRoutes.count == _configuration.numberOfRoutes) {
return;
}
//are we still calculating the routes?
if ([self hasState:SKTNavigationStateCalculatingRoute]) {
[_calculatedRoutes addObject:routeInformation];
//stop progress for the calculated route
SKTRouteProgressView *progressVIew = _mainView.calculatingRouteView.progressViews[_calculatedRoutes.count - 1]; // HERE!!!
[progressVIew startProgress];
//show the info for the calculated route
[_mainView.calculatingRouteView showInfoViewAtIndex:_calculatedRoutes.count - 1];
[self updateCalculatedRouteInformationAtIndex:_calculatedRoutes.count - 1];
//start progress for next route if needed
if (_calculatedRoutes.count < _mainView.calculatingRouteView.numberOfRoutes) {
SKTRouteProgressView *progressVIew = _mainView.calculatingRouteView.progressViews[_calculatedRoutes.count];
[progressVIew startProgress];
}
if (!_selectedRoute) {
_selectedRoute = routeInformation;
[SKRoutingService sharedInstance].mainRouteId = _selectedRoute.routeID;
[self zoomOnSelectedRoute];
_mainView.calculatingRouteView.selectedInfoIndex = 0;
_mainView.calculatingRouteView.startButton.hidden = NO;
}
} else if ([self hasState:SKTNavigationStateRerouting]) { //nope, we're being rerouted
_selectedRoute = routeInformation;
_navigationInfo.currentDTA = _selectedRoute.distance;
[self updateDTA];
_navigationInfo.currentETA = _selectedRoute.estimatedTime;
[self updateETA];
[SKRoutingService sharedInstance].mainRouteId = _selectedRoute.routeID;
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(removeReroutingState) object:nil];
[self performSelector:#selector(removeReroutingState) withObject:nil afterDelay:kMinReroutingDisplayTime];
}
});
}
The exception reported is ...
8 libobjc.A.dylib - objc_exception_throw + 332 (objc-exception.mm:568)
9 CoreFoundation - [__NSArrayM objectAtIndex:] + 240 (NSArray.m:410)
10 MyApp -[SKTNavigationManager routingService:didFinishRouteCalculationWithInfo:]_block_invoke + 676 (SKTNavigationManager.m:463)
Any ideas? I could do a check on the size of _calculatedRoutes before reading from the progressViews array, but the problem I have is there is additional code after the line that accesses both. In other words, I can avoid the line, but how do I fix the method to work correctly?
The code inside SKTNavigationManager (and inside the whole SDKTools project) is offered as open code - it is not part of the SDK itself but constitutes some demo code/ helper classes that should help you address common scenarios (building a simple navigation UI, dealing with offline maps, etc.).
Some developers use this code as documentation, others use it "as is" in their projects, others start from it and customize it to their liking.
In you particular case I'm not sure how you did arrive at this inconsistent state (with the vanilla code, in the demo project I cannot replicate this) - if you believe that you have a concurrency issue feel free to insert additional checks or synchronization mechanisms.
I have added the loading bar in below snippet.It’s worked fine which I have deleted small amount of data(say 30 to 150 mb). But the problem is when I deleted large size of data, the loading bar doesn't get loaded(approx 190mb).
dispatch_async(dispatch_get_main_queue(), ^{
[self addloading];
if(_isProgress)
return;
_lastDeleteItemIndexAsked = index;
NSInteger catalogue_id =[[[_currentData objectAtIndex:index%[_currentData count]] valueForKey:#"catalogue_id"] integerValue];
BOOL update_avilable = [[[online_date_array objectAtIndex:index%[_currentData count]] valueForKey:#"update_version"] boolValue];
if(update_avilable)
[[self GridViewDelegate] deletecatlogue_for_update:catalogue_id];
else
[[self GridViewDelegate] deletecatlogue:catalogue_id];
[online_date_array replaceObjectAtIndex:index%[online_date_array count] withObject:[NotificationHandler check_online_date_of_catalogue:catalogue_id]];
[_gmGridView reloadObjectAtIndex:index%[_currentData count] withAnimation:GMGridViewItemAnimationFade];
[self stopLoadingfor_delete];
});
Is it size or execution time?
I'm going to assume that self is a UI object. If it is, the block should be:
Controller* __weak weakSelf = self;
dispatch_async(queue, ^{
Controller* strongSelf = weakSelf;
if (strongSelf) {
...
}
else {
// self has been deallocated in the meantime.
}
});
Thus you may prefer a MVVC solution, where a view model does the hard work, not the view controller.
I am having some trouble updating my UI using performSelectorOnMainThread. Here is my situation. In my viewDidLoad I set up an activity indicator and a label. Then I call a selector to retrieve some data from a server. Then I call a selector to update the UI after a delay. Here's the code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.reloadSchools = [[UIAlertView alloc] init];
self.reloadSchools.message = #"There was an error loading the schools. Please try again.";
self.reloadSchools.title = #"We're Sorry";
self.schoolPickerLabel = [[UILabel alloc]init];
self.schoolPicker = [[UIPickerView alloc] init];
self.schoolPicker.delegate = self;
self.schoolPicker.dataSource = self;
self.server = [[Server alloc]init];
schoolList = NO;
_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:_activityIndicator];
[self.view bringSubviewToFront:_activityIndicator];
[_activityIndicator startAnimating];
[NSThread detachNewThreadSelector: #selector(getSchoolList) toTarget: self withObject: nil];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:20.0];
}
The selector updateUI checks to see if the data was retrieved, and calls a selector on the main thread to update the UI accordingly. Here is the code for these parts:
-(void)updateUI
{
self.schools = [_server returnData];
if(!(self.schools == nil)) {
[self performSelectorOnMainThread:#selector(fillPickerView) withObject:nil waitUntilDone:YES];
}
else {
[self performSelectorOnMainThread:#selector(showError) withObject:nil waitUntilDone:YES];
}
}
-(void)showError {
NSLog(#"show error");
[_activityIndicator stopAnimating];
[self.reloadSchools show];
}
-(void)fillPickerView {
NSLog(#"fill picker view");
schoolList = YES;
NSString *schoolString = [[NSString alloc] initWithData:self.schools encoding:NSUTF8StringEncoding];
self.schoolPickerLabel.text = #"Please select your school:";
self.shoolArray = [[schoolString componentsSeparatedByString:#"#"] mutableCopy];
[self.schoolPicker reloadAllComponents];
[_activityIndicator stopAnimating];
}
When the selector fillPickerView is called the activity indicator keeps spinning, the label text doesn't change, and the picker view doesn't reload its content. Can someone explain to me why the method I am using isn't working to update my ui on the main thread?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//load your data here.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI in main thread.
});
});
First of all you should not be using detachNewThreadSelector. You should use GCD and submit your background task to an async queue. Threads are costly to create. GCD does a much better job of managing system resources.
Ignoring that, your code doesn't make a lot of sense to me. You submit a method, getSchoolList, to run on a background thread. You don't show the code that you are running in the background.
Then use performSelector:withObject:afterDelay to run the method updateUI on the main thread after a fixed delay of 20 seconds.
updateUI checks for self.schools, which presumably was set up by your background thread, and may or may not be done. If self.schools IS nil, you call fillPickerView using performSelectorOnMainThread. That doesn't make sense because if self.schools is nil, there is no data to fill the picker.
If self.schools is not nil, you display an error, again using performSelectorOnMainThread.
It seems to me that the logic on your check of self.schools is backwards. If it is nil you should display an error and if it is NOT nil you should fill the picker.
Next problem: In both cases you're calling performSelectorOnMainThread:withObject:waitUntilDone: from the main thread. Calling that method from the main thread doesn't make sense.
Third problem: It doesn't make sense to wait an arbitrary amount of time for a background task to run to completion, and then either succeed or fail. You won't have any idea what's going on for the full 20 seconds. If the background task finishes sooner, you'll never know.
Instead, you should have your background task notify the main thread once the task is done. That would be a valid use of performSelectorOnMainThread:withObject:waitUntilDone:, while calling it from the main thread is not. (Again, though, you should refactor this code to use GCD, not using threads directly.
It seems pretty clear that you are in over your head. The code you posted needs to be rewritten completely.
i make a AdView extends UIView like this
AdView:
//nerver call dealloc when adview release
-(void)dealloc
{
//stop thread
bStart = NO;
//...
[super dealloc];
}
-(id)init
{
//.....
bStart = YES;
//the self will retain by NSThread,i try to call [self performBackground..:onThrad] or timer the same too.
NSThread* thead = [[NSThread alloc] initWithTagert:self ...:#select(onThread)];
[thread start];
[thread release];
}
-(void)onThread
{
while(bStart)
{
//....
}
}
the controller
{
AdView* view = [[AdView alloc] init];
view.delegate = self;// i am ture delegate is not retain
[self.view addSubView:view];
[view release]
}
Adview has never to call dealloc when contoller release,
who konws how to fix it.
As others noted you are passing self to the target initialization which retains it. That's why you have an extra retain causing the object not being deallocated.
That said, let me give you two pieces of advice here:
Use ARC. It's 2013, we suffered with manual reference counting for about enough time.
Use GCD. It's 2013, we suffered with manual threads management for about enough time.
A modern version of your code would look like
- (instancetype)init {
//...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doStuffAsynchronously];
});
//...
}
- (void)doStuffAsynchronously { ... }
EDIT
As JFS advices in the comments, if you need to start and stop the background execution you should consider using a NSOperation within a NSOperationQueue. A naive (but still functional) implementation would be:
#property (nonatomic, strong) NSOperationQueue * operationQueue;
//...
- (instancetype)init {
//...
self.operationQueue = [NSOperationQueue new];
[operationQueue addOperationWithBlock:^{
[self doStuffAsynchronously];
}];
//...
}
- (void)doStuffAsynchronously { ... }
- (void)stopDoingStuff {
[self.operationQueue cancelAllOperations];
}
A neater approach, though, would be to subclass NSOperation, starting it by adding it to a queue and stopping it by invoking stop.
The thread retains the target self in start. So the object can not go away as long as the thread runs.
The controller should stop the thread by calling something like adView.bStart = NO; (which of course you have to implement).
I have recently been debugging a zombie issue with operations and found out that calling cancelAllOperations on the queue didn't cancel the operation in question, and in fact, the operation queue was empty even though the operation was still running.
The structure was a viewcontroller asynchronously loading a set of images off the web and perform some changes on them. Relevant (anonymised) exerpts follow:
#implementation MyViewController
- (id) init
{
(...)
mOperationQueue = [[NSOperationQueue alloc] init];
(...)
}
- (void) viewDidAppear:(BOOL)animated
{
(...)
MyNSOperation * operation = [[MyNSOperation alloc] initWithDelegate:self andData:data];
[mOperationQueue addOperation:operation];
[operation release];
(...)
}
- (void) dealloc
{
(...)
[mOperationQueue cancelAllOperations];
[mOperationQueue release];
(...)
}
- (void) imagesLoaded:(NSArray *)images
{
(...)
}
And the operation in question:
#implementation MyNSOperation
- (id) initWithDelegate:(id)delegate andData:(NSDictionary *)data
{
self = [super init];
if (self)
{
mDelegate = delegate; // weak reference
mData = [data retain];
(...)
}
return self;
}
- (void) main
{
NSAutoReleasePool * pool = [[NSAutoReleasePool alloc] init];
mImages = [[NSMutableArray alloc] init];
// load and compose images
mAlteredImages = (...)
[self performSelectorOnMainThread:#selector(operationCompleted) withObject:nil waitUntilDone:YES];
[pool release];
}
- (void)operationCompleted
{
if (![self isCancelled])
{
[mDelegate imagesLoaded:mAlteredImages];
}
}
The observed flow is as follows:
The viewcontroller is shown, calling init and viewDidAppear starting the operation.
[mOperationQueue operations] contains exactly one element;
Shortly after, the operation enters main and
The viewcontroller is exited by the user before the operation completes.
dealloc is called on the viewcontroller (because the operation keeps a weak reference)
[mOperationQueue operations] contains zero (!) elements
cancelAllOperations is sent to the operation queue
[NSOperation cancel] is not called, resulting in an app-visible bogus state.
dealloc finishes
the operation completes
isCancelled returns false, resulting in a zombie call
The documentation of NSOperationQueue however explicitly states that "Operations remain queued until they finish their task." which looks like a breach of contract.
I've fixed the crash by keeping a reference to the operation and manually sending cancel, but I would like to know why the original approach isn't working to prevent further problems. Can someone shed some light on this?
Thanks in advance.
cancelAllOperations does not cancel an already started operation. It only informs the operation about that fact and let the operation cancel themself, whenever it want. Thus, you can get a raise condition. Proceed with deallocacion, after you are sure that the operation is canceled.