祭出demo
現(xiàn)在很多App都改用h5來處理了咕别。使用過程中引發(fā)了很多白屏現(xiàn)象技健,很多種原因會引起白屏。比如網(wǎng)絡(luò)不通惰拱,JS加載慢雌贱,加載失敗,JS文件缺失等。
就我測試看帽芽,h5沒有本地化的,網(wǎng)絡(luò)不通翔冀,頁面白屏非常正常导街,這個都不用去監(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ò)請求來監(jiān)控js文件的加載
1.1、創(chuàng)建NSURLProtocol的子類
@interface QLRainURLProtocol : NSURLProtocol
@end
@implementation QLRainURLProtocol
@end
1.2卡乾、 從所有請求里甄別到j(luò)s文件的請求
//對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、 對URLRequest做處理幔妨,同時標(biāo)記處理
//我們可以對request進行處理鹦赎。。最后返回一個處理后的request實例误堡。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[mutableReqeust willChangeValueForKey:@"timeoutInterval"];
mutableReqeust.timeoutInterval = 30.0f;//設(shè)置超時時間為30秒,經(jīng)測試沒什么用古话。
[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 {
//因為要處理失敗的文件陪踩,所以就不在這里移除失敗的內(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];
}
/*
每個connection都有個clinet 通過client可以把請求過程中的各種狀態(tài)和數(shù)據(jù)轉(zhuǎn)發(fā)出去肩狂,交給原始請求。
*/
到這里對js文件請求的監(jiān)控都能獲取到了基跑。
2婚温、如何判斷JS文件加載慢
JS 文件是否加載慢,可以通過抓包查看媳否,如下圖
從上面的圖片可以看到vendor.*****.js
這個文件花了23秒
栅螟。js文件沒加載完成前,客戶端上看到的現(xiàn)象就是白屏的篱竭。
我們可以判斷JS文件加載超過5秒
力图,我們就認(rèn)為是慢加載了。其實超過1秒
的加載就可以認(rèn)為是慢加載掺逼,因為一個文件1秒
吃媒,10個文件就是10秒
了,對用戶講,10秒
已經(jīng)很長時間了赘那。
3刑桑、如何判斷JS文件加載失敗
在下面的方法里可以知道JS文件加載失敗了
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self.client URLProtocol:self didFailWithError:error];
}
/*
這里可以監(jiān)控哪個JS文件加載失敗了
*/
4、如何跟蹤所有的JS文件加載
我想了一個策略募舟,對每個JS請求都做一個標(biāo)記祠斧,并且另起一個單例來管理這些標(biāo)記。這樣就不會混亂拱礁,不知道哪個請求是哪個文件的了琢锋。
- (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)你掃描請求時呢灶,發(fā)現(xiàn)請求超過5秒
的就開始上報文件名吴超,發(fā)現(xiàn)請求失敗
的也上報文件名。
6鸯乃、管理請求和標(biāo)記的單例
它的作用是記錄請求標(biāo)記鲸阻,同時記錄請求的開始時間戳。
@interface UrlProtocolManager:NSObject
@property (nonatomic, strong)NSMutableDictionary <NSString *,NSDictionary*>*requestDictionary;//存儲請求標(biāo)識
@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秒執(zhí)行一次
//獲取全局隊列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建一個定時器飒责,并將定時器的任務(wù)交給全局隊列執(zhí)行(并行赘娄,不會造成主線程阻塞)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, global);
// 設(shè)置觸發(fā)的間隔時間
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//timeout * NSEC_PER_SEC 代表設(shè)置定時器觸發(fā)的時間間隔為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];//監(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)控到的請求會被循環(huán)監(jiān)控遣臼,進入死循環(huán)
通過在+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
里監(jiān)控到的請求加個標(biāo)識,在+ (BOOL)canInitWithRequest:(NSURLRequest *)request
里識別這個標(biāo)識拾并,當(dāng)發(fā)現(xiàn)有標(biāo)識時就不監(jiān)控了揍堰,但是沒有標(biāo)識的,就告知需要監(jiān)控嗅义。這樣就不會死循環(huán)了屏歹。
7.2、定時掃描監(jiān)控的標(biāo)志請求之碗。
當(dāng)時想過用NSTimer
蝙眶,但是NSTimer
會被用戶的UI操作中斷,所以就選了GCD
里使用Timer
這樣就不會中斷了褪那。我的demo
里是每7秒
掃描一次標(biāo)志請求幽纷,發(fā)現(xiàn)請求超過6秒
的就定義為慢加載,同時移除該請求標(biāo)志博敬,避免重復(fù)識別友浸。