NSNumber и другие не освобождаются после выпуска

Я пытаюсь узнать больше о ручном подсчете ссылок. Я создал класс с переменной экземпляра и ее геттером/сеттером, который использует ручной выпуск:

@interface Foo: NSObject {
    id bar;
}
- (id)bar;
- (void)setBar:(id)value;
@end

@implementation Foo
- (id)bar {
    return bar;
}

- (void)setBar:(NSNumber*)value {
    [bar release];
    bar = value;
}
@end

Затем я использую его следующим образом:

int main() {

    Foo *f = [[Foo alloc] init];
    NSNumber *n = [[NSNumber alloc] initWithInteger: 3];

    [f setBar: n];
    [n release];

    NSLog(@"%@\n", [[f bar] description]);

    return 0;
}

Я скомпилировал его с помощью Address Sanitizer и ожидаю, что он будет ловить, поскольку [[f bar] description] должен вызывать метод для освобожденного объекта:

clang -fno-objc-arc -fsanitize=address -g -framework Foundation main.m
ASAN_OPTIONS=detect_leaks=1

Но к моему удивлению работает нормально!

2018-04-08 18:18:38.470300-0400 a.out[3457:291626] 3

Затем я попробовал это с NSDate вместо этого, чтобы увидеть, имеет ли это значение:

NSDate *n = [[NSDate alloc] init];

[c setBar: n];
[n release];

И это делает:

ASAN:DEADLYSIGNAL
=================================================================
==3379==ERROR: AddressSanitizer: SEGV on unknown address 0x7fff0b800018 (pc 0x7fff577f3e9d bp 0x7ffee4491670 sp 0x7ffee44915f8 T0)
==3379==The signal is caused by a READ memory access.
    #0 0x7fff577f3e9c in objc_msgSend (libobjc.A.dylib:x86_64h+0x6e9c)
    #1 0x7fff5841b014 in start (libdyld.dylib:x86_64+0x1014)
...

Я просмотрел метод dealloc NSNumber и обнаружил, что он не вызывается:

@interface Dealloc: NSObject
@end

@implementation Dealloc
- (void)dealloc {
    NSLog(@"dealloc");
}
@end

...

method_exchangeImplementations(class_getInstanceMethod([Dealloc class], @selector(dealloc)),
                               class_getInstanceMethod([NSNumber class], @selector(dealloc)));

2018-04-08 18:18:38.470300-0400 a.out[3457:291626] 3

По сравнению с NSDate:

2018-04-08 18:21:03.899490-0400 a.out[3483:293512] dealloc
2018-04-08 18:21:03.900538-0400 a.out[3483:293512] dealloc
2018-04-08 18:21:03.900598-0400 a.out[3483:293512] dealloc

Затем я попробовал это с кучей других классов:

  • NSString не освобождается
  • NSMeasurement выделяется
  • NSArray не освобождается
  • NSDictionary не освобождается
  • NSData не освобождается
  • NSURL освобождается

Что тут происходит? Почему только некоторые из них подхватываются Address Sanitizer

Обновлять:

Как указали Роб и rmaddy, с небольшими целочисленными значениями для NSNumber он хранится как помеченный указатель, который фактически не выделяет никакой дополнительной памяти:

(lldb) p/t n (__NSCFNumber *) $1 = 0b0000000000000000000000000000000000000000000000000010101000100111 (int)42

Последний бит установлен в 1, указывая на то, что он помечен, и все, кроме первых 4 битов, является целым числом 42.

Для постоянной строки (__NSCFConstantString) с [[NSString alloc] initWithString:@"blah"] это не похоже на помеченный указатель, поскольку последний бит не установлен, но это все же странный адрес:

(lldb) p n
(__NSCFConstantString *) $1 = 0x0000000100002068 @"blah"
(lldb) p/t n
(__NSCFConstantString *) $2 = 0b0000000000000000000000000000000100000000000000000010000001101000 @"blah"

NSDate и NSMeasurement все отображаются в «нормальном» диапазоне адресов> 0x600000000000.

Пустой NSArray имеет адрес в этом диапазоне, но не освобождает его, но заполняет его объектами:

[[NSArray alloc] initWithObjects:@2, @3, @5, nil]

person Luke    schedule 08.04.2018    source источник
comment
Попробуйте NSNumber с большим произвольным значением, например 65465.23424. Маленькие целые числа специально оптимизированы. То же самое со статическими значениями NSString.   -  person rmaddy    schedule 09.04.2018
comment
@rmaddy [[NSNumber alloc] initWithFloat:65465.23424] освобождается. Существуют ли другие специальные оптимизации, такие как какой-то пул для NSString и других типов коллекций?   -  person Luke    schedule 09.04.2018
comment
Почти любое использование @"..." является статической строкой, которая не будет освобождена. Не уверен насчет типов коллекций.   -  person rmaddy    schedule 09.04.2018
comment
Я предполагаю, что вы знаете, что ваш сеттер недействителен, и вы ожидаете, что он потерпит неудачу. Вы не можете предполагать, что типы Foundation получат release. Вы можете только предположить, что они будут управлять памятью так, как если бы они это делали (т. е. они не будут утекать, но никогда не обещают, что вы выйдете из строя). Небольшие целые числа, пустые коллекции и статические строки имеют особый регистр. Я думаю, что на 64-битных платформах почти все целые числа являются помеченными указателями. Я считаю, что короткие строки также могут стать помеченными указателями. Просто нет никаких обещаний, что release или dealloc действительно будут вызываться, только то, что это сработает.   -  person Rob Napier    schedule 09.04.2018
comment
Введение в теговые указатели: mikeash.com/pyblog/   -  person Rob Napier    schedule 09.04.2018
comment
@RobNapier Да, похоже, это указатель с тегом! Для [[NSNumber alloc] initWithInt: 42] я получаю (lldb) p/t n (__NSCFNumber *) $1 = 0b0000000000000000000000000000000000000000000000000010101000100111 (int)42 Теперь я буду изучать другие типы.   -  person Luke    schedule 09.04.2018


Ответы (1)


Если вы хотите научиться ручному сохранению, есть две важные детали:

  1. Не пытайтесь использовать для этого какие-либо классы фреймворка, за исключением NSObject. Подкласс NSObject и проведите свои эксперименты над указанным подклассом. Объекты фреймворка часто будут иметь всевозможные странные детали реализации, которые будут... удивительны.
  2. Не используйте retainCount или CFGetRetainCount(). Полученное значение не имеет смысла в реальном мире. Думайте об удержании как о дельтах; вы заставляете его увеличиваться, и для каждого увеличения вы должны вызывать его уменьшение.

Кроме того, изучите область памяти в отладчике Xcode.

person bbum    schedule 09.04.2018