10

Mam kontroler widoku, który pobiera zasób w kolejce GCD tła. Przekażę mojej funkcji pobierania blok zwrotny do wykonania po zakończeniu pobierania i zawsze wykonuje ten blok w głównym wątku.dealloc wywołana na tle aplikacji powodującej awarię kolejki GCD zbudowanej za pomocą ARC

Problem występuje, gdy mój kontroler widoku zostanie odrzucony przez użytkownika przed zakończeniem pobierania. Podejrzewam, że to się dzieje, gdy mój kontroler widoku zostanie zwolniony, blok wywołania zwrotnego jest jedyną rzeczą, która ma silne odniesienie do kontrolera. Blok wywołania zwrotnego jest zachowywany tylko w wątku tła, więc po jego zwolnieniu wszystkie obiekty przechwycone w ramach bloku wywołania zwrotnego są również wydawane, ale w kolejce w tle.

To jest problem: zwolnienie w kolejce w tle powoduje, że dealloc działa w tej samej kolejce, a nie w głównej kolejce. To z kolei wywołuje dealloc w tle i awarie aplikacji:

2012-01-19 12:47:36.349 500px iOS[4892:12107] bool _WebTryThreadLock(bool), 0x306c10: Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now... 
[Switching to process 16643 thread 0x4103] 
[Switching to process 16643 thread 0x4103] 
(gdb) where 
#0 0x307fd3c8 in _WebTryThreadLock() 
#1 0x307ff1b0 in WebThreadLock() 
#2 0x33f7865e in -[UITextView dealloc]() 
#3 0x0005d2ce in -[GCPlaceholderTextView dealloc] (self=0x309010, _cmd=0x340ce6b8) at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/GCPlaceholderTextView.m:113 
#4 0x337cac42 in -[NSObject(NSObject) release]() 
#5 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview]() 
#6 0x33e836cc in -[UIScrollView removeFromSuperview]() 
#7 0x33f762f0 in -[UITextView removeFromSuperview]() 
#8 0x33e01de2 in -[UIView dealloc]() 
#9 0x337cac42 in -[NSObject(NSObject) release]() 
#10 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview]() 
#11 0x33e01de2 in -[UIView dealloc]() 
#12 0x33f437e4 in -[UIScrollView dealloc]() 
#13 0x337cac42 in -[NSObject(NSObject) release]() 
#14 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview]() 
#15 0x33e836cc in -[UIScrollView removeFromSuperview]() 
#16 0x33e01de2 in -[UIView dealloc]() 
#17 0x337cac42 in -[NSObject(NSObject) release]() 
#18 0x33dee5f4 in -[UIView(Hierarchy) removeFromSuperview]() 
#19 0x33e01de2 in -[UIView dealloc]() 
#20 0x337cac42 in -[NSObject(NSObject) release]() 
#21 0x33e5a00e in -[UIViewController dealloc]() 
#22 0x00035f16 in -[PXPhotoViewController dealloc] (self=0x5158d0, _cmd=0x340ce6b8) at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoViewController.m:118 
#23 0x337cac42 in -[NSObject(NSObject) release]() 
#24 0x337e5046 in sendRelease() 
#25 0x331fc92e in _Block_object_dispose() 
#26 0x0003c33a in __destroy_helper_block_() at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoViewController.m:878 
#27 0x331fc88e in _Block_release() 
#28 0x331fc91c in _Block_object_dispose() 
#29 0x000c8d32 in __destroy_helper_block_() at /Users/ash/Dropbox/500px/500px-ios/500px iOS/500px iOS/PXPhotoFetcher.m:557 
#30 0x331fc88e in _Block_release() 
#31 0x35eec8ec in _dispatch_call_block_and_release() 
#32 0x35ee2de2 in _dispatch_queue_drain() 
#33 0x35ee2f32 in _dispatch_queue_invoke() 
#34 0x35ee24f2 in _dispatch_worker_thread2() 
#35 0x34ecb590 in _pthread_wqthread() 
#36 0x34ecbbc4 in start_wqthread() 

Kod mogę wykonać w głównym wątku wygląda następująco:

[[PXPhotoFetcher sharedPXPhotoFetcher] fetchPhotoDetailsWithPriority:PhotoRequestLowPriority 
    withCallback:^(PXPhotoModel *thePhotoModel) { 
     // a callback function which captures self in its scope 
    } forModel:model]; 

buduję na 4.3, więc jeśli Używam __unsafe_unretained odwołanie do siebie w bloku oddzwaniania, to naprawi mój bieżący problem, ale wprowadzi nowy problem posiadania zwisającego wskaźnika.

Co jest zalecane do zaprojektowania rozwiązania tego problemu? Inne obejścia obejmowały overriding release, więc zawsze był wywoływany w głównym wątku, ale to oczywiście nie zadziała w środowisku ARC.

Wydaje się, że powinien to być znacznie częstszy problem z ARC i GCD, ale nie mogę znaleźć niczego w Internecie. Czy to dlatego, że jestem kierowany na < iOS 5 i nie mogę używać słabych referencji?

Odpowiedz

5

AKTUALIZACJA: Poniższe rozwiązanie w rzeczywistości nie działało na iOS 4. Z jakiegoś powodu działało na 5, ale nie na 4, więc wymyśliłem lepsze rozwiązanie.

Problem polega na tym, że blok jest niszczony w tle, więc umieszczam go w zmiennej lokalnej, a następnie w bloku w tle wywołuję go, a następnie przekazuję go do bloku na głównym wątku asynchronicznie, tak, aby został tam zwolniony . Jest to także bałagan, ale wygląda następująco:

void(^block)(void) = ^{/*do all the things*/}; 

dispatch_async(queue, ^{ 

    block(); 

    dispatch_async(dispatch_get_main_queue(), ^{ 
     if ([block isKindOfClass:[NSString class]]) 
      NSLog(@"Whoa, bro"); 
    }); 
}); 

Kod na głównym wątku to tylko podstęp, aby upewnić się, że kompilator nie tylko zoptymalizować kod całkowicie z dala; Potrzebuję obiektu bloku, który ma zostać zwolniony na końcu przez główny wątek. Ten kod wydaje się działać z poziomem optymalizacji kompilatora -Os.

Więc wymyśliłem rozwiązanie mojego problemu, mimo że jest super hacky. Od czasu tej poprawki nie udało mi się odtworzyć problemu, chociaż myślę, że to bardzo kiepski projekt architektoniczny.

Podsumowując, problem polega na tym, że mam blok w kolejce w tle, która jest niszczona. Ten blok jest obiektem i posiada silne odniesienie do bloku wywołania zwrotnego, który zawiera silne odniesienie do siebie. Blok tła zostaje zwolniony z kolejki tła. Więc to, co zrobiłem, to zawijanie połączenia w innym wywołaniu wysyłki do głównej kolejki.Więc moja metoda pobierania wychodzi z tego:

dispatch_async(backgroundQueue, ^{ 
    /* do all the things */ 
}); 

do tego:

dispatch_async(dispatch_get_main_queue(), ^{ 
    dispatch_async(backgroundQueue, ^{ 
     /* do all the things */ 
    }); 
}); 

Kod asynchronicznie wywołuje blok do głównej kolejki, które następnie wywołuje blok do kolejki w tle. Ponieważ blok tła jest obiektem i należy do zakresu głównej kolejki, jest on uwalniany w głównym wątku, co powoduje również ostateczne usunięcie z UITextView powodujące awarię w głównej kolejce, a także rozwiązanie mojego problemu.

Oczywistym rozwiązaniem architektonicznym jest użycie odwołania __weak do mojego bloku wywołania zwrotnego, ale będę musiał poczekać, aż zrezygnuję z obsługi iOS 4.3.

+1

Jeśli wolisz słabe odniesienie, możesz sprawdzić http://mikeash.com/pyblog/introducing-mazeroingweakref.html – tapi

+0

To trochę brzydkie, ale to sprytna sztuczka. Chciałbym mieć lepszą odpowiedź. –

+0

Tak, popatrzyłem na rozwiązanie Mike'a Asha, ale ponieważ w krótkim okresie będę wspierać system operacyjny iOS 4.3, pozostanę z tym hackem przez jakiś czas. –

0

Twoja analiza wydaje się być dobra. Więc może koniec bloku może utrzymać kontroler i zrobić autorelease na głównym wątku (prawdopodobnie po krótkim opóźnieniu, aby uniknąć warunków wyścigu).

+0

Kompiluję z ARC, więc wywołania do zachowania i autoreasowania są nieprawidłowe. Mogłem go sfałszować za pomocą wywołania dispatch_after przechwytującego zakres bloku, ale myślę, że znalazłem lepsze rozwiązanie. –

+0

Doh. Choć może to działałoby po prostu wysłanie bloku, który ma odniesienie (myślę, że to jest to, co zrobiłeś, ale na wylot). –

0

Nigdy nie można zagwarantować, że obiekt zostanie zwolniony w dowolnym wątku, jak się dowiedziałeś.

Najlepszym rozwiązaniem, które znalazłem, jest twój dealloc, sprawdź, czy jesteś w głównym wątku. Jeśli nie, performSelector w głównym wątku z resztą dealloc i czekać na zakończenie.

Alternatywnie, zaimplementuj tę samą logikę za pomocą bloków: jeśli nie w głównym wątku, to dispatch_sync do głównej kolejki z resztą dealloc.

+0

Ale to nie zadziała w ARC, ponieważ nie mogę jednoznacznie nazwać 'dealloc', nawet' [super dealloc] ', więc nie ma możliwości przełączenia wykonania na główny wątek. –

+0

Dealloc nadal istnieje. ARC po prostu nazywa to automatycznie dla ciebie. – user1139069

2

Ash,

Twój problem jest wczesnym dealokacji jako kolejek są zburzone. Dlatego przestań to robić. W jaki sposób? Jesteś prawie na miejscu. Z kolejki tła będziesz chciał SYNCHRONICZNIE przekazać dane do kontrolera w głównym wątku. W ten sposób, gdy wszystko się rozwija, zwalniają w bezpiecznej kolejności. Na przykład:

dispatch_async(backgroundQueue, ^{ 

    /* download stuff */ 

    dispatch_sync(dispatch_get_main_queue(), ^{ 

     // Pass the data to the controller. 
    }); 
}); 

Wzór ten pozwala zagwarantować prawidłowe dostarczanie danych do dowolnego elementu UI w głównym wątku, a następnie pozwala wdzięku uwolnienia.

Andrew

Edycja dla drugiego odpowiedzieć zmiennej __block:

__block UIViewController *vc = danglingVC; 

dispatch_async(backgroundQueue, ^{ 

    /* download stuff */ 

    dispatch_async(dispatch_get_main_queue(), ^{ 

     // Pass the data to the controller. 

     vc = nil; 
    }); 
}); 

Z ARC wymusić uwolnień ustawiając silną gniazdo pamięci do zera. Zauważ, że mogę teraz dokonać asynchronicznego przeniesienia do głównego wątku. Myślę, że jest to jasne i jednoznaczne rozwiązanie twojego problemu. Nie zależy od subtelnych interakcji pomiędzy ARC, GCD i blokami.

Andrew

+1

Nie zmieni to kolejności, w której rzeczy zostaną zwolnione, tylko kolejność, w której uruchamiany jest blok wewnętrzny. –

+0

BJ, zmienia się kolejność, w której uruchamiane są rzeczy. Wywołanie synchroniczne uniemożliwia opróżnianie kolejki asynchronicznej. W związku z tym zatrzymanie na kontrolerze nie zostaje zwolnione, dopóki pobieranie nie zostanie zakończone, a dane dostarczone do kontrolera. To dramatycznie zmienia kolejność dealokacji w bezpieczne zamówienie. Andrew – adonoho

+0

@adonoho To nie jest przedwczesne wydanie na żadnym z moich obiektów, to kwestia zakresu. Nawet jeśli korzystam z twojego rozwiązania 'dispatch_sync', obiekt bloku przekazany w pierwszej linii (do' backgroundQueue') jest tym, co ostatecznie zachowuje mój kontroler widoku, i że obiekt blokowy będzie zawsze uwalniany w wątku tła, nawet przy wywołaniu synchronicznym . –

3

W rzeczywistości GCD pozwala zachować i zwolnić do tego celu kolejki wysyłkowe. To jest faktycznie udokumentowane na: "Memory Management for Dispatch Queues".

dispatch_retain(first_queue); 
dispatch_async(a_queue, ^{ 
          do_not_wait_for_me(); 
          dispatch_async(first_queue, ^{ i_am_done_now(); }); 
          dispatch_release(first_queue); 
         }); 

W scenariuszu zamienić first_queue na twoją główną kolejkę wysyłkową. Zachowując główną kolejkę wysyłkową, upewniasz się, że nie zostanie ona zwolniona do czasu, aż wywołanie zwrotne zostanie zakończone.

Inny przykład można znaleźć w: „Performing a Completion Block When a Task Is Done.

+0

To nie rozwiązuje problemu, w którym uitextview jest * deallocated * w wątku tła. –

+0

Czy możesz podać mi trochę kontekst związany z UITextView. Zarówno w przykładzie, jak i odpowiedzi nie widzę odniesienia do UITextView z wyjątkiem danych wyjściowych debugowania. Ponadto w twojej odpowiedzi wydaje się, że używasz wzorca, który sprawia, że ​​twój blok jest lokalną strukturą danych stosu. Jest to wzorzec, który ["Blocks Programming Topics: Patterns Tovoid"] (http://developer.apple.com/library/ios/#documentation/Cocoa/Conoa/Blocks/Articles/bxUsing.html#//apple_ref/doc/uid/TP40007502-CH5-SW6) odradza z powodu problemów z zakresem. – flukey

0

Alternatywą byłoby przechować odniesienie do obiektu w zakresie zewnętrznej wysyłkowy asynchronicznym autonomicznym.Na przykład:

// create a dispatch queue 
dispatch_queue_t sdq = dispatch_queue_create("com.fred.exampleQueue", NULL); 

// my object references container 
__block NSMutableArray *ressources = [[NSMutableArray alloc] init]; 

dispatch_async(sdq, ^{ 
    __block MyLoader *ml = [[MyAsyncLoader alloc] initWithCallback:^(id result) 
    { 
     NSLog(@"loader result: %@", result); 
     // since I ask for ressource in my final CallBack it's still here 
     // and my loader too, I can now dispose of it. 
     [ressources removeObject:ml]; 
    }]; 

    // perform async loading and call my callback when it's done... 
    [ml asyncLoad]; 
    // save my object 
    [ressources addObject:ml]; 
}); 
0

Ostatnio spotkałem się z podobnym problemem. Miałem luksus używania zerowania słabych referencji w ARC, aby rozwiązać ten konkretny problem. Jednak rozważyłem alternatywne rozwiązanie, które powinno działać z MRC.

__block UIViewController *vc = ...; 
[vc retain]; 
dispatch_async(backgroundQueue, ^{ 
    // ... do some things with vc 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     // ... do more things with vc 
     [vc release]; // vc might be dealloc'd here but not before 
    }); 
}); 

kwalifikator __block przechowywania w vc zapewnia, że ​​blok nie zachowuje przedmiotów określonych przez vc. Kontroler widoku jest zachowywany, dopóki blok, który zostanie wywołany w głównej kolejce, nie zwalnia go i prawdopodobnie zostanie zwolniony w tym momencie.