Pule automatycznego wydawania są wymagane do zwracania nowo utworzonych obiektów z metody. Np. Rozważ ten fragment kodu:
- (NSString *)messageOfTheDay {
return [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
}
Ciąg utworzony w metodzie będzie miał liczbę zatrzymań równą jeden. Teraz, kto zrównoważy liczbę zatrzymań z uwolnieniem?
Sama metoda? Niemożliwe, musi zwrócić utworzony obiekt, więc nie może go zwolnić przed zwróceniem.
Program wywołujący metodę? Program wywołujący nie spodziewa się pobrać obiektu, który wymaga zwolnienia, nazwa metody nie oznacza, że nowy obiekt jest tworzony, mówi tylko, że obiekt został zwrócony i ten zwrócony obiekt może być nowym wymagającym wydania, ale może bądź istniejącym, który nie. To, co metoda zwraca, może nawet zależeć od pewnego stanu wewnętrznego, więc osoba dzwoniąca nie może wiedzieć, czy musi zwolnić ten obiekt i nie powinna się tym przejmować.
Gdyby osoba dzwoniąca musiała zawsze zwalniać cały zwrócony obiekt zgodnie z konwencją, wówczas każdy obiekt, który nie został nowo utworzony, musiałby zawsze zostać zachowany przed zwróceniem go z metody i musiałby zostać zwolniony przez osobę dzwoniącą, gdy wykroczy poza zakres, chyba że jest zwracany ponownie. Byłoby to wysoce nieefektywne w wielu przypadkach, ponieważ można całkowicie uniknąć zmiany liczby zatrzymań w wielu przypadkach, jeśli dzwoniący nie zawsze zwolni zwrócony obiekt.
Właśnie dlatego istnieją pule autorelease, więc pierwsza metoda faktycznie się stanie
- (NSString *)messageOfTheDay {
NSString * res = [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
return [res autorelease];
}
Wywołanie autorelease
obiektu dodaje go do puli autorelease, ale co to tak naprawdę znaczy, dodając obiekt do puli autorelease? Cóż, oznacza to powiedzenie systemowi: „ Chcę, żebyś wypuścił ten obiekt dla mnie, ale później, nie teraz; ma liczbę zatrzymań, która musi być zrównoważona przez wydanie, inaczej pamięć wycieknie, ale nie mogę tego zrobić sam w tej chwili, ponieważ potrzebuję obiektu, aby pozostał przy życiu poza moim obecnym zasięgiem, a mój rozmówca też tego nie zrobi dla mnie, nie ma on wiedzy, że należy to zrobić. Więc dodaj go do swojej puli, a kiedy to wyczyścisz basen, posprzątaj też mój obiekt dla mnie ”.
Dzięki ARC kompilator decyduje o tym, kiedy zachować obiekt, kiedy zwolnić obiekt, a kiedy dodać go do puli autorelease, ale nadal wymaga obecności pul autorelease, aby móc zwrócić nowo utworzone obiekty z metod bez wycieku pamięci. Firma Apple właśnie dokonała sprytnych optymalizacji generowanego kodu, które czasami eliminują pule autorelease w czasie wykonywania. Te optymalizacje wymagają, aby zarówno dzwoniący, jak i odbierający korzystali z ARC (pamiętaj, że mieszanie ARC i non-ARC jest legalne i również oficjalnie obsługiwane), a jeśli tak, to w rzeczywistości można to zrobić tylko w czasie wykonywania.
Rozważ ten kod ARC:
// Callee
- (SomeObject *)getSomeObject {
return [[SomeObject alloc] init];
}
// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
Kod generowany przez system może zachowywać się podobnie do następującego kodu (jest to bezpieczna wersja, która pozwala swobodnie mieszać kod ARC i kod inny niż ARC):
// Callee
- (SomeObject *)getSomeObject {
return [[[SomeObject alloc] init] autorelease];
}
// Caller
SomeObject * obj = [[self getSomeObject] retain];
[obj doStuff];
[obj release];
(Uwaga: zatrzymanie / zwolnienie w dzwoniącym jest tylko obronnym zachowaniem bezpieczeństwa, nie jest bezwzględnie wymagane, bez niego kod byłby całkowicie poprawny)
Lub może zachowywać się tak, jak ten kod, na wypadek, gdyby oba wykryły użycie ARC w czasie wykonywania:
// Callee
- (SomeObject *)getSomeObject {
return [[SomeObject alloc] init];
}
// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
[obj release];
Jak widać, Apple eliminuje atuorelease, a więc także opóźnione uwalnianie obiektu po zniszczeniu puli, a także zachowanie bezpieczeństwa. Aby dowiedzieć się więcej o tym, jak to jest możliwe i co naprawdę dzieje się za kulisami, sprawdź ten post na blogu.
Przejdźmy teraz do właściwego pytania: po co używać @autoreleasepool
?
W przypadku większości programistów pozostał tylko jeden powód, aby używać tego konstruktu w kodzie, a mianowicie zachować niewielką powierzchnię pamięci, jeśli ma to zastosowanie. Np. Rozważ tę pętlę:
for (int i = 0; i < 1000000; i++) {
// ... code ...
TempObject * to = [TempObject tempObjectForData:...];
// ... do something with to ...
}
Załóż, że każde połączenie do tempObjectForData
może utworzyć noweTempObject
która jest zwracana automatycznie. Pętla for utworzy milion takich obiektów tymczasowych, które zostaną zebrane w bieżącej puli autorelease, a tylko po zniszczeniu tej puli wszystkie obiekty tymczasowe zostaną zniszczone. Do tego czasu w pamięci znajduje się milion takich obiektów tymczasowych.
Jeśli zamiast tego napiszesz taki kod:
for (int i = 0; i < 1000000; i++) @autoreleasepool {
// ... code ...
TempObject * to = [TempObject tempObjectForData:...];
// ... do something with to ...
}
Następnie nowa pula jest tworzona przy każdym uruchomieniu pętli for i jest niszczona na końcu każdej iteracji pętli. W ten sposób w pamięci może wisieć co najwyżej jeden obiekt tymczasowy, mimo że pętla działa milion razy.
W przeszłości często musiałeś samodzielnie zarządzać pulami autorelease podczas zarządzania wątkami (np. Za pomocą NSThread
), ponieważ tylko główny wątek automatycznie ma pulę autorelease dla aplikacji Cocoa / UIKit. Ale jest to dziś dość spuścizna, ponieważ dziś prawdopodobnie nie używałbyś wątków na początek. Używałbyś GCD DispatchQueue
lub NSOperationQueue
tych, a ci dwaj zarządzają dla ciebie pulą autorelease najwyższego poziomu, utworzoną przed uruchomieniem bloku / zadania i zniszczoną, gdy już to zrobisz.