NSURLProtocol + UIWebView + 某些域 = app UI 已冻结
NSURLProtocol + UIWebView + certain domains = app UI frozen
我们正在为 iOS 构建浏览器。我们决定尝试使用自定义 NSURLProtocol
子类来实现我们自己的缓存方案并执行用户代理欺骗。它很好地完成了这两件事......问题是,导航到某些站点(msn.com 是最糟糕的)将导致 整个应用程序的 UI 到最多冻结十五秒。显然有什么东西阻塞了主线程,但它不在我们的代码中。
此问题仅在 UIWebView
和自定义协议结合使用时出现。如果我们换成 WKWebView
(由于各种原因我们不能使用),问题就消失了。类似地,如果我们不注册协议以使其永远不会被使用,问题就会消失。
协议的作用似乎也无关紧要;我们编写了一个简单的虚拟协议,它除了转发响应外什么都不做(post 的底部)。我们将该协议放入没有任何其他代码的基本测试浏览器中——结果相同。我们还尝试使用其他人的 (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)
}
}
我刚刚检查了 NSURLProtocol
与 msn.com 的行为,发现在某些时候 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?
问题无法用 WKWebView
重现,因为 NSURLProtocol
不适用于它。有关详细信息,请参阅 next question。
我们正在为 iOS 构建浏览器。我们决定尝试使用自定义 NSURLProtocol
子类来实现我们自己的缓存方案并执行用户代理欺骗。它很好地完成了这两件事......问题是,导航到某些站点(msn.com 是最糟糕的)将导致 整个应用程序的 UI 到最多冻结十五秒。显然有什么东西阻塞了主线程,但它不在我们的代码中。
此问题仅在 UIWebView
和自定义协议结合使用时出现。如果我们换成 WKWebView
(由于各种原因我们不能使用),问题就消失了。类似地,如果我们不注册协议以使其永远不会被使用,问题就会消失。
协议的作用似乎也无关紧要;我们编写了一个简单的虚拟协议,它除了转发响应外什么都不做(post 的底部)。我们将该协议放入没有任何其他代码的基本测试浏览器中——结果相同。我们还尝试使用其他人的 (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)
}
}
我刚刚检查了 NSURLProtocol
与 msn.com 的行为,发现在某些时候 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?
问题无法用 WKWebView
重现,因为 NSURLProtocol
不适用于它。有关详细信息,请参阅 next question。