美文网首页
UIWebView白屏的监控&监控JS文件加载

UIWebView白屏的监控&监控JS文件加载

作者: rainbowboy | 来源:发表于2018-08-22 12:35 被阅读380次

    祭出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秒的就定义为慢加载,同时移除该请求标志,避免重复识别。

    祭出demo

    更新时间: 2018-08-22

    相关文章

      网友评论

          本文标题:UIWebView白屏的监控&监控JS文件加载

          本文链接:https://www.haomeiwen.com/subject/fvojiftx.html