GCD,NSThread和performSelector:onThread:问题 [英] GCD, NSThread, and performSelector:onThread: issues

查看:198
本文介绍了GCD,NSThread和performSelector:onThread:问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试调试一些包含以下错误消息的iOS崩溃日志:

I'm attempting to debug some iOS crash logs that contain the following error message:


***因未捕获而终止应用异常'NSDestinationInvalidException',原因:'*** - [SomeClass
performSelector:onThread:withObject:waitUntilDone:modes:]:target
线程在等待执行时退出

*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[SomeClass performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform

代码的相关部分是:

- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
    NSThread* currentThread = [NSThread currentThread];
    if (currentThread != myThread) {
        //call over to the correct thread
        [self performSelector:@selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES];
    }
    else {
        //we're okay to invoke the target now
        [invocation invoke];
    }
}

这类似于问题在这里讨论,除了我不是要取消我的 onThread:线程。实际上,在我的情况下, onThread:正在传递对应用程序主线程的引用,因此除非整个应用程序正在终止,否则它不应该终止。

This is similar to the issue discussed here, except that I'm not trying to cancel my onThread: thread. In fact, in my case onThread: is being passed a reference to the application's main thread, so it should not be possible for it to terminate unless the entire app is terminating.

所以第一个问题是,错误消息中引用的目标线程是我传递给 onThread的那个:,或等待在 onThread:线程上完成调用的那个?

So the first question is, is the "target" thread referred to in the error message the one I'm passing to onThread:, or the one that's waiting for the invocation to complete on the onThread: thread?

我假设它是第二个选项,好像主线程真的已经终止后台线程的崩溃无论如何都是有点无意义。

I've assumed that it's the second option, as if the main thread really has terminated the crash of the background thread is kind of moot anyways.

考虑到这一点,并基于 的参考文档 performSelector:onThread:...

With that in mind, and based upon the following discussion from the reference docs for performSelector:onThread:...:


特别注意事项

此方法向runloop注册它的
当前上下文,并取决于runloop b以正常的
为基础运行以正确执行。一个常见的上下文,你可以调用
这个方法并最终注册一个不是
的runloop自动定期运行是由
调度队列调用的。如果在
a调度队列上运行时需要这种类型的功能,你应该使用dispatch_after和相关方法来获得你想要的行为。

This method registers with the runloop of its current context, and depends on that runloop being run on a regular basis to perform correctly. One common context where you might call this method and end up registering with a runloop that is not automatically run on a regular basis is when being invoked by a dispatch queue. If you need this type of functionality when running on a dispatch queue, you should use dispatch_after and related methods to get the behavior you want.

...我修改了我的代码,更喜欢使用GCD而不是 performSelector:onThread:... ,如下所示:

...I've modified my code to prefer the use of GCD over performSelector:onThread:..., as follows:

- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
    NSThread* currentThread = [NSThread currentThread];
    if (currentThread != myThread) {
        //call over to the correct thread
        if ([myThread isMainThread]) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [invocation invoke];
            });
        }
        else {
            [self performSelector:@selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES];
        }
    }
    else {
        //we're okay to invoke the target now
        [invocation invoke];
    }
}

这似乎工作正常(虽然不知道如果它修复了崩溃,因为这是一次非常罕见的崩溃)。也许有人可以评论这种方法是否比原始方法更容易或更不容易崩溃?

Which seems to work fine (though no idea if it fixes the crash, as it's an exceedingly rare crash). Perhaps someone can comment on whether this approach is more or less prone to crashing than the original?

无论如何,主要的问题是只有一种明显的方法来使用GCD时目标线程是主线程。在我的情况下,这是真的,但无论目标线程是否是主线程,我都希望能够使用GCD。

Anyways, the main problem is that there's only an obvious way to use GCD when the target thread is the main thread. In my case, this is true, but I'd like to be able to use GCD regardless of whether or not the target thread is the main thread.

所以更重要的问题是,有没有办法从任意 NSThread 映射到相应的队列GCD?理想情况下是 dispatch_queue_t dispatch_get_queue_for_thread(NSThread * thread),因此我可以将我的代码修改为:

So the more important question is, is there a way to map from an arbitrary NSThread to a corresponding queue in GCD? Ideally something along the lines of dispatch_queue_t dispatch_get_queue_for_thread(NSThread* thread), so that I can revise my code to be:

- (void) runInvocationOnMyThread:(NSInvocation*)invocation {
    NSThread* currentThread = [NSThread currentThread];
    if (currentThread != myThread) {
        //call over to the correct thread
        dispatch_sync(dispatch_get_queue_for_thread(myThread), ^{
            [invocation invoke];
        });
    }
    else {
        //we're okay to invoke the target now
        [invocation invoke];
    }
}

这是可能的,还是没有直接映射从 NSThread 到可以应用的GCD队列?

Is this possible, or is there not a direct mapping from NSThread to GCD queue that can be applied?

推荐答案

鉴于您声明的包装需要线程关联的第三方API的目标,您可以尝试使用转发代理等确保只在正确的线程上调用方法。这样做有一些技巧,但我设法鞭打了一些可能有用的东西。

Given your stated goal of wrapping a 3rd party API that requires thread affinity, you might try something like using a forwarding proxy to ensure methods are only called on the correct thread. There are a few tricks to doing this, but I managed to whip something up that might help.

我们假设你有一个对象 XXThreadSensitiveObject ,界面如下所示:

Let's assume you have an object XXThreadSensitiveObject with an interface that looks something like this:

@interface XXThreadSensitiveObject : NSObject

- (instancetype)init NS_DESIGNATED_INITIALIZER;

- (void)foo;
- (void)bar;
- (NSInteger)addX: (NSInteger)x Y: (NSInteger)y;

@end

目标是 -foo -bar -addX:Y:始终在同样的线程。

And the goal is for -foo, -bar and -addX:Y: to always be called on the same thread.

我们还要说如果我们在主线程上创建这个对象,那么我们的期望是主线程是受祝福的线程,所有调用都应该在主线程,但如果它是从任何非主线程创建的,那么它应该生成自己的线程,以便它可以保证线程关联性向前发展。 (因为GCD托管的线程是短暂的,所以没有办法与GCD托管线程具有线程关联。)

Let's also say that if we create this object on the main thread, then our expectation is that the main thread is the blessed thread and all calls should be on the main thread, but that if it's created from any non-main thread, then it should spawn its own thread so it can guarantee thread affinity going forward. (Because GCD managed threads are ephemeral, there is no way to have thread affinity with a GCD managed thread.)

一种可能的实现可能如下所示:

One possible implementation might look like this:

// Since NSThread appears to retain the target for the thread "main" method, we need to make it separate from either our proxy
// or the object itself.
@interface XXThreadMain : NSObject
@end

// This is a proxy that will ensure that all invocations happen on the correct thread.
@interface XXThreadAffinityProxy : NSProxy
{
@public
    NSThread* mThread;
    id mTarget;
    XXThreadMain* mThreadMain;
}
@end

@implementation XXThreadSensitiveObject
{
    // We don't actually *need* this ivar, and we're skankily stealing it from the proxy in order to have it.
    // It's really just a diagnostic so we can assert that we're on the right thread in method calls.
    __unsafe_unretained NSThread* mThread;
}

- (instancetype)init
{
    if (self = [super init])
    {
        // Create a proxy for us (that will retain us)
        XXThreadAffinityProxy* proxy = [[XXThreadAffinityProxy alloc] initWithTarget: self];
        // Steal a ref to the thread from it (as mentioned above, this is not required.)
        mThread = proxy->mThread;
        // Replace self with the proxy.
        self = (id)proxy;
    }
    // Return the proxy.
    return self;
}

- (void)foo
{
    NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
    NSLog(@"-foo called on %@", [NSThread currentThread]);
}

- (void)bar
{
    NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
    NSLog(@"-bar called on %@", [NSThread currentThread]);
}

- (NSInteger)addX: (NSInteger)x Y: (NSInteger)y
{
    NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread]));
    NSLog(@"-addX:Y: called on %@", [NSThread currentThread]);
    return x + y;
}

@end

@implementation XXThreadMain
{
    NSPort* mPort;
}

- (void)dealloc
{
    [mPort invalidate];
}

// The main routine for the thread. Just spins a runloop for as long as the thread isnt cancelled.
- (void)p_threadMain: (id)obj
{
    NSThread* thread = [NSThread currentThread];
    NSParameterAssert(![thread isMainThread]);

    NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];

    mPort = [NSPort port];

    // If we dont register a mach port with the run loop, it will just exit immediately
    [currentRunLoop addPort: mPort forMode: NSRunLoopCommonModes];

    // Just loop until the thread is cancelled.
    while (!thread.cancelled)
    {
        [currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
    }

    [currentRunLoop removePort: mPort forMode: NSRunLoopCommonModes];

    [mPort invalidate];
    mPort = nil;
}

- (void)p_wakeForThreadCancel
{
    // Just causes the runloop to spin so that the loop in p_threadMain can notice that the thread has been cancelled.
}

@end

@implementation XXThreadAffinityProxy

- (instancetype)initWithTarget: (id)target
{
    mTarget = target;
    mThreadMain = [[XXThreadMain alloc] init];

    // We'll assume, from now on, that if mThread is nil, we were on the main thread.
    if (![NSThread isMainThread])
    {
        mThread = [[NSThread alloc] initWithTarget: mThreadMain selector: @selector(p_threadMain:) object:nil];
        [mThread start];
    }

    return self;
}

- (void)dealloc
{
    if (mThread && mThreadMain)
    {
        [mThread cancel];
        const BOOL isCurrent = [mThread isEqual: [NSThread currentThread]];
        if (!isCurrent && !mThread.finished)
        {
            // Wake it up.
            [mThreadMain performSelector: @selector(p_wakeForThreadCancel) onThread:mThread withObject: nil waitUntilDone: YES modes: @[NSRunLoopCommonModes]];
        }
    }
    mThreadMain = nil;
    mThread = nil;
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature *sig = [[mTarget class] instanceMethodSignatureForSelector:selector];
    if (!sig)
    {
        sig = [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
    }
    return sig;
}

- (void)forwardInvocation:(NSInvocation*)invocation
{
    if ([mTarget respondsToSelector: [invocation selector]])
    {
        if ((!mThread && [NSThread isMainThread]) || (mThread && [mThread isEqual: [NSThread currentThread]]))
        {
            [invocation invokeWithTarget: mTarget];
        }
        else if (mThread)
        {
            [invocation performSelector: @selector(invokeWithTarget:) onThread: mThread withObject: mTarget waitUntilDone: YES modes: @[ NSRunLoopCommonModes ]];
        }
        else
        {
            [invocation performSelectorOnMainThread: @selector(invokeWithTarget:) withObject: mTarget waitUntilDone: YES];
        }
    }
    else
    {
        [mTarget doesNotRecognizeSelector: invocation.selector];
    }
}

@end

在这里订购有点不可思议,但 XXThreadSensitiveObject 可以完成它的工作。 XXThreadAffinityProxy 是一个瘦代理,除了确保调用发生在正确的线程上之外什么都不做, XXThreadMain 是只是下属线程的主程序和其他一些小机制的持有者。它本质上只是一个保留周期的解决方法,否则将在线程和具有线程哲学所有权的代理之间创建。

The ordering here is a little wonky, but XXThreadSensitiveObject can just do its work. XXThreadAffinityProxy is a thin proxy that does nothing other than ensuring that the invocations are happening on the right thread, and XXThreadMain is just a holder for the subordinate thread's main routine and some other minor mechanics. It's essentially just a workaround for a retain cycle that would otherwise be created between the thread and the proxy which has philosophical ownership of the thread.

这里需要知道的是线程是一个相对繁重的抽象,并且是有限的资源。这种设计假设您要制作其中一件或两件事,并且他们将会长寿。这种使用模式在包装期望线程亲和性的第三方库的上下文中是有意义的,因为这通常是单例,但是这种方法不会扩展到少数几个线程。

The thing to know here is that threads are a relatively heavy abstraction, and are a limited resource. This design assumes that you're going to make one or two of these things and that they will be long lived. This usage pattern makes sense in the context of wrapping a 3rd party library that expects thread affinity, since that would typically be a singleton anyway, but this approach won't scale to more than a small handful of threads.

这篇关于GCD,NSThread和performSelector:onThread:问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆