IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> WKWebView之离线加载以及遇到的问题 -> 正文阅读

[移动开发]WKWebView之离线加载以及遇到的问题

目录

?

前言

一、离线包是什么?

二、方案调研

NSURLProtocol

WKURLSchemeHandler

三、具体实施

1、离线包的分发

2、服务器对请求接口处理

3、客户端下载离线包

4、webview设置拦截

5、WKURLSchemeHandler处理

四、开发中遇到的问题

1、拦截http、https崩溃问题

2、线程切换导致的The task has already been stopped的崩溃

3、xhr upload的crash

总结


前言

? ? 现在成熟的APP里基本都会用到WebView,在开发中高效的引入 Web 内容实现 Hybrid 应用成为可能。但是,它的弊端也是相当明显,加载一个网页常常白屏一两秒钟。在现如今的网络时代,这是不能忍受的。

? ? ? ? 这个时候离线包加载就应用而生了,它不仅支持了WebView的高灵活性,并且具备了相对来说更快速的页面响应。


一、离线包是什么?

? ? ? ? 离线包技术是利用了从服务器动态下载zip资源包,解压到本地,并拦截WebView网络请求直接读取本地资源进行及时响应的一种技术方案。

二、方案调研

NSURLProtocol

? ? ? ? ?该技术方案是通过拦截所有经过 URL Loading System 的请求,在WebView发出URL Loading System请求的时候就可以被拦截。具体拦截方式如下:

  Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    if ([(id)cls respondsToSelector:sel]) {
     #pragma clang diagnostic push
     #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

         // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理
        [(id)cls performSelector:sel withObject:@"http"];
         [(id)cls performSelector:sel withObject:@"https"];

    #pragma clang diagnostic pop
     }

然后需要一个继承自NSURLProtocol的子类,来重写

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

方法,因为webView所有网络请求都会走这个方法,所以可以在这里分析所有URL并且将信息传递到其他处理类中但是这里对WKWebView 请求并进行替换,但是,拦截其请求需要使用私有接口,存在兼容性风险以及上架被拒的风险。

? ? ? ?WKWebView是基于webkit来实现的,它是独立于主进程外单独的进程进行的。所有的请求以及一些逻辑走的都是webkit里面的东西,所以他对于网页的加载之之类的操作也不会走系统本省的URL Loading System,这么说来他的请求不能被NSURLProtocol拦截也是理所当然的了。

WKURLSchemeHandler

? ? ? ? ?该方案是iOS11苹果推出来的,负责自定义scheme的网络处理。??:这里说的自定义scheme,所以如果你贸然直接拦截http、https就会导致崩溃。所以,目前市面上或者你看到的的方案中大多都是需要在webview编写的时候,与前端同学协商好特定的scheme,拦截特定的scheme。但是,很多时候我们遇到的场景是软件的前端同学早就做完了,这时候你再去找人家将需要加载资源的scheme设置成你们自定义的scheme,这既增加了人家的工作量,又对前端代码产生入侵。显然极不合理。

? ? ? ? 那么,就没有办法拦截http、https的网络请求了吗?答案是可以的,之所以我们不能拦截http、https的网络请求,正是由于系统的handlesURLScheme:方法进行了处理。我们可以利用runtime技术hook住系统的handlesURLScheme:方法,将该方法返回NO,这样就可以将网络请求交给我们自己的handler处理了。

三、具体实施

1、离线包的分发

? ? ? ? 我们可以将webview对应的资源文件,如:js、css、mp3等打包成一个zip包。并且zip包以对应的功能为命名,如:h5_web_xxx。并生成一个与之对应的信息表。

2、服务器对请求接口处理

  1. 服务器提供一个网络请求的接口,接口返回对应xxx zip包的信息,如:对应的版本、名字、md5(用来下载后进行校验)等。
  2. 客户端在启动APP时候,将目前本地所持有的离线包信息作为请求参数,请求第一步的接口。
  3. 服务器根据客户端参数,判断客户端需要全量更新、增量更新、不用更新。并返回对应更新的地址。

3、客户端下载离线包

? ? ? ? 客户端通过第二步的接口返回值,可以使用 AFNetworking 进行下载,将 zip 格式的离线包保存至沙盒中的/Library/Caches,将zip包的md5与接口返回的md5值进行比对。如果md5比对通过,就使用 SSZipArchive 将离线包解压至 /Documents/hybrid-xxx 目录中。并将本地的离线包信息更新,作为下次请求的参数。如果比对不通过,不进行解压,移除下载的资源包。

? ? ? ?注意:下载完成解压的时候判断如果是全量更新,需要在解压之前将之前下载解压的资源从本地移除。如果是增量更新就直接下载解压到相应的位置。不用更新则不去下载。

4、webview设置拦截

? ? ? ? 在webview初始化时,通过URL中的字段判断是否支持off_line来判断是否进行拦截,同时只能支持iOS11以上系统。

//设置自定义handler来拦截请求
if([self.m_WebUrl containsString:@"off_line"] &&
    @available(iOS 11, *)) {
    BBURLSchemeHandler *handler = [[BBURLSchemeHandler alloc] init];
    [wkConfigure setURLSchemeHandler:handler forURLScheme:@"http"];
    [wkConfigure setURLSchemeHandler:handler forURLScheme:@"https"];
}

因为之前也提到了不能直接拦截http、https的scheme。所以需要hook住handlesURLScheme:方法。我们写一个WKWebView的分类来处理


@implementation WKWebView (BBSchemeHandler)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod1 = class_getClassMethod(self, @selector(handlesURLScheme:));
        Method swizzledMethod1 = class_getClassMethod(self, @selector(bbhandlesURLScheme:));
        method_exchangeImplementations(originalMethod1, swizzledMethod1);
    });
}
 
+ (BOOL)bbhandlesURLScheme:(NSString *)urlScheme {
    if ([urlScheme isEqualToString:@"http"] || 
        [urlScheme isEqualToString:@"https"]) {
        return NO;  //这里让返回NO,不走系统断言,走自己的handler处理
    } else {
        return [self bbhandlesURLScheme:urlScheme];
    }
}
@end

5、WKURLSchemeHandler处理

  1. 将请求的task保存到本地的数组中,管理task
  2. 通过task拿到对应的网络请求URL
  3. 通过去掉http:// 、https:// 用之后的路径查找对应的资源文件
  4. 找到资源文件,封装对应的response,回调给task (移除本地数组对task的管理)
  5. 找不到对应的本地资源文件,进行网络请求
  6. 请求完毕后,将请求的reponse和data返回给task(移除本地数组对task的管理)

另外我们需要监听webview的stop回调方法,将task从管理数组中移除。具体代码:

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask{
        
    // 获取系统版本 ios13才特殊处理防止post崩溃
    NSString *versionNum = [[UIDevice currentDevice] systemVersion];
    if ([versionNum containsString:@"13."]) {
        SEL selector = sel_registerName("_setLoadResourcesSerially:");
        id webViewClass = NSClassFromString(@"WebView");
        if ([webViewClass respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [webViewClass performSelector:selector withObject:@NO];
    #pragma clang diagnostic pop
        }
    }
    
    [self.hashTable addObject:urlSchemeTask];
    
    NSString *filePath = [self filePath:urlSchemeTask.request];
    BOOL resourceExist = [[NSFileManager defaultManager] fileExistsAtPath:filePath];
    if (resourceExist && filePath.length > 0) {
        
        NSString *mineType = [self fileMIMETypeWithCAPIAtFilePath:filePath];
        NSData *data = [NSData dataWithContentsOfFile:filePath];
        [self sendRequestWithUrlSchemeTask:urlSchemeTask mimeType:mineType requestData:data];

    } else {
        
        [[[NSURLSession sharedSession] dataTaskWithRequest:urlSchemeTask.request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            dispatch_async(dispatch_get_main_queue(), ^{
                
                if (!urlSchemeTask) {
                    return;
                }
                
                if ([self.hashTable containsObject:urlSchemeTask]) {
                    
                    if (error){
                        [urlSchemeTask didFailWithError:error];
                    } else {
                        @try {
                            [urlSchemeTask didReceiveResponse:response];
                            [urlSchemeTask didReceiveData:data];
                            [urlSchemeTask didFinish];
                        } @catch (NSException *exception) {

                        } @finally {
                            
                        }
                    }
                    [self.hashTable removeObject:urlSchemeTask];
                }

            });
            
        }] resume];
    }
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask {
    if ([self.hashTable containsObject:urlSchemeTask]) {
        [self.hashTable removeObject:urlSchemeTask];
    }
}


- (void)sendRequestWithUrlSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
                              mimeType:(NSString *)mimeType
                           requestData:(NSData *)requestData  API_AVAILABLE(ios(11.0)) {

    if ([self.hashTable containsObject:urlSchemeTask]) {
        NSData *data = requestData ? requestData : [NSData data];
        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:[self s_getResponseHeaders]];

        @try {
            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:data];
            [urlSchemeTask didFinish];
        } @catch (NSException *exception) {
            
        } @finally {
            
        }
    }
}


- (NSString *)fileMIMETypeWithCAPIAtFilePath:(NSString *)path {

    CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
    CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass (UTI, kUTTagClassMIMEType);
    CFRelease(UTI);
    if (!MIMEType) {
        return @"application/octet-stream";
    }
    return (__bridge NSString *)(MIMEType);

}

/// response的header
- (NSDictionary *)s_getResponseHeaders {
    return @{@"Access-Control-Allow-Origin":@"*",
             @"Access-Control-Allow-Headers":@"Content-Type"
    };
}

四、开发中遇到的问题

1、拦截http、https崩溃问题

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: ''http' is a URL scheme that WKWebView handles natively' 
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: ''https' is a URL scheme that WKWebView handles natively'

如果没有hook系统的handlesURLScheme:系统会进入一个断言导致崩溃。以上已经给出答案,分类hook,返回no即可。

2、线程切换导致的The task has already been stopped的崩溃

出现这个问题是因为线程切换,导致WKURLSchemeTask状态无法及时同步。对一个已经stoped的task发送didReceiveResponse:、didReceiveData:方法。

解决方案:如上我代码中hashTable一个可变数组,当然你也可以采用字典。当task触发

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask

该方法时,将task从数组中移除。或者将字典中task对应的value置为NO。在给task发消息时候,判断task的状态。

3、xhr upload的crash

?这是当时的一个崩溃堆栈,为了解决这个问题也是相当崩溃的。因为查找这个WKURLSchemeHandler的时候找到的都是怎么用,没有说这个问题的。后来不断的看崩溃信息。发现友盟统计的崩溃信息中,只有iOS13系统崩溃,然后找到对应的设备测试,并没有崩溃。后来拿用户的崩溃日志,发现都是调用了一个接口后崩溃的。根据对应的接口找到对应的网页。进入后果然崩溃了。

? ? ? ?在经历了很久的苦恼后,最终在WebKit的issue中找到关键点:WebKit的issue?这里描述的崩溃原因是:当WKWebView创建一个httpBody中的data时,会调用WebCore::blobRegistry方法,但返回的是一个空指针,从而导致了崩溃。

? ? ? 并且在stackoverflow中也找到了对应的问题stackoverflow

? ? ?解决方法就是我代码中的

// 获取系统版本 ios13才特殊处理防止post崩溃
    NSString *versionNum = [[UIDevice currentDevice] systemVersion];
    if ([versionNum containsString:@"13."]) {
        SEL selector = sel_registerName("_setLoadResourcesSerially:");
        id webViewClass = NSClassFromString(@"WebView");
        if ([webViewClass respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [webViewClass performSelector:selector withObject:@NO];
    #pragma clang diagnostic pop
        }
    }


总结

WKWebView是一个独立进程,对于开发者来说,WKWebView相当于一个黑盒。所以,在做这个功能的时候一定要留好判断条件,如果发生问题,及时关闭离线拦截,及时止损。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:34:46  更:2022-07-17 16:36:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 3:52:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码