首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

iOS开发基础146-深入解析WKWebView

编程知识
2024年08月03日 17:28

WKWebView是苹果在iOS 8中引入的重要组件,它替代了UIWebView,为开发者提供了高性能、高稳定性的网页显示和交互能力。在本文中,我们将深入探讨WKWebView的底层架构、关键特性、使用方法和高级功能。

一、WKWebView的底层架构

WKWebView基于WebKit框架,采用多进程架构,将页面渲染和JavaScript执行放在独立的Web进程中,这样做的好处是主应用进程与Web内容进程隔离,能显著提升应用的稳定性和安全性。其架构主要包括以下几个部分:

1. Web内容进程

负责HTML解析、CSS解析、JavaScript执行、页面渲染等操作。这些操作都是在独立的进程中进行,防止网页崩溃影响整个应用。

2. 网络进程

负责网络请求的管理和缓存数据的处理,从数据源获取网页内容,并传输给Web内容进程。

3. UI进程

主要负责与用户的交互,如接收用户输入、发送消息给Web内容进程等。UI进程与Web内容进程通过IPC(进程间通信)进行信息的传递。

如下图所示是WKWebView的架构示意图:

+------------------+                +------------------+
|                  | <------------> |                  |
|      UI进程       |                |    Web内容进程     |
|                  |    IPC 通信      |                  |
+------------------+                +------------------+
         ^                                   ^
         |                                   |
         v                                   v
+------------------+                +------------------+
|                  |                |                  |
|     WKWebView    |                |     页面引擎      |
|                  |                |                  |
+------------------+                +------------------+

二、WKWebView的基本使用

1. 初始化WKWebView

要使用WKWebView,首先需要进行基本初始化和配置工作。不同于UIWebView,初始化WKWebView时需指定其配置属性。

#import <WebKit/WebKit.h>

@interface ViewController ()
@property (nonatomic, strong) WKWebView *webView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建配置对象
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    
    // 初始化WKWebView
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
    
    // 设置内边距
    [self.view addSubview:self.webView];
    
    // 加载一个网页示例
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

@end

2. 加载本地文件

除了加载网络资源外,WKWebView还可以加载本地文件:

NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *baseURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
NSString *htmlContent = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
[self.webView loadHTMLString:htmlContent baseURL:baseURL];

3. 导航控制

WKWebView提供了丰富的导航控制方法,帮助我们处理网页的前进、后退和刷新等操作:

// 刷新当前页面
[self.webView reload];

// 停止加载
[self.webView stopLoading];

// 后退到上一页面
[self.webView goBack];

// 前进到下一页面
[self.webView goForward];

4. 获取网页内容

WKWebView的一个强大功能是可以直接执行JavaScript代码并获取返回值:

[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id result, NSError *error) {
    if (!error) {
        NSLog(@"Page title: %@", result);
    }
}];

三、WKWebView的代理与回调

WKWebView提供了两个主要的代理协议:WKNavigationDelegateWKUIDelegate,它们分别处理导航和用户界面方面的回调。

1. WKNavigationDelegate

该协议管理网页内容的加载过程,包括开始、完成、失败等事件:

@interface ViewController () <WKNavigationDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置导航代理
    self.webView.navigationDelegate = self;
    
    // 加载网页
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

// 页面开始加载
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"页面开始加载");
}

// 内容开始返回
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"内容开始返回");
}

// 页面加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    NSLog(@"页面加载完成");
}

// 页面加载失败
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    NSLog(@"页面加载失败,错误: %@", error.localizedDescription);
}
@end

2. WKUIDelegate

该协议处理网页中的UI事件,比如显示JavaScript的alertconfirmprompt对话框:

@interface ViewController () <WKUIDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置用户界面代理
    self.webView.UIDelegate = self;
    
    // 加载网页
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

// JavaScript alert框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *ok = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }];
    [alert addAction:ok];
    [self presentViewController:alert animated:YES completion:nil];
}

// JavaScript confirm框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"确认" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *ok = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }];
    UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }];
    [alert addAction:ok];
    [alert addAction:cancel];
    [self presentViewController:alert animated:YES completion:nil];
}

// JavaScript prompt框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler {
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"输入" message:prompt preferredStyle:UIAlertControllerStyleAlert];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    UIAlertAction *ok = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        NSString *input = alert.textFields.firstObject.text;
        completionHandler(input);
    }];
    UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(nil);
    }];
    [alert addAction:ok];
    [alert addAction:cancel];
    [self presentViewController:alert animated:YES completion:nil];
}

@end

四、WKWebView的进阶使用

1. 与JavaScript交互

通过WKScriptMessageHandler协议,WKWebView可以和网页中的JavaScript进行双向交互。

前提配置

需要在WKWebViewConfiguration中配置内容控制器WKUserContentController并注册JavaScript消息处理器:

@interface ViewController () <WKScriptMessageHandler>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *contentController = [[WKUserContentController alloc] init];
    [contentController addScriptMessageHandler:self name:@"nativeHandler"];
    config.userContentController = contentController;

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    [self.view addSubview:self.webView];

    NSString *html = @"<html><body><button onclick=\"window.webkit.messageHandlers.nativeHandler.postMessage('Hello from JS!');\">Click Me</button></body></html>";
    [self.webView loadHTMLString:html baseURL:nil];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeHandler"]) {
        NSLog(@"Received message from JS: %@", message.body);
    }
}

- (void)dealloc {
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeHandler"];
}

@end

这样,当点击网页按钮时,JavaScript会将消息发送到原生代码并触发userContentController:didReceiveScriptMessage:回调。

2. Loading进度条

通过监听WKWebViewestimatedProgress属性,我们可以实现网页加载过程中的进度条显示:

@interface ViewController ()

@property (nonatomic, strong) UIProgressView *progressView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 初始化WKWebView
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.webView];

    // 初始化进度条
    self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
    self.progressView.frame = CGRectMake(0, 88, self.view.bounds.size.width, 2);
    [self.view addSubview:self.progressView];

    // 观察estimatedProgress属性
    [self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];

    // 加载网页
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

- (void)dealloc {
    [self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.progressView.progress = self.webView.estimatedProgress;
        if (self.webView.estimatedProgress >= 1.0) {
            [UIView animateWithDuration:0.5 animations:^{
                self.progressView.alpha = 0.0;
            }];
        } else {
            self.progressView.alpha = 1.0;
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

3. 处理文件上传

WKWebView支持文件上传,通过实现UIDocumentPickerViewController,我们可以定制上传文件的操作:

@interface ViewController () <WKUIDelegate, UIDocumentPickerDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 初始化WKWebView
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
    self.webView.UIDelegate = self;
    [self.view addSubview:self.webView];

    // 加载网页
    NSURL *url = [NSURL URLWithString:@"https://example.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray<NSURL *> * _Nullable URLs))completionHandler {
    UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen];
    documentPicker.delegate = self;
    documentPicker.completionHandler = ^(NSArray<NSURL *> * _Nonnull urls) {
        completionHandler(urls);
    };
    [self presentViewController:documentPicker animated:YES completion:nil];
}

@end

五、WKWebView的性能优化

由于WKWebView在实际使用中可能会面临性能问题,以下是一些性能优化的建议:

1. 缓存策略

通过使用合适的缓存策略,你可以避免重复加载相同的资源,从而提高加载速度。如使用URLCache配置:

NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:1024 * 1024 * 10
                                                    diskCapacity:1024 * 1024 * 50
                                                        diskPath:@"wkwebview_cache"];
[NSURLCache setSharedURLCache:urlCache];

NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:30];
[self.webView loadRequest:request];

2. 异步加载资源

避免同步加载资源导致主线程阻塞,可以使用异步加载的方法来处理:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSURL *url = [NSURL URLWithString:@"https://example.com/resource"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.webView loadData:data MIMEType:@"text/html" characterEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"https://example.com"]];
    });
});

3. 减少DOM操作

在需要频繁操作DOM时,尽量将多个操作合并为一次,以减少引擎的渲染负担:

function updateContent() {
    let container = document.getElementById('container');
    let fragment = document.createDocumentFragment();

    for (let i = 0; i < 1000; i++) {
        let div = document.createElement('div');
        div.textContent = `Item ${i}`;
        fragment.appendChild(div);
    }

    container.appendChild(fragment);
}

六、OC与JavaScript通信进阶

如果只是传递简单的用户信息数据,除了通过 WKScriptMessageHandler 的方式,还有以下几种方法可以将数据从客户端(Objective-C/Swift)传递给 JavaScript。

1、通过 URL Scheme

这种方法主要是在加载网页的时候,将用户信息作为查询参数(query parameter)嵌入到 URL 中传递给页面。这种方式适用于初始加载页面的数据传递。

// 构建用户信息数据
NSString *userInfo = @"userId=12345&userName=JohnDoe";
NSString *urlString = [NSString stringWithFormat:@"https://example.com?%@", userInfo];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];

在 JavaScript 中可以通过 window.location.search 获取查询参数。

2、通过 evaluateJavaScript 执行 JavaScript

evaluateJavaScript:completionHandler: 是一个简单直接的方法,可以在客户端执行任意 JavaScript 代码并通过回调获取执行结果。

// 构建JavaScript代码
NSString *userId = @"12345";
NSString *userName = @"JohnDoe";
NSString *jsCode = [NSString stringWithFormat:@"setUserInfo('%@', '%@');", userId, userName];

// 执行JavaScript代码
[self.webView evaluateJavaScript:jsCode completionHandler:^(id result, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error.localizedDescription);
    }
}];

在网页中,需要定义对应的 JavaScript 函数来接收这些数据:

<script>
function setUserInfo(userId, userName) {
    console.log("User ID: " + userId);
    console.log("User Name: " + userName);
    // 其他业务逻辑
}
</script>

3、通过 User Scripts

如果你想在页面加载的初始阶段注入数据,可以使用 WKUserScript 来添加 JavaScript 预处理。

// 构建JavaScript代码
NSString *userId = @"12345";
NSString *userName = @"JohnDoe";
NSString *scriptSource = [NSString stringWithFormat:@"window.userInfo = {userId: '%@', userName: '%@'};", userId, userName];

// 创建用户脚本
WKUserScript *userScript = [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];

// 添加用户脚本到配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:userScript];

// 创建并加载 WKWebView
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
NSURL *url = [NSURL URLWithString:@"https://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];

通过上述方法,页面在加载时就会自动注入用户信息,网页可以在任何地方直接访问 window.userInfo

4、通过 Document.cookie(不推荐)

虽然不太推荐,但我们也可以通过设置 Document.cookie 将信息传递给网页。以下是示例:

NSString *userId = @"12345";
NSString *userName = @"JohnDoe";
NSString *cookieScript = [NSString stringWithFormat:@"document.cookie = 'userId=%@; path=/'; document.cookie = 'userName=%@; path=/';", userId, userName];
[self.webView evaluateJavaScript:cookieScript completionHandler:^(id result, NSError *error) {
    if (error) {
        NSLog(@"Error: %@", error.localizedDescription);
    }
}];

在网页中,可以通过 JavaScript 解析 document.cookie 获取用户信息。

function getCookie(name) {
    let value = `; ${document.cookie}`;
    let parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

let userId = getCookie('userId');
let userName = getCookie('userName');
console.log("User ID: " + userId);
console.log("User Name: " + userName);

选择

以上方法各有优劣,根据实际使用场景选择适合的方法:

  • 如果是初始加载时传递数据,用 URL Scheme 比较简单直接。
  • 如果需要在页面加载后随时传递数据,evaluateJavaScript:completionHandler: 非常灵活。
  • 需要在页面加载前就注入数据,WKUserScript 是一种好方法。
  • Document.cookie 方式虽然可以传递数据,但不推荐用于敏感信息。

七、总结

WKWebView提供了现代化的网页视图解决方案,具有高性能、高稳定性的优势。通过理解其底层架构、掌握常用和进阶的使用方法、如何与JavaScript进行交互和处理实际应用中的各种需求,你可以更好地实现复杂的网页加载与交互功能,提升应用的用户体验和性能。

From:https://www.cnblogs.com/chglog/p/18340886
本文地址: http://shuzixingkong.net/article/754
0评论
提交 加载更多评论
其他文章 2024-08-03:用go语言,给定一个从 0 开始的字符串数组 `words`, 我们定义一个名为 `isPrefixAndSuffix` 的布尔函数,该函数接受两个字符串参数 `str1` 和
2024-08-03:用go语言,给定一个从 0 开始的字符串数组 words, 我们定义一个名为 isPrefixAndSuffix 的布尔函数,该函数接受两个字符串参数 str1 和 str2。 当 str1 同时是 str2 的前缀和后缀时,函数返回 true;否则返回 false。 例如,i
2024-08-03:用go语言,给定一个从 0 开始的字符串数组 `words`, 我们定义一个名为 `isPrefixAndSuffix` 的布尔函数,该函数接受两个字符串参数 `str1` 和 2024-08-03:用go语言,给定一个从 0 开始的字符串数组 `words`, 我们定义一个名为 `isPrefixAndSuffix` 的布尔函数,该函数接受两个字符串参数 `str1` 和
OpenCV计算机视觉学习(16)——仿射变换学习笔记
如果需要其他图像处理的文章及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 在计算机视觉和图像处理中,仿射变换是一种重要的几何变换方法。它可以通过线性变换和平移来改变图像的
OpenCV计算机视觉学习(16)——仿射变换学习笔记 OpenCV计算机视觉学习(16)——仿射变换学习笔记 OpenCV计算机视觉学习(16)——仿射变换学习笔记
Blazor Web 应用如何实现Auto模式
本文介绍Blazor Web应用Auto交互呈现模式的实现方案,如下示例是基于 Known 框架来实现的,该解决方案共有3个项目,具体实现步骤如下: 1. 前后端共用项目 创建前后端共用类库项目Sample,定义系统的实体类、数据模型、服务接口、常量、枚举等,项目工程文件内容如下: &lt;Proj
实现一个终端文本编辑器来学习golang语言:序言
欢迎!这个系列的博文会带你使用golang语言来编写一个你自己的文本编辑器。 首先想说说写这个系列文章的动机。 其实作为校招生加入某头部互联网大厂一转眼已经快4年了。可以说该大厂算是比较早的用golang语言作为主要后端开发技术栈的公司了,绝大部分后端项目的语言选型都是golang。最近一年会发现许
实现一个终端文本编辑器来学习golang语言:序言
FFmpeg在游戏视频录制中的应用:画质与文件大小的综合比较
我们游戏内的视频录制目前只支持avi固定码率,在玩家见面会上有玩家反馈希望改善录制画质,我最近在研究了有关视频画质的一些内容并做了一些统计。 录制视频大小对比 首先在游戏引擎中增加了对录制mp4格式的支持,并且使用h246编码可以直接在网页上播放无法再做转码 测试场景:视频尺寸固定大小为: 1904
实现一个终端文本编辑器来学习golang语言:第二章Raw模式下的输入输出
从第二章开始,在每个小节的最后都会有一些代码实操作业,你可以选择自己完成(比较推荐),再对照我的实现方式,当然也可以直接看我的代码实现。不过,之后的各个功能实现,我都会基于我先前的代码实现版本,在它的基础上进行扩展。 首先,我们先来解决第一章遗留的第一个问题:输入数据会被stdin缓存,直到遇到换行
实现一个终端文本编辑器来学习golang语言:第二章Raw模式下的输入输出
[rCore学习笔记 021]多道程序与分时任务
写在前面 本随笔是非常菜的菜鸡写的。如有问题请及时提出。 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 导读 这里就是第三章的开头了,由于我的巨菜,导致天天半天理解不了关键点所在,唉,实在是太折磨人. 遵照上一
[rCore学习笔记 021]多道程序与分时任务 [rCore学习笔记 021]多道程序与分时任务 [rCore学习笔记 021]多道程序与分时任务
全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type
Python 中的对象和类型是一个非常重要的概念。在 Python 中,一切都是对象,包括数字、字符串、列表等,每个对象都有自己的类型。
全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type 全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type 全网最适合入门的面向对象编程教程:31 Python的内置数据类型-对象Object和类型Type