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.
Related
In iOS app widget, I can see on only some devices, doubled data (see figure below). I have tried to identify device, iOS version, but it seems to be "random". Plus, I am unable to debug this by myself, because on every of my devices, all is rendered correctly and doing blind debugging is not working (several updates on AppStore but still with the same error).
In widget, I download (in background thread) new data from web and put them (in dispatch_get_main_queue()) into labels, images etc. All is working OK, but sometimes the old data are not "cleared". In my design file for widget, I have cleared all "default" texts, so this is not this problem.
Doubled icon & texts 4.1°C and 7.9°C are overlapping
Main part of my widget code is (shortened by removing other labels, tables and geolocation):
- (void)viewDidLoad
{
[super viewDidLoad];
if ([self.extensionContext respondsToSelector:#selector(widgetLargestAvailableDisplayMode)])
{
//this is iOS >= 10
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishDownload:) name:#"FinishDownload" object:nil];
self.preferredContentSize = CGSizeMake(320, 160);
[self updateData];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self updateData];
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateData];
}
-(void)updateData
{
[[[DataManager SharedManager] settings] Reload];
[[CoreDataManager SharedManager] reset];
if ([[DataManager SharedManager] DownloadDataWithAfterSelector:#"FinishDownload"] == NO)
{
//no need to download update - refill data now
//if downloading - wait for download
[self FillData];
}
}
}
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[self FillData];
});
}
-(void)FillData
{
//a lot of code - example of setting temperature
NSString *str = [NSString stringWithFormat:#"%# °C", act.temp_act];
self.lblTemp.text = str;
[self.lblTemp sizeToFit];
if (self.completionHandler != nil)
{
self.completionHandler(NCUpdateResultNewData);
}
}
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler
{
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResultFailed
// If there's no update required, use NCUpdateResultNoData
// If there's an update, use NCUpdateResultNewData
//completionHandler(NCUpdateResultNewData);
NSLog(#"=== widgetPerformUpdateWithCompletionHandler === ");
self.completionHandler = completionHandler;
[self updateData];
}
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsMake(0, 0, 5, 5);
}
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize
{
if (activeDisplayMode == NCWidgetDisplayModeExpanded)
{
self.preferredContentSize = CGSizeMake(320, 160);
}
else if (activeDisplayMode == NCWidgetDisplayModeCompact)
{
self.preferredContentSize = maxSize;
}
}
View Lifecycle
Do not duplicate the work in viewDidLoad and viewWillAppear/viewDidAppear.
A view that was loaded will hit all three methods. Use viewDidLoad for operations that must be performed exactly once for the life of the UIViewController.
Potential problem:
Triggering 3 invocations, possibly conflicting, to [self updateData] back to back, possibly with competing NCUpdateResult completion handlers3.
Balance Observers
It appears that addObserver is never balanced by a removeObserver. A good location for these registration methods is a set of balanced messages, such as the view___Appear and view___Disappear methods, as outlined in this StackOverflow answer.
Potential problem:
Lasting registration to notifications on objects that may go out of scope.
Do not cache OS handlers
Possible misuse of NCUpdateResultNewData completion handler: the NCUpdateResult is passed to widgetPerformUpdateWithCompletionHandler to be used for that specific invocation, not stored for multiple reuse. It should probably be handed down to updateData as a parameter rather than stored in a global, in turn passed to FillData, and eventually cleared after a one-time use.
if (nil != self.completionHandler) {
self.completionHandler(NCUpdateResultNewData);
self.completionHandler = nil; // One time use
}
Every invocation to widgetPerformUpdateWithCompletionHandler has its own cycle, as outlined in this StackOverflow answer.
Layout & Autolayout
Be aware that the iOS is making a snapshot of your widget ; in Interface Builder, make sure that you use proper layering of views. Pay special attention to transparency and drawing flags. Leverage Autolayout to resize/size/snap objects
Check the UILabel's options in Interface Builder, make sure 'opaque' is unchecked. If the label is set as opaque, it might not be properly clearing the entire view when you change the text. You probably want to check on the 'clears graphics context' property as well, which should be checked.
In the code you add a Notification observer. You do not remove the observer.
I suspect that the notification will be fired multiple times which will result jn a race condition or something.
Solution:
- check hoe often the addObserver is executed. (Including screen changes like back-forward etc)
remove the observer when the notification is caught.
clear / remove the observer when leaving the VC
Besides: check / reduce the action in the ViewWillAppear and ViwDidAppear.
How can I substitute usleep with NSTimer in the following code:
/**
* DETERMINATE BAR with LABEL
*/
- (void)showDeterminateBarWithLabel:(CDVInvokedUrlCommand *)command {
// obtain commands
bool dim = [[command.arguments objectAtIndex:0] boolValue];
int increment = [[command.arguments objectAtIndex:1] intValue];
NSNumber* incrementValue = #(increment);
NSString* text = [command.arguments objectAtIndex:2];
// initialize indicator with options, text, detail
self.progressIndicator = nil;
self.progressIndicator = [MBProgressHUD showHUDAddedTo:self.webView.superview animated:YES];
self.progressIndicator.mode = MBProgressHUDModeDeterminateHorizontalBar;
self.progressIndicator.labelText = text;
// Check for dim : true ? false
if (dim == true) {
self.progressIndicator.dimBackground = YES;
}
// Load Progress bar with ::incrementValue
[self.progressIndicator showWhileExecuting:#selector(progressTask:) onTarget:self withObject:incrementValue animated:YES];
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:#""];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
- (void)progressTask:(NSNumber *)increment{
// get increment value
int _increment = [increment intValue];
float progress = 0.0f;
while (progress < 1.0f) {
progress += 0.01f;
self.progressIndicator.progress = progress;
// increment in microseconds (100000mms = 1s)
usleep(_increment);
}
}
This code is taken from here.
In short, you can't use to block a thread. Blocking threads with any kind of a sleep or delay is bad design and should be avoided outside of exceptionally rare cases.
Blocking the main thread in an iOS / OS X application is strictly forbidden. The main runloop must be allowed to run or your app will be, at best, unresponsive and, at worst, just won't work.
Instead, use an NSTimer to that periodically calls back into your code to update the value. It won't block execution.
You cannot. Those two are quite different and this code requires blocking operation. Edit: Because it’s executed on a background thread.
The method -progressTask: is executed from this method, which is started on a new thread:
- (void)launchExecution {
#autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// Start executing the requested task
[targetForExecution performSelector:methodForExecution withObject:objectForExecution];
#pragma clang diagnostic pop
// Task completed, update view in main thread (note: view operations should
// be done only in the main thread)
[self performSelectorOnMainThread:#selector(cleanUp) withObject:nil waitUntilDone:NO];
}
}
It relies on synchronous execution and using NSTimer would requre starting NSRunLoop and letting it run for some time, which would be actually possible, but just don’t.
Hint: If you prefer Objective-C approach, call +[NSThread sleepForTimeInterval:] with argument in seconds.
I'm having this wierd problem with the app freezing at a certain point. I'm guessing its got to do with how I'm using NSConditionLock.
Theres a library I have been given to use, which consists of a series of survey questions, but it works in such a way that it races directly to the last question without accepting answers, hence the need to pause the thread and accept input from the user.
I haven't used it before so maybe someone could help if I'm implementing it wrongly?
Please let me know if the code provided is insufficient.
- (void)viewDidLoad
{
[super viewDidLoad];
//INITIALISE CONDITION LOCK WITH CONDITION 0
condition=[[NSConditionLock alloc]initWithCondition: 0];
}
- (IBAction)startPressed:(UIButton*)sender {
if (sender.tag == 1) {
//START BACKGROUND THREAD
surveyThread = [[NSThread alloc] initWithTarget:self selector:#selector(runProjecttest) object:nil];
[surveyThread start];
}
else
{
//DO SOME STUFF AND THEN UNLOCK
[condition unlockWithCondition:1];
}
}
- (void) runProjecttest:(AbstractTask *)rendertask
{
// DO STUFF AND SHOW UI ON MAIN THREAD, THEN LOCK
[self performSelectorOnMainThread:#selector(showUI:) withObject:task waitUntilDone:YES];
[condition lockWhenCondition: 1];
}
EDIT: In short, I want the Objc equivalent of this java snippet...
this.runOnUiThread(showUI);
try
{
//SLEEP
Thread.sleep(1000*60*60*24*365*10);
}
catch (InterruptedException e)
{
//WAKE
setResponse(at,showUI);
}
EDIT 2: ShowUI method on Paul's request.
[self removePreviousSubViews];
switch ([task getType]) {
case SingleChoiceType:
{
NSLog(#"SingleChoiceType");
isMultipleChoice = NO;
[self addSingleChoiceView:nil];
break;
}
case TextType:
{
NSLog(#"TextType");
self.txtTextType.keyboardType=UIKeyboardTypeDefault;
[self addTextTypeView:nil];
break;
}
...more cases
}
-(void)addTextTypeView:(NSSet *)objects
{
self.txtTextType.text = #"";
CGRect frame = self.txtQuestionType.frame;
// frame.size = [self.txtQuestionType sizeThatFits: CGSizeMake(self.txtQuestionType.frame.size.width, FLT_MAX)];
frame.size.height = [self textViewHeightForAttributedText:self.txtQuestionType.text andWidth:self.txtQuestionType.frame.size.width andTextView:self.txtQuestionType];
self.txtQuestionType.frame=frame;
self.textTypeView.frame = CGRectMake((self.view.frame.size.width - self.textTypeView.frame.size.width)/2, ( self.txtQuestionType.frame.origin.y+self.txtQuestionType.frame.size.height), self.textTypeView.frame.size.width, self.textTypeView.frame.size.height);
[self.view addSubview: self.textTypeView];
}
I agree with BryanChen, I think you may have another issue. Without details on the survey library, it is impossible to confirm, but assuming that it is a UIViewController than accepts touch inputs to progress through a series of questions, it is hard to see why it is a threading issue - it simply shouldn't advance without user interaction.
That aside, your use of NSCondtionLock doesn't look right either.
Essentially an NSConditionLock has an NSInteger that represents the current 'condition', but just think of it of a number. There are then two basic operations you can perform -
lockWhenCondition:x will block the current thread until the 'condition' is 'x' and the lock is available. It will then claim the lock.
unlockWithCondition:y releases the lock and sets the condition to 'y'
There are also methods to set timeouts (lockBeforeDate) and try to claim the lock without blocking (tryLock, tryLockWhenCondition).
To synchronise two threads, the general pattern is
Initialise Lock to condition 'x'
Thread 1 lockWhenCondition:x -This thread can claim the lock because it is x
Thread 2 lockWhenCondition:y - This thread will block because the lock is x
Thread 1 completes work, unlockWithCondition:y - This will enable Thread 2 to claim the lock and unblock that thread
Your code looks strange, because you are starting a thread in your if clause but unlocking in an else clause. I would have thought you would have something like -
-(IBAction)startPressed:(UIButton*)sender {
if (sender.tag == 1) {
//START BACKGROUND THREAD
surveyThread = [[NSThread alloc] initWithTarget:self selector:#selector(runProjecttest) object:nil];
[surveyThread start];
[condition:lockWithCondition:1]; // This will block until survey thread completes
[condition:unlockWithCondition:0]; // Unlock and ready for next time
}
}
- (void) runProjecttest:(AbstractTask *)rendertask
{
// DO STUFF AND SHOW UI ON MAIN THREAD, THEN LOCK
[condition lockWhenCondition: 0];
[self performSelectorOnMainThread:#selector(showUI:) withObject:task waitUntilDone:YES];
[condition unlockWithCondition:1];
}
BUT This looks like a recipe for deadlock to me, because you are performing the showUI selector on the main thread that is blocked waiting for the survey thread to complete.
Which brings us back to the question, what does showUI do and why is it skipping directly to the end?
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?
My app have a Trip entity which has a to-many relationship to Spot entity, and I need to load trip and its spot list in the background thread. It works fine in debug version, but in release version, the spot list is empty! So I dug a little and found, it cannot work in release version unless I set the compiler optimization level to -O0. I think it may be a bug of the compiler.
Is there any suggestion to make it work for higher optimization level, or I have to release a non-optimized app? Thanks!
Here is the code:
Main thread
[self performSelectorInBackground:#selector(calcRoute:) withObject:self.trip.objectID];
Background thread
- (void)calcRoute:(NSManagedObjectID *)tripId
{
//get trip entity
CoreDataHelper * helper = nil; //responsible for init model, persistentStore and context
TripEntity * t = nil;
helper = [[CoreDataHelper alloc] initWithAnother:mainThreadHelper]; //only take the resource name and storeURL
t = (TripEntity *)[helper.context objectWithID:tripId]; //retrieve trip
if (0 == t.spotList.count) {
[self performSelectorOnMainThread:#selector(drawRouteFailed) withObject:nil waitUntilDone:FALSE];
return; //I got here!
}
//...
}
Finally I solved the problem. The reason is: I 'used' the main thread trip entity. Here is the actual code:
- (void)calcRoute:(NSManagedObjectID *)tripId
{
//get trip entity
CoreDataHelper * helper = nil; //responsible for init model, persistentStore and context
TripEntity * t = nil;
if (self.backgroundDrow) { //sometimes I don't need background drawing
helper = [[CoreDataHelper alloc] initWithAnother:mainThreadHelper]; //only take the resource name and storeURL
t = (TripEntity *)[helper.context objectWithID:tripId]; //retrieve trip
} else
t = self.mainThreadTrip; //HERE is the problem!!!
if (0 == t.spotList.count) {
[self performSelectorOnMainThread:#selector(drawRouteFailed) withObject:nil waitUntilDone:FALSE];
return; //I got here!
}
//...
}
After I comment t = self.mainThreadTrip;, the background loading becomes OK!
But I still don't get the point! Would someone tell me the real reason? Thanks a lot!