GCD 串行队列与 NSTimer,dispatch_sync 不等待 NSTimer 完成
GCD serial queue with NSTimer, dispatch_sync doesn't wait for the NSTimer to complete
我目前正在尝试制作一个 queueHandler
,它将对象数组作为输入,用于在简单的 Double 机器人上执行驱动命令。我目前正在尝试使用 GCD 来连续执行我的函数,但是当我在我的队列中使用 dispatch_sync
时,不会等到 NSTimer
有 运行 它的进程,但会继续尝试执行数组中下一个对象的命令。
我有 3 个函数,一个简单地用 2 个对象初始化 NSMutableArray
(loadCommands) 和 运行s queueHandler
,这在我切换开关时调用。然后 queueHandler
从对象(type、timing、queueNr)中读取变量以确定将执行什么类型的驱动功能以及执行多长时间。我认为这可以在 switch 语句中完成,我认为如果应用程序可以在主线程上执行该函数会很棒(没关系!)但它应该等到 NSTimer
有 运行 它的课程。我认为用 dispatch_sync 封装 switch case 可以解决这个问题,但它会立即跳到循环中的下一个迭代并尝试执行下一个函数,该函数向后驱动 3 秒。
当我用数组中的单个对象测试它时,命令将毫无问题地执行。我想我正在以某种方式锁定主线程。也许等待 NSTimer
语句中 @selector
函数的 return 值有帮助?
我只玩了 Objective C 大约 10 天,如果能得到任何帮助,我将不胜感激!
- (void)loadCommands {
//create an objectArray and put 2 objects inside it.
NSMutableArray *driveCommandsArray = [[NSMutableArray alloc] initWithCapacity:4];
//Command 1
DRCommands *C1 = [[DRCommands alloc] init];
C1.timing = 3;
C1.type = 1;
C1.queueNr = 1;
[driveCommandsArray addObject:C1];
//Command 2
DRCommands *C2 = [[DRCommands alloc] init];
C2.timing = 3;
C2.type = 2;
C2.queueNr = 2;
[driveCommandsArray addObject:C2];
//call queueHandler
[self queueHandler:driveCommandsArray];
}
队列处理程序:
- (void)queueHandler: (NSMutableArray*) commandArray {
//Now, I'm not sure what I'm doing here, I watched a tutorial that
//solved a vaguely similar problem and he put a dispatch_async before the
//dispatch_sync. I can't run the dispatch_sync clause inside the case
//statement without this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Inside handler!");
unsigned long count;
count = [commandArray count]; //retrieve length/number of objects from the array.
unsigned long a;
for (a = 0; a < count;) {
//run the loop until all objects has been managed.
DRCommands* myObj = (DRCommands*)[commandArray objectAtIndex:a];
//create 2 serial queues.
dispatch_queue_t myQ1;
myQ1 = dispatch_queue_create("myQ1", NULL);
dispatch_queue_t myQ2;
myQ2 = dispatch_queue_create("myQ2", NULL);
int queueID = myObj.queueNr; //retrieve place in queue (not really used yet)
int timeID = myObj.timing; //retrieve the amount of time the command shall be run through the NSTimer
int typeID = myObj.type; //type of command
NSLog(@"Inside for loop!");
if (queueID == a+1) {
a++;
switch (typeID) {
{
case 1:
NSLog(@"inside case 1");
dispatch_sync(myQ1, ^{ //doesn't wait for NSTimer to finish,
//letting the Double drive forward for 3 seconds,
//before resuming operations.
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveForward) userInfo:nil repeats:YES];
});
break;
}
{
case 2:
NSLog(@"inside case 2");
dispatch_sync(myQ2, ^{
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveBackward) userInfo:nil repeats:YES];
});
break;
}
//add more cases
{
default:
break;
}
}
}
NSLog(@"Exited for loop, and count is %lu", a);
}
});
}
驱动命令:
//Go forward X seconds.
- (void)jDriveForward {
shouldDriveForward_ = YES; //sets a condition which is recognized by a callback function to run the device forward.
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveForward_ = NO;
}
}
//Go backwards X seconds.
- (void)jDriveBackward {
shouldDriveBackward_ = YES;
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveBackward_ = NO;
}
}
我正在使用
实验中提供的驱动功能API
我在函数 driveDoubleShouldUpdate 中使用 "token",例如 "shouldDriveForward_",它在 NSTimer 的持续时间内为 TRUE。我必须在该函数内调用我的驱动方法,以使机器人不会默认进入空闲模式。因此,只要 X 持续时间为真,则向前或向后行驶的功能处于活动状态。
- (void)doubleDriveShouldUpdate:(DRDouble *)theDouble {
float drive = (driveForwardButton.highlighted) ? kDRDriveDirectionForward : ((driveBackwardButton.highlighted) ? kDRDriveDirectionBackward : kDRDriveDirectionStop);
float turn = (driveRightButton.highlighted) ? 1.0 : ((driveLeftButton.highlighted) ? -1.0 : 0.0);
[theDouble drive:drive turn:turn];
//below are custom functions
//The NSTimer I'm using keep the BOOL values below TRUE for X seconds,
//making the robot go forward/backward through this callback
//method, which I must use
if(shouldDriveForward_ == YES) {
[theDouble variableDrive:(float)1.0 turn:(float)0.0];
}
if(shouldDriveBackward_ == YES) {
[theDouble variableDrive:(float)-1.0 turn:(float)0.0];
}
}
您只需要一个串行调度队列即可将您的任务添加到其中。
我将从定义一个任务 class 开始,它实现您的各种命令 - 如果需要,您可以子class。
DRCommand.h
#import <Foundation/Foundation.h>
@interface DRCommand : NSObject
@property uint duration;
-(void) dispatch;
@end
DRCommand.m
#import "DRCommand.h"
@implementation DRCommand
-(void)dispatch {
[self startCommand];
sleep(self.duration);
[self stopCommand];
}
-(void) startCommand {
NSLog(@"Override this method to actually do something");
}
-(void) stopCommand {
NSLog(@"Override this method to stop doing something");
}
@end
那么您的 运行 队列代码将类似于
-(void) runQueue {
DRCommand *c1=[DRCommand new];
c1.duration=5;
DRCommand *c2=[DRCommand new];
c2.duration=7;
DRCommand *c3=[DRCommand new];
c3.duration=3;
NSArray *taskArray=@[c1,c2,c3];
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
for (DRCommand *command in taskArray) {
dispatch_async(queue, ^{
[command dispatch];
});
}
}
请注意,您将拥有 DRCommand
的子class,例如 DRForwardCommand
、DRBackwardCommand
等等,每个都有适当的 startCommand
和 stopCommand
方法。
你在这里把 GCD 和 NSTimer
的组合搞得一团糟。没有什么可以说它们不能混合使用,但是全 GCD 方法可能更容易让您理解。我想我已经看出了你在这里要做的事情的要点,并且把一些可能有用的东西拼凑在一起。我放了 the whole project up on GitHub,但这是它的主要部分:
#import "ViewController.h"
typedef NS_ENUM(NSUInteger, DRCommandType) {
DRCommandUnknown = 0,
DRCommandTypeForward = 1,
DRCommandTypeBackward = 2,
};
@interface DRCommand : NSObject
@property DRCommandType type;
@property NSTimeInterval duration;
@end
@implementation DRCommand
@end
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *commandNameLabel;
@property (weak, nonatomic) IBOutlet UILabel *secondsRemainingLabel;
@property (strong, atomic) DRCommand* currentlyExecutingCommand;
@property (copy, atomic) NSNumber* currentCommandStarted;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do an initial UI update
[self updateUI];
}
- (IBAction)loadCommands:(id)sender
{
DRCommand *C1 = [[DRCommand alloc] init];
C1.duration = 3.0;
C1.type = DRCommandTypeForward;
DRCommand *C2 = [[DRCommand alloc] init];
C2.duration = 3.0;
C2.type = DRCommandTypeBackward;
[self handleCommands: @[ C1, C2 ]];
}
- (void)handleCommands: (NSArray*)commands
{
// For safety... it could be a mutable array that the caller could continue to mutate
commands = [commands copy];
// This queue will do all our actual work
dispatch_queue_t execQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// We'll target the main queue because it simplifies the updating of the UI
dispatch_set_target_queue(execQueue, dispatch_get_main_queue());
// We'll use this queue to serve commands one at a time...
dispatch_queue_t latchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// Have it target the execQueue; Not strictly necessary but codifies the relationship
dispatch_set_target_queue(latchQueue, execQueue);
// This timer will update our UI at 60FPS give or take, on the main thread.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (1.0/60.0) * NSEC_PER_SEC, (1.0/30.0) * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{ [self updateUI]; });
// Suspend the latch queue until we're ready to go
dispatch_suspend(latchQueue);
// The first thing to do for this command stream is to start UI updates
dispatch_async(latchQueue, ^{ dispatch_resume(timer); });
// Next enqueue each command in the array
for (DRCommand* cmd in commands)
{
dispatch_async(latchQueue, ^{
// Stop the queue from processing other commands.
dispatch_suspend(latchQueue);
// Update the "machine state"
self.currentlyExecutingCommand = cmd;
self.currentCommandStarted = @([NSDate timeIntervalSinceReferenceDate]);
// Set up the event that'll mark the end of the command.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(cmd.duration * NSEC_PER_SEC)), execQueue, ^{
// Clear out the machine state for the next command
self.currentlyExecutingCommand = nil;
self.currentCommandStarted = nil;
// Resume the latch queue so that the next command starts
dispatch_resume(latchQueue);
});
});
}
// After all the commands have finished, add a cleanup block to stop the timer, and
// make sure the UI doesn't have stale text in it.
dispatch_async(latchQueue, ^{
dispatch_source_cancel(timer);
[self updateUI];
});
// Everything is queued up, so start the command queue
dispatch_resume(latchQueue);
}
- (void)updateUI
{
// Make sure we only ever update the UI on the main thread.
if (![NSThread isMainThread])
{
dispatch_async(dispatch_get_main_queue(), ^{ [self updateUI]; });
return;
}
DRCommand* currentCmd = self.currentlyExecutingCommand;
switch (currentCmd.type)
{
case DRCommandUnknown:
self.commandNameLabel.text = @"None";
break;
case DRCommandTypeForward:
self.commandNameLabel.text = @"Forward";
break;
case DRCommandTypeBackward:
self.commandNameLabel.text = @"Backward";
break;
}
NSNumber* startTime = self.currentCommandStarted;
if (!startTime || !currentCmd)
{
self.secondsRemainingLabel.text = @"";
}
else
{
const NSTimeInterval startTimeDbl = startTime.doubleValue;
const NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
const NSTimeInterval duration = currentCmd.duration;
const NSTimeInterval remaining = MAX(0, startTimeDbl + duration - currentTime);
self.secondsRemainingLabel.text = [NSString stringWithFormat: @"%1.3g", remaining];
}
}
@end
如果有任何您想要更多解释的部分,请在评论中告诉我。
注意:这里的另一个答案是命令执行 sleep
;我的方法是完全异步的。哪种方法适合您将取决于您的命令实际上在做什么,问题中并不清楚。
我目前正在尝试制作一个 queueHandler
,它将对象数组作为输入,用于在简单的 Double 机器人上执行驱动命令。我目前正在尝试使用 GCD 来连续执行我的函数,但是当我在我的队列中使用 dispatch_sync
时,不会等到 NSTimer
有 运行 它的进程,但会继续尝试执行数组中下一个对象的命令。
我有 3 个函数,一个简单地用 2 个对象初始化 NSMutableArray
(loadCommands) 和 运行s queueHandler
,这在我切换开关时调用。然后 queueHandler
从对象(type、timing、queueNr)中读取变量以确定将执行什么类型的驱动功能以及执行多长时间。我认为这可以在 switch 语句中完成,我认为如果应用程序可以在主线程上执行该函数会很棒(没关系!)但它应该等到 NSTimer
有 运行 它的课程。我认为用 dispatch_sync 封装 switch case 可以解决这个问题,但它会立即跳到循环中的下一个迭代并尝试执行下一个函数,该函数向后驱动 3 秒。
当我用数组中的单个对象测试它时,命令将毫无问题地执行。我想我正在以某种方式锁定主线程。也许等待 NSTimer
语句中 @selector
函数的 return 值有帮助?
我只玩了 Objective C 大约 10 天,如果能得到任何帮助,我将不胜感激!
- (void)loadCommands {
//create an objectArray and put 2 objects inside it.
NSMutableArray *driveCommandsArray = [[NSMutableArray alloc] initWithCapacity:4];
//Command 1
DRCommands *C1 = [[DRCommands alloc] init];
C1.timing = 3;
C1.type = 1;
C1.queueNr = 1;
[driveCommandsArray addObject:C1];
//Command 2
DRCommands *C2 = [[DRCommands alloc] init];
C2.timing = 3;
C2.type = 2;
C2.queueNr = 2;
[driveCommandsArray addObject:C2];
//call queueHandler
[self queueHandler:driveCommandsArray];
}
队列处理程序:
- (void)queueHandler: (NSMutableArray*) commandArray {
//Now, I'm not sure what I'm doing here, I watched a tutorial that
//solved a vaguely similar problem and he put a dispatch_async before the
//dispatch_sync. I can't run the dispatch_sync clause inside the case
//statement without this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Inside handler!");
unsigned long count;
count = [commandArray count]; //retrieve length/number of objects from the array.
unsigned long a;
for (a = 0; a < count;) {
//run the loop until all objects has been managed.
DRCommands* myObj = (DRCommands*)[commandArray objectAtIndex:a];
//create 2 serial queues.
dispatch_queue_t myQ1;
myQ1 = dispatch_queue_create("myQ1", NULL);
dispatch_queue_t myQ2;
myQ2 = dispatch_queue_create("myQ2", NULL);
int queueID = myObj.queueNr; //retrieve place in queue (not really used yet)
int timeID = myObj.timing; //retrieve the amount of time the command shall be run through the NSTimer
int typeID = myObj.type; //type of command
NSLog(@"Inside for loop!");
if (queueID == a+1) {
a++;
switch (typeID) {
{
case 1:
NSLog(@"inside case 1");
dispatch_sync(myQ1, ^{ //doesn't wait for NSTimer to finish,
//letting the Double drive forward for 3 seconds,
//before resuming operations.
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveForward) userInfo:nil repeats:YES];
});
break;
}
{
case 2:
NSLog(@"inside case 2");
dispatch_sync(myQ2, ^{
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveBackward) userInfo:nil repeats:YES];
});
break;
}
//add more cases
{
default:
break;
}
}
}
NSLog(@"Exited for loop, and count is %lu", a);
}
});
}
驱动命令:
//Go forward X seconds.
- (void)jDriveForward {
shouldDriveForward_ = YES; //sets a condition which is recognized by a callback function to run the device forward.
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveForward_ = NO;
}
}
//Go backwards X seconds.
- (void)jDriveBackward {
shouldDriveBackward_ = YES;
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveBackward_ = NO;
}
}
我正在使用
实验中提供的驱动功能API我在函数 driveDoubleShouldUpdate 中使用 "token",例如 "shouldDriveForward_",它在 NSTimer 的持续时间内为 TRUE。我必须在该函数内调用我的驱动方法,以使机器人不会默认进入空闲模式。因此,只要 X 持续时间为真,则向前或向后行驶的功能处于活动状态。
- (void)doubleDriveShouldUpdate:(DRDouble *)theDouble {
float drive = (driveForwardButton.highlighted) ? kDRDriveDirectionForward : ((driveBackwardButton.highlighted) ? kDRDriveDirectionBackward : kDRDriveDirectionStop);
float turn = (driveRightButton.highlighted) ? 1.0 : ((driveLeftButton.highlighted) ? -1.0 : 0.0);
[theDouble drive:drive turn:turn];
//below are custom functions
//The NSTimer I'm using keep the BOOL values below TRUE for X seconds,
//making the robot go forward/backward through this callback
//method, which I must use
if(shouldDriveForward_ == YES) {
[theDouble variableDrive:(float)1.0 turn:(float)0.0];
}
if(shouldDriveBackward_ == YES) {
[theDouble variableDrive:(float)-1.0 turn:(float)0.0];
}
}
您只需要一个串行调度队列即可将您的任务添加到其中。
我将从定义一个任务 class 开始,它实现您的各种命令 - 如果需要,您可以子class。
DRCommand.h
#import <Foundation/Foundation.h>
@interface DRCommand : NSObject
@property uint duration;
-(void) dispatch;
@end
DRCommand.m
#import "DRCommand.h"
@implementation DRCommand
-(void)dispatch {
[self startCommand];
sleep(self.duration);
[self stopCommand];
}
-(void) startCommand {
NSLog(@"Override this method to actually do something");
}
-(void) stopCommand {
NSLog(@"Override this method to stop doing something");
}
@end
那么您的 运行 队列代码将类似于
-(void) runQueue {
DRCommand *c1=[DRCommand new];
c1.duration=5;
DRCommand *c2=[DRCommand new];
c2.duration=7;
DRCommand *c3=[DRCommand new];
c3.duration=3;
NSArray *taskArray=@[c1,c2,c3];
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
for (DRCommand *command in taskArray) {
dispatch_async(queue, ^{
[command dispatch];
});
}
}
请注意,您将拥有 DRCommand
的子class,例如 DRForwardCommand
、DRBackwardCommand
等等,每个都有适当的 startCommand
和 stopCommand
方法。
你在这里把 GCD 和 NSTimer
的组合搞得一团糟。没有什么可以说它们不能混合使用,但是全 GCD 方法可能更容易让您理解。我想我已经看出了你在这里要做的事情的要点,并且把一些可能有用的东西拼凑在一起。我放了 the whole project up on GitHub,但这是它的主要部分:
#import "ViewController.h"
typedef NS_ENUM(NSUInteger, DRCommandType) {
DRCommandUnknown = 0,
DRCommandTypeForward = 1,
DRCommandTypeBackward = 2,
};
@interface DRCommand : NSObject
@property DRCommandType type;
@property NSTimeInterval duration;
@end
@implementation DRCommand
@end
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *commandNameLabel;
@property (weak, nonatomic) IBOutlet UILabel *secondsRemainingLabel;
@property (strong, atomic) DRCommand* currentlyExecutingCommand;
@property (copy, atomic) NSNumber* currentCommandStarted;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do an initial UI update
[self updateUI];
}
- (IBAction)loadCommands:(id)sender
{
DRCommand *C1 = [[DRCommand alloc] init];
C1.duration = 3.0;
C1.type = DRCommandTypeForward;
DRCommand *C2 = [[DRCommand alloc] init];
C2.duration = 3.0;
C2.type = DRCommandTypeBackward;
[self handleCommands: @[ C1, C2 ]];
}
- (void)handleCommands: (NSArray*)commands
{
// For safety... it could be a mutable array that the caller could continue to mutate
commands = [commands copy];
// This queue will do all our actual work
dispatch_queue_t execQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// We'll target the main queue because it simplifies the updating of the UI
dispatch_set_target_queue(execQueue, dispatch_get_main_queue());
// We'll use this queue to serve commands one at a time...
dispatch_queue_t latchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// Have it target the execQueue; Not strictly necessary but codifies the relationship
dispatch_set_target_queue(latchQueue, execQueue);
// This timer will update our UI at 60FPS give or take, on the main thread.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (1.0/60.0) * NSEC_PER_SEC, (1.0/30.0) * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{ [self updateUI]; });
// Suspend the latch queue until we're ready to go
dispatch_suspend(latchQueue);
// The first thing to do for this command stream is to start UI updates
dispatch_async(latchQueue, ^{ dispatch_resume(timer); });
// Next enqueue each command in the array
for (DRCommand* cmd in commands)
{
dispatch_async(latchQueue, ^{
// Stop the queue from processing other commands.
dispatch_suspend(latchQueue);
// Update the "machine state"
self.currentlyExecutingCommand = cmd;
self.currentCommandStarted = @([NSDate timeIntervalSinceReferenceDate]);
// Set up the event that'll mark the end of the command.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(cmd.duration * NSEC_PER_SEC)), execQueue, ^{
// Clear out the machine state for the next command
self.currentlyExecutingCommand = nil;
self.currentCommandStarted = nil;
// Resume the latch queue so that the next command starts
dispatch_resume(latchQueue);
});
});
}
// After all the commands have finished, add a cleanup block to stop the timer, and
// make sure the UI doesn't have stale text in it.
dispatch_async(latchQueue, ^{
dispatch_source_cancel(timer);
[self updateUI];
});
// Everything is queued up, so start the command queue
dispatch_resume(latchQueue);
}
- (void)updateUI
{
// Make sure we only ever update the UI on the main thread.
if (![NSThread isMainThread])
{
dispatch_async(dispatch_get_main_queue(), ^{ [self updateUI]; });
return;
}
DRCommand* currentCmd = self.currentlyExecutingCommand;
switch (currentCmd.type)
{
case DRCommandUnknown:
self.commandNameLabel.text = @"None";
break;
case DRCommandTypeForward:
self.commandNameLabel.text = @"Forward";
break;
case DRCommandTypeBackward:
self.commandNameLabel.text = @"Backward";
break;
}
NSNumber* startTime = self.currentCommandStarted;
if (!startTime || !currentCmd)
{
self.secondsRemainingLabel.text = @"";
}
else
{
const NSTimeInterval startTimeDbl = startTime.doubleValue;
const NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
const NSTimeInterval duration = currentCmd.duration;
const NSTimeInterval remaining = MAX(0, startTimeDbl + duration - currentTime);
self.secondsRemainingLabel.text = [NSString stringWithFormat: @"%1.3g", remaining];
}
}
@end
如果有任何您想要更多解释的部分,请在评论中告诉我。
注意:这里的另一个答案是命令执行 sleep
;我的方法是完全异步的。哪种方法适合您将取决于您的命令实际上在做什么,问题中并不清楚。