祭出demo
现在很多App都改用h5来处理了。使用过程中引发了很多白屏现象,很多种原因会引起白屏。比如网络不通,JS加载慢,加载失败,JS文件缺失等。
就我测试看,h5没有本地化的,网络不通,页面白屏非常正常,这个都不用去监控了。JS文件不存在这种情况的可能性也不大,如果JS文件缺失导致白屏,已经构成了生产事故了。
我在这里讲讲JS加载慢和JS加载失败导致的白屏现象。
1、如何监控到JS文件 加载
通过监控UIWebView里的网络请求来监控js文件的加载
1.1、创建NSURLProtocol的子类
@interface QLRainURLProtocol : NSURLProtocol
@end
@implementation QLRainURLProtocol
@end
1.2、 从所有请求里甄别到js文件的请求
//对js文件做监控
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString * urlstring = [request.URL absoluteString];
if ([urlstring containsString:@".js"]) {
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:rainMarkKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
1.3、 对URLRequest做处理,同时标记处理
//我们可以对request进行处理。。最后返回一个处理后的request实例。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[mutableReqeust willChangeValueForKey:@"timeoutInterval"];
mutableReqeust.timeoutInterval = 30.0f;//设置超时时间为30秒,经测试没什么用。
[mutableReqeust didChangeValueForKey:@"timeoutInterval"];
[NSURLProtocol setProperty:@YES
forKey:rainMarkKey
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
1.4、开始加载
- (void)startLoading
{
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES];
}
1.5、 加载结束
- (void)stopLoading {
//因为要处理失败的文件,所以就不在这里移除失败的内容了。
[self.connection cancel];
}
1.6、发起connection的代理方法
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self.client URLProtocol:self didFailWithError:error];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection{
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
[self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[self.client URLProtocol:self didCancelAuthenticationChallenge:challenge];
}
#pragma mark - NSURLConnectionDataDelegate
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response{
if (response != nil) {
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[[self client] URLProtocolDidFinishLoading:self];
}
/*
每个connection都有个clinet 通过client可以把请求过程中的各种状态和数据转发出去,交给原始请求。
*/
到这里对js文件请求的监控都能获取到了。
2、如何判断JS文件加载慢
JS 文件是否加载慢,可以通过抓包查看,如下图
js文件抓包
从上面的图片可以看到vendor.*****.js
这个文件花了23秒
。js文件没加载完成前,客户端上看到的现象就是白屏的。
我们可以判断JS文件加载超过5秒
,我们就认为是慢加载了。其实超过1秒
的加载就可以认为是慢加载,因为一个文件1秒
,10个文件就是10秒
了,对用户讲,10秒
已经很长时间了。
3、如何判断JS文件加载失败
在下面的方法里可以知道JS文件加载失败了
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self.client URLProtocol:self didFailWithError:error];
}
/*
这里可以监控哪个JS文件加载失败了
*/
4、如何跟踪所有的JS文件加载
我想了一个策略,对每个JS请求都做一个标记,并且另起一个单例来管理这些标记。这样就不会混乱,不知道哪个请求是哪个文件的了。
- (void)startLoading
{
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES];
NSString *fileName = [self.request.URL.absoluteString lastPathComponent];
NSString *markKey = [fileName stringByAppendingFormat:@"%@_%ld",fileName,(long)[UrlProtocolManager shareUrlProtocolManager].requestTag];
self.remarkKey = markKey;
[UrlProtocolManager shareUrlProtocolManager].requestTag++;
[[UrlProtocolManager shareUrlProtocolManager] addRequestWithRemarkKey:markKey andFileName:fileName];
}
5、上传监控到的JS文件名
当你扫描请求时,发现请求超过5秒
的就开始上报文件名,发现请求失败
的也上报文件名。
6、管理请求和标记的单例
它的作用是记录请求标记,同时记录请求的开始时间戳。
@interface UrlProtocolManager:NSObject
@property (nonatomic, strong)NSMutableDictionary <NSString *,NSDictionary*>*requestDictionary;//存储请求标识
@property (nonatomic, assign)NSInteger requestTag;
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation UrlProtocolManager
+ (instancetype)shareUrlProtocolManager {
static UrlProtocolManager *shareInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [[UrlProtocolManager alloc]init];
});
return shareInstance;
}
-(instancetype)init {
self = [super init];
if (self) {
self.requestDictionary = @{}.mutableCopy;
self.requestTag = 0;
[self createTimer];
}
return self;
}
//启动计时器
- (void)createTimer {
NSLog(@"%s",__FUNCTION__);
NSInteger timeout = 7;//7秒执行一次
//获取全局队列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个定时器,并将定时器的任务交给全局队列执行(并行,不会造成主线程阻塞)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, global);
// 设置触发的间隔时间
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//timeout * NSEC_PER_SEC 代表设置定时器触发的时间间隔为timeout s
//0 * NSEC_PER_SEC 代表时间允许的误差是 0s
__weak typeof(self)weakSelf = self;
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"start timer");
NSMutableString *resultStr = @"".mutableCopy;
NSArray *allKeys = weakSelf.requestDictionary.allKeys;
NSInteger allKeyCount = allKeys.count;
if (allKeyCount > 0) {
for (NSInteger i = 0; i < allKeyCount; i ++) {
NSString *dicKey = allKeys[i];
NSDictionary *dic = [weakSelf.requestDictionary objectForKey:dicKey];
if (dic != nil) {
NSNumber *timeMarkLong = [dic objectForKey:fileUrlProtocolRecodeTimeKey];
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
if (currentTime - [timeMarkLong doubleValue] >= sexSeconds ) {
NSString *fileName = [dic objectForKey:fileUrlProtocolNameKey];
[resultStr appendFormat:@"%@_",fileName];
[weakSelf.requestDictionary removeObjectForKey:dicKey];//监控到的key记录之后就移除掉
}
}
}
NSLog(@"resultStr is \n%@",resultStr);
}
});
dispatch_resume(_timer);
}
- (void)addRequestWithRemarkKey:(NSString *)remarkKey andFileName:(NSString *)fileName {
NSTimeInterval timeMark = [[NSDate date] timeIntervalSince1970];
NSDictionary *fileDic = @{
fileUrlProtocolNameKey:fileName,
fileUrlProtocolRecodeTimeKey:@(timeMark)
};
[[UrlProtocolManager shareUrlProtocolManager].requestDictionary setObject:fileDic forKey:remarkKey];
}
- (void)removeRequestWithRemarkKey:(NSString *)remarkKey {
[[UrlProtocolManager shareUrlProtocolManager].requestDictionary removeObjectForKey:remarkKey];
}
- (void)destroyTimer {
dispatch_source_cancel(_timer);
}
@end
7、开发过程中遇到的坑
7.1、监控到的请求会被循环监控,进入死循环
通过在+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
里监控到的请求加个标识,在+ (BOOL)canInitWithRequest:(NSURLRequest *)request
里识别这个标识,当发现有标识时就不监控了,但是没有标识的,就告知需要监控。这样就不会死循环了。
7.2、定时扫描监控的标志请求。
当时想过用NSTimer
,但是NSTimer
会被用户的UI操作中断,所以就选了GCD
里使用Timer
这样就不会中断了。我的demo
里是每7秒
扫描一次标志请求,发现请求超过6秒
的就定义为慢加载,同时移除该请求标志,避免重复识别。
网友评论