Jak uniknąć przechwytywania self w blokach podczas wdrażania API?

Mam działającą aplikację i pracuję nad przekonwertowaniem jej do ARC w Xcode 4.2. Jedno z ostrzeżeń pre-check polega na przechwyceniu self silnie w bloku prowadzącym do cyklu zatrzymania. Zrobiłem prosty przykład kodu, aby zilustrować problem. Wierzę, że rozumiem, co to oznacza, ale nie jestem pewien, "poprawny" lub zalecany sposób realizacji tego typu scenariusza.

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

Próbka kodu:

// 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 zmodyfikować, aby było zgodne z konwencjami ARC?

Author: XJones, 2011-10-21

8 answers

Krótka odpowiedź

Zamiast uzyskiwać bezpośredni dostęp do self, powinieneś uzyskać do niego pośredni dostęp, z referencji, która nie zostanie zachowana. Jeśli nie używasz automatycznego zliczania referencji (ARC) , możesz to zrobić:

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

Słowo kluczowe __block oznacza zmienne, które można modyfikować wewnątrz bloku (nie robimy tego), ale również nie są one automatycznie zachowywane, gdy blok jest zachowany (chyba że używasz ARC). Jeśli to zrobisz, musisz być pewien, że nic innego spróbuje wykonać blok po zwolnieniu instancji MyDataProcessor. (Biorąc pod uwagę strukturę twojego kodu, nie powinno to stanowić problemu.) Czytaj więcej o __block.

Jeśli używasz ARC , semantyka __block Zmienia się i odniesienie zostanie zachowane, w takim przypadku powinieneś zadeklarować je __weak.

Długa odpowiedź

Powiedzmy, że masz taki kod:

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

Problem polega na tym, że jaźń zachowuje odniesienie do bloku; tymczasem blok musi zachować odniesienie do self, aby pobrać swoją właściwość delegata i wysłać delegatowi metodę. Jeśli Wszystko inne w aplikacji wyda swoje odniesienie do tego obiektu, jego liczba zatrzymań nie będzie równa zeru (ponieważ blok wskazuje na niego) i blok nie robi nic złego (ponieważ obiekt wskazuje na niego), a więc para obiektów wycieknie do sterty, zajmując pamięć, ale na zawsze nieosiągalny bez debuggera. Tragiczne, naprawdę.

Tę sprawę można łatwo naprawić, robiąc to zamiast tego:

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

W tym kodzie self zatrzymuje blok, blok zatrzymuje delegata i nie ma cykli (widocznych stąd; delegat może zatrzymać nasz obiekt, ale to już nie zależy od nas). Ten kod nie będzie ryzykował wycieku w ten sam sposób, ponieważ wartość właściwości delegata jest przechwytywana podczas tworzenia bloku, a nie sprawdzana podczas wykonywania. Efektem ubocznym jest to, że jeśli zmienisz delegat po utworzeniu tego bloku 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 nie miałeś nic przeciwko temu zachowaniu, nadal nie możesz użyć tej sztuczki w Twoim przypadku: {]}

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

Tutaj przekazujesz self bezpośrednio do delegata w wywołaniu metody, więc musisz go gdzieś tam umieścić. Jeśli masz kontrolę nad definicją typu bloku, najlepiej byłoby przekazać delegat do bloku jako parametr:

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

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

Jeśli nie możesz zmienić bloku, możesz sobie z nim poradzić. Powodem, dla którego cykl zachowywania jest ostrzeżeniem, a nie błędem, jest to, że niekoniecznie oznacza doom dla Twojej aplikacji. Jeśli MyDataProcessor jest w stanie zwolnić bloki po zakończeniu operacji, zanim jego rodzic spróbuje je zwolnić, cykl zostanie przerwany i wszystko będzie dobrze posprzątane. Jeśli możesz być tego pewien, to właściwą rzeczą do zrobienia byłoby użycie #pragma do tłumienia ostrzeżeń dla tego bloku kodu. (Lub użyć znacznika kompilatora per-file. Ale nie wyłączaj ostrzeżenia dla całego projektu.)

Można również przyjrzeć się użyciu podobnego triku powyżej, deklarując odniesienie słabe lub niezabezpieczone i używając go 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 zatrzymywania wynik, choć wszystkie zachowują się nieco inaczej: __weak spróbuje zerować odniesienie, gdy obiekt zostanie zwolniony; __unsafe_unretained pozostawi nieprawidłowy wskaźnik; __block faktycznie doda kolejny poziom indrection i pozwoli Ci zmienić wartość odniesienia z wewnątrz bloku (nieistotne w tym przypadku, ponieważ dp nie jest używane nigdzie indziej).

To, co jest najlepsze zależy od tego, jaki kod możesz zmienić, a czego nie. Ale mam nadzieję, że to mam kilka pomysłów, jak to zrobić.

 508
Author: benzado,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:28

Istnieje również opcja, aby stłumić ostrzeżenie, 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 trzeba małpować wokół z __weak, self aliasing i wyraźne prefiksy ivar.

 24
Author: zoul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-14 07:51:08

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

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

Następnie w kodzie możesz zrobić:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
 14
Author: dmpontifex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-21 00:11:39

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

EDIT: po przejściu do notatek do wydania ARC obiekt zadeklarowany z __block storage jest nadal zachowany. Użyj __weak (preferowane) lub __unsafe_unretained (dla zgodnoś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];
 11
Author: Tony,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-24 15:22:42

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

__typeof(self) __weak welf = self;

Ustawiłem to jako fragment kodu XCode z przedrostkiem "welf" w methods/functions, który uderza po wpisaniu tylko "my".

 11
Author: Kendall Helmstetter Gelner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:30:49

W tym samym czasie, w ciągu ostatnich kilku lat, udało nam się osiągnąć sukces.]}

Kiedy odwołujesz się do jaźni lub jej własności wewnątrz bloku, który jest silnie zachowywany przez jaźń, niż pokazuje to powyżej Ostrzeżenie.

Więc aby go uniknąć musimy zrobić to 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 zachowywania występuje zwykle, gdy niektóre jak dwa obiekty odnoszące się do siebie, przez które oba mają odniesienie count =1 i ich metoda delloc nigdy nie jest wywoływana.

 5
Author: Anurag Bhakuni,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-06 04:00:00

Nowym sposobem na to jest użycie @weakify i @ strongify marco

@weakify(self);
[self methodThatTakesABlock:^ {
    @strongify(self);
    [self doSomething];
}];

Więcej informacji o @ Weakify @ Strongify Marco

 0
Author: Jun Jie Gan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-26 09:03:09

Jeśli jesteś pewien, że Twój kod nie utworzy cyklu zachowywania lub że cykl zostanie przerwany później, najprostszym sposobem na wyciszenie 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, dla którego to działa, jest to, że podczas gdy dostęp do kropek właściwości jest brany pod uwagę przez Analizę Xcode, a zatem

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

Jest postrzegane jako posiadające zachowanie przez X Z y (po lewej stronie przypisania) i przez y Z x (po prawej stronie), wywołania metod nie podlegają tej samej analizie, nawet jeśli są wywołania metody property-access, które są równoważne dot-access, 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 tworząca zachowywanie (przez y Z X) i nie jest generowane ostrzeżenie o cyklu zachowywania.

 -1
Author: Ben Artin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-07 06:08:58