Модульные тесты для управления памятью в Cocoa/Objective-C
Как вы могли бы написать unit test -using OCUnit, чтобы гарантировать, что объекты будут выпущены/сохранены должным образом в Cocoa/Objective-C?
Наивный способ сделать это - проверить значение retainCount
, но, конечно, вы никогда не должны использовать retainCount
. Можете ли вы просто проверить, присвоено ли объектной ссылке значение nil
, чтобы указать, что она была выпущена? Кроме того, какие гарантии у вас есть о сроках, когда объекты фактически освобождены?
Я надеюсь на краткое решение только нескольких строк кода, так как я, вероятно, буду использовать это широко. На самом деле могут быть два ответа: один, который использует пул автозапуска, а другой - нет.
Чтобы уточнить, я не ищу способ всесторонне протестировать каждый объект, который я создаю. Невозможно unit test провести какое-либо поведение всесторонне, не говоря уже о управлении памятью. По крайней мере, было бы неплохо проверить поведение выпущенных объектов для регрессионного тестирования (и убедиться, что одна и та же ошибка, связанная с памятью, не происходит дважды).
Об ответах
Я принял BJ Homer ответ, потому что я нашел, что это самый простой и сжатый способ достижения что я имел в виду, учитывая предостережение о том, что слабые указатели, снабженные Automatic Reference Counting, недоступны в производственных версиях XCode (до 4.2?) По состоянию на 23 июля 2011 года. Мне также было приятно узнать, что
ARC может быть включена для каждого файла; это не требует, чтобы ваш весь проект использует его. Вы можете скомпилировать ваши модульные тесты с помощью ARC и оставьте свой основной проект на ручном удержании, и этот тест будет все еще работают.
При этом для более детального изучения потенциальных проблем, связанных с управлением памятью модулей тестирования в Objective-C, я настоятельно рекомендую Peter Hosey подробный отклик.
Ответы
Ответ 1
Если вы можете использовать недавно введенный автоматический подсчет ссылок (еще не выпущенный в производственных версиях Xcode, но описанный здесь), то вы могут использовать слабые указатели для проверки того, что что-то было передержано.
- (void)testMemory {
__weak id testingPointer = nil;
id someObject = // some object with a 'foo' property
@autoreleasepool {
// Point the weak pointer to the thing we expect to be dealloc'd
// when we're done.
id theFoo = [someObject theFoo];
testingPointer = theFoo;
[someObject setTheFoo:somethingElse];
// At this point, we still have a reference to 'theFoo',
// so 'testingPointer' is still valid. We need to nil it out.
STAssertNotNil(testingPointer, @"This will never happen, since we're still holding it.")
theFoo = nil;
}
// Now the last strong reference to 'theFoo' should be gone, so 'testingPointer' will revert to nil
STAssertNil(testingPointer, @"Something didn't release %@ when it should have", testingPointer);
}
Обратите внимание, что это работает в ARC из-за этого изменения семантики языка:
Сохраняемый указатель объекта - это либо нулевой указатель, либо указатель на действительный объект.
Таким образом, действие установки указателя на nil гарантированно освободит объект, на который он указывает, и нет способа (при ARC) освободить объект, не удаляя указатель на него.
Следует отметить, что ARC может быть включена для каждого файла; это не требует, чтобы весь ваш проект использовал его. Вы можете скомпилировать ваши модульные тесты с помощью ARC и оставить свой основной проект на ручном удержании, и этот тест все равно будет работать.
Вышеописанное не обнаруживает чрезмерного освобождения, но это довольно легко поймать с помощью NSZombieEnabled
.
Если ARC просто не вариант, вы можете сделать что-то подобное с Mike Ash MAZeroingWeakRef
. Я не использовал его много, но он, похоже, обеспечивает аналогичную функциональность для указателей __weak обратно совместимым способом.
Ответ 2
Можете ли вы просто проверить, присвоено ли объектной ссылке значение nil
, чтобы указать, что она была выпущена?
Нет, потому что отправка сообщения release
объекту и присвоение переменной nil
- это две разные и несвязанные вещи.
Самое близкое, что вы можете получить, это то, что присвоение чего-либо сильному/сохраняющему или копирующему свойству, которое преобразуется в сообщение доступа, приводит к тому, что предыдущее значение свойства должно быть освобождено (что выполняется установщиком). Тем не менее, наблюдая за значением свойства, используя KVO, скажем, не означает, что вы узнаете, когда объект будет выпущен; в частности, когда объект-владелец освобождается, вы не получите уведомление, когда он отправит release
непосредственно принадлежащему ему объекту. Вы также получите предупреждающее сообщение на своей консоли (поскольку объект-владелец умер, когда вы его наблюдали), и вы не хотите получать шумные предупреждающие сообщения от unit test. Кроме того, вам нужно будет специально наблюдать за каждым свойством каждого объекта, чтобы вытащить этот пропущенный, и вам может не хватать ошибку.
A release
сообщение об объекте не влияет на любые переменные, указывающие на этот объект. Также не освобождается.
Это немного меняется при ARC: переменные с слабой репутацией автоматически присваиваются nil
, когда объект, на который ссылается, уходит. Тем не менее это мало вам помогает, потому что сильно привязывающие переменные по определению не будут: если есть сильная ссылка на объект, объект не будет (ну, не должен) уйти, потому что сильная ссылка будет (должен) держать его в живых. Объект, который умирает перед ним, является одной из проблем, которые вы ищете, а не тем, что вы хотите использовать в качестве инструмента.
Теоретически вы можете создать слабую ссылку на каждый объект, который вы создаете, но вам нужно будет обращаться к каждому объекту специально, создавая для него переменную вручную в вашем коде. Как вы можете себе представить, огромная боль и некоторые пропустить объекты.
Кроме того, какие гарантии у вас есть о сроках, когда объекты действительно выпущены?
Объект освобождается, отправив ему сообщение release
, поэтому объект освобождается, когда он получает это сообщение.
Возможно, вы имели в виду "освобождение". Освобождение просто приближает его к этому моменту; объект может быть выпущен много раз и все еще имеет долгую жизнь впереди, если каждый релиз просто уравновешивает предыдущее сохранение.
Объект освобождается при его освобождении в последний раз. Это происходит немедленно. Печально известный retainCount
даже не опускается до 0, как выяснилось много умных людей, которые пытались написать while ([obj retainCount] > 0) [obj release];
.
На самом деле могут быть два ответа: один, который использует пул автозапуска, а другой - нет.
Решение, использующее пул авторесурсов, работает только для объектов, которые автореализуются; по определению объекты, не автореализованные, не входят в пул. Весьма правдиво и иногда желательно, чтобы никогда не автореклассировать определенные объекты (особенно те, что вы создаете много тысяч). Более того, вы не можете заглянуть в пул, чтобы увидеть, что в нем, а что нет, или попытаться совать каждый объект, чтобы увидеть, если он мертв.
Как вы могли бы написать unit test -использование OCUnit, например, для обеспечения того, чтобы объекты были правильно выпущены/сохранены в Cocoa/Objective-C?
Лучшее, что вы можете сделать, это установить NSZombieEnabled
в YES
в setUp
и восстановить его предыдущее значение в tearDown
. Это приведет к перегрузкам/недодержкам, но не к утечкам.
Даже если вы можете написать unit test, который тщательно проверяет управление памятью, он все равно будет несовершенным, поскольку он может тестировать только тестируемые объекты модели кода и, возможно, определенные контроллеры. В вашем приложении все еще могут быть утечки и сбои, вызванные кодом вида, ссылками на nib-борделей и некоторыми опциями ( "Release When Closed" ) и т.д.
Нет никакого теста вне приложения, которое вы можете написать, что гарантирует, что ваше приложение будет без ошибок.
Тем не менее, тест, который вы представляете, будь он автономным и автоматическим, будет довольно круто, даже если он не сможет проверить все. Поэтому я надеюсь, что я ошибаюсь и есть способ.
Ответ 3
Возможно, это не то, что вы ищете, но в качестве мысленного эксперимента я задавался вопросом, может ли это сделать что-то близкое к тому, что вы хотите: что, если вы создали механизм для отслеживания поведения сохранения/выпуска для определенных объектов тестировать. Поработайте так:
- создать переопределение NSObject dealloc
- создайте
CFMutableSetRef
и настройте пользовательские функции сохранения/выпуска, чтобы ничего не делать
- выполните процедуру unit test, например
registerForRRTracking: (id) object
- выполните процедуру unit test, например
clearRRTrackingReportingLeaks: (BOOL) report
, которая сообщит о любом объекте в наборе в тот момент времени.
- вызов
[tracker clearRRTrackignReportingLeaks: NO];
в начале вашего unit test
- вызовите метод register в вашем unit test для каждого объекта, который вы хотите отслеживать, и он будет удален автоматически на dealloc.
- В конце вашего тестового вызова
[tracker clearRRTrackingReportingLeaks: YES];
и он перечислит все объекты, которые не были удалены должным образом.
вы можете переопределить NSObject alloc
и просто отследить все, но я думаю, что ваш набор будет слишком большим (!!!).
Еще лучше было бы поместить CFMutableSetRef
в отдельный процесс и, таким образом, не повлиять на объем вашей памяти во время работы слишком много. Добавляет сложность и время выполнения межпроцессного взаимодействия. Могу ли использовать частную кучу (или зону - все еще существуют?), Чтобы изолировать ее в меньшей степени.