Different block behavior between debug and release

2019-09-11 04:27发布

问题:

My program works perfectly. I assure you with my life, 0 bugs. Proudly, I tried to package the application as an .ipa file for ad-hoc distribution to my beta tester using TestFlight.

The program didn't work. Animations which are supposed to happen never happened. Network code breaks. The button to fade out the music beautifully didn't do anything at all.

It turns out that the culprit is the new and shiny blocks. When I test my program in the Simulator or on my device, I used the default "Debug" build configuration. But when I archive it for distribution (and I believe later for submission to the App Store), XCode uses another configuration which is "Release". Investigating further, the difference is due to the optimization level (you can find it on XCode's Build Settings): Debug uses None (-O0) but Release uses Fastest, Smallest (-Os). Little did I know, that it's Fastest, Smallest, and Doesn't Work (tm). Yes, blocks behave differently between those 2 configurations.

So, I set out to solve the problem. I've simplified my soon-to-change-the-world app into its bare bones, shown in the image I've attached to this post. The view controller has an instance variable x with initial value 0. If we press b, it will spawn a thread that will continuously check the value of x, changing the bottom label when x becomes 1. We can change the value of x using button a.

Here is my naive code (I'm using ARC btw):

@implementation MBIViewController
{
    int _x;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _x = 0;
}

- (void)updateLabel
{
    self.topLabel.text = [NSString stringWithFormat:@"x: %d", _x];
}

- (IBAction)buttonAPressed:(id)sender {
    _x = 1;
    [self updateLabel];
}

- (IBAction)buttonBPressed:(id)sender {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (_x != 1) {
            // keep observing for value change
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            self.bottomLabel.text = @"b changed me becase x changed!";
        });
    });
}

@end

_x is an instance variable, so it is reasonable to think that the block will access it using a pointer to "self", not on a local copy. And it works on the debug configuration!

But it doesn't work on Release build. So perhaps the block is using a local copy after all? OK, so let's explicitly use self:

while (self->_x != 1) {
    // keep observing for value change
}

Doesn't work either in Release. OK, so let's access the damn variable directly using pointer:

int *pointerToX = &_x;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (*pointerToX != 1) {
        // keep observing for value change
    }
    // other codes
});

Still doesn't work. This is when it dawned to me that the smart optimizing compiler assumes that there is no possible way in this multithreaded world that the result of the comparison will change, so perhaps it substituted it to be always TRUE or some other voodoo.

Now, when I use this, things start working again:

while (_x != 1) {
    // keep observing for value change
    NSLog(@"%d", _x);
}

So, to bypass the compiler optimizing out the comparison, I resorted to making a getter:

- (int)x
{
    return _x;
}

And then checking the value using that getter:

while (self.x != 1) {
    // keep observing for value change
}

It now works, because self.x is actually a call to a function and the compiler is polite enough to let the function actually do its job. However, I think this is a rather convoluted way to do something so simple. Is there any other way you would have coded it, another pattern that you will use, if you are faced with the task of "observing for change of value inside a block"? Thanks a lot!

回答1:

If you use a variable and do not modify it in a loop, the compiler optimization can cause the actual access to the variable to be optimized out, because your statement can be evaluated beforehand at compile time.

In order to prevent this, you can use the "volatile" keyword, which prevents the compiler from applying this type of optimization.

It does work with getters and setters, because then you need to send a message to your instance, which serves as a synchronization point.



回答2:

Try declaring _x as follows:

__block int _x;

Normally variables that are also used in blocks are copied. This will indicate to the compiler that if _x is modified in the block the changes should be visible outside it. It might fix your problem.