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

发布于 2024-12-28 04:36:40 字数 3473 浏览 6 评论 0原文

我有一个视图控制器,可以在后台 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 构建,因此如果我使用对 self 的 __unsafe_unretained 引用在回调块中,这将解决我当前的问题,但会引入悬空指针的新问题。

您对为此构建解决方案有何建议?其他解决方法包括覆盖release,以便总是在主线程上调用,但这显然在 ARC 环境中不起作用。

这看起来应该是 ARC 和 GCD 的一个更常见的问题,但我在网上找不到任何东西。难道只是因为我的目标是< iOS 5 不能使用弱引用?

I have a view controller that downloads an asset in a background GCD queue. I pass my downloading function a callback block to execute once the download is finished, and it always executes this block on the main thread.

The problem occurs if my view controller is dismissed by the user before the download is finished. I suspect what's happening is, once my view controller is dismissed, the callback block is the only thing retaining a strong reference to the controller. The callback block is only retained in a background thread, so once it's released, all the objects captured in the scope of the callback block are also released, albeit in a background queue.

This is the problem: being released in a background queue is causing dealloc to be run in that same queue, not the main queue. This, in turn, calls dealloc in the background and the app crashes:

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

The code I execute on the main thread looks like this:

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

I'm building for 4.3, so if I use an __unsafe_unretained reference to self in the callback block, this will fix my current problem but introduce the new problem of having a dangling pointer.

What do you recommend for architecting a solution for this? Other workarounds included overriding release so that it was always invoked on the main thread, but this obviously isn't going to work in the ARC environment.

This seems like it should be a far more common problem with ARC and GCD, but I can't find anything online. Is it just because I'm targeting < iOS 5 and can't use weak references?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

夜吻♂芭芘 2025-01-04 04:36:40

更新:事实上,下面的解决方案在 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 最终释放,导致主队列上发生崩溃,也解决了我的问题。

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

UPDATE: The below solution didn't, in fact, work on iOS 4. It worked on 5 for some reason, but not 4, so I came up with a better solution.

The problem is caused by the block being destroyed in the background, so I put it into a local variable and, within a background block, invoke it and then pass it to a block on the main thread asynchronously so it's released there. It's also messy, but looks like the following:

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

The code on the main thread is just a trick to make sure the compiler doesn't just optimize away the code altogether; I need the block object to be released lastly by the main thread. This code appears to work with the -Os compiler optimization level.

So I've come up with a solution to my problem, though it is super hacky. I've been unable to reproduce the problem since this fix, though it is a very poor architectural design I think.

To recap, the problem is I have a block in a background queue that's being destroyed. That block is an object and it owns a strong reference to a callback block which contains a strong reference to self. The background block is being released from the background queue. So what I've done is wrap the call in another dispatch call, to the main queue. So my fetch method goes from this:

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

To this:

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

The code asynchronously dispatches a block to the main queue which then dispatches a block to the background queue. Since the background block is an object and belongs to the main queue's scope, it is released on the main thread, causing the eventual dealloc'ing of the UITextView causing the crash to occur on the main queue, as well, solving my problem.

The obvious architectural solution is to use a __weak reference to self in my callback block, but I'll have to wait until I drop support for iOS 4.3.

无法回应 2025-01-04 04:36:40

实际上,GCD 允许为此目的保留和释放调度队列。它实际上记录在: "调度队列的内存管理"

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。通过保留主调度队列,您将确保在回调完成之前它不会被释放。

另一个示例可以在以下位置找到:“任务完成时执行完成块。"

Actually, GCD allows retain and release for dispatch queues for this purpose. It is actually documented at: "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);
                         });

In your scenario I would replace first_queue with your main dispatch queue. By retaining the main dispatch queue, you will ensure that it does not get released until the callback is finished.

Another example can be found at: "Performing a Completion Block When a Task Is Done."

无悔心 2025-01-04 04:36:40

Ash,

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

dispatch_async(backgroundQueue, ^{

    /* download stuff */

    dispatch_sync(dispatch_get_main_queue(), ^{

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

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

Andrew

使用 __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 和块之间的微妙交互。

安德鲁

Ash,

Your problem is an early deallocation as your queues are being torn down. Hence, stop doing that. How? You're almost there. From your background queue, you will want to SYNCHRONOUSLY return the data to the controller on the main thread. That way, when everything unwinds, they deallocate in a safe order. For example:

dispatch_async(backgroundQueue, ^{

    /* download stuff */

    dispatch_sync(dispatch_get_main_queue(), ^{

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

This pattern lets you guarantee proper delivery of your data to any UI component on the main thread and then allows graceful release.

Andrew

Edit for second answer with __block variable:

__block UIViewController *vc = danglingVC;

dispatch_async(backgroundQueue, ^{

    /* download stuff */

    dispatch_async(dispatch_get_main_queue(), ^{

        // Pass the data to the controller.

        vc = nil;
    });
});

With ARC you force releases by setting the strong storage slot to nil. Note that I can now make the transfer to the main thread asynchronous. I think this is a clear and explicit solution to your problem. It doesn't depend upon subtle interactions between ARC, GCD and blocks.

Andrew

私野 2025-01-04 04:36:40

你的分析看起来很合理。因此,块的末尾可能可以保留控制器,并在主线程上执行自动释放(可能在短暂的延迟后,以避免竞争条件)。

Your analysis seems sound. So maybe the end of the block could retain the controller, and do an autorelease on the main thread (probably after a short delay, so as to avoid race conditions).

紫瑟鸿黎 2025-01-04 04:36:40

正如您所发现的,您永远无法保证某个对象将在任何特定线程上被释放。

我发现的最好的解决方案是在你的 dealloc 中,检查你是否在主线程上。如果没有,则在主线程上执行 Selector 并执行 dealloc 的其余部分并等待完成。

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

You can never guarantee that an object will be dealloced on any particular thread, as you have found out.

The best solution I have found is in your dealloc, check to see if you are on the main thread. If not, performSelector on the main thread with the rest of the dealloc and wait for completion.

Alternatively, implement that same logic with blocks: if not on he main thread, dispatch_sync to the main queue with the rest of the dealloc.

浅唱ヾ落雨殇 2025-01-04 04:36:40

另一种方法是将对象的引用存储在调度异步之外的范围中。例如:

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

An alternative would be to store a reference of the object in a scope outside dispatch async. For example:

// 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];
});
月隐月明月朦胧 2025-01-04 04:36:40

我最近遇到了类似的问题。我有幸在 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
    });
});

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

I came across a similar problem recently. I had the luxury of using zeroing weak references in ARC to solve this particular problem. However I did consider an alternative solution that should work with 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
    });
});

The __block storage qualifier on vc ensures that the block does not retain the object referred to by vc. The view controller is retained until the block that gets called on the main queue releases it and possibly gets dealloc'd at that point.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文