ios - NSURLProtocol + UIWebView + 某些域 = app UI frozen

标签 ios uiwebview nsurlprotocol

我们正在构建适用于 iOS 的浏览器。我们决定尝试使用自定义 NSURLProtocol 子类来实现我们自己的缓存方案并执行用户代理欺骗。它很好地完成了这两件事……问题是,导航到某些站点(msn.com 是最糟糕的站点)会导致整个应用程序 UI 卡住长达十五秒。显然有什么东西阻塞了主线程,但它不在我们的代码中。

此问题仅出现在 UIWebView 和自定义协议(protocol)的组合中。如果我们换入 WKWebView(由于各种原因我们不能使用),问题就会消失。同样,如果我们不注册协议(protocol)以使其永远不会被使用,问题就会消失。

协议(protocol)的作用似乎也无关紧要;我们编写了一个简单的虚拟协议(protocol),它除了转发响应外什么都不做(帖子底部)。我们将该协议(protocol)放入没有任何其他代码的基本测试浏览器中——结果相同。我们还尝试使用其他人的 (RNCachingURLProtocol) 并观察到相同的结果。似乎这两个组件与某些页面的简单组合导致了卡住。我不知所措,无法尝试解决(甚至调查)这个问题,非常感谢任何指导或提示。谢谢!

import UIKit

private let KEY_REQUEST_HANDLED = "REQUEST_HANDLED"

final class CustomURLProtocol: NSURLProtocol {
    var connection: NSURLConnection!

    override class func canInitWithRequest(request: NSURLRequest) -> Bool {
        return NSURLProtocol.propertyForKey(KEY_REQUEST_HANDLED, inRequest: request) == nil
    }

    override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
        return request
    }

    override class func requestIsCacheEquivalent(aRequest: NSURLRequest, toRequest bRequest: NSURLRequest) -> Bool {
        return super.requestIsCacheEquivalent(aRequest, toRequest:bRequest)
    }

    override func startLoading() {
        var newRequest = self.request.mutableCopy() as! NSMutableURLRequest
        NSURLProtocol.setProperty(true, forKey: KEY_REQUEST_HANDLED, inRequest: newRequest)
        self.connection = NSURLConnection(request: newRequest, delegate: self)
    }

    override func stopLoading() {
        connection?.cancel()
        connection = nil
    }

    func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
        self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
    }

    func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
        self.client!.URLProtocol(self, didLoadData: data)
    }

    func connectionDidFinishLoading(connection: NSURLConnection!) {
        self.client!.URLProtocolDidFinishLoading(self)
    }

    func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
        self.client!.URLProtocol(self, didFailWithError: error)
    }
}

最佳答案

我刚刚检查了 msn.com 的 NSURLProtocol 行为,发现在某些时候 startLoading 方法在 WebCoreSynchronousLoaderRunLoopMode 模式下被调用。这会导致主线程阻塞。

浏览CustomHTTPProtocol Apple sample code ,我找到了描述这个问题的评论。修复通过以下方式实现:

@interface CustomHTTPProtocol () <NSURLSessionDataDelegate>

@property (atomic, strong, readwrite) NSThread * clientThread; ///< The thread on which we should call the client.

/*! The run loop modes in which to call the client.
 *  \details The concurrency control here is complex.  It's set up on the client 
 *  thread in -startLoading and then never modified.  It is, however, read by code 
 *  running on other threads (specifically the main thread), so we deallocate it in 
 *  -dealloc rather than in -stopLoading.  We can be sure that it's not read before 
 *  it's set up because the main thread code that reads it can only be called after 
 *  -startLoading has started the connection running.
 */
@property (atomic, copy, readwrite) NSArray * modes;

- (void)startLoading
{
    NSMutableArray *calculatedModes;
    NSString *currentMode;

    // At this point we kick off the process of loading the URL via NSURLSession. 
    // The thread that calls this method becomes the client thread.

    assert(self.clientThread == nil); // you can't call -startLoading twice

    // Calculate our effective run loop modes.  In some circumstances (yes I'm looking at 
    // you UIWebView!) we can be called from a non-standard thread which then runs a 
    // non-standard run loop mode waiting for the request to finish.  We detect this 
    // non-standard mode and add it to the list of run loop modes we use when scheduling 
    // our callbacks.  Exciting huh?
    //
    // For debugging purposes the non-standard mode is "WebCoreSynchronousLoaderRunLoopMode" 
    // but it's better not to hard-code that here.

    assert(self.modes == nil);
    calculatedModes = [NSMutableArray array];
    [calculatedModes addObject:NSDefaultRunLoopMode];
    currentMode = [[NSRunLoop currentRunLoop] currentMode];
    if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) {
        [calculatedModes addObject:currentMode];
    }
    self.modes = calculatedModes;
    assert([self.modes count] > 0);

    // Create new request that's a clone of the request we were initialised with, 
    // except that it has our 'recursive request flag' property set on it.

    // ... 

    // Latch the thread we were called on, primarily for debugging purposes.

    self.clientThread = [NSThread currentThread];

    // Once everything is ready to go, create a data task with the new request.

    self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes];
    assert(self.task != nil);

    [self.task resume];
}

有些 Apple 工程师很有幽默感。

Exciting huh?

参见 full apple sample了解详情。

问题无法通过 WKWebView 重现,因为 NSURLProtocol 不适用于它。参见 next question了解详情。

关于ios - NSURLProtocol + UIWebView + 某些域 = app UI frozen,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31327785/

相关文章:

ios - 在 iOS 中 - 如何在不改变其位置的情况下使 UILabel 适合其文本?

javascript - 如何使用 LoadHTMLString 方法在 WebView 中加载大型 HTML 字符串?

ios - UITableView 上的 UIWebView 阻止 tablecell 选择

ios - NSURLProtocol - 处理 https 请求

ios - 可以使用 NSURLProtocol 的子类、使用 MPMovieController 或 AVFoundation 来播放视频吗?

ios - 为什么不在 Xcode 中编译测试?

ios - 如何在 iOS 中以编程方式检查日历订阅的 ics feed

ios - 如何确保使用了我的自定义 URL 协议(protocol)?

iOS 谷歌原生广告不可点击

ios - 如何删除 WKWebview cookies