Различное поведение блоков между конфигурацией отладки и выпуска

Проблемное приложение, упрощенное

Моя программа работает отлично. Уверяю вас своей жизнью, 0 ошибок. С гордостью я попытался упаковать приложение в виде файла.ipa для специального распространения на своем бета-тестере с помощью TestFlight.

Программа не работает. Анимации, которые должны происходить, никогда не случались. Разрывы сетевого кода. Кнопка для красивого затухания музыки вообще ничего не делала.

Оказывается, виновником являются новые и блестящие блоки. Когда я тестировал свою программу в симуляторе или на своем устройстве, я использовал стандартную конфигурацию сборки "Debug". Но когда я заархивирую его для распространения (и, как мне кажется, позже для отправки в App Store), XCode использует другую конфигурацию, которая называется "Release". Дальнейшее расследование связано с уровнем оптимизации (вы можете найти его в настройках сборки XCode): в Debug используется None (-O0), а в Release используется Fastest, Smallest (-Os). Мало ли я знал, что это самый быстрый, самый маленький и не работает (тм). Да, блоки ведут себя по-разному между этими двумя конфигурациями.

Итак, я решил решить проблему. Я упростила свое приложение, которое скоро изменит мир, в его голые кости, показанные на картинке, которую я прикрепил к этой записи. Контроллер представления имеет переменную экземпляра x с начальным значением 0. Если мы нажмем b, он создаст поток, который будет непрерывно проверять значение x, изменяя нижнюю метку, когда x становится 1. Мы можем изменить значение x с помощью кнопки. а.

Вот мой наивный код (кстати, я использую ARC):

@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 - это переменная экземпляра, поэтому разумно думать, что блок будет обращаться к ней, используя указатель на "self", а не на локальную копию. И это работает на отладочной конфигурации!

Но это не работает в Release build. Так что, возможно, блок все-таки использует локальную копию? ОК, так что давайте явно используем self:

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

Не работает ни в Release. Хорошо, давайте обратимся к переменной damn напрямую, используя указатель:

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

Все еще не работает. Именно тогда мне пришло в голову, что интеллектуальный оптимизирующий компилятор предполагает, что в этом многопоточном мире нет никакого способа изменить результат сравнения, поэтому, возможно, он заменил его на ИСТИНА или какое-то другое вуду.

Теперь, когда я использую это, все снова начинает работать:

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

Итак, чтобы обойти компилятор, оптимизирующий сравнение, я прибег к созданию геттера:

- (int)x
{
    return _x;
}

И затем проверка значения с помощью этого геттера:

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

Теперь это работает, потому что self.x на самом деле является вызовом функции, а компилятор достаточно вежлив, чтобы позволить функции фактически выполнять свою работу. Тем не менее, я думаю, что это довольно запутанный способ сделать что-то настолько простое. Есть ли другой способ, которым вы бы его закодировали, другой шаблон, который вы будете использовать, если столкнетесь с задачей "наблюдения за изменением значения внутри блока"? Большое спасибо!

2 ответа

Решение

Если вы используете переменную и не изменяете ее в цикле, оптимизация компилятора может привести к тому, что фактический доступ к переменной будет оптимизирован, поскольку ваш оператор может быть предварительно оценен во время компиляции.

Чтобы предотвратить это, вы можете использовать ключевое слово "volatile", которое запрещает компилятору применять этот тип оптимизации.

Он работает с геттерами и сеттерами, потому что тогда вам нужно отправить сообщение вашему экземпляру, который служит точкой синхронизации.

Попробуйте объявить _x следующим образом:

__block int _x;

Обычно переменные, которые также используются в блоках, копируются. Это будет указывать компилятору, что если _x изменено в блоке, изменения должны быть видны вне его. Это может решить вашу проблему.

Другие вопросы по тегам