I've gotten in a few cases when something receives multiple refresh calls in quick succession, eg:
- ViewController receives multiple KVO notifications.
- Datamanger class that is called from setters to refresh when multiple settings change.
Ideally I would like to execute only the last refresh call from a series (drop all the intermediate ones).
Right now I'm using an isRefreshing property and a needRefresh to block excessive refreshes, eg:
- (id)init {
...
[self observeValueForKeyPath:#"isRefreshing" ....];
}
- (void)setParameter:(NSInteger)parameter {
....
[self refresh];
}
/* and many more kinds of updates require a refresh */
- (void)setAnotherProperty:(NSArray*)array {
....
[self refresh];
}
- (void)refresh {
if (self.isRefreshing) {
self.needRefresh = YES;
return;
}
self.isRefreshing = YES;
...
self.isRefreshing = NO;
}
- observeValueForKeyPath..... {
if (!self.isRefreshing && self.needsRefresh) {
self.needsRefresh = NO;
[self refresh];
}
}
Is there a better solution for this kind of problem?
You can create a NSOperationQueue with concurrency set to one and only submit a new operation to it when its operation count is zero. (Or use cancellation logic to remove pending jobs so that only one new one is queued if there's a job in progress.)
What you're doing is reasonable for a single-threaded system but would become fairly complicated for multiple threads.
Looks like you should delay refreshing for a while.
You can use different techniques to do so. It is enough only one flag.
For example you may use async block to make a delay for a one main run-loop cycle
- (void)setParameter:(NSInteger)parameter {
....
[self requestRefrhesh];
}
- (void)setAnotherProperty:(NSArray*)array {
....
[self requestRefrhesh];
}
...
-(void) requestRefrhesh {
if (self.refreshRequested) {
return;
} else {
self.refreshRequested = YES;
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run in main UI thread
//make your UI changes here
self.refreshRequested = NO;
});
}
}
Related
I have three methods, two of them run at the same time. And the third method should be started only when the first and second method together complete their work. Either the first or second method, competitors, can finish their work first.
- (void)method1 {
//DO Long Work
isMethod1Complete = YES;
[self method3];
}
- (void)method2 {
//DO Long Work
isMethod2Complete = YES;
[self method3];
}
- (void)method3 {
if (isMethod1Complete && isMethod2Complete) {
//DO Work once
}
}
Method 3 should always be called once. But the problem is that there is a situation that method1 and method2 have finished working at the same time, and method3 is called twice. Tell me how to solve this problem in objective c for iOS?
Update:A concrete example, I have two services that call delegates when they finish their work.
- (void)method1Handler {
isMethod1Complete = YES;
[self method3];
}
- (void)method2Handler {
isMethod1Complete = YES;
[self method3];
}
How can this be solved without blocks?
For blocks, Rob's example is the best.
You say:
I have three methods, two of them run at the same time.
That means that they must be asynchronous or running on background queues (otherwise there's no way for them to run at the same time).
So, the idea is that you should give them both completion handlers (which will be called when they're done):
- (void)method1WithCompletion:(void(^ _Nonnull)(void))completion {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
//DO Long Work asynchronously
completion();
});
}
- (void)method2WithCompletion:(void(^ _Nonnull)(void))completion {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
//DO Long Work asynchronously
completion();
});
}
- (void)method3 {
// final task
}
In the above example, I added explicit dispatch_async calls to a background queue to ensure that the two long tasks run asynchronously. But if the code is already doing something asynchronous (e.g. a network request), then you will likely not need these dispatch_async calls, but just put the completion() call inside the completion handler provided by whatever API you are already using. But without more information about what method1 and method2 are doing, I cannot be more specific.
But, setting that aside, once your method1 and method2 have their own completion handlers, you can use dispatch_group_notify to identify what should be done when all of the dispatch_group_enter calls are balanced by their corresponding dispatch_group_leave calls:
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self method1WithCompletion:^{
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self method2WithCompletion:^{
dispatch_group_leave(group);
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self method3];
});
In subsequent comments, you mentioned that you are not using a completion block-based API, but rather a delegate-protocol-based API. You have a few options, for example:
You can use the same above closure pattern, but just save the completion handlers as block properties, e.g.:
For example, define block properties:
#property (nonatomic, copy, nullable) void (^completionOne)(void);
#property (nonatomic, copy, nullable) void (^completionTwo)(void);
Then, your method1 and method2 would save these blocks:
- (void)method1WithCompletion:(void(^ _Nonnull)(void))completion {
self.completionOne = completion;
// start your time consuming asynchronous process
}
// and your completion delegate method can then call the saved closure
// and then remove it
- (void)method1DidComplete {
self.completionOne();
self.completionOne = nil;
}
- (void)method2WithCompletion:(void(^ _Nonnull)(void))completion {
self.completionTwo = completion;
// start second asynchronous process
}
// same as above
- (void)method2DidComplete {
self.completionTwo();
self.completionTwo = nil;
}
The delegate-protocol completion API would then just call the saved block properties (and probably reset them to nil to free the memory associated with those blocks).
Then you can use the dispatch group notify process as shown in my original answer, above.
Alternatively, rather than using blocks, you can just use dispatch group by itself. For example, define dispatch group property:
#property (nonatomic, strong, nullable) dispatch_group_t group;
Then, you create your group and start your two tasks:
self.group = dispatch_group_create();
[self method1];
[self method2];
dispatch_group_notify(self.group, dispatch_get_main_queue(), ^{
[self method3];
});
And, the two methods then dispatch_group_enter when you start the tasks and dispatch_group_leave in their respective completion handler delegate methods:
- (void)method1 {
dispatch_group_enter(self.group);
// start first asynchronous process
}
// in your delegate completion method, you "leave" the group
- (void)method1DidComplete {
dispatch_group_leave(self.group);
}
- (void)method2 {
dispatch_group_enter(self.group);
// start second asynchronous process
}
- (void)method2DidComplete {
dispatch_group_leave(self.group);
}
- (void)method3 {
// you might as well remove the group now that you're done with it
self.group = nil;
// final task
NSLog(#"doing three");
}
Personally, I would generally lean towards the first option (that way, the dispatch group stuff is contained in a single method), but either approach works.
Why not dispatching the "call" to method3 in a serial queue?
dispatch_queue_t notSimQ;
notSimQ = dispatch_queue_create("notSimQ", NULL);
- (void)method1 {
//DO Long Work
isMethod1Complete = YES;
dispatch_async( notSimQ, // or sync
^{
[self method3];
});
}
- (void)method2 { … } // similiasr
- (void)method3 { … } // unchanged
The calls to method3 are never in competition.
- (void)method1 {
//DO Long Work
isMethod2Complete = YES;
dispatch_async(dispatch_get_main_queue(), ^(void){
[self method3];
}
}
- (void)method2 { … }
- (void)method3 { … }
- (void)viewDidLoad {
[super viewDidLoad];
[self method1];
[self method2];
}
- (void)method1 {
//DO Long Work
isMethod1Complete = YES;
}
- (void)method2 {
//DO Long Work
isMethod2Complete = YES;
dispatch_async(dispatch_get_main_queue(), ^(void){
[self method3];
}
}
- (void)method3 {
if (isMethod1Complete && isMethod2Complete) {
//DO Work once
}
}
While loading when we click on back button or any cell of the table the action is called multiple times after loading is finish.Here the the code snippet that what i'm doing when I start the loading and stop the loading.
+(void)showLoader_OnView{
APP_DELEGATE.window.userInteractionEnabled = NO;
[MBProgressHUD showHUDAddedTo:APP_DELEGATE.window animated:YES];
}
To stop the loading:-
+(void)hideLoader {
APP_DELEGATE.window.userInteractionEnabled =YES;
[MBProgressHUD hideAllHUDsForView:APP_DELEGATE.window animated:YES];
}
please help me.
Update
actually i'm taking data from server. whenever user will go to next window then in viewWillAppear function i call a function which will hit the api to get the data.
-(void)performAutoSync
{
#try
{
if(self.shouldPerformAutoSync)//Necessary conditions to check the auto sync
{
[AppConstants showLoader_OnView]; //here i call the loader.
self.shouldPerformAutoSync = NO;
if(!self.isSyncing)
{
if(!syncBl)
{
syncBl = [[SyncBL alloc] init];
syncBl.delegate = self;
}
if(!syncDl)
syncDl = [[SyncDL alloc] init];
// [self saveModifiedDataForCurrentViewController];
[self delayToAutoSync];
NSMutableDictionary *dictMainData = [NSMutableDictionary new];
[dictMainData setObject:[syncDl fetchCompleteDataAndPrepareDictionary:YES] forKey:#"data"];//#"MainData"];
[syncBl performAutoSync:dictMainData];
}
}
}
#catch (NSException *exception) {
BILog(#"%#",exception);
}
}
Don't block the main thread.
Seeing that you invoke [AppConstants showLoader_OnView] from performAutoSync, and that showLoader_OnView in turn executes:
[MBProgressHUD showHUDAddedTo:APP_DELEGATE.window animated:YES]
I can only assume that performAutoSync is executed in the main thread. This, of course, blocks the UI until your operations are completed.
You should redesign so that you won't need all your state variables, globals, global calls, and take advantage of multi-threading.
Also, remove this, as it qualifies as a kludge;
APP_DELEGATE.window.userInteractionEnabled = NO
I have a UIViewController that does the following in viewDidLoad
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
items = [[DataFetcher sharedInstance] getItems handler:^(NSArray *currentItems){
if (currentItems.count % 30 == 0) { //don't wait for all the items to be ready show by chunks of 30
items = currentItems;
[tableView reloadData];
}
items = currentItems;
}];//Pretty complex call that takes some time to finish there is WebService calls, data parsing, storing some results ...
dispatch_async(dispatch_get_main_queue(), ^{
[tableView reloadData];
});
});
What I need to do is to stop getItems when I pop this viewController. It's pointless and it takes CPU Time and energy (This call may take up to a minute on some cases).
I am assuming I should be doing this in viewWillDisappear but how exactly?
You can use NSBlockOperation. Periodically check if it's been cancelled, and stop doing work if it has been:
- (void)getItemsWithHandler:(void (^)(NSArray *currentItems))handler {
self.operation = [NSBlockOperation blockOperationWithBlock:^{
if (self.operation.isCancelled) {
return;
}
// Do something expensive
if (self.operation.isCancelled) {
return;
}
// Do something else expensive
for (int i = 0; i < 10000; i++) {
if (self.operation.isCancelled) {
return;
}
// Do expensive things in a loop
}
}];
}
- (void) cancelGetItemsRequest {
[self.operation cancel];
self.operation = nil;
}
Alternatively, you can put a bunch of NSBlockOperations in an NSOperationQueue. You can set dependencies for the work, and cancel the entire queue at once if you want.
Cancelling asynchronous operations is nicely supported in NSOperation and NSOperationQueue, but it's quite a bit more complicated. In any case, your asynchronous code has to check from time to time whether it is cancelled or should still continue.
Best thing is to have a property "cancelled" somewhere, that you set when you don't want any more work to be done, and whenever you try to do more work, you check that property.
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've got class:
ClassX.m
#property (assign) BOOL wasProcessed;
-(void) methodA { //<- this can be called many times in short period of time
dispatch_async(dispatch_get_main_queue(), ^{
[self methodB];
});
}
- (void) methodB {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
Since dispatch_async is used so a few calls to methodB can be processed concurrently at the same time and following code needs to be atomic:
if (!self.wasProcessed) {
self.wasProcessed = YES; //e.g two calls can enter here before setting YES and it would be bad because I want to process it only one time
How can those 2 lines be made atomic (checking and setting variable)? I dont want to make atomic code that is after "self.wasProcessed = YES;" so moving whole if to #synchronize(self) won't be good solution. If there is anything wrong with my thinking please point it out as I'm not very experienced in those topics, Thank you.
Try #synchronized. While the enclosed code is being executed on a thread, it will block other threads from executing it.
- (void) methodB {
#synchronized(self) {
if (!self.wasProcessed) {
self.wasProcessed = YES;
//... some code
}
}
}
-(void) methodA {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
[self methodB];
}];
});
}
Your's methodB will be only called in main thread, so it will be never performed simultaneously.