Pull to refresh

Comments 68

Method swizzling — еще одна из возможностей рунтайма, которой бывает полезно воспользоваться. Позволяет подменить любой метод своей реализацией. С сохранением возможности вызова оригинального метода. Т.е. для любого системного и своего (но в случае своих классов такие плюшки редко нужны) объекта, можно добавить свою прослойку в любой метод. Использовать с осторожностью. :)

А еще associated objects. Если простыми словами, вы можете отдать во владение любому NSObject-у другой NSObject, он его будет retain-ить и отпустит, когда будет освобождать свою память.
В рантайме много плюшек, но тут описаны как раз скрытые возможности без его использования. Так что это не «еще одна из»
В статье упоминаются class_getInstanceSize и _objc_autoreleasePoolPrint. Первый, относится к рантайму, а второй, к его скрытым возможностям.

Какие еще плюшки рантайма вы используете в разработке? Я только те две, что указал в своем первом комментарии.
В Method Swizzling очень много подводных камней.
Чаще всего программист забывает о том, что подмену метода может осуществлять не только его код, но и другие библиотеки, причем, возможно, в то же самое время в другом потоке. Тема в целом сложная, и раскрыть её в одном комментарии не получится, но скажу, что когда в одном проекте возникла необходимость в Swizzling, я просмотрел все доступные библиотеки для этого, и во всех были ошибки. Затем стал смотреть, как это реализовано в крупных библиотеках (типа ReactiveCocoa от GitHub), и везде находил всё те же баги (пример).

Короче, пришлось собрать список возможных подводных камней, и написать свою библиотеку RSSwizzle, где уж точно всё сделано как надо. :)

В общем, Swizzling штука хоть местами и необходимая, но весьма опасная. В production применять стоит крайне осторожно, только если не осталось других способов решить проблему.
Хм, а напишите статью об этом. Будет очень интересно почитать. Я когда-то делал свизлинг «в лоб», но всех проблем не учитывал.
Если писать статью, то хорошо оформлять, и с картинками, на это нужно время. Но попробую.
да статья конечно же не помешала бы… Я тоже пришел к выводу что технику swizling не стоит использовать вообще а вместо нее лучше использовать class_replaceMethod нужно использовать, но вижу вы там еще более серьезно подошли к вопросу. Однако позвольте небольшое замечание к библиотеке. Имхо библиотека должна предотвращать возможность выстрелить себе в ногу, а поэтому идея swiizling методов суперкласса мне кажется не очень хорошей. В конце концов если программисту захочется swizl-ить суперклассовые методы он может вызвать функцию для суперкласса напрямую.
associated_objects довольно тормозные, и зачастую используются в качестве костылей.
В той же статье, по вашей ссылке, явно можно обойтись без них, но многие люди начинают это использовать именно так.
Я бы вообще предложил не говорить о них вслух, дабы не популяризировать :)
Довольно тормозные по сравнению с чем?
Вобще тема хаков и скрытых возможностей полна таких вещей, которые следует использовать с осторожностью. :) На то это и хаки и скрытые возможности, а не best practice.
Тормозные относительно простого доступа/записи к свойству/ivar'у.
А если не затруднит, можно ссылку на сравнительные тесты производительности со стандартными коллекциями? (с учетом atomic/nonatomic опций).
Сильно подозреваю, что по скорости будет сравнимо с обычными properties.
Ссылку не дам, т.к. это был синтетический локальный тест в рамках этого проекта.

Но суть была примерно такая:
for (NSInteger i = 0; i < loop; i++) {
    [[[Test alloc] init] associatedObject];
}
///
- (NSObject *)associatedObject {
    static char const associatedObjectKey;
    id associatedObject = objc_getAssociatedObject(self, &associatedObjectKey);
    if (!associatedObject) {
        associatedObject = [[NSObject alloc] init];
        objc_setAssociatedObject(self, &associatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return associatedObject;
}

////////

for (NSInteger i = 0; i < loop; i++) {
    [[[Test alloc] init] ivarObject];
}

////
- (NSObject *)ivarObject {
    if (!_ivarObject) {
        self.ivarObject = [[NSObject alloc] init];
    }
    return _ivarObject;
}




Если у вас есть другая информация, то буду рад посмотреть/почитать.

З.Ы. что-то я ниасилил спойлер…
А нет, осилил, нужно было просто обновить страницу...
Не дай бог встретить в проекте безымянные методы)
Я встретил. Поэтому и написал про них ;)
«Нормальная практика», на гитхабе есть достаточно «либ» которые юзают этот «подход».
хм, навскидку не вспомнил, поиски тоже не увенчались успехом.
Помню как минимум один контрол: popup или popover, у него немало звезд, и вот такие методы у него были.
Иногда встречаются вещи типа «foo:::», «bar::» и т.п.
Тут используется для вызова reduce блока, с произвольным числом параметров.
Ну, здесь полуанонимные функции. Если бы там было
- (id):(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 :(id)obj13 :(id)obj14 :(id)obj15;

вот тогда было бы в тему.
UFO just landed and posted this here
На мой взгляд, пункты 2,6,10 — общеизвестные

Как показывает мой опыт, даже опытные разработчики могут не знать что-то из этих пунктов.

а остальные, согласно Вашим собственным ремаркам, мало применимы

«Мало применимы» не означает «бесполезны».
7. Доступ к публичным ivar-ам как в структурах

Кстати, именно по этой причине советуют проверять self в конструкторах, вида:

if (self) {
_foo = @"Bar";
}

В случае если self будет nil'ом, то получим:

self->_foo = @"Bar";

и, следовательно, разыменовывание нулевого указателя, что есть плохо.

А в случае с отправкой сообщений/доступу к свойствам эту проверку можно опустить

self.foo = @"Bar";

здесь краша уже не будет в любом случае.
self.foo = @"Bar";
Это тоже плохая практика, по своему. Во время инициализации self находится в «подвешенном» состоянии, и пока инициализация не закончена, лучше не посылать ему сообщения. Считаю что первый вариант (с ivar и проверкой self) лучше.
Don’t Message self in Objective-C init (or dealloc)
qualitycoding.org/objective-c-init/
Простите, но не соглашусь.
Что значит «подвешенное» состояние? После alloc'а у нас есть полноценный объект, который нужно заполнить актуальными данными.
Здесь нет какого-либо неопределенного поведения или сайд-эффектов.

P.S. а автор статьи по ссылке, имхо, просто льет воду не подкрепляя какими-то фактами. Я согласен что писать логику в конструкторе это не есть хорошая практика, но говорить что инициализация в инициализаторе это плохо — звучит очень бредово.

P.P.S. а может я просто не понял о чем написано в статье...
В конце статьи написано:
Having said “avoid sending messages to self in init and dealloc,” I now want to soften that statement. There are two places when it might be OK after all:

At the very end of init, and
At the very beginning of dealloc.


То есть, автор всё же не против, но «нужно думать головой» и не посылать сообщения ещё не созданному или уже удалённому объекту.
В простых случаях, таких как «чистые» геттеры в init, например, ничего плохого не может случится.

Но есть более сложные ситуации, и одна из них произошла у меня в реальном проекте. Я подробностей уже не помню, помню что в методе init я регистрировал свой класс-«одиночку» в KVO.

Вскоре обнаружилось, что в рантайме у меня создавались два инстанса синглтона. С тех пор я в init ничего лишнего не пишу.
Ну вот, т.е. все зависит от ситуации, а вы в прошлом комментарии описали догму «Don’t Message self in Objective-C init (or dealloc)».
Не знаю в чем у вас была проблема с KVO, но делать логику в конструкторе — плохо.
Мне кажется, что в данном случае проблема была в самой реализации синглтона. Если он сделан через dispatch_once и все нужные методы перегружены, то дважды быть созданным не может ну вообще никак.
Это было давно и неправда сделано через +initialize, когда не было еще ни GCD, ни блоков…
Тогда верю. Тем не менее, это лишь подтверждает сомнительность таких ограничений в современном мире.

Согласен, вносить логику в к-тор и д-тор плохо, но обычный мутатор синтезированного свойства нельзя назвать логикой.
А если кто-то, допустим, переопределит (в категории или в унаследованном классе) ваш обычный мутатор вот таким образом?

- (void) setProperty: (MyClass*) property
{
    _property = property;
    _property.delegate = self;
}


Такого не может быть?
В принципе это возможно, согласен. Но!

Что в этом реально плохого? Если установка делегата нам полюбому нужна, то её и в init написать можно. Если делегат не зануляется в dealloc, то программист сам создал грабли.
А что если делить на ноль и разыменовывать нулевой указатель?
Не юзать деление и указатели?

P.S. категории тоже сомнительная фича, имхо
А чем плохо переопределять сеттеры/геттеры? Или что в этом примере такого криминального? Мне правда интересно.
Ничего плохого, просто вы привели это как пример плохого кода, хотя с ним все в порядке, или я неправильно понял?
С самим сеттером всё в порядке. Но если этот сеттер будет вызван в init'е, то property может начать посылать сообщения не до конца инициализированному объекту, чего мы явно не предполагали.

Рекомендация (кстати, официальная от Apple) не использовать в инициализаторах сеттеры как раз и связана с тем, что они (сеттеры) могут вызывать различные побочные эффекты, которые невозможно предугадать. Тот же, кто будет переопределять сеттер, тоже не должен мучиться вопросом «а что, если он используется в инициализаторе, не приведёт ли это к использованию не до конца проинициализированного объекта?» и т.п.
Надежнее использовать переменные экземпляра.
С другой стороны встает вопрос:
«Должен ли я задать это свойство как ivar? Или я должен воспользоваться специально выделенным для этого инкапсулированным методом

Ну и зачем тогда вообще нужен сеттер? Сделать этот ivar публичным и разрешить всем в него писать, делов-то.
И не будет никаких сайд-эффектов, даже тех, которые необходимы.

И, как я уже писвл выше, на момент init'а объект уже является полноценным, нет никакого «подвешенного» состояния.

Если вы хотите меня переубедить в этом вопросе, то приведите, пожалуйста, реальные примеры из жизни, где посылка сообщений из init'а приводит к каким-то проблемам. А потом мы вместе подумаем где в этом примере ошибка.
И, как я уже писвл выше, на момент init'а объект уже является полноценным, нет никакого «подвешенного» состояния.

Думаю, неправильно говорить, что на момент вызова init объект является полноценным, поскольку его поля не инициализированы и в этом кроется проблема. Скажем, если в некотором сеттере или другом вызываемом методе идет обращение к другому ivar или property, которое еще не инициализировано, это может привести к неожиданному поведению. (более сложный случай, когда есть переопределение сеттера в потомке с обращением к другому неинициализированному полю) Без примеров, поскольку у меня нет цели вас переубедить :)

Ссылка по теме на старую статью в блоге Майка Эша.
Реальные примеры из жизни обычно в несколько тысяч строк, и на локализацию ошибки уходит не 2 минуты, так что просто приведу в коде то, о чем в ветке уже говорилось.

Программист пишет объект для работы с сетевым матчем мультиплеера, в инициализаторе использует сеттер:
@interface MatchSession : NSObject
@property (nonatomic, strong) GKMatch *match;
@end

@implementation MatchSession
- (instancetype)initWithMatch:(GKMatch *)match
{
    self = [super init];
    if (self) {
        self.match = match; // 1
    }
    return self;
}
@end


Далее, другой программист пишет наследника этого класса, в котором реализует приём и сохранение сообщений от матча.
@interface ExtendedMatchSession : MatchSession <GKMatchDelegate>
@property (nonatomic, readonly) NSMutableArray *messages;
@end

@implementation ExtendedMatchSession
- (instancetype)initWithMatch:(GKMatch *)match
{
    self = [super initWithMatch:match];
    if (self) {
        _messages = [NSMutableArray new]; // 5
        match.delegate = self;
    }
    return self;
}
- (void)setMatch:(GKMatch *)match{
    [super setMatch:match];
    match.delegate = self; // 2
}
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID{
    [_messages addObject:data]; // 4
}
@end


И имеем программиста в Apple, который реализовал GKMatch примерно так:
- (void)setDelegate:(id<GKMatchDelegate>)delegate{
    _delegate = delegate;
    for (NSData *data in self.undeliveredData){
        [delegate match:self didReceiveData:data fromPlayer:@"..."]; // 3
    }
    // ...
}


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

А если б в первом инициализаторе просто написать _match = match;, то никаких проблем бы не было.

(Если кто-то упустил, почему сообщения потеряются — следите за комментариями: "// 1" — номер указывает очередность выполнения участка кода).

P.S. Кстати, GKMatch реализован именно так — отсылает ранеее полученные сообщения в момент установки делегата.

Пример получился немного корявый, но надеюсь суть про side эффекты ясна.
mish, Yan169, я понял о чем вы)
Насколько я понимаю прямой связи между посылкой сообщений из init'а и описанным багом нет, такого же эффекта можно добиться и другим способом.
В общем проблема не в init'е, а в самом коде.
Гм, вроде из моего примера очевидно, что использование сеттера вместо ivar в инициализаторе привело к неожиданному багу.
Но если вы считаете, что в коде MatchSession всё нормально, то как тогда по-вашему должен выглядеть код ExtendedMatchSession?
Если бы он был вызван _не_ из init'а, а сразу после него

Foo *f = [Foo new];
[f setMatch:m];


то этого бы не произошло?
Опять-таки, проблема не в init'е, а в корявом flow.
Конечно не произошло бы, т.к. инициализация объекта (включая переопределенный инициализатор) полностью бы завершилась. Посмотрите пример внимательнее.
UFO just landed and posted this here
Это несерьезный костыль, нарушающий принцип подстановки Лисков. Потом первый программист будет гадать, почему в коде сессия инициализируется с матчем (через инъекцию, или фабрику, расширяя базовый класс), а в инициализатор приходит nil.
Кроме того, нарушается инкапсуляция, т.к. код рассчитан на то, что исходник родительского класса известен и останется неизменным. Но это не так, там вполне обоснованно может появиться например NSParameterAssert(match).

В конкретный момент времени создать в коде корректно работающую конструкцию конечно не проблема, но если цель — надежный и слабо связанный код без неожиданных сюрпризов из-за малейших изменений — стоит придерживаться официальных рекомендаций:
Setter methods can have additional side-effects. They may trigger KVC notifications, or perform further tasks if you write your own custom methods.

You should always access the instance variables directly from within an initialization method because at the time a property is set, the rest of the object may not yet be completely initialized. Even if you don’t provide custom accessor methods or know of any side effects from within your own class, a future subclass may very well override the behavior.
Повторюсь проблема в том, что мы имея до конца не созданный объект, вызываем некоторые методы, которые в общем случае (кроме случая, когда метод написан специально для инициализации) полагают, что объект уже полностью готов. Кроме того если в потомке такой метод будет переопределен, то сам потомок может быть не готов к тому, что его вызовут раньше (до того как сам потомок начнет инициализацию). Как раз последнее и хотел показать Yan169, если я правильно понял.
Ох, лучше уж ничего не писать о таких вещах. Знаю некоторых людей, которых хлебом не корми — дай ввернуть где-нибудь «новизну». Вчера смотрел WWDC, сегодня с горящими глазами тыкает в проект то-се, не разбирая где надо и где не очень.

Потом везде оказываются ассоциативные объекты, связь с сервером реализована через Core Data, в половине мест вместо таблиц использованы Collevtion View и т.п.
Везде бывают крайности. Истина посередине ;)
ёксель-пиксель, «связь с сервером реализована через Core Data» — это как?
вероятно имеется ввиду NSIncrementalStore
вот так. К сожалению, это принципиально возможно, если написать свой Incremental Storage.
Читал про NSIncrementalStore, но не пробовал на практике. По описанию показалась весьма рациональной реализацией клиент-серверного взаимодействия.

Буду рад узнать какие у нее подводные камни, или может быть почему концепция в целом не рабочая.
Работать-то оно будет, но глючно. Ну к примеру есть у вас RESTful API, к процедурному ж вообще модель реляционную не привяжешь. Вы, скажем, удалили какую-то сущность, и кор дата пытается подчистить и обнулить все связи, которые указывали на эту сущность. Но это давно уже сделала серверная база данных! Получается, нужно писать хардкод — и код IS от таких хардкодов становится ужасно нечитаемым.

Хотите еще? Кор дата однопоточна, поэтому при запросе к серверу вам нужно создать синхронное NSUrlConnection-соединение. А если нужна авторизация NTLM и обработка Challenge-a? Создавайте семафоры, покрывайте код критическими секциями и т.п., но кровь из носа все должно отработать в одном потоке.

Во всех абсолютно случаях это избыточно и не нужно, любой самописный «фрэймворк» с сериализацией в объекты из JSON сработает куда лучше.
// Выделяем память, заполненную нулями
void *newObject = calloc(1, class_getInstanceSize([TestObject class]));
// Задаём isa прямой записью в память
Class *c = (Class *)newObject;
c[0] = [TestObject class];
// Здесь __bridge_transfer-каст нужен для передачи объекта в ARC - иначе утечёт
obj = (__bridge_transfer TestObject *)newObject;
// Посылаем init - объект готов!
obj = [obj init];

Неактуально.

www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
On iOS for arm64, the isa field of Objective-C objects is no longer a pointer.
Say what?

On iOS for arm64, the isa field of Objective-C objects is no longer a pointer.
If it's not a pointer anymore, what is it?

Some of the bits still encode the pointer to the object's class. But neither OS X nor iOS actually uses all 64 bits of virtual address space. The Objective-C runtime may use these extra bits to store per-object data like its retain count or whether it has been weakly referenced.
Why change it?

Performance. Re-purposing these otherwise unused bits increases speed and decreases memory size. On iOS 7 the focus is on optimizing retain/release and alloc/dealloc.
What does this mean for my code?

Don't read obj->isa directly. The compiler will complain if you do. Trust the Compiler. The Compiler is your friend. Use [obj class] or object_getClass(obj) instead.

Don't write obj->isa directly. Use object_setClass() instead.

If you override +allocWithZone:, you may initialize your object's isa field to a «raw» isa pointer. If you do, no extra data will be stored in that isa field and you may suffer the slow path through code like retain/release. To enable these optimizations, instead set the isa field to zero (if it is not already) and then call object_setClass().

If you override retain/release to implement a custom inline retain count, consider removing that code in favor of the runtime's implementation.

The 64-bit iOS simulator currently does not use non-pointer isa. Test your code on a real arm64 device.
Вы правы. Если заменить прямую запись на
object_setClass((__bridge id)newObject, [TestObject class]);

будет портабельно.
тоже об этом подумал, но уже после того как написал сообщение. :(
К свойствам лучше обращаться через ".", а вот обычные методы лучше вызывать через "[]". Иначе начинает сильно страдать читаемость кода.


Спорно. Читаемость кода сильней страдает от [UIView class] (вместо UIView.class). Идемпотентные методы можно и нужно вызывать через точку.

instancetype для copy тоже надо применять осторожно, метод не всегда возвращает instancetype (-[NSMutableArray copy] вернет NSArray, например).
> Читаемость кода сильней страдает от [UIView class] (вместо UIView.class)

Это субъективно.
Справедливо. Писать надо так, как написано в coding style проекта/команды, но последнее время мне близок coding style Гитхаба.
мне уже иногда кажется, что я там работаю, половину их команды по osx/ios разработке знаю )
Насчет пункта 8. Как раз таки для init и new необязательно писать instancetype

Читать тут
Необязательно, но для соблюдения единообразия на мой взгляд лучше всё-таки писать.
Плюсую, автовывод типа для -init — по сути хак в компиляторе. Лучше писать instancetype явно.
Sign up to leave a comment.