dealloc在使用ARC构建的后台GCD队列崩溃应用程序上调用

时间:2012-01-19 18:14:13

标签: objective-c ios memory-management automatic-ref-counting grand-central-dispatch

我有一个视图控制器,可以在后台GCD队列中下载资源。我将下载函数传递给回调块,以便在下载完成后执行,并且它总是在主线程上执行此块。

如果我的视图控制器在下载完成之前被用户解除,则会出现问题。我怀疑发生了什么,一旦我的视图控制器被解除,回调块是唯一保留对控制器的强引用的东西。回调块仅保留在后台线程中,因此一旦释放,回调块范围内捕获的所有对象也将被释放,尽管在后台队列中。

这就是问题:在后台队列中释放会导致dealloc在同一队列中运行,而不是在主队列中运行。反过来,这会在后台调用dealloc并且应用程序崩溃:

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 ()

我在主线程上执行的代码如下所示:

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

我正在构建4.3,所以如果我在回调块中使用__unsafe_unretained引用self,这将解决我当前的问题,但引入了一个悬空指针的新问题。

您建议为此构建解决方案是什么?其他解决方法包括overriding release,因此它总是在主线程上调用,但这显然不适用于ARC环境。

这似乎应该是ARC和GCD更常见的问题,但我在网上找不到任何东西。是因为我的目标是< iOS 5并不能使用弱引用?

7 个答案:

答案 0 :(得分:5)

更新:以下解决方案实际上并没有在iOS 4上运行。由于某种原因,它在5上工作,但不是4,所以我想出了一个更好的解决方案。

问题是由于块在后台被销毁引起的,所以我将它放入一个局部变量中,并在后台块中调用它,然后异步地将它传递给主线程上的块,以便在那里释放它。它也很混乱,但看起来如下:

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");
    });
});

主线程上的代码只是一个技巧,可以确保编译器不仅仅完全优化代码;我需要主线程最后释放块对象。此代码似乎与-Os编译器优化级别一起使用。

所以我想出了一个解决我的问题的办法,虽然这是超级hacky。自从这次修复以来,我一直无法重现这个问题,尽管我认为这是一个非常糟糕的建筑设计。

总结一下,问题是我在后台队列中有一个被销毁的块。该块是一个对象,它拥有对回调块的强引用,该回调块包含对self的强引用。后台块正从后台队列中释放。所以我所做的就是在另一个调度调用中将调用包装到主队列中。所以我的fetch方法就是这样的:

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

对此:

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

代码将一个块异步调度到主队列,然后主队列将一个块调度到后台队列。由于后台块是一个对象并且属于主队列的作用域,因此它在主线程上释放,导致UITextView的最终解除分区导致崩溃在主队列上发生,同时解决了我的问题。 / p>

明显的架构解决方案是在我的回调块中使用__weak自我引用,但我必须等到我放弃对iOS 4.3的支持。

答案 1 :(得分:3)

实际上,GCD允许为此目的保留和释放调度队列。它实际上记录在:"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);
                         });

在您的场景中,我会将first_queue替换为您的主调度队列。通过保留主调度队列,您将确保在回调完成之前不会释放它。

另一个例子可以在:“Performing a Completion Block When a Task Is Done.

找到

答案 2 :(得分:2)

灰,

你的问题是早期解除分配,因为你的队列正在被拆除。因此,停止这样做。怎么样?你快到了。从后台队列中,您将需要SYNCHRONOUSLY同步将数据返回到主线程上的控制器。这样,当一切都解开时,它们会以安全的顺序解除分配。例如:

dispatch_async(backgroundQueue, ^{

    /* download stuff */

    dispatch_sync(dispatch_get_main_queue(), ^{

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

此模式可以保证将数据正确传递到主线程上的任何UI组件,然后允许正常释放。

安德鲁

使用__block变量编辑第二个答案:

__block UIViewController *vc = danglingVC;

dispatch_async(backgroundQueue, ^{

    /* download stuff */

    dispatch_async(dispatch_get_main_queue(), ^{

        // Pass the data to the controller.

        vc = nil;
    });
});

使用ARC,您可以通过将强存储槽设置为nil来强制释放。请注意,我现在可以异步转移到主线程。我认为这是一个明确而明确的解决方案。它不依赖于ARC,GCD和块之间的微妙交互。

安德鲁

答案 3 :(得分:0)

您的分析似乎很合理。所以也许块的结尾可以保留控制器,并在主线程上进行自动释放(可能是在短暂的延迟之后,以避免竞争条件)。

答案 4 :(得分:0)

你永远不能保证在你发现的任何特定线程上都会释放一个对象。

我找到的最佳解决方案是在你的dealloc中,检查你是否在主线程上。如果没有,请在主线程上使用dealloc的其余部分执行executeSelector并等待完成。

或者,使用块实现相同的逻辑:如果不在主线程上,则使用dealloc的其余部分将dispatch_sync分配到主队列。

答案 5 :(得分:0)

另一种方法是将对象的引用存储在dispatch async之外的作用域中。例如:

// 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];
});

答案 6 :(得分:0)

我最近遇到了类似的问题。我有幸使用ARC中的归零弱引用来解决这个特殊问题。但是我确实考虑了一个可以与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
    });
});

__block上的vc存储限定符可确保该块不会保留vc引用的对象。视图控制器将保留,直到在主队列上调用的块释放它,并且可能在该点被取消激活。