iOS如何实现功能完善的WebView

217 阅读20分钟

前言

WebView是常用的组件,对于大型项目来说。WebView和iOS需要有良好的业务、UI兼容性及双向通信能力。

假设一个新的项目,需要实现一个WebView,该从哪些方面着手呢?

功能清单

类别功能
✅ 基础功能兼容 WKWebView\UIWebView、加载本地html/网络url、双向通信JS桥、生命周期函数
✅ 状态管理Cookie 同步、User-Agent 设置、缓存策略
✅ 安全性白名单、防注入、通信加密
✅ 稳定性错误处理、内存管理、生命周期控制
✅ 性能离线缓存、资源预加载、多实例管理
✅ 可维护性日志、调试支持、模块化设计

基础功能

1.兼容WKWebView和UIWebView

功能功能细节建议补充点
兼容 WKWebView 和 UIWebView根据系统版本或类是否存在,动态创建 WKWebView(iOS 8+ 支持)或回退到 UIWebView1. 使用 NSClassFromString 判断是否支持 WKWebView
2. 封装统一接口,避免业务层感知底层差异
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种方式

  1. WKScriptMessageHandlerJavaScriptCore 上下文注入回调函数的方式.

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的生命周期函数

现在从 生命周期兼容 的角度,来系统性地梳理在同时兼容 UIWebViewWKWebView 时,如何统一处理它们的加载生命周期函数,并说明每个生命周期函数适合实现什么功能。

功能功能细节建议补充点
监听页面开始加载
webViewDidStartLoad
webView:(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 可能未就绪
页面加载完成
webViewDidFinishLoad
webView: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 显示

如何统一处理生命周期(封装建议) 实现如何统一处理 UIWebViewWKWebView 的生命周期,做到:对外暴露一套统一的代理方法,内部自动适配两种 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';">

限制只能加载自身域名资源,禁止 evalinline script 等。

4. ✅ 建议补充点
攻击类型防护建议
XSS后端输出编码、前端输入过滤、启用 CSP
JS 注入不允许动态执行用户输入的 JS
协议劫持拦截 jsbridge:// 等自定义协议并校验参数
Cookie 窃取设置 HttpOnlySecure

三、通信加密(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 + 白名单 + 参数校验
金融类 AppHTTPS + 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;
}

UIWebViewWKWebView 都会使用此缓存(但 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 的页面
用户行为预测如点击“我的”前,预加载个人中心页
内存控制预加载后若长时间不用,应释放

可维护性