祭出demo
現(xiàn)在很多App都改用h5來處理了。使用過程中引發(fā)了很多白屏現(xiàn)象,很多種原因會(huì)引起白屏。比如網(wǎng)絡(luò)不通,JS加載慢,加載失敗,JS文件缺失等。
就我測(cè)試看,h5沒有本地化的,網(wǎng)絡(luò)不通,頁(yè)面白屏非常正常,這個(gè)都不用去監(jiān)控了。JS文件不存在這種情況的可能性也不大,如果JS文件缺失導(dǎo)致白屏,已經(jīng)構(gòu)成了生產(chǎn)事故了。
我在這里講講JS加載慢和JS加載失敗導(dǎo)致的白屏現(xiàn)象。
1、如何監(jiān)控到JS文件 加載
通過監(jiān)控UIWebView里的網(wǎng)絡(luò)請(qǐng)求來監(jiān)控js文件的加載
1.1、創(chuàng)建NSURLProtocol的子類
@interface QLRainURLProtocol : NSURLProtocol
@end
@implementation QLRainURLProtocol
@end
1.2、 從所有請(qǐng)求里甄別到j(luò)s文件的請(qǐng)求
//對(duì)js文件做監(jiān)控
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString * urlstring = [request.URL absoluteString];
if ([urlstring containsString:@".js"]) {
//看看是否已經(jīng)處理過了,防止無限循環(huán)
if ([NSURLProtocol propertyForKey:rainMarkKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
1.3、 對(duì)URLRequest做處理,同時(shí)標(biāo)記處理
//我們可以對(duì)request進(jìn)行處理。。最后返回一個(gè)處理后的request實(shí)例。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[mutableReqeust willChangeValueForKey:@"timeoutInterval"];
mutableReqeust.timeoutInterval = 30.0f;//設(shè)置超時(shí)時(shí)間為30秒,經(jīng)測(cè)試沒什么用。
[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、 加載結(jié)束
- (void)stopLoading {
//因?yàn)橐幚硎〉奈募?,所以就不在這里移除失敗的內(nèi)容了。
[self.connection cancel];
}
1.6、發(fā)起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];
}
/*
每個(gè)connection都有個(gè)clinet 通過client可以把請(qǐng)求過程中的各種狀態(tài)和數(shù)據(jù)轉(zhuǎn)發(fā)出去,交給原始請(qǐng)求。
*/
到這里對(duì)js文件請(qǐng)求的監(jiān)控都能獲取到了。
2、如何判斷JS文件加載慢
JS 文件是否加載慢,可以通過抓包查看,如下圖

從上面的圖片可以看到vendor.*****.js這個(gè)文件花了23秒。js文件沒加載完成前,客戶端上看到的現(xiàn)象就是白屏的。
我們可以判斷JS文件加載超過5秒,我們就認(rèn)為是慢加載了。其實(shí)超過1秒的加載就可以認(rèn)為是慢加載,因?yàn)橐粋€(gè)文件1秒,10個(gè)文件就是10秒了,對(duì)用戶講,10秒已經(jīng)很長(zhǎng)時(shí)間了。
3、如何判斷JS文件加載失敗
在下面的方法里可以知道JS文件加載失敗了
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self.client URLProtocol:self didFailWithError:error];
}
/*
這里可以監(jiān)控哪個(gè)JS文件加載失敗了
*/
4、如何跟蹤所有的JS文件加載
我想了一個(gè)策略,對(duì)每個(gè)JS請(qǐng)求都做一個(gè)標(biāo)記,并且另起一個(gè)單例來管理這些標(biāo)記。這樣就不會(huì)混亂,不知道哪個(gè)請(qǐng)求是哪個(gè)文件的了。
- (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、上傳監(jiān)控到的JS文件名
當(dāng)你掃描請(qǐng)求時(shí),發(fā)現(xiàn)請(qǐng)求超過5秒的就開始上報(bào)文件名,發(fā)現(xiàn)請(qǐng)求失敗的也上報(bào)文件名。
6、管理請(qǐng)求和標(biāo)記的單例
它的作用是記錄請(qǐng)求標(biāo)記,同時(shí)記錄請(qǐng)求的開始時(shí)間戳。
@interface UrlProtocolManager:NSObject
@property (nonatomic, strong)NSMutableDictionary <NSString *,NSDictionary*>*requestDictionary;//存儲(chǔ)請(qǐng)求標(biāo)識(shí)
@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;
}
//啟動(dòng)計(jì)時(shí)器
- (void)createTimer {
NSLog(@"%s",__FUNCTION__);
NSInteger timeout = 7;//7秒執(zhí)行一次
//獲取全局隊(duì)列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建一個(gè)定時(shí)器,并將定時(shí)器的任務(wù)交給全局隊(duì)列執(zhí)行(并行,不會(huì)造成主線程阻塞)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, global);
// 設(shè)置觸發(fā)的間隔時(shí)間
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//timeout * NSEC_PER_SEC 代表設(shè)置定時(shí)器觸發(fā)的時(shí)間間隔為timeout s
//0 * NSEC_PER_SEC 代表時(shí)間允許的誤差是 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];//監(jiān)控到的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、開發(fā)過程中遇到的坑
7.1、監(jiān)控到的請(qǐng)求會(huì)被循環(huán)監(jiān)控,進(jìn)入死循環(huán)
通過在+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request里監(jiān)控到的請(qǐng)求加個(gè)標(biāo)識(shí),在+ (BOOL)canInitWithRequest:(NSURLRequest *)request里識(shí)別這個(gè)標(biāo)識(shí),當(dāng)發(fā)現(xiàn)有標(biāo)識(shí)時(shí)就不監(jiān)控了,但是沒有標(biāo)識(shí)的,就告知需要監(jiān)控。這樣就不會(huì)死循環(huán)了。
7.2、定時(shí)掃描監(jiān)控的標(biāo)志請(qǐng)求。
當(dāng)時(shí)想過用NSTimer,但是NSTimer會(huì)被用戶的UI操作中斷,所以就選了GCD里使用Timer這樣就不會(huì)中斷了。我的demo里是每7秒掃描一次標(biāo)志請(qǐng)求,發(fā)現(xiàn)請(qǐng)求超過6秒的就定義為慢加載,同時(shí)移除該請(qǐng)求標(biāo)志,避免重復(fù)識(shí)別。