Jak uniknąć przechwytywania siebie w blokach podczas implementacji interfejsu API?

222

Mam działającą aplikację i pracuję nad konwersją jej na ARC w Xcode 4.2. Jedno z ostrzeżeń wstępnych polega na selfsilnym uchwyceniu w bloku prowadzącym do cyklu zatrzymania. Zilustrowałem prosty przykład kodu, aby zilustrować problem. Wydaje mi się, że rozumiem, co to oznacza, ale nie jestem pewien, czy „poprawny” lub zalecany sposób realizacji tego typu scenariusza.

  • self jest instancją klasy MyAPI
  • poniższy kod jest uproszczony, aby pokazać tylko interakcje z obiektami i blokami istotne dla mojego pytania
  • Załóżmy, że MyAPI pobiera dane ze zdalnego źródła, a MyDataProcessor działa na tych danych i generuje dane wyjściowe
  • procesor jest skonfigurowany z blokami do komunikowania postępu i stanu

przykładowy kod:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Pytanie: co robię „źle” i / lub jak należy to zmienić, aby zachować zgodność z konwencjami ARC?

XJones
źródło

Odpowiedzi:

509

Krótka odpowiedź

Zamiast selfbezpośredniego dostępu , należy uzyskać do niego dostęp pośrednio, z referencji, która nie zostanie zachowana. Jeśli nie korzystasz z automatycznego zliczania referencji (ARC) , możesz to zrobić:

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Słowo __blockkluczowe oznacza zmienne, które można modyfikować wewnątrz bloku (nie robimy tego), ale także nie są one automatycznie zachowywane, gdy blok jest zachowywany (chyba że używasz ARC). Jeśli to zrobisz, musisz mieć pewność, że nic więcej nie będzie próbowało wykonać bloku po zwolnieniu instancji MyDataProcessor. (Biorąc pod uwagę strukturę kodu, nie powinno to stanowić problemu.) Przeczytaj więcej na temat__block .

Jeśli używasz ARC , semantyka __blockzmian i odniesienie zostaną zachowane, w takim przypadku powinieneś je zadeklarować __weak.

Długa odpowiedź

Powiedzmy, że masz taki kod:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

Problem polega na tym, że self zachowuje odniesienie do bloku; tymczasem blok musi zachować odniesienie do siebie, aby pobrać właściwość delegata i wysłać delegatowi metodę. Jeśli wszystko inne w aplikacji zwolni odniesienie do tego obiektu, jego liczba zatrzymań nie będzie wynosić zero (ponieważ blok wskazuje na niego), a blok nie robi nic złego (ponieważ obiekt wskazuje na niego), a więc para obiektów wycieknie na stos, zajmując pamięć, ale na zawsze nieosiągalna bez debuggera. Naprawdę tragiczne.

Ten przypadek można łatwo naprawić, wykonując następujące czynności:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

W tym kodzie self zachowuje blok, blok zachowuje delegata i nie ma żadnych cykli (stąd widoczny; delegat może zachować nasz obiekt, ale to obecnie nie jest w naszych rękach). Ten kod nie ryzykuje wycieku w ten sam sposób, ponieważ wartość właściwości delegowania jest przechwytywana podczas tworzenia bloku, a nie sprawdzana podczas jego wykonywania. Efektem ubocznym jest to, że jeśli zmienisz delegata po utworzeniu tego bloku, blok nadal będzie wysyłał wiadomości o aktualizacji do starego delegata. To, czy tak się stanie, zależy od Twojej aplikacji.

Nawet jeśli jesteś fajny z tego zachowania, nadal nie możesz użyć tej sztuczki w twoim przypadku:

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

Tutaj przekazujesz selfbezpośrednio do delegata w wywołaniu metody, więc musisz go gdzieś tam zabrać. Jeśli masz kontrolę nad definicją typu bloku, najlepszą rzeczą byłoby przekazanie delegata do bloku jako parametru:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

To rozwiązanie pozwala uniknąć cyklu przechowywania i zawsze wywołuje bieżącego delegata.

Jeśli nie możesz zmienić bloku, możesz sobie z tym poradzić . Powodem, dla którego cykl przechowywania jest ostrzeżeniem, a nie błędem, jest to, że niekoniecznie oznacza to zgubę dla twojej aplikacji. Jeśli MyDataProcessorjest w stanie zwolnić bloki po zakończeniu operacji, zanim jego rodzic spróbuje je zwolnić, cykl zostanie przerwany i wszystko zostanie poprawnie wyczyszczone. Jeśli możesz być tego pewien, to dobrym pomysłem byłoby użycie a, #pragmaaby ukryć ostrzeżenia dla tego bloku kodu. (Lub użyj flagi kompilatora na plik. Ale nie wyłączaj ostrzeżenia dla całego projektu.)

Możesz także rozważyć zastosowanie podobnej sztuczki powyżej, uznając referencję za słabą lub nie utrzymaną i używając tego w bloku. Na przykład:

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

Wszystkie trzy powyższe dadzą ci odniesienie bez zachowania wyniku, chociaż wszystkie zachowują się nieco inaczej: __weakspróbują zerować odniesienie, gdy obiekt zostanie zwolniony; __unsafe_unretainedpozostawi niepoprawny wskaźnik; __blockdoda inny poziom pośredni i pozwoli zmienić wartość referencji w obrębie bloku (w tym przypadku dpnie ma znaczenia, ponieważ nie jest nigdzie indziej używany).

To, co najlepsze, będzie zależeć od tego, jaki kod możesz zmienić, a czego nie. Ale mam nadzieję, że dostarczyło ci to kilku pomysłów, jak postępować.

benzado
źródło
1
Świetna odpowiedź! Dzięki, lepiej rozumiem, co się dzieje i jak to wszystko działa. W tym przypadku mam kontrolę nad wszystkim, więc w razie potrzeby przeprojektuję niektóre obiekty.
XJones
18
O_O Właśnie przechodziłem z nieco innym problemem, utknąłem w czytaniu, a teraz opuszczam tę stronę, czując się dobrze poinformowanym i fajnym. Dzięki!
Orc JMR,
jest poprawne, że jeśli z jakiegoś powodu w momencie wykonania bloku dpzostanie zwolniony (na przykład, jeśli był to kontroler widoku i został wyskakujący), to wiersz [dp.delegate ...spowoduje EXC_BADACCESS?
peetonn 30.01.2013
Czy właściwość przechowująca blok (np. DataProcess.progress) powinna być stronglub weak?
djskinner
1
Możesz rzucić okiem na libextobjc, który udostępnia dwa przydatne makra o nazwie @weakify(..)i @strongify(...)który pozwala na użycie selfw bloku w sposób nie zachowujący.
25

Istnieje również możliwość wyłączenia ostrzeżenia, gdy masz pewność, że cykl zostanie przerwany w przyszłości:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

W ten sposób nie musisz ćwiczyć __weak, selfaliasingu i wyraźnego prefiksu ivar.

zoul
źródło
8
Brzmi jak bardzo zła praktyka, która wymaga więcej niż 3 linii kodu, które można zastąpić __weak id slaveSelf = self;
Ben Sinclair
3
Często istnieje większy blok kodu, który może skorzystać z ukrytych ostrzeżeń.
zoul
2
Tyle że __weak id weakSelf = self;ma zasadniczo inne zachowanie niż tłumienie ostrzeżenia. Pytanie zaczęło się od „... jeśli masz pewność, że cykl przechowywania zostanie przerwany”
Tim
Zbyt często ludzie na ślepo osłabiają zmienne, tak naprawdę nie rozumiejąc konsekwencji. Na przykład widziałem, jak ludzie osłabiają obiekt, a następnie robią to w bloku: [array addObject:weakObject];jeśli słaby obiekt został zwolniony, powoduje to awarię. Oczywiście nie jest to preferowane w stosunku do cyklu przechowywania. Musisz zrozumieć, czy twój blok faktycznie żyje wystarczająco długo, aby uzasadnić osłabienie, a także czy chcesz, aby akcja w bloku zależała od tego, czy słaby obiekt jest nadal ważny.
mahboudz
14

Dla wspólnego rozwiązania mam je zdefiniować w nagłówku prekompilacji. Unika przechwytywania i nadal włącza pomoc kompilatora, unikając jego użyciaid

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

Następnie w kodzie możesz wykonać:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Damien Pontifex
źródło
Zgadzam się, może to spowodować problem w bloku. ReactiveCocoa ma inne interesujące rozwiązanie tego problemu, które pozwala kontynuować korzystanie z selftwojego bloku @weakify (self); blok id = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex
@dmpontifex to makro z libextobjc github.com/jspahrsummers/libextobjc
Elechtron
11

Uważam, że rozwiązanie bez ARC działa również z ARC, używając __blocksłowa kluczowego:

EDYCJA: Zgodnie z Informacjami o przejściu na ARC obiekt zadeklarowany z __blockpamięcią jest nadal zachowywany. Użyj __weak(preferowane) lub __unsafe_unretained(dla kompatybilności wstecznej).

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];
Tony
źródło
Nie zdawałem sobie sprawy, że __blocksłowo kluczowe unikało zachowania swojego odniesienia. Dzięki! Zaktualizowałem moją monolityczną odpowiedź. :-)
benzado
3
Według dokumentów Apple'a „W trybie ręcznego liczenia referencji __block id x; powoduje, że nie zachowuje x. W trybie ARC __block id x; domyślnie zachowuje x (tak jak wszystkie inne wartości)”.
XJones
11

Łącząc kilka innych odpowiedzi, oto, czego teraz używam dla wpisanego słabego „ja” do użycia w blokach:

__typeof(self) __weak welf = self;

Ustawiłem to jako fragment kodu XCode z prefiksem zakończenia „welf” w metodach / funkcjach, który trafia po wpisaniu tylko „my”.

Kendall Helmstetter Gelner
źródło
Jesteś pewny? Ten link i dokumentacja clang wydają się sądzić, że oba mogą i powinny być używane do przechowywania odwołania do obiektu, ale nie linku, który spowoduje zatrzymanie cyklu: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner
Z dokumentacji clang: clang.llvm.org/docs/BlockLanguageSpec.html „W językach Objective-C i Objective-C ++ zezwalamy na specyfikację __weak dla zmiennych __block typu object. Jeśli zbieranie śmieci nie jest włączone, ten kwalifikator powoduje te zmienne należy zachować bez wysyłania komunikatów zatrzymujących. ”
Kendall Helmstetter Gelner
6

ostrzeżenie => „przechwycenie siebie w bloku może prowadzić do zatrzymania cyklu”

gdy odwołujesz się do siebie lub jego własności w bloku, który jest silnie zachowywany przez siebie, niż pokazuje to powyżej ostrzeżenia.

więc aby tego uniknąć, musimy zrobić z tego tydzień ref

__weak typeof(self) weakSelf = self;

więc zamiast używać

blockname=^{
    self.PROPERTY =something;
}

powinniśmy użyć

blockname=^{
    weakSelf.PROPERTY =something;
}

uwaga: cykl zachowania występuje zwykle wtedy, gdy dwa obiekty odnoszące się do siebie, przez które oba mają liczbę referencji = 1 i ich metoda delloc nigdy nie zostanie wywołana.

Anurag Bhakuni
źródło
-1

Jeśli masz pewność, że kod nie utworzy cyklu przechowywania lub że cykl zostanie przerwany później, najprostszym sposobem wyciszenia ostrzeżenia jest:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

[self dataProcessor].progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

[self dataProcessor].completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

Powodem tego jest fakt, że podczas gdy dostęp do właściwości za pomocą kropek jest uwzględniany w analizie Xcode, a zatem

x.y.z = ^{ block that retains x}

jest postrzegany jako mający zachowanie x przez y (po lewej stronie przypisania) i y przez x (po prawej stronie), wywołania metod nie podlegają tej samej analizie, nawet jeśli są wywołaniami metod dostępu do właściwości które są równoważne z dostępem punktowym, nawet jeśli te metody dostępu do właściwości są generowane przez kompilator, więc w

[x y].z = ^{ block that retains x}

tylko prawa strona jest postrzegana jako tworzenie zatrzymania (przez y x) i nie jest generowane ostrzeżenie o cyklu zatrzymania.

Ben Artin
źródło