I am working on an application that was designed by someone else. The application is full of calls like this:
[NSThread detachNewThreadSelector:#selector(loadPhotoImage) toTarget:self withObject:nil];
and
DownloadPhotoThread *thread = [[DownloadPhotoThread alloc] initWithFriendArray:_friendsList delegate:self];
[self.arrDownloadThreads addObject:thread];
[thread start];
And randomly I will get into situations where the entire ui locks, and the phone/simulator no longer respond to touches. Just to be clear, the device never unfreezes. If I hit pause during a debug session, I see a thread is sitting on a start or detachNewThreadSelector line.
Today I was able to narrow down a cause when these locks happen. I just added the Zendesk form controller (found here: https://github.com/zendesk/zendesk_ios_sdk/blob/master/DropboxSampleApp/FormViewController.m)
Which has this code:
- (void) textViewDidChange:(UITextView *)textView
{
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row > 1) {
CGSize s = [description sizeThatFits:CGSizeMake(description.frame.size.width, 10000)];
float dh = MAX(s.height, 115);
description.frame = CGRectMake(description.frame.origin.x,
description.frame.origin.y,
description.frame.size.width,
dh);
return dh;
}
return 44.0;
}
I can easily reproduce the lockup condition by typing a character and return over and over into this text box. (this resizes the height of the table view)
In some cases I am able to prevent the lockup condition by wrapping the offender with a dispatch_async() block like this:
DownloadPhotoThread *thread = [[DownloadPhotoThread alloc] initWithFriendArray:_friendsList delegate:self];
[self.arrDownloadThreads addObject:thread];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[thread start];
});
But in other cases I can't because the offending code is in a complex library such as ASIHTTPRequest or XMPPFramework. Do you have any advice on what exactly is happening here?
Another thing. When I hit pause in this condition, this is what the main thread says:
Thread 1
com.apple.main-thread
0 objc_msgSend
....
25 UIApplicationMain
26 main
pausing and unpausing will always find main at some assembly instruction inside objc_msgSend.
Thanks for your help cuz I'm seriously stumped here.
It turns out that the crash was caused by a bug in https://github.com/zendesk/zendesk_ios_sdk/ . I've made a pull request and submitted to them for review.
Related
When launching app first, app do prefill its local persistent store from backend. It happens on the DISPATCH_QUEUE_PRIORITY_LOW, but it tears when user scroll in tableView meantime. What else can I do? Heavy stuff are already on lowest priority.
DISPATCH_QUEUE_PRIORITY_BACKGROUND has even lower priority. If that does not help I think you could:
Pre-fill on a serial queue. Most iOS devices have more than 2 cores and 1 of those should be able to handle table scrolling.
Pre-fill slower. It may be that you fill the memory-bandwidth or flush the L2-cache during your pre-fill. This could be hard to solve. Maybe you can periodically reload all visible table cells to keep that code from going stale, but it may also interfere with user scrolling.
You could do a couple of things I believe
1) you could start with an empty dataset and when you're done getting all the data use ''' self.tableview.reloadData() ''' in your building block.
2) if that's not possible, then you could always present a loader so the rest of the UI is disabled while the data is being created, this one is pretty easy to use https://github.com/jdg/MBProgressHUD
I have one idea, but here i suggest use NSOperation (if you have enough time to refactor some of your code). You can organize your download process throw NSOperationQueue.
For example,
- (NSOperationQueue *)downloadQueue {
if (!_downloadQueue) {
_downloadQueue = [[NSOperationQueue alloc] init];
_downloadQueue.name = #"Download Queue";
_downloadQueue.maxConcurrentOperationCount = 1;
}
return _downloadQueue;
}
Subclass NSOperation add override main func, where you can write your donwload code
- (void)main {
// 4
#autoreleasepool {
// download code here
}
Next step - check when user start scrolling tableview (or whatever user interaction you want) and start/ stop operations executing on that event). for example, for uitableview it will look like:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
// 1
[self.model suspendAllOperations];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// 2
[self.model resumeAllOperations];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self.model resumeAllOperations];
}
Model here is a NSObject subclass, that handles all download process (independently of UI) where you can suspend/resume operations.
- (void)suspendAllOperations {
[_downloadQueue setSuspended:YES];
}
- (void)resumeAllOperations {
[_downloadQueue setSuspended:NO];
}
- (void)cancelAllOperations {
[_downloadQueue cancelAllOperations];
}
So, when you expect heavy operations in your UI, you can stop your background process and resume it when you need. Also you can change maxConcurrentOperationCount for your best performance (this param you can set after some testing / measurement )
Hope this helps.
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'm using the MBProgressHUD library in my app, but there are times that the progress hud doesn't even show when i query extensive amount of data, or show right after the processing of data is finished (by that time i don't need the hud to be displayed anymore).
In another post i found out that sometimes UI run cycles are so busy that they don't get to refresh completely, so i used a solution that partially solved my problem: Now every request rises the HUD but pretty much half the times the app crashes. Why? That's where I need some help.
I have a table view, in the delegate method didSelectRowAtIndexPath i have this code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[NSThread detachNewThreadSelector:#selector(showHUD) toTarget:self withObject:nil];
...
}
Then, I have this method:
- (void)showHUD {
#autoreleasepool {
[HUD show:YES];
}
}
At some other point I just call:
[HUD hide:YES];
And well, when it works it works, hud shows, stays and then disappear as expected, and sometimes it just crashes the application. The error: EXC_BAD_ACCESS . Why?
By the way, the HUD object is already allocated in the viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
...
// Allocating HUD
HUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view addSubview:HUD];
HUD.labelText = #"Checking";
HUD.detailsLabelText = #"Products";
HUD.dimBackground = YES;
}
You need to perform your processing on another thread, otherwise the processing is blocking MBProgressHud drawing until it completes, at which point MBProgressHud is hidden again.
NSThread is a bit too low-level for just offloading processing. I'd suggest either Grand Central Dispatch or NSOperationQueue.
http://jeffreysambells.com/2013/03/01/asynchronous-operations-in-ios-with-grand-central-dispatch
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
/* Prepare the UI before the processing starts (i.e. show MBProgressHud) */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/* Processing here */
dispatch_async(dispatch_get_main_queue(), ^{
/* Update the UI here (i.e. hide MBProgressHud, etc..) */
});
});
This snippet will let you do any UI work on the main thread, before dispatching the processing to another thread. It then returns to the main thread once the processing is done, to allow you to update the UI.
I have implemented the following NSOperation, to draw N custom views
- (void)main {
for (int i=0; i<N; i++) {
<< Alloc and configure customView #i >>
//(customView is a UIView with some drawing code in drawrect)
[delegate.view addSubview:customView];
}
NSLog(#"Operation completed");
}
in the drawRect method of the customView I have
- (void)drawRect {
<<Drawing code>>
NSLog(#"Drawed");
delegate.drawedViews++;
if (delegate.drawedViews==VIEWS_NUMBER) {
[delegate allViewsDrawn];
}
}
So the delegate get the notification when all the views are drawn.
The problem is that after the "Operation completed" log it takes about 5 seconds before I can see the first "Drawed" log.
Why is this happening? And generally speaking, how should I behave in order to find out which line of code is taking so much time being executed?
------ EDIT ------
Sometimes (like 1 out of 10 times) I was getting crashes doing this because I shouldn't call addsubview from the NSOperation since it is not thread-safe. So I changed it to:
[delegate.view performSelectorOnMainThread:#selector(addSubview:) withObject:customView waitUntilDone:NO];
Now I don't have crashes anymore, but the process takes a very long time to be executed! Like 5 times more than before.
Why is it so slow?
To make things work properly we need to forget about NSOperation and use this "trick"
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(main_queue, ^{
[self createCustomViews];
dispatch_async(main_queue, ^{
[self addAnotherCustomViewToView];
});
});
I really need help here. I'm desperate at this point.
I have NSOperation that when added to the NSOperationQueue is not being triggered.
I added some logging to see the NSOperation status and this is the result:
Queue operations count = 1
Queue isSuspended = 0
Operation isCancelled? = 0
Operation isConcurrent? = 0
Operation isFinished? = 0
Operation isExecuted? = 0
Operation isReady? = 1
Operation dependencies? = 0
The code is very simple. Nothing special.
LoadingConflictEvents_iPad *loadingEvents = [[LoadingConflictEvents_iPad alloc] initWithNibName:#"LoadingConflictEvents_iPad" bundle:[NSBundle mainBundle]];
loadingEvents.modalPresentationStyle = UIModalPresentationFormSheet;
loadingEvents.conflictOpDelegate = self;
[self presentModalViewController:loadingEvents animated:NO];
[loadingEvents release];
ConflictEventOperation *operation = [[ConflictEventOperation alloc] initWithParameters:wiLr.formNumber pWI_ID:wiLr.wi_id];
[queue addOperation:operation];
NSLog(#"Queue operations count = %d",[queue operationCount]);
NSLog(#"Queue isSuspended = %d",[queue isSuspended]);
NSLog(#"Operation isCancelled? = %d",[operation isCancelled]);
NSLog(#"Operation isConcurrent? = %d",[operation isConcurrent]);
NSLog(#"Operation isFinished? = %d",[operation isFinished]);
NSLog(#"Operation isExecuted? = %d",[operation isExecuting]);
NSLog(#"Operation isReady? = %d",[operation isReady]);
NSLog(#"Operation dependencies? = %d",[[operation dependencies] count]);
[operation release];
Now my operation do many things on the main method, but the problem is never being called. The main is never executed.
The most weird thing (believe me, I'm not crazy .. yet). If I put a break point in any NSLog line or in the creation of the operation the main method will be called and everything will work perfectly.
This have been working fine for a long time. I have been making some changes recently and apparently something screw things up. One of those changes was to upgrade the device to iOS 5.1 SDK (iPad).
To add something, I have the iPhone (iOS 5.1) version of this application that use the same NSOperation object. The difference is in the UI only, and everything works fine.
Oh, and this only fails on the actual device. In the simulator everything works ok.
Any help will be really appreciated.
Regards,
If the operation is concurrent, you need to implement -start, not -main.
Ok, I finally solve this issue.
The problem I was having was due to an NSOperation running in the background all the time (but in a different queue). This operation was blocking any other thread (NSOperation) to be executed. This was happening only in iPad 1 because is not dual core.
My NSOperation was doing something like this on the main method:
- (void)main
{
while (![self isCancelled]) {
//Do stuff
}
}
And the NSOperation was contantly doing it the whole time
Silly me, I didn't give the OS time to work with other threads, so adding a sleep made the trick
- (void)main
{
while (![self isCancelled]) {
[NSThread sleepForTimeInterval:0.5];
//Do Stuff
}
}
That sleep gives the OS chance to work with other threads.
Why putting a breakpoint was doing the work? ... the debugger stopped all the threads and because the breakpoint was in my other NSOperation, that thread was executed.
I had the same issue, but the resolution to mine was not the same, so I thought I'd throw my answer in here too for future people like me.
My problem was that in my NSOperation subclass, I had:
#property CGPoint start;
which, when synthesized, creates the method:
-(CGPoint)start
which overrides NSOperation's -(void)start; method, which is the signifier for a non-concurrent NSOperation, and was preventing all the regular stuff that goes on in -(void)start from happening, which thus prevents the -(void)main method from being called at all.
Once I renamed my property to something other than start, it worked fine.
Ok, this issue is only happening with iPad 1 devices with application compiled with the SDK 5.1.
I tried with an iPad 1 with iOS 4.2.1 and iPad 1 with iOS 5.1. Both gives the same problems.
For sure this was working in both iPads with application compiled with SDK 4.3