UIWebView白屏的監(jiān)控&監(jiān)控JS文件加載

祭出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 文件是否加載慢,可以通過抓包查看,如下圖


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í)別。

祭出demo

更新時(shí)間: 2018-08-22

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容