Pull to refresh

Comments 50

Внезапно читая в тексте упоминание dtrace – подсознательно ожидал увидеть упоминание darkproger'а. Так и вышло.

А вообще – отличное расследование, но я так и не уловил конец. Кто таки вызывал -resizeToContents неправильно?
Вызывал кто-то глубоко в коде, не используя результат вызова (прямо как в примере). А причина, почему вызывалось через performSelector — скорее всего из-за использования приватного метода. Проще, наверное было сделать так, чем поправить библиотеку, вынести метод в заголовок, сапдейтиться и использовать правильный метод. Кто знает ;)
Я еще поищу, но там было что-то вроде
if ([label respondsToSelector:@selector(resizeToContents)]) {
   [label resizeToContents];
}
if ([label respondsToSelector:@selector(resizeToContents)]) {
   [label performSelector:@selector(resizeToContents)];
}
>Мораль: Читайте документацию внимательно.

Мораль на самом деле: в некоторых языках можно выстрелить себе в ногу из любого положения и даже с закрытыми глазами. Более того, в ряде из них вы обязательно это сделаете, даже если не будете трогать пистолет.
Зря вы так, прекрасный язык. Сочетает в себе лучшие черты SmallTalk'а и C. Скорость и типобезопасность, соответственно.
Разве нет?

((void (*)(void))0)(); // no magic here ,)
Ну да, эта статья как раз об этом.
Про типобезопасность Objective C шутка, надеюсь?
Даже «соответственно» написал))
Мда, если кто-то в вашем присутствии подумает вслух «А вдруг я программист приложений для IOS?», просто дайте ему ссылку на этот топик
1) Т.е. resizeToContents вызывался через performSelector:? Из статьи это не очевидно.

2) Использовать performSelector для методов, возвращающих что-то кроме id нельзя.
В таком случае было бы логично, если б реализация performSelector проверяла NSMethodSignature селектора и использовала NSInvocation когда нужно.
Странно, что по-умолчанию так не сделано — вместо этого имеем грабли которые так лежат, что на них просто нельзя не наступить.
Первый вопрос снимается. Ответ нашел.
Ну как бы там id написан, потому и не проверяет. Хотя что мне в Obj-C не нравится — там много неопределённого поведения в тех местах, где нужно было бы кинуть исключение.
Точно, id в документации написан. Тогда исключение было логично.
Вообще, по моим ощущениям, @selector(...) в сигнатуре увеличивает вероятность ошибки при вызове метода на порядок. Как, собственно, и любая другая динамика, мешающая компилятору проверить корректность кода.
Дешевле (в плане быстродействия) возложить эти проверки на разработчика, также как к примеру со всякими memcpy, strcpy и т.п.
А разве XCode не давал warning на performSeletor? Возвращает-то он id, а id присваивается float'у. Неявное преобразование типов.
Насколько я понял -performSeletor: вызывался для метода, который возвращает CGFloat без прививания переменной. В этом случае XCode не выдает даже warning'а. Что то вроде:

[self performSelector:@selector(selectorReturnCGFloat)];
Там возвращаемое значение ничему не присваивалось, см. habrahabr.ru/post/161921/#comment_5560051. Потому и не было warning'а. Было бы неплохо, если б хотя бы статический анализатор предупреждал о такой ошибке.
Интересно, выдавал ли предупреждение анализатор?
Шикарная статья! Великолепный подход к поиску багов!

Получается вся проблема лечится использованием NSInvocation вместо селектора? :)
А можно всегда в подобных конструкциях:
if ([label respondsToSelector:@selector(resizeToContents)]) {
   [label resizeToContents];
}

использовать NSInvocation? Просто, чтобы перестраховываться.
Конкретно в этом случае — не надо.
Надо в случае такого кода:
if ([label respondsToSelector:@selector(resizeToContents)]) {
   [label performSelector:@selector(resizeToContents)];
}


А в общем случае, нужно просто смотреть, что возвращает метод
Получается, ошибка не в
[label respondsToSelector:selector(resizeToContents)]

а в
[label performSelector:selector(resizeToContents)];

?
И почему бы не использовать
[label resizeToContents]

?
А, все, увидел про приватный метод. Спасибо.
И место ошибки тоже понял.
Извиняюсь за глупые вопросы.
Прекрасное детективное расследование ;)

P.S. Сам как-то 2 дня убил на бодание с багом в большом приложении, которое в итоге свелось к багу в gcc, из-за которого он неудачно оптимизировал деление на константу на PPC архитектуре (через несколько умножений / сложений / других операций). С -O0 все работало, с -O2 оптимизатор преставлял команды так, что деление происходило уже не верно, а приводило это все к тому, только в одном единственном месте, size от std::vector-а начинал возвращать бред, и дальше по цепочке. Это было, на OS X 10.3 еще кажется, на PowerPC G4 маках. Более свежие версии gcc потом этот баг уже исправляли, но было весело все равно.

Занятно, есть ряд неточностей, но всё равно неплохо.
Правда имхо стоит еще написать почему не работает на симуляторе, а работает на устройстве.
Это связано с тем что в arm совершенно другая модель работы с float'ами.
Там используется набор регистров (S0..16, D0..16 или D0..32, если есть расширенный SIMD), и нет никакого fpu-стека, поэтому неиспользование значения после вызова функции никак не влияет на состояние VFP. В любом случае, в регистре остаётся результат, даже если прочитали возвращаемое значение.
interface UIView (SFAdditions) /** Handy getters and setters */ @property (nonatomic, assign) CGFloat width; @property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGFloat left; @property (nonatomic, assign) CGFloat right; @property (nonatomic, assign) CGFloat bottom; @property (nonatomic, assign) CGFloat top; @property (nonatomic, assign) CGSize size; end

Безотносительно бага (который, кстати, сказать, не был бы возможен, если с самого начала читать доки), за вышеприведнный код я бы руки отрывал. Нельзя делать категории к системным объектам со столь общими именами. Добавляйте префикс (постфикс, что угодно) к именам методов. Иначе на хабре будут и дальше детективные раследования.
А вы наверное без ошибок пишете идеальный код? Завидую вам, я так не умею :-(
Еще полезно код форматировать как код.
RTFM

Занятный материал. Самое интересное, как такое обходить и не только в objective-c.
Очень «просто» обходить — включать мозг и читать документацию.
Если бы этого было достаточно, то мы бы жили в идеальном мире.
О! Поди и без багов можно также писать? Научите, а?

Вера в непокобелимость людей и восприятие их как роботов, которые не подвержены челофактору — есть первый шаг на пути в п… ц.
Очень «просто» исправлять.
Обходить это не помогает, т.к. постоянная концентрация на 100% человеку не под силу.
Если бы все возможные факторы риска были записаны в таблицу в памяти, по которой в каждой точке можно бы было сверять… Но объем оперативных данных в мозгу слишком мал для такой задачи :-)
как хорошо, что я не программирую на Objective C
Замечательная статья!

Богатый рантайм ObjC даёт питательную почву для всевозможных «магических» багов.

Кстати, фундаментальный подход к их обнаружению — редкость в наши дни. Даже в lldb/gdb многие не пользуются функциями типа «mem r -c 512», не говоря уже о просмотре регистров.
отличное расследование, учтём.
Классическая ошибка. Не первый раз читаю расследование на подобную тему, но эта была занимательная! Хороший детективчик :)
О, у меня как раз сейчас тоже в коде, доставшемся от индусов, периодически во фрэйме ячейки NAN случается. Правда, NSLog не спасает. Поищу возможные performSelector-ы и поменяю на прямой вызов методов. Спасибо!
Вот такой подход к решению проблем и отличает быдлокодеров от настоящих Разработчиков.
А никого не смущает тот факт, что метод resizeToContents вообще возвращает значение? Судя по названию метода я бы сказал, что данный метод вообще не должен возвращать значений.

Но расследование интересное, да.
Более того, смущает еще тот факт, что это было обращение к методу дочернего класса, через указатель на класс базовый.
UFO just landed and posted this here
Спасибо. Радует, что есть еще настоящие программисты, а не «работает и по...»)

Напомнило как для PS2 игры искал баг с «иногда разворачивающимся рандомно мотоциклом»:
после недели логгирования (уменьшающего вероятность бага) оказалось, что компилятор в коде прерывания стримящегося звука использовал float регистры (не восстанавливаются после прерывания) тупо для более быстрого копирования мелких целочисленных структур =)
а еще есть одна штука с CGRect интересная:

When accessing the x, y, width, or height of a CGRect, always use the CGGeometry functions instead of direct struct member access. From Apple's CGGeometry reference:

All functions described in this reference that take CGRect data structures as inputs implicitly standardize those rectangles before calculating their results. For this reason, your applications should avoid directly reading and writing the data stored in the CGRect data structure. Instead, use the functions described here to manipulate rectangles and to retrieve their characteristics.
For example:

CGRect frame = self.view.frame;

CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);

Not:
CGRect frame = self.view.frame;

CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;



github.com/NYTimes/objective-c-style-guide советую почитать
Ну это смотря откуда его выковыривать. Особенно «доставляет» выковыривать frame из UIView, который приводит прямоугольник с положительным ширине и высоте. При этом меняя origin, да.
Спасибо огромное за статью. Очень похоже что я нарвался на этот же баг, но performSelector-а ни одного во всем проекте нет. Пока что добавлю NSLog. Даже не знаю как быть — в проекте полно сторонних либ. Сам падение получить не могу, анализирую логи с крашлитикса.
Sign up to leave a comment.

Articles