前言
WebView是常用的组件,对于大型项目来说。WebView和iOS需要有良好的业务、UI兼容性及双向通信能力。
假设一个新的项目,需要实现一个WebView,该从哪些方面着手呢?
功能清单
| 类别 | 功能 |
|---|---|
| ✅ 基础功能 | 兼容 WKWebView\UIWebView、加载本地html/网络url、双向通信JS桥、生命周期函数 |
| ✅ 状态管理 | Cookie 同步、User-Agent 设置、缓存策略 |
| ✅ 安全性 | 白名单、防注入、通信加密 |
| ✅ 稳定性 | 错误处理、内存管理、生命周期控制 |
| ✅ 性能 | 离线缓存、资源预加载、多实例管理 |
| ✅ 可维护性 | 日志、调试支持、模块化设计 |
基础功能
1.兼容WKWebView和UIWebView
| 功能 | 功能细节 | 建议补充点 |
|---|---|---|
兼容 WKWebView 和 UIWebView | 根据系统版本或类是否存在,动态创建 WKWebView(iOS 8+ 支持)或回退到 UIWebView。 | 1. 使用 NSClassFromString 判断是否支持 WKWebView2. 封装统一接口,避免业务层感知底层差异 3. 注意 WKWebView 需导入 WebKit 框架 |
// HybridWebViewManager.h
@interface HybridWebViewManager : NSObject
@property (nonatomic, strong, readonly) UIView *webView; // 返回 UIView,屏蔽具体类型
- (void)loadRequest:(NSURLRequest *)request;
@end
// HybridWebViewManager.m
#import <WebKit/WebKit.h>
@implementation HybridWebViewManager {
WKWebView *_wkWebView;
UIWebView *_uiWebView;
}
- (UIView *)webView {
return _wkWebView ?: (UIView *)_uiWebView;
}
- (instancetype)init {
if (self = [super init]) {
Class wkWebViewClass = NSClassFromString(@"WKWebView");
if (wkWebViewClass) {
// iOS 8+,优先使用 WKWebView
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
_wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
} else {
// iOS 7 及以下,使用 UIWebView
_uiWebView = [[UIWebView alloc] initWithFrame:CGRectZero];
_uiWebView.delegate = self;
}
}
return self;
}
- (void)loadRequest:(NSURLRequest *)request {
if (_wkWebView) {
[_wkWebView loadRequest:request];
} else if (_uiWebView) {
[_uiWebView loadRequest:request];
}
}
@end
2.兼容本地html和网络url
| 功能 | 功能细节 | 建议补充点 |
|---|---|---|
| 加载本地 HTML / 网络 URL | 支持加载 App bundle 中的 HTML 文件(如 index.html)或远程 URL。 | - 使用 fileURLWithPath 构造本地路径- 设置 baseURL 保证资源正确加载- 网络请求注意 ATS 配置 |
// 加载本地 HTML 文件(如 index.html)
- (void)loadLocalHTML:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"html"];
NSURL *fileURL = [NSURL fileURLWithPath:path isDirectory:NO];
// 设置 baseURL 为资源目录,确保 JS/CSS 能正确加载
NSURL *baseURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (_wkWebView) {
[_wkWebView loadFileURL:fileURL allowingReadAccessToURL:baseURL];
} else if (_uiWebView) {
[_uiWebView loadRequest:[NSURLRequest requestWithURL:fileURL]];
// 或使用:[_uiWebView loadHTMLString:htmlString baseURL:baseURL];
}
}
// 加载网络 URL
- (void)loadRemoteURL:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self loadRequest:request];
}
//面向业务调用者的封装
- (void)loadRequest:(NSURLRequest *)request {
if (_wkWebView) {
[_wkWebView loadRequest:request];
} else if (_uiWebView) {
[_uiWebView loadRequest:request];
}
}
3.双向通信JS桥(JS Bridge)
| 功能 | 功能细节 | 建议补充点 |
|---|---|---|
| Native 与 JS 双向通信(JS Bridge) | 实现 JS 调用 Native 方法,Native 调用 JS 函数,用于数据交互、功能调用。 | - 统一注册/调用接口 - WKWebView 使用 WKScriptMessageHandler- UIWebView 使用 JavaScriptCore 注入对象- 支持回调机制 |
实现JS bridge有2种方式
WKScriptMessageHandler和JavaScriptCore上下文注入回调函数的方式.
2.“注入 JS Bridge 全局对象” 是一种更常见、更结构化、也更易于前端使用的 JS 桥实现方式。
a.上下文注入回调函数的方式
// 注册 JS Handler(JS 调用 Native)
- (void)registerHandler:(NSString *)handlerName handler:(void(^)(id data, void(^)(id)))callback {
if (_wkWebView) {
WKUserContentController *userContent = _wkWebView.configuration.userContentController;
[userContent addScriptMessageHandler:self name:handlerName];
// 存储 callback,用于后续响应(需自行管理字典)
} else if (_uiWebView && _uiWebView.scrollView) {
// 在 webViewDidFinishLoad 后获取 context
JSContext *context = [_uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[handlerName] = ^(NSDictionary *data, JSValue *responseCallback) {
// 模拟回调机制
callback(data, ^(id responseData) {
if (responseData) {
[self evaluateJavaScript:[NSString stringWithFormat:@"(%@)(%@)", responseCallback.toString, responseData] completion:nil];
}
});
};
}
}
// Native 调用 JS
- (void)callJSHandler:(NSString *)functionName data:(id)data responseCallback:(void(^)(id))callback {
NSString *js = [NSString stringWithFormat:@"window.%@(%@)", functionName, data ? [self jsonStringify:data] : @"null"];
if (callback) {
// 生成唯一 ID 并注册临时回调
NSString *cbId = [self generateCallbackId];
[self addResponseCallback:cbId block:callback];
js = [js stringByAppendingFormat:@"; setTimeout(() => %@('%@', result), 0)", @"__nativeCallback", cbId];
}
[self evaluateJavaScript:js completion:nil];
}
// 执行 JS(兼容两种 WebView)
- (void)evaluateJavaScript:(NSString *)script completion:(void(^)(id, NSError*))completion {
if (_wkWebView) {
[_wkWebView evaluateJavaScript:script completionHandler:completion];
} else if (_uiWebView) {
id result = [_uiWebView stringByEvaluatingJavaScriptFromString:script];
if (completion) {
completion(result, nil);
}
}
}
使用步骤
** 步骤 1:在 Native 端注册 handler**
比如你希望 H5 能调用 Native 的“登录”功能:
// 在你的 HybridWebViewManager 初始化后调用
[self.bridgeManager registerHandler:@"login" handler:^(id data, void(^responseCallback)(id)) {
// 1. 接收 JS 传来的数据
NSString *username = data[@"username"];
NSLog(@"JS 请求登录,用户名:%@", username);
// 2. 执行 Native 登录逻辑(比如弹出原生登录框、调用 SDK 等)
[self presentNativeLoginWithUsername:username completion:^(BOOL success, NSString *token) {
// 3. 登录完成后,通过 responseCallback 回调给 JS
if (success) {
responseCallback(@{@"result": @"success", @"token": token});
} else {
responseCallback(@{@"result": @"failed"});
}
}];
}];
步骤 2:H5 页面中调用(JS 端)
前端开发者就可以这样写:
// 调用 Native 的 login 方法
JSBridge.callHandler('login', { username: 'Tom' }, function(response) {
alert('登录结果: ' + JSON.stringify(response));
if (response.result === 'success') {
document.getElementById('status').innerText = '登录成功';
}
});
为了让 JS 能调用 callHandler,你需要在 Native 注入一段 JS 代码:
/ Injected JS Code (JSBridge.js)
(function() {
window.JSBridge = {
callHandler: function(handlerName, data, callback) {
// 生成唯一回调 ID
var callbackId = 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
// 保存回调函数(如果传了 callback)
if (typeof callback === 'function') {
window.JSBridge._callbacks[callbackId] = callback;
}
// 通过 postMessage 调用 Native
window.webkit.messageHandlers[handlerName].postMessage({
data: data,
callbackId: callbackId
});
},
// 接收 Native 回调
_onResponse: function(callbackId, responseData) {
var callback = window.JSBridge._callbacks[callbackId];
if (callback) {
callback(responseData);
delete window.JSBridge._callbacks[callbackId];
}
},
_callbacks: {}
};
})();
然后在 Native 中注入这段 JS(见之前回答)。
Native 如何接收并分发 callHandler 请求?
当 JS 调用 JSBridge.callHandler('login', ...) 时,会触发 messageHandlers.login。
你需要在 WKScriptMessageHandler 中解析并分发:
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *handlerName = message.name; // 如 "login"
NSDictionary *body = message.body; // { data: {}, callbackId: "cb_123" }
id data = body[@"data"];
NSString *callbackId = body[@"callbackId"];
// 从注册的 handlers 字典中查找对应处理函数
void(^handler)(id, void(^)(id)) = _registeredHandlers[handlerName];
if (handler) {
// 执行处理函数,并传入一个 responseCallback
handler(data, ^(id responseData) {
// Native 回调给 JS
NSString *js = [NSString stringWithFormat:@"JSBridge._onResponse('%@', %@)",
callbackId, [self jsonStringify:responseData]];
[_webView evaluateJavaScript:js completion:nil];
});
}
}
其中 _registeredHandlers 是一个 NSMutableDictionary,用来保存你注册的所有 handler
_registeredHandlers = [[NSMutableDictionary alloc] init];
- (void)registerHandler:(NSString *)handlerName handler:(void(^)(id, void(^)(id)))block {
_registeredHandlers[handlerName] = block;
}
b.注入js bridge对象方式
“注入 JS Bridge 全局对象” 是一种更常见、更结构化、也更易于前端使用的 JS 桥实现方式。
1.什么是“注入 JS Bridge 对象”?
就是在 H5 页面加载完成后,通过 Native 主动向 WebView 中注入一个全局的 JavaScript 对象(如 JSBridge) ,前端就可以通过这个对象调用 Native 提供的功能。
// 前端调用示例
JSBridge.login({ username: 'tom' }, function(result) {
console.log('登录结果:', result);
});
这种方式让前端开发体验更好 —— 就像调用一个普通的 JS SDK 一样,不需要关心底层通信机制。
2.实现原理
| WebView 类型 | 注入方式 |
|---|---|
WKWebView | 通过 WKUserScript 注入 JS 代码 |
UIWebView | 在 webViewDidFinishLoad: 中使用 stringByEvaluatingJavaScriptFromString: 执行注入脚本 |
3.示例代码:注入js bridge对象
注入 JSBridge 全局对象 的代码能否写的简单点,比如oc中有一个JSBridge对象,我只需要在.h .m文件中添加新的方法, 向h5注入JSBridge对象即可。不用再写额外代码。
目标效果(H5 端)
// 前端调用 Native 方法,无需额外配置
JSBridge.login({user: "Tom"}, function(result) {
console.log("登录成功:", result);
});
JSBridge.share({title: "分享标题", url: "https://example.com"});
实现思路
我们通过 Objective-C 的 Runtime 机制,自动扫描 JSBridge 子类中的方法,注入到 WebView 中,并建立通信桥接。
第一步:创建 JSBridge 基类(.h)
// JSBridge.h
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
@interface JSBridge : NSObject
// 子类实现的方法会自动暴露给 JS
// 方法签名必须是:
// - (void)methodName:(NSDictionary *)data responseCallback:(void(^)(id))callback;
- (void)registerToWebView:(WKWebView *)webView;
@end
第二步:实现自动注册(.m)
/ JSBridge.m
#import "JSBridge.h"
#import <objc/runtime.h>
@implementation JSBridge
- (void)registerToWebView:(WKWebView *)webView {
// 1. 获取当前类的所有实例方法
unsigned int methodCount;
Method *methods = class_copyMethodList([self class], &methodCount);
NSMutableString *jsCode = [NSMutableString string];
for (unsigned int i = 0; i < methodCount; i++) {
SEL sel = method_getName(methods[i]);
NSString *selectorName = NSStringFromSelector(sel);
// 只处理以 ':' 结尾(有参数)且包含 responseCallback 的方法
if ([selectorName hasSuffix:@":"] && [selectorName rangeOfString:@"responseCallback:"].location != NSNotFound) {
// 提取方法名(去掉冒号和参数名)
// 例如:login:(NSDictionary *)data responseCallback:(void(^)(id))cb
// 提取为:login
NSArray *parts = [selectorName componentsSeparatedByString:@":"];
NSString *jsMethodName = parts.firstObject;
// 生成 JS 代码:JSBridge.login = function(data, cb) { ... }
NSString *js = [NSString stringWithFormat:@"window.JSBridge.%@ = function(data, callback) { window.webkit.messageHandlers.__jsbridge.postMessage({method:'%@', data:data, callbackId:'cb_' + Date.now()}); };", jsMethodName, jsMethodName];
[jsCode appendString:js];
}
}
free(methods);
// 2. 注入 JS 代码
WKUserScript *script = [[WKUserScript alloc] initWithSource:jsCode
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:NO];
[webView.configuration.userContentController addUserScript:script];
// 3. 注册 Native 消息处理器
[webView.configuration.userContentController addScriptMessageHandler:self name:@"__jsbridge"];
}
#pragma mark - WKScriptMessageHandler
// 所有 JS 调用都走这里
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
NSDictionary *body = message.body;
NSString *methodName = body[@"method"];
id data = body[@"data"];
NSString *callbackId = body[@"callbackId"];
// 查找对应的方法 SEL
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@:responseCallback:", methodName]);
if ([self respondsToSelector:sel]) {
// 调用 OC 方法,并传入 responseCallback
void(^responseCallback)(id) = ^(id responseData) {
NSString *js = [NSString stringWithFormat:@"window.JSBridge._onResponse('%@', %@)", callbackId, [self jsonStringify:responseData]];
[(WKWebView *)message.webView evaluateJavaScript:js completion:nil];
};
((void(*)(id, SEL, NSDictionary*, void(^)(id)))objc_msgSend)(self, sel, data, responseCallback);
}
}
// 辅助方法:将对象转为 JSON 字符串
- (NSString *)jsonStringify:(id)object {
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error];
return data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : @"null";
}
@end
✅ 第三步:使用方式(业务开发)
1. 创建子类 MyJSBridge
// MyJSBridge.h
#import "JSBridge.h"
@interface MyJSBridge : JSBridge
- (void)login:(NSDictionary *)data responseCallback:(void(^)(id))callback;
- (void)share:(NSDictionary *)data responseCallback:(void(^)(id))callback;
@end
// MyJSBridge.m
#import "MyJSBridge.h"
@implementation MyJSBridge
- (void)login:(NSDictionary *)data responseCallback:(void(^)(id))callback {
NSString *username = data[@"username"];
NSLog(@"JS 请求登录: %@", username);
// 模拟登录成功
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
callback(@{@"result": @"success", @"token": @"abc123"});
});
}
- (void)share:(NSDictionary *)data responseCallback:(void(^)(id))callback {
NSString *title = data[@"title"];
NSString *url = data[@"url"];
NSLog(@"分享: %@ - %@", title, url);
callback(@{@"shared": @YES});
}
@end
2. 注册到 WebView
// 在 ViewController 中
MyJSBridge *jsBridge = [[MyJSBridge alloc] init];
[jsBridge registerToWebView:self.webView]; // 👈 只需这一行!
第四步:H5 页面无需额外代码
<script>
// 页面加载后即可使用
function doLogin() {
JSBridge.login({username: "Tom"}, function(result) {
alert("登录结果: " + JSON.stringify(result));
});
}
function doShare() {
JSBridge.share({title: "分享", url: "https://example.com"});
}
</script>
3.UIWebView和WKWebView的生命周期函数
现在从 生命周期兼容 的角度,来系统性地梳理在同时兼容 UIWebView 和 WKWebView 时,如何统一处理它们的加载生命周期函数,并说明每个生命周期函数适合实现什么功能。
| 功能 | 功能细节 | 建议补充点 |
|---|---|---|
监听页面开始加载webViewDidStartLoadwebView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation | - UIWebView:实现 UIWebViewDelegate 的 - webViewDidStartLoad:- WKWebView:实现 WKNavigationDelegate 的 - webView:didStartProvisionalNavigation:→ 表示页面开始请求,网络连接已建立 | - 可用于: ✅ 显示加载进度条/菊花 ✅ 记录加载开始时间(性能监控) ✅ 拦截非法 URL(如跳转到非白名单域名) - 建议封装统一回调,如 -[HybridDelegate webView:didStartLoading] |
| 页面内容开始接收(HTML 流式解析) (仅 WKWebView 明确提供)webView:didCommitNavigation: | - UIWebView:无直接对应方法,webViewDidStartLoad 后即开始接收- WKWebView:- webView:didCommitNavigation: 表示已收到服务器响应,开始渲染页面 | - 可用于: ✅ 隐藏“白屏”状态 ✅ 开始 DOM 操作(如注入 JS) ✅ 更精准的“首屏渲染”时间打点 - 注意:此时页面可能还未完全解析,JS 可能未就绪 |
页面加载完成webViewDidFinishLoadwebView:didFinishNavigation: | - UIWebView:- webViewDidFinishLoad:- WKWebView:- webView:didFinishNavigation:→ 页面主文档加载完成,包括同步资源(JS/CSS/图片) | - 可用于: ✅ 注入 JS Bridge 对象(推荐在此阶段,可降低url页面的加载时间) ✅ 执行初始化 JS 代码(如设置环境变量) ✅ 获取页面标题 webView.title✅ 隐藏加载动画 - 建议:在此统一触发 onPageFinished 事件 |
页面加载失败webView:didFailLoadWithError:webView:didFailNavigation:withError: | - UIWebView:- webView:didFailLoadWithError:- WKWebView:- webView:didFailNavigation:withError:→ 加载失败,如网络断开、超时、DNS 错误 | - 可用于: ✅ 显示错误页面(如离线页) ✅ 记录错误日志(上报) ✅ 提供“重试”按钮 - 建议:统一错误码处理,区分网络错误、超时、取消等 |
决定是否加载请求webView:shouldStartLoadWithRequest:navigationType:webView:decidePolicyForNavigationAction:decisionHandler: | - UIWebView:- webView:shouldStartLoadWithRequest:navigationType: 返回 BOOL- WKWebView:- webView:decidePolicyForNavigationAction:decisionHandler: 通过 decisionHandler(WKNavigationActionPolicyAllow/Cancel) 决定 | - 可用于: ✅ JS Bridge 的早期通信方案(拦截 custom scheme) ✅ 白名单/黑名单控制 ✅ 外部浏览器跳转(如 tel:、mailto:)✅ 协议拦截(如 jsbridge://login?user=tom)- 建议:封装统一的 shouldStartLoad 回调,返回 Allow/Cancel |
| 页面即将跳转(重定向) ( WKWebView 特有)webView:didReceiveServerRedirectForProvisionalNavigation: | - UIWebView:无明确回调,包含在 webViewDidStartLoad 中- WKWebView:- webView:didReceiveServerRedirectForProvisionalNavigation: | - 可用于: ✅ 监控重定向链路 ✅ 防止无限重定向 ✅ 更新当前 URL 显示 |
✅ 如何统一处理生命周期(封装建议)
实现如何统一处理 UIWebView 和 WKWebView 的生命周期,做到:对外暴露一套统一的代理方法,内部自动适配两种 WebView 的回调机制。
// HybridWebViewManager.h
@protocol HybridWebViewDelegate <NSObject>
- (void)hybridWebView:(HybridWebViewManager *)manager didStartLoading:(NSURLRequest *)request;
- (void)hybridWebView:(HybridWebViewManager *)manager didFinishLoading:(NSURLRequest *)request;
- (void)hybridWebView:(HybridWebViewManager *)manager didFailLoadingWithError:(NSError *)error;
- (BOOL)hybridWebView:(HybridWebViewManager *)manager shouldStartLoadWithRequest:(NSURLRequest *)request;
@end
状态同步
1.Cookie同步
为什么需要 Cookie 同步?
UIWebView使用NSHTTPCookieStorage+sharedCookie,与系统共享。WKWebView使用 独立的WKHTTPCookieStore,默认不与NSHTTPCookieStorage同步。- 如果 H5 页面需要登录态(如
sessionid),必须手动同步 Cookie,否则登录状态丢失!
2.解决方案:双向同步 Cookie
a. 从 Native 设置 Cookie(登录后写入)
// 统一设置 Cookie(兼容两种 WebView)
+ (void)setCookieForURL:(NSURL *)url name:(NSString *)name value:(NSString *)value {
// 写入 NSHTTPCookieStorage(UIWebView & 系统请求可用)
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieName: name,
NSHTTPCookieValue: value,
NSHTTPCookieDomain: url.host,
NSHTTPCookiePath: @"/",
NSHTTPCookieSecure: @(url.scheme isEqualToString:@"https"),
NSHTTPCookieExpires: [NSDate dateWithTimeIntervalSinceNow:30*24*3600] // 30天
}];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
// 如果是 WKWebView,还需写入其独立存储
if ([self isUsingWKWebView]) {
WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;
[cookieStore setCookie:cookie completionHandler:^{
// 可选:回调
}];
}
}
2. 启动时同步 NSHTTPCookie → WKWebView
// App 启动或 WebView 创建时调用
+ (void)syncCookiesToWKWebView:(WKWebView *)wkWebView {
NSArray<NSHTTPCookie *> *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
WKHTTPCookieStore *cookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore;
for (NSHTTPCookie *cookie in cookies) {
[cookieStore setCookie:cookie completionHandler:nil];
}
}
3. 监听 WKWebView Cookie 变化(反向同步)
// 监听 WKWebView 的 Cookie 变化(如 H5 设置了 newCookie)
- (void)startMonitoringCookies:(WKWebView *)wkWebView {
WKHTTPCookieStore *cookieStore = wkWebView.configuration.websiteDataStore.httpCookieStore;
[cookieStore observeValueForKeyPath:@"cookies" ofObject:cookieStore change:nil context:nil];
// 实际中建议使用 KVO 或定时同步
[NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer *timer) {
[cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *cookies) {
// 将 WK 的 Cookie 同步回 NSHTTPCookieStorage
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:nil mainDocumentURL:nil];
}];
}];
}
#### 4.建议补充点
| 场景 | 建议 |
| --------- | ---------------------------------------------------------------------- |
| 用户登录后 | 调用 `setCookie` 写入 `sessionid` |
| 用户登出 | 删除 Cookie:`[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies` 过滤删除 |
| 多 WebView | 建议全局统一 Cookie 管理类 |
| 安全性 | 敏感 Cookie 设置 `Secure`、`HttpOnly`
2.User-Agent 设置(User-Agent Override)
a.为什么需要设置 User-Agent?
- 让服务器识别是“App 内嵌浏览器”,返回适配的 H5 页面。
- 区分不同 App、版本、平台(如
MyApp/1.0.0 (iOS 15.0))。 - 避免被识别为普通 Safari。
b.实现方式
UIWebView:直接设置
UIWebView会自动读取NSUserDefaults中的UserAgent。
// 在 AppDelegate 或启动时设置全局 UA
NSDictionary *originalHeaders = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
NSString *customUA = [originalHeaders objectForKey:@"UserAgent"];
if (!customUA) {
customUA = [NSString stringWithFormat:@"%@ MyApp/1.0.0", [[UIDevice currentDevice] userAgent]];
[[NSUserDefaults standardUserDefaults] setObject:customUA forKey:@"UserAgent"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
WKWebView:通过 customUserAgent 属性
// 创建 WKWebView 前设置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.applicationNameForUserAgent = @"MyApp"; // 会追加到 UA 末尾
// 或直接覆盖
WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:config];
webView.customUserAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MyApp/1.0.0";
3. 统一设置方法(推荐)
+ (void)setupUserAgentForWebView:(UIView *)webView {
NSString *baseUA = @"MyApp/1.0.0";
NSString *fullUA = [NSString stringWithFormat:@"%@ %@", [self getCurrentSystemUA], baseUA];
if ([webView isKindOfClass:[WKWebView class]]) {
((WKWebView *)webView).customUserAgent = fullUA;
}
// UIWebView 通过 NSUserDefaults 设置(只需设置一次)
}
4.建议补充点
| 场景 | 建议 |
|---|---|
| 全局设置 | 建议在 AppDelegate 中统一设置 |
| 动态切换 | 某些页面需要不同 UA(如强制 PC 模式) |
| 获取当前 UA | 可通过 JS 注入 navigator.userAgent 获取 |
| 安全 | 不要暴露敏感信息(如设备序列号) |
3、缓存策略(Cache Policy)
UIWebView:使用NSURLCache,默认缓存静态资源。WKWebView:使用独立缓存(位于Library/Caches/WebKit),默认也缓存。- 缓存过多会占用磁盘空间。
- 某些资源需要强制刷新(如活动页)。
a.为什么需要控制缓存?
UIWebView:使用NSURLCache,默认缓存静态资源。WKWebView:使用独立缓存(位于Library/Caches/WebKit),默认也缓存。- 缓存过多会占用磁盘空间。
- 某些资源需要强制刷新(如活动页)。
b.缓存控制策略
设置全局缓存大小
// 在 AppDelegate 中设置
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 设置最大缓存 50MB
NSUInteger cacheSize = 50 * 1024 * 1024;
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
diskCapacity:cacheSize
diskPath:@"WebKitCache"];
[NSURLCache setSharedURLCache:cache];
return YES;
}
WKWebView 独立缓存清除
// 清除 WKWebView 缓存
+ (void)clearWKWebViewCache {
NSSet<WKWebsiteDataRecord *> *dataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
NSDate *from = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:dataTypes
modifiedSince:from
completionHandler:^{
NSLog(@"WKWebView 缓存已清除");
}];
}
禁用缓存(调试或特定页面)
// 创建 request 时设置缓存策略
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
request.timeoutInterval = 30;
自定义缓存逻辑(高级)
- 使用
NSURLSession+NSURLProtocol拦截请求,实现更精细缓存控制。 - 对 JS/CSS/图片等资源设置不同过期时间。
4.建议补充点
| 场景 | 建议 |
|---|---|
| 正式环境 | 启用缓存,提升加载速度 |
| 调试阶段 | 可临时禁用缓存 |
| 用户反馈“页面未更新” | 提供“清除缓存”功能 |
| 大型资源 | 建议使用 CDN + 强缓存(带版本号) |
| 离线包 | 可结合 NSCache 或文件系统实现离线资源管理 |
安全性
✅ 白名单机制(Whitelist)
✅ 防注入攻击(XSS / JS 注入防护)
✅ 通信加密(Secure Communication)
一、白名单机制
防止 WebView 加载非法、钓鱼、恶意网站,仅允许访问可信域名。
1. 域名白名单配置(建议用 plist 或远程配置)
// WhitelistManager.h
@interface WhitelistManager : NSObject
+ (BOOL)isURLAllowed:(NSURL *)url;
+ (void)loadWhitelistFromRemote; // 可从服务器拉取
@end
// WhitelistManager.m
static NSSet<NSString *> *whitelistDomains;
+ (void)initialize {
// 本地默认白名单
NSArray *domains = @[
@"example.com",
@"api.example.com",
@"static.example.com"
];
whitelistDomains = [NSSet setWithArray:domains];
}
+ (BOOL)isURLAllowed:(NSURL *)url {
if (!url.host) return NO;
for (NSString *domain in whitelistDomains) {
if ([url.host isEqualToString:domain] ||
[url.host hasSuffix:[@"." stringByAppendingString:domain]]) {
return YES;
}
}
return NO;
}
2. 在生命周期中拦截非法请求
// 在 HybridWebViewManager 的 shouldStartLoad 中调用
- (BOOL)hybridWebViewShouldStartLoad:(NSURLRequest *)request {
NSURL *url = request.URL;
// 1. 拦截非法域名
if (![WhitelistManager isURLAllowed:url]) {
NSLog(@"🚫 拦截非法域名: %@", url.absoluteString);
[self showAlert:@"访问被拒绝" message:@"该页面不在允许访问的范围内。"];
return NO;
}
// 2. 拦截危险协议
NSString *scheme = url.scheme;
if ([scheme hasPrefix:@"file"] ||
[scheme hasPrefix:@"data"] ||
[scheme hasPrefix:@"javascript"]) {
return NO;
}
return YES;
}
3. ✅ 建议补充点
| 场景 | 建议 |
|---|---|
| 动态更新 | 白名单可从服务器拉取,支持热更新 |
| 调试环境 | 可临时放宽限制(如允许 localhost) |
| 外链跳转 | 非白名单链接可引导至 Safari 打开 |
二、防注入攻击(Anti-XSS / Anti-Injection)
XSS(跨站脚本) :H5 页面存在漏洞,被注入恶意 JS
JSBridge 滥用:前端通过 eval 或动态 script 标签调用 Native 方法
URL 参数注入:?callback=alert(1) 触发执行
1. 严格校验 JS 调用参数
// 在 JSBridge 方法中校验输入
- (void)login:(NSDictionary *)data responseCallback:(void(^)(id))callback {
// 必须包含 username,且为字符串
NSString *username = data[@"username"];
if (!username || ![username isKindOfClass:[NSString class]] || username.length == 0) {
callback(@{@"code": @-1, @"msg": @"参数错误"});
return;
}
// 过滤特殊字符(可选)
NSCharacterSet *invalidChars = [[NSCharacterSet alphanumericCharacterSet] invertedSet];
if ([username rangeOfCharacterFromSet:invalidChars].location != NSNotFound) {
callback(@{@"code": @-2, @"msg": @"用户名包含非法字符"});
return;
}
// 正常处理
...
}
2. 禁止 evaluateJavaScript: 执行不可信代码
// ❌ 危险!不要这样做
[webView evaluateJavaScript:userInput completion:nil];
// ✅ 正确:只执行预定义方法
[webView evaluateJavaScript:@"JSBridge._onResponse('cb_123', {'result': 'ok'})" completion:nil];
3. 使用 Content Security Policy(CSP)
在 H5 页面 <head> 中添加:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline';
img-src *;
style-src 'self' 'unsafe-inline';
frame-src 'none';">
限制只能加载自身域名资源,禁止 eval、inline script 等。
4. ✅ 建议补充点
| 攻击类型 | 防护建议 |
|---|---|
| XSS | 后端输出编码、前端输入过滤、启用 CSP |
| JS 注入 | 不允许动态执行用户输入的 JS |
| 协议劫持 | 拦截 jsbridge:// 等自定义协议并校验参数 |
| Cookie 窃取 | 设置 HttpOnly、Secure |
三、通信加密(Secure Communication)
防止 JS 与 Native 之间传输的数据被窃听或篡改。
1. JS → Native 通信加密(可选)
虽然通常不必要(因为运行在本地),但对高敏感场景可加密:
// H5 端发送前加密
function callNative(method, data) {
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), 'secret-key').toString();
JSBridge.call(method, { encrypted });
}
// OC 端解密
- (void)call:(NSDictionary *)data responseCallback:(void(^)(id))callback {
NSString *encrypted = data[@"encrypted"];
NSData *decoded = [[NSData alloc] initWithBase64EncodedString:encrypted options:0];
// 使用 AES 解密...
}
2.Native → H5 通信安全
- 所有
evaluateJavaScript调用应避免拼接用户数据 - 使用
NSJSONSerialization序列化参数,防止 JSON 注入
// ✅ 安全拼接
NSDictionary *response = @{@"result": @"success", @"data": userData};
NSString *json = [self jsonStringify:response];
NSString *js = [NSString stringWithFormat:@"JSBridge._onResponse('%@', %@)", callbackId, json];
[webView evaluateJavaScript:js completion:nil];
3. 网络层加密(关键!)
- 所有 WebView 加载的页面必须使用 HTTPS
- 启用 App Transport Security (ATS)
<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
4. 双向认证(mTLS,金融级安全)
对极高安全要求场景,可实现客户端证书认证:
- Native 携带客户端证书请求 H5 页面
- 服务端验证证书合法性
5.✅ 建议补充点
| 场景 | 建议 |
|---|---|
| 普通应用 | HTTPS + 白名单 + 参数校验 |
| 金融类 App | HTTPS + mTLS + CSP + 日志审计 |
| JSBridge 通信 | 不传敏感信息,必要时端到端加密 |
| 密钥管理 | 使用 Keychain 存储,避免硬编码 |
✅ 总结:WebView 安全三要素
| 安全能力 | 实现方式 | 适用场景 |
|---|---|---|
| 白名单 | 域名过滤 + 协议拦截 | 所有 Hybrid 应用必备 |
| 防注入 | 参数校验 + CSP + 禁用 eval | 防止 XSS 和非法调用 |
| 通信加密 | HTTPS + ATS + 可选数据加密 | 数据敏感类 App 强制要求 |
最佳实践清单Checklist
✅ 启用 ATS,强制 HTTPS
✅ 配置域名白名单,拦截非法跳转
✅ 校验所有 JSBridge 输入参数
✅ H5 页面启用 CSP 策略
✅ 敏感操作使用 Native 实现,不依赖 JS
✅ 定期审计 WebView 加载的页面来源
✅ 提供“清除缓存/数据”功能,便于应急
稳定性
对于任何应用来说,健壮的错误处理机制都是必不可少的。它不仅能够提升用户体验,还能帮助开发者快速定位问题所在。
一、错误处理
1. 捕获并处理加载错误
// WKWebView 的错误处理
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
// 显示友好的错误信息给用户
[self showUserFriendlyErrorMessage:error];
}
- (void)showUserFriendlyErrorMessage:(NSError *)error {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"加载失败"
message:[error localizedDescription]
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:okAction];
[self presentViewController:alert animated:YES completion:nil];
}
2.前端与后端通信中的错误处理
在使用 JSBridge 进行前后端交互时,也需要考虑如何处理可能出现的错误情况。
function callNative(method, data, callback) {
try {
JSBridge.call(method, data, function(response) {
if (response.error) {
console.error('调用 Native 方法失败:', response.error);
return;
}
callback(response.result);
});
} catch (e) {
console.error('调用 Native 方法发生异常:', e);
}
}
二、 内存管理(Memory Management)
内存泄漏是导致应用性能下降甚至崩溃的一个常见原因。合理地管理 WebView 的内存使用,可以显著提高应用的稳定性和响应速度。
1.释放不再需要的资源
在不需要 WebView 时,记得及时释放其占用的资源。
- (void)dealloc {
self.webView.navigationDelegate = nil;
self.webView.UIDelegate = nil;
self.webView = nil;
}
2.优化图片和资源加载
尽量减少大尺寸图片或其他资源的直接加载,可以考虑使用懒加载或压缩技术。
三、 生命周期控制(Lifecycle Control)
正确地管理 WebView 的生命周期,不仅可以避免不必要的资源消耗,还能确保数据的一致性和用户体验的连贯性。
根据应用状态调整 WebView
例如,在应用进入后台时暂停 WebView 的活动,以节省电量和网络流量;当应用重新回到前台时恢复 WebView 的操作。
- (void)applicationDidEnterBackground:(UIApplication *)application {
// 暂停所有 WebView 操作
[self.webView stopLoading];
self.webView.navigationDelegate = nil;
self.webView.UIDelegate = nil;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// 恢复 WebView 操作
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
}
性能
一、离线缓存(Offline Cache)
🎯 目的 在网络差或无网络时,仍能快速加载页面,减少白屏,提升首屏速度。
1. 使用 NSURLCache 缓存静态资源(HTML/JS/CSS)
// AppDelegate.m 中配置全局缓存
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 设置内存缓存 4MB,磁盘缓存 50MB
NSURLCache *cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
diskCapacity:50 * 1024 * 1024
diskPath:@"WebViewCache"];
[NSURLCache setSharedURLCache:cache];
return YES;
}
UIWebView 和 WKWebView 都会使用此缓存(但 WKWebView 有独立缓存机制)。
2. 自定义缓存:将 H5 页面打包为“离线包”
- 将 HTML、JS、CSS、图片等资源打包成
.zip文件。 - 下载后解压到本地沙盒目录。
- WebView 加载时优先读取本地文件。
// 示例:加载本地离线包
NSString *offlinePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"offline/index.html"];
NSURL *localURL = [NSURL fileURLWithPath:offlinePath];
[webView loadRequest:[NSURLRequest requestWithURL:localURL]];
3. 动态更新策略(热更新)
- 每次启动时请求服务端获取资源版本号(如
manifest.json)。 - 对比本地版本,如有更新则后台下载新包。
- 下载完成后切换指向新资源目录。
4.建议补充点
| 场景 | 建议 |
|---|---|
| 活动页、营销页 | 推荐使用离线包,首屏加载快 |
| 频繁更新内容 | 使用版本比对 + 增量更新 |
| 大资源(视频) | 可缓存,但需提供清除入口 |
| 缓存失效 | 设置 TTL(Time-To-Live)或监听服务端通知 |
二、资源预加载(Resource Preload)
🎯 目的 在用户尚未访问前,提前加载可能需要的资源,实现“秒开”体验。
1. 预加载 WebView 实例(冷启动优化)
// 在 App 启动时创建一个隐藏的 WKWebView(预热)
- (void)prepareWebViewInBackground {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
self.preloadedWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
// 加载一个空白页面,触发 WebKit 初始化
[self.preloadedWebView loadHTMLString:@"<html><body></body></html>" baseURL:nil];
}
⚡️ WKWebView 第一次创建非常慢(WebKit 启动耗时 ~400ms),预创建可显著提升后续页面打开速度。
2. 预加载常用 H5 页面资源
// 在首页或空闲时预加载关键页面
- (void)preloadCriticalPages {
NSArray *urls = @[
@"https://m.example.com/home",
@"https://m.example.com/profile"
];
for (NSString *urlStr in urls) {
NSURL *url = [NSURL URLWithString:urlStr];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
req.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // 优先走缓存
[self.preloadedWebView loadRequest:req]; // 后台加载
}
}
3.建议补充点
| 场景 | 建议 |
|---|---|
| 启动页后跳转 H5 | 预创建 WebView |
| Tab 页面中有 H5 | 提前加载非当前 Tab 的页面 |
| 用户行为预测 | 如点击“我的”前,预加载个人中心页 |
| 内存控制 | 预加载后若长时间不用,应释放 |