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

Актуализация:

Както посочиха Rob и 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
Предполагам, че знаете, че вашият сетер е невалиден и очаквате да се провали. Не можете да приемете, че типовете фондации ще получат 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