本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。
本文档只针对WkWebView场景下如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看iOS SDK 开发手册。
1. 前言
通过iOS端Native场景使用HTTPDNS这篇文档,我们已经知道如何在iOS平台上的Native场景中如何使用HTTPDNS,实现防劫持、调度精准、解析及时生效的能力。
而iOS上还有一个频繁发生网络请求的场景:WkWebView。WKWebView是WebKit框架提供的一个现代Web视图,用于在iOS应用中显示网页内容。它替代了旧的UIWebView,提供了更好的性能、更多的功能和更高的安全性。我们期望,在WkWebView加载网页的时候,也能使用HTTPDNS,提升网络安全性与网络性能。
2. 技术现状
随着iOS系统的不断发展,WKWebView集成HTTPDNS的技术方案也在持续演进:
iOS 17.0之前:Apple官方并未在WkWebView上开放DNS解析相关的hook接口,也并未直接开放自定义网络请求实现的接口,需要通过hook 私有API拦截流量的复杂方案实现。
iOS 17.0及以后:Apple引入了ProxyConfiguration相关API,为WKWebView提供了官方的代理配置能力,可以通过“接近完美”的方式拦截所有网络请求,使得优雅集成HTTPDNS成为可能。
根据Apple官方统计数据(截至2025年6月4日),在系统版本分布上,iOS 17+已经占全部iPhone设备的91%,且在持续增长中。因此,考虑到HTTPDNS为WkWebView场景带来的是防劫持、调度精准、解析及时生效等非功能性提升,建议只需要在iOS 17+的系统版本上接入HTTPDNS,通过一个比较终态的接入方案,覆盖大部分客户,且旧版本系统用户,也会在后续的陆续版本升级中,逐渐享受这个能力。
3. 推荐方案:iOS 17+基于本地代理的方案
3.1 方案概述
iOS 17.0引入了ProxyConfiguration相关API,允许应用为WKWebView配置本地代理服务器。通过这种方式,我们可以在本地启动一个代理服务器,拦截WKWebView的所有网络请求,在代理层面实现HTTPDNS域名解析,然后将请求转发到真实服务器。
相比传统技术方案,本地代理方案具有以下显著优势:
稳定性:基于iOS 17+官方API,无需依赖私有API或混淆技术,稳定性和兼容性最佳。
适用性:对WebView完全透明,无需处理cookie、重定向、CORS等复杂细节。支持HTTP/HTTPS/WebSocket等所有协议,覆盖面更广。
易维护:实现逻辑清晰,维护成本相对较低。
3.2 接入参考
考虑创建本地代理服务、解析HTTP请求、基于HTTPDNS解析结果创建连接、数据转发等步骤有一定实现成本,我们在Demo中提供了一个实现作为参考。您可以结合业务实际情况,按需调整这份代码,使得更符合您的业务需要。
直接引入Demo中的`HttpdnsLocalHttpProxy.h`和`HttpdnsLocalHttpProxy.m`文件后,在WkWebView初始化时,只需要进行如下配置,结合HTTPDNS,就可以实现WebView中的网络请求,都使用HTTPDNS进行域名解析了。
// 创建 WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 配置WebView代理
BOOL proxyConfigured = [HttpdnsLocalHttpProxy installIntoWebViewConfiguration:config];
if (proxyConfigured) {
NSLog(@"WKWebViewProxyScenario: Proxy configuration applied successfully");
} else {
NSLog(@"WKWebViewProxyScenario: Using system network (proxy unavailable)");
}
// 配置DNS解析器
[HttpdnsLocalHttpProxy setDNSResolverBlock:^NSString *(NSString *hostname) {
// 获取HTTPDNS服务实例
HttpDnsService *httpdns = [HttpDnsService sharedInstance];
if (!httpdns) {
return hostname;
}
// 使用HTTPDNS解析域名
HttpdnsResult *result = [httpdns resolveHostSync:hostname byIpType:HttpdnsQueryIPTypeAuto];
if (result) {
NSString *resolvedIP = nil;
// 使用IPv4地址
if ([result hasIpv4Address]) {
resolvedIP = [result firstIpv4Address];
NSLog(@"WKWebViewProxyScenario: HTTPDNS IPv4 resolved: %@ -> %@", hostname, resolvedIP);
} else if ([result hasIpv6Address]) {
// 如果没有IPv4地址,使用IPv6地址
resolvedIP = [result firstIpv6Address];
NSLog(@"WKWebViewProxyScenario: HTTPDNS IPv6 resolved: %@ -> %@", hostname, resolvedIP);
}
if (resolvedIP) {
return resolvedIP;
}
}
NSLog(@"WKWebViewProxyScenario: didn't get an IP from HTTPDNS, fallback to use original hostname: %@", hostname);
return hostname;
}];
[HttpdnsLocalHttpProxy setLogLevel:HttpdnsProxyLogLevelDebug];
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
在上线前,您需要阅读和理解代码实现逻辑,并进行充分测试,确保完美兼容。
3.3 实现细节
本章节介绍本地代理方案的核心实现流程,仅摘录核心代码片段。如果需要完整理解技术细节,请参考我们开源的代码实现。
创建本地代理服务:使用Network framework在本地启动HTTP代理服务器。
说明如果在当前应用或者多个应用中存在多个本地代理服务器,待创建的新的本地代理服务器的端口可能会存在冲突,可以使用随机端口和重试机制降低冲突的可能。
// 配置网络连接参数 nw_parameters_t parameters = nw_parameters_create_secure_tcp( NW_PARAMETERS_DISABLE_PROTOCOL, // 禁用TLS,代理本身不加密 NW_PARAMETERS_DEFAULT_CONFIGURATION ); // 启用地址重用,便于快速重启和端口复用 nw_parameters_set_reuse_local_address(parameters, true); // 创建本地回环地址端点,仅监听本地连接 NSString *portString = [NSString stringWithFormat:@"%d", port]; nw_endpoint_t localEndpoint = nw_endpoint_create_host("127.0.0.1", [portString UTF8String]); nw_parameters_set_local_endpoint(parameters, localEndpoint); // 使用配置创建网络监听器 _listener = nw_listener_create(parameters);
解析从WkWebView下发的
connect
代理建连请求。无论是何种请求,都会通过Connect
方法建立隧道,然后使用这条连接承载HTTP协议报文。// 接收客户端请求数据 nw_connection_receive(connection, 1, 4096, ^(dispatch_data_t content, ...) { NSString *requestLine = /* 解析请求数据 */; if ([requestLine hasPrefix:@"CONNECT "]) { // HTTPS 隧道请求:CONNECT example.com:443 HTTP/1.1 [self handleHTTPSConnect:request connection:connection]; } });
实现双向数据转发:在客户端和目标服务器之间建立透明的数据通道。
- (void)relayDataFrom:(nw_connection_t)source to:(nw_connection_t)destination { nw_connection_receive(source, 1, 8192, ^(dispatch_data_t content, ...) { if (content && dispatch_data_get_size(content) > 0) { // 转发数据到目标连接 nw_connection_send(destination, content, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, false, ^(nw_error_t error) { if (!error) { // 递归继续接收数据 [self relayDataFrom:source to:destination]; } }); } }); }
配置WKWebView代理:使用iOS 17.0引入的ProxyConfiguration完成WKWebView代理配置。
if (@available(iOS 17.0, *)) { // 创建代理端点配置 NSString *proxyHost = @"127.0.0.1"; NSString *proxyPortString = [NSString stringWithFormat:@"%d", _proxyPort]; nw_endpoint_t proxyEndpoint = nw_endpoint_create_host([proxyHost UTF8String], [proxyPortString UTF8String]); // 创建HTTP CONNECT代理配置 nw_proxy_config_t proxyConfig = nw_proxy_config_create_http_connect(proxyEndpoint, NULL); if (proxyConfig) { WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; NSArray<nw_proxy_config_t> *proxyConfigs = @[proxyConfig]; // 检查API可用性并设置代理配置 if ([dataStore respondsToSelector:@selector(setProxyConfigurations:)]) { [dataStore setProxyConfigurations:proxyConfigs]; configuration.websiteDataStore = dataStore; } } }
集成HTTPDNS:代理直接使用HTTPDNS解析结果向目标服务器建连。
// 使用HTTPDNS解析域名并创建到目标服务器的连接 NSString *resolvedHost = [self resolveHostname:host]; NSString *portString = [NSString stringWithFormat:@"%d", port]; nw_endpoint_t remoteEndpoint = nw_endpoint_create_host([resolvedHost UTF8String], [portString UTF8String]); // 创建连接,禁用TLS的TCP连接参数(代理本身不加密) nw_parameters_t params = nw_parameters_create_secure_tcp( NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION ); // 启用地址重用,便于端口快速重绑定 nw_parameters_set_reuse_local_address(params, true); nw_connection_t connection = nw_connection_create(remoteEndpoint, params); // 设置连接队列 nw_connection_set_queue(connection, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
3.4 降级及优化方案
HttpdnsLocalHttpProxy
在设计之初即充分考虑了多种异常场景,内建多层防护与自动降级(Fallback)机制,旨在提供高性能本地代理能力的同时,保障系统的高可用性与最终网络请求的可达性,即便在组件故障或外部依赖异常的情况下,也能最大程度减少对用户体验的影响。
以下是不同的异常场景及应对:
场景 | 触发条件 | 降级或保护措施 | 最终效果 |
代理启动 | 端口冲突 | 自动重试随机端口 | 提高启动成功率 |
启动阻塞 | 启动超时退出 | 避免应用卡死 | |
代理运行 |
| 标记服务为“未运行” | 触发后续降级策略 |
外部访问尝试 | 仅监听 127.0.0.1 | 拒绝局域网访问,保障安全 | |
WebView 配置 | 服务未运行 | 使用非持久化 | 回退到系统默认网络 |
系统版本低于iOS17 | 跳过代理配置 | 与系统默认行为保持一致 | |
DNS 解析 | 自定义解析器抛异常 | 捕获异常,使用原始域名,走localDNS解析 | 避免因解析失败影响连接 |
网络连接 | 目标服务器连接失败 | 返回 | 提供标准化故障反馈 |
通过以上多层设计,HttpdnsLocalHttpProxy
能够在复杂多变的网络环境中实现可靠运行。其完备的降级机制确保即使部分组件失效,仍能保障请求通路不中断。
4. 自定义WKURLSchemeHandler的方案
EMAS目前提供了一个实验性质的参考方案:EMASCurlWeb,该方案在Github上开源,可以考虑参考这个方案,自行结合业务需要,实现一个托管WkWebView网络请求的完整方案。在此基础上,还可以根据业务需要,进一步实现资源预取、自定义缓存等能力。
此方案从使用界面来说提供了比较好的体验,无需额外考虑缓存、cookie、重定向等细节,但它内部实现复杂,需要接入时在一定程度上理解和改造这个方案来适配业务需要。
5. 全局拦截NSURLProtocol的方案
基于NSURLProtocol
可拦截iOS系统上基于上层网络库NSURLConnection/NSURLSession
发出的网络请求,WkWebView发出的请求同样包含在内。步骤如下:
通过以下接口注册自定义
NSURLProtocol
,用于拦截WkWebView上层网络请求,并创建新的网络请求接管数据发送、接收、重定向等处理逻辑,将结果反馈给原始请求。[NSURLProtocol registerClass:[HttpDnsNSURLProtocolImpl class]];
自定义
NSURLProtocol
处理过程概述:在
canInitWithRequest
中过滤需要做HTTPDNS域名解析的请求。请求拦截后,做HTTPDNS域名解析。
解析完成后,同普通请求一样,替换URL.host字段,替换HTTP Header Host域,并接管该请求的数据发送、接收、重定向等处理。
通过
NSURLProtocol
的接口,将请求处理结果反馈到WebView原始请求。
自定义
NSURLProtocol
的实现逻辑可参考我们提供的demo中的 HttpDnsNSURLProtocolImpl.m。
由于Apple官方并未公开太多协议细节,上述方案在生产环境需要结合业务情况,自行处理cookie、重定向等细节问题,才具备可行性。
6. 方案总结与对比
本文档介绍了在iOS端WKWebView
场景下集成HTTPDNS的三种主要技术方案:基于iOS 17+的本地代理方案、自定义WKURLSchemeHandler
方案以及全局拦截NSURLProtocol
的方案。
每种方案都有其特定的适用场景、优缺点和实现复杂度。为帮助您快速决策,下表对这三种方案进行了横向对比:
维度 / 方案 | 本地代理(ProxyConfiguration)(iOS 17 +) | 自定义 WKURLSchemeHandler(EMASCurlWeb 示例) | 全局 NSURLProtocol 拦截 |
官方支持度 | Apple 在 iOS 17 引入的正式 API,完全公开、长期可用 | 基于私有 WKURLSchemeHandler API,但需要自行实现完整网络栈 | 使用公开 NSURLProtocol API,但在 WKWebView 内部细节缺乏官方文档 |
适用系统版本 | iOS 17 及以上 | iOS 11 及以上(WKWebView 可用) | iOS 11 及以上 |
协议覆盖 | HTTP / HTTPS / WebSocket / HTTP2(透明转发原始流量) | 受限于自定义实现:HTTP / HTTPS(需自行扩展其他协议) | 仅限 NSURLSession / NSURLConnection 可处理的协议 |
实现复杂度 | 中:需实现本地代理、端口管理、双向转发 | 高:需重写请求流水线、缓存、Cookie、重定向等 | 中:需处理请求复写、线程与缓存交互 |
对业务代码侵入 | 低:WKWebView 只需配置代理 | 低:替换 WKWebView 的 scheme handler | 中:全局注册 NSURLProtocol,可能影响现有 NSURLSession 逻辑 |
Cookie / 缓存 / CORS | 代理层透明,不额外处理 | 由方案内部模拟,需自测完整性 | 开发者需自行维护,容易遗漏边缘场景 |
维护成本 | 低:依赖官方 API,版本升级风险小 | 高:社区示例,需长期跟进 WKWebView 变更 | 中:系统 API 稳定,但需关注WKWebView 内部行为变化 |
失效降级策略 | 支持:代理故障可回退系统网络 | 需自行实现 | 需自行实现 |
推荐场景 | 主推方案:目标用户 > iOS 17;对安全与兼容要求高 | 对旧系统版本仍需 HTTPDNS,且愿意投入较多研发资源 | 轻量改造、快速验证;对复杂请求兼容性要求不高 |
综合考虑稳定性、开发维护成本和未来趋势,我们强烈推荐使用基于iOS 17+的本地代理方案。
随着iOS 17+系统版本的高速普及,该方案能以最低的成本为绝大多数用户带来稳定、可靠的HTTPDNS服务,有效解决域名劫持问题,并提升网络性能。其优雅的实现方式和官方支持,确保了方案的长期可行性。对于低于iOS 17的系统版本,采取平滑降级策略,即直接使用系统默认的网络请求,待用户升级系统后自动享受HTTPDNS带来的优势。
其他两种方案由于其固有的复杂性和不确定性,仅建议在有特殊需求且具备深厚技术储备的团队,经过充分评估和测试后谨慎采用。