iOS开发基础45-UIWebview与WKWebView

UIWebView 与 JS 交互:UIWebView 用法、NSInvocation 动态调用、JSBridge 与 WKWebView 迁移

本文系统梳理 UIWebView 的加载、导航、代理、JS 交互,以及 NSInvocation/NSMethodSignature 动态方法调用封装;同时标注 UIWebView 已废弃并补充 WKWebView 现代替代方案与 WKScriptMessageHandler。


一、UIWebView 简介

重要:UIWebView 在 iOS 12 起被正式废弃(deprecated),苹果推荐使用 WKWebView(iOS 8+)。App Store 审核自 2020 年 4 月起拒绝包含 UIWebView 的新应用上架,2020 年 12 月起拒绝仍使用 UIWebView 的应用更新。新项目必须使用 WKWebView,本文 UIWebView 内容仅用于维护老项目和理解原理。

1. 什么是 UIWebView

UIWebView 是 iOS 早期内置的网页浏览控件,基于 WebKit 引擎,功能类似 Safari。可加载远程网页和本地多种文件类型:

  • 网页:HTML、HTM
  • 文档:PDF、DOC、PPT、TXT、XLS
  • 多媒体:MP4 等

2. 加载资源

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

// 加载本地 HTML 字符串
NSString *html = @"<html><body><h1>Hello</h1></body></html>";
[webView loadHTMLString:html baseURL:nil];

// 加载本地二进制数据(PDF/图片等)
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"doc" ofType:@"pdf"]];
[webView loadData:data MIMEType:@"application/pdf" textEncodingName:@"UTF-8" baseURL:nil];

3. 键盘工具条语言

UIWebView 内输入时,键盘工具条("完成/前往"等)默认显示英文。可通过 Info.plist 设置 Localization native development region(CFBundleDevelopmentRegion)为 zh_CN 让工具条显示中文。该设置影响 App 默认语言区域,UIWebView 键盘语言主要跟随系统语言,此设置为辅助手段。


二、UIWebView 常用属性和方法

1. 导航方法

[webView reload];       // 重新加载(刷新)
[webView stopLoading];  // 停止加载
[webView goBack];       // 回退
[webView goForward];    // 前进

2. 常用属性

属性 说明
canGoBack 是否能回退(BOOL,只读)
canGoForward 是否能前进(BOOL,只读)
loading 是否正在加载(BOOL,只读)
scalesPageToFit 是否自动缩放页面以适应屏幕
dataDetectorTypes 自动检测的数据类型(电话/链接/地址等)
scrollView 内部滚动视图(可自定义 inset、delegate)
allowsInlineMediaPlayback 是否允许内嵌播放视频(YES 时不自动全屏)
mediaPlaybackRequiresUserAction 视频播放是否需要用户手动触发
// 检测所有数据类型(电话、链接、地址、日历事件等)
webView.dataDetectorTypes = UIDataDetectorTypeAll;

// 自动缩放适应屏幕
webView.scalesPageToFit = YES;

// 导航状态判断
if (webView.canGoBack) {
    [webView goBack];
}
if (webView.loading) {
    [webView stopLoading];
}

三、UIWebView 代理

控制器遵循 UIWebViewDelegate 协议,监听加载过程:

@interface MyViewController () <UIWebViewDelegate>
@end

代理方法

// 开始加载
- (void)webViewDidStartLoad:(UIWebView *)webView {
    NSLog(@"开始加载");
}

// 加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSLog(@"加载完成");
    // 可在此获取页面标题
    NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
}

// 加载失败
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    NSLog(@"加载失败: %@", error.localizedDescription);
}

// 加载请求前拦截(返回 YES 允许加载,NO 取消)
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *url = request.URL.absoluteString;
    // 拦截自定义 scheme,实现 JS 调用 OC
    if ([url hasPrefix:@"customscheme://"]) {
        [self handleCustomScheme:url];
        return NO;
    }
    return YES;
}

navigationType 枚举:

值 说明
UIWebViewNavigationTypeLinkClicked 用户点击链接
UIWebViewNavigationTypeFormSubmitted 提交表单
UIWebViewNavigationTypeBackForward 前进/回退
UIWebViewNavigationTypeReload 重新加载
UIWebViewNavigationTypeFormResubmitted 重新提交表单
UIWebViewNavigationTypeOther 其他

四、NSInvocation 与 NSMethodSignature

NSInvocation 用于将方法调用(target、selector、参数、返回值)封装为对象,可在稍后调用或传递,是消息转发和动态调用的基础工具。

注意:NSInvocation 基于 Objective-C 动态消息分发,在 Swift 中不可用。Swift 中替代方案为闭包(closure)、Selector 或 NSSelectorFromString(仅能调用无参数或参数为对象的方法)。

1. 基本用法

- (void)invokeExample {
    // 1. 获取方法签名(描述方法的参数和返回值类型)
    SEL selector = @selector(sendMessageWithNumber:andContent:status:);
    NSMethodSignature *signature = [self methodSignatureForSelector:selector];
    if (!signature) {
        NSLog(@"方法不存在");
        return;
    }
    
    // 2. 创建 NSInvocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    
    // 3. 设置参数
    // 参数索引从 2 开始:0 是 target(self),1 是 selector(_cmd),2 起是真实参数
    NSString *number = @"10086";
    NSString *content = @"Hello";
    NSString *status = @"Success";
    [invocation setArgument:&number atIndex:2];
    [invocation setArgument:&content atIndex:3];
    [invocation setArgument:&status atIndex:4];
    
    // 4. 调用方法
    [invocation invoke];
    
    // 5. 获取返回值(如果方法有返回值)
    // id returnValue;
    // [invocation getReturnValue:&returnValue];
}

- (void)sendMessageWithNumber:(NSString *)number andContent:(NSString *)content status:(NSString *)status {
    NSLog(@"number: %@, content: %@, status: %@", number, content, status);
}

参数索引为什么从 2 开始:Objective-C 方法调用本质是 objc_msgSend(self, _cmd, arg1, arg2, ...),前两个隐藏参数是 self(target,索引 0)和 _cmd(selector,索引 1),真实参数从索引 2 开始。

2. NSMethodSignature 常用属性

属性/方法 说明
numberOfArguments 参数总数(含 target 和 selector)
methodReturnLength 返回值长度(0 表示 void)
getArgumentTypeAtIndex: 获取指定索引参数的类型编码
methodReturnType 返回值类型编码

五、NSInvocation 封装(performSelector:withObjects:)

通过分类封装一个通用方法,支持传入参数数组调用任意多参数方法:

// NSObject+PerformSelector.h
@interface NSObject (PerformSelector)
- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;
@end

// NSObject+PerformSelector.m
@implementation NSObject (PerformSelector)

- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects {
    // 获取方法签名
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
    if (!signature) {
        @throw [NSException exceptionWithName:@"NSInvalidArgumentException"
                                       reason:[NSString stringWithFormat:@"未找到方法 %@", NSStringFromSelector(aSelector)]
                                     userInfo:nil];
    }
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = aSelector;
    
    // 真实参数个数 = 总参数 - 2(target 和 selector)
    NSUInteger argCount = signature.numberOfArguments - 2;
    NSUInteger count = MIN(objects.count, argCount);
    
    for (NSUInteger i = 0; i < count; i++) {
        id obj = objects[i];
        [invocation setArgument:&obj atIndex:i + 2];
    }
    
    [invocation invoke];
    
    // 获取返回值(仅当返回值为对象类型时有效)
    if (signature.methodReturnLength > 0) {
        id returnValue;
        [invocation getReturnValue:&returnValue];
        return returnValue;
    }
    return nil;
}

@end

使用示例:

NSArray *params = @[@"10086", @"Hello"];
[self performSelector:@selector(sendMessageWithNumber:andContent:) withObjects:params];

局限性:

  1. 参数只能是对象类型(id),基本类型(int/BOOL/CGFloat 等)需包装为 NSNumber/NSValue 后传入,但方法参数声明为基本类型时无法正确解包。
  2. 返回值只能正确接收对象类型(id),基本类型返回值用 id 接收会导致野指针或错误。
  3. 此封装适合参数和返回值均为对象类型的方法。

六、JavaScript 交互(JSBridge)

UIWebView 时代,OC 与 JS 交互通过两种方式:OC 调用 JS 用 stringByEvaluatingJavaScriptFromString:,JS 调用 OC 通过拦截自定义 URL scheme。

1. OC 调用 JS

// 执行 JS 代码(同步执行,会阻塞主线程)
NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
NSLog(@"页面标题: %@", result);

// 调用 JS 函数并传参
NSString *js = [NSString stringWithFormat:@"showAlert('%@')", @"Hello from OC"];
[webView stringByEvaluatingJavaScriptFromString:js];

注意:stringByEvaluatingJavaScriptFromString: 是同步方法,会阻塞主线程直到 JS 执行完毕,JS 代码耗时过长会导致 UI 卡顿。该方法在 iOS 12+ 随 UIWebView 一同废弃,WKWebView 中用异步的 evaluateJavaScript:completionHandler: 替代。

2. JS 调用 OC(拦截 URL Scheme)

JS 端:

// 无参数调用
function callOC() {
    window.location.href = "customscheme://call";
}

// 带参数调用(参数放在 query 中)
function callOCWithParams() {
    window.location.href = "customscheme://sendMessageWithNumber_andContent_?10086&Hello";
}

OC 端(拦截并解析):

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *urlString = request.URL.absoluteString;
    NSString *scheme = @"customscheme://";
    
    if ([urlString hasPrefix:scheme]) {
        // 解析路径和参数
        NSString *path = [urlString substringFromIndex:scheme.length];
        NSArray *components = [path componentsSeparatedByString:@"?"];
        
        // 方法名:将 _ 替换为 : (如 sendMessageWithNumber_andContent_ -> sendMessageWithNumber:andContent:)
        NSString *methodName = [components.firstObject stringByReplacingOccurrencesOfString:@"_" withString:@":"];
        SEL selector = NSSelectorFromString(methodName);
        
        if ([self respondsToSelector:selector]) {
            // 解析参数
            NSArray *params = components.count > 1 ? [components[1] componentsSeparatedByString:@"&"] : @[];
            
            // 用封装的 performSelector:withObjects: 调用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self performSelector:selector withObjects:params];
#pragma clang diagnostic pop
        }
        return NO; // 取消加载,阻止页面跳转
    }
    return YES;
}

- (void)sendMessageWithNumber:(NSString *)number andContent:(NSString *)content {
    NSLog(@"JS 调用 OC: number=%@, content=%@", number, content);
    
    // OC 执行完毕后,可通过注入 JS 将结果返回给 JS
    NSString *js = [NSString stringWithFormat:@"onOCResult('%@')", @"success"];
    [webView stringByEvaluatingJavaScriptFromString:js];
}

原理:JS 修改 window.location.href 会触发 UIWebView 的 shouldStartLoadWithRequest: 回调,OC 在此拦截自定义 scheme 并解析方法名和参数,实现 JS 调用 OC。这是 UIWebView 时代 JSBridge 的基础原理,开源库 WebViewJavascriptBridge 在此基础上封装了更完善的通信机制。

返回值处理:JS 调用 OC 是异步的(通过 URL 跳转),OC 无法直接 return 值给 JS。OC 执行完毕后需通过 stringByEvaluatingJavaScriptFromString: 注入 JS 代码调用 JS 回调函数,将结果传回 JS。


七、WKWebView(现代替代方案)

WKWebView 是 iOS 8+ 引入的现代 WebView,取代 UIWebView,性能更好(Nitro JS 引擎)、内存占用更低、支持更多现代 Web 标准,JS 交互通过 WKScriptMessageHandler 更优雅。

1. 基本用法

#import <WebKit/WebKit.h>

@interface WebViewController () <WKNavigationDelegate, WKScriptMessageHandler>
@property (nonatomic, strong) WKWebView *webView;
@end

@implementation WebViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    
    // 注册 JS 调用 OC 的消息通道(name 即 JS 端 window.webkit.messageHandlers.<name>.postMessage())
    [config.userContentController addScriptMessageHandler:self name:@"callOC"];
    
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    
    // 加载请求
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.apple.com"]]];
}

#pragma mark - WKNavigationDelegate

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

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

- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    NSLog(@"加载失败: %@", error);
}

// 拦截请求(类似 UIWebView 的 shouldStartLoadWithRequest)
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSString *url = navigationAction.request.URL.absoluteString;
    if ([url hasPrefix:@"customscheme://"]) {
        // 处理自定义 scheme
        decisionHandler(WKNavigationActionPolicyCancel); // 取消
        return;
    }
    decisionHandler(WKNavigationActionPolicyAllow); // 允许
}

#pragma mark - WKScriptMessageHandler(JS 调用 OC)

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"callOC"]) {
        NSLog(@"JS 调用 OC: %@", message.body); // message.body 可以是字符串/字典/数组
    }
}

// OC 调用 JS(异步,不阻塞主线程)
- (void)callJS {
    [self.webView evaluateJavaScript:@"showAlert('Hello from OC')" completionHandler:^(id result, NSError *error) {
        if (error) {
            NSLog(@"JS 执行失败: %@", error);
        } else {
            NSLog(@"JS 返回: %@", result);
        }
    }];
}

// 移除消息 handler(避免循环引用,dealloc 前必须移除)
- (void)dealloc {
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"callOC"];
}

@end

JS 端调用 OC:

// WKWebView 中 JS 调用 OC
window.webkit.messageHandlers.callOC.postMessage({name: "test", value: 123});

2. UIWebView vs WKWebView

对比项 UIWebView WKWebView
状态 iOS 12 废弃,App Store 拒绝 推荐(iOS 8+)
JS 引擎 旧引擎(无 JIT) Nitro(含 JIT,性能高)
内存 高(与 App 同进程) 低(独立进程,崩溃不影响 App)
OC 调 JS 同步(阻塞主线程) 异步(completionHandler)
JS 调 OC 拦截 URL scheme WKScriptMessageHandler(原生支持)
进度回调 无(需 KVO) estimatedProgress(自带)
标题 需执行 JS 获取 title 属性(自带)
现代 Web 标准 支持差 支持好

八、Swift 版本对照

WKWebView(Swift)

import WebKit

class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
    var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let config = WKWebViewConfiguration()
        config.userContentController.add(self, name: "callOC")
        
        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.navigationDelegate = self
        view.addSubview(webView)
        
        webView.load(URLRequest(url: URL(string: "https://www.apple.com")!))
    }
    
    // MARK: - WKNavigationDelegate
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("加载完成")
    }
    
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let url = navigationAction.request.url?.absoluteString, url.hasPrefix("customscheme://") {
            decisionHandler(.cancel)
            return
        }
        decisionHandler(.allow)
    }
    
    // MARK: - WKScriptMessageHandler
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "callOC" {
            print("JS 调用 OC: \(message.body)")
        }
    }
    
    // OC 调用 JS
    func callJS() {
        webView.evaluateJavaScript("showAlert('Hello')") { result, error in
            if let error = error {
                print("JS 错误: \(error)")
            }
        }
    }
    
    deinit {
        webView.configuration.userContentController.removeScriptMessageHandler(forName: "callOC")
    }
}

Swift 中 NSInvocation 不可用,动态调用替代方案为闭包或 perform(_:with:)(仅支持最多 2 个对象参数)。


九、总结

  • UIWebView:iOS 早期 WebView,iOS 12 起废弃,App Store 审核拒绝使用。支持加载远程/本地资源、导航(前进后退刷新)、代理监听加载、stringByEvaluatingJavaScriptFromString: 同步执行 JS、JS 调用 OC 通过拦截自定义 URL scheme。仅用于维护老项目。
  • NSInvocation:封装方法调用(target/selector/参数/返回值)的对象,参数索引从 2 开始(0 target、1 selector),支持 invoke 调用和 getReturnValue: 获取返回值。基于 OC 动态消息分发,Swift 中不可用。封装 performSelector:withObjects: 可通用调用多参数方法,但仅限参数和返回值均为对象类型。
  • JSBridge 原理:UIWebView 中 JS 修改 window.location.href 触发 shouldStartLoadWithRequest:,OC 拦截自定义 scheme 解析方法名和参数实现 JS 调 OC;OC 调 JS 用 stringByEvaluatingJavaScriptFromString:(同步阻塞)。JS 调 OC 是异步的,返回值需 OC 执行后注入 JS 回调传回。
  • WKWebView(推荐):iOS 8+ 现代 WebView,Nitro JS 引擎性能高、独立进程内存低、崩溃不影响 App;OC 调 JS 用 evaluateJavaScript:completionHandler:(异步不阻塞);JS 调 OC 用 WKScriptMessageHandler(window.webkit.messageHandlers.<name>.postMessage()),原生支持无需拦截 URL;自带 estimatedProgress 和 title;dealloc 前必须 removeScriptMessageHandlerForName: 避免循环引用。
  • 迁移建议:新项目直接用 WKWebView;老项目逐步从 UIWebView 迁移到 WKWebView,JS 交互从 URL 拦截迁移到 WKScriptMessageHandler。

posted @ 2015-08-26 17:13  Mr.陳  阅读(1061)  评论(0)    收藏  举报