天天看点

iOS WebView中的JS交互

前言:

一段时间了,手头的工作照常进行中,之前有好几个主题一直想写,但又搁置了,主要是主题有点大,看样子路还是要一步一步走的;

前几天,收到了一份面试通知,一家比较大的互联网企业吧,想想自己也没什么面试经验,总想着要换个环境,也想换个角度审视一下自己,所以就去了,真心是没准备什么,临行前把几个常用算法翻出来看了看……这里就说说我的体会吧(可能会占一定篇幅,谁让这是我的地盘呢)

到了之后,人力的同学带我上楼,先是笔试1h,之后是面试,大概又是1h吧;笔试的内容,基础的比如:weak、assign等关键字的使用场景,KVO的实现机制等,const/static的用法,CALayer层的使用,响应者链的概念,多线程GCD、NSOperation手写代码示例,手写单例和递归函数;还有一些内容比较新,因为有几个内容是最近一些主页论坛分享出来的文章,或是一直以来讨论的主题,比如:如何高效的切割圆角,app的性能优化等;

大概答了一些,觉得自己说不明白的也就没写,面试官来了,就开始聊我回答的问题,涉及到的其实主要还是内存管理,多线程的使用,数据结构的常见问题,比如链表,二叉树遍历,快排算法等一些基础的算法思想……最后也问道了app框架上,设计模式等等;

其实很多东西都能说一点,但是了解的又不够深入,总的来说,这是一次失败的面试,但同时也是一次宝贵的经验,面试技巧上就不提了,因为我做的也不好,哪能一两次面试就总结出一堆真知灼见来,不过这次面试的内容,还是引起了我的思考:从开始接触iOS开发,到今天差不多2年半了,时间并不短,对开发的理解也在逐渐变化,以前觉得实现需求顺利就可以了,忽视了软件开发其实是一个比较完成的体系,简单的实现可能只是冰山一角,深入的了解底层的原理,真的非常重要,新的知识也要及时接触和实践,在这些方面我做的还明显不够,好了,说了一堆废话,算是与君共勉,接下来我们来看这次的主题;

UIWebView和WKWebView:

iOS8之前,对于webview的展示,我们使用UIWebView,在iOS8之后,苹果推出了WKWebView,webkit使用WKWebView来代替UIWebView和OS X的WebView,新提供的WKWebView运行JS可以和Safari一样快,内存上也优化了很多,这个大家在使用时可以很明显的看出来,我就不举例了;

UIWebView使用NSURLCache缓存,通过setSharedURLCache可以设置成我们自己的缓存,但WKWebview并不支持NSURLCache,相关缓存的清除我一会会给出;

对于简单的webview加载,其实两个控件用起来大同小异,都有各自的协议回调相应的页面加载过程,由于在新项目中已经不再支持iOS8以前的系统了,所以自然就想到了用WKWebView;

需求上需要进行JS的交互,这也就是我本次要讨论的主题,让我们先来熟悉一下WKWebView;

WKWebView API:

在使用之前需要先导入framework:

iOS WebView中的JS交互

接下来可以看下WKWebView的API:(我只列举一部分)

@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
           
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
           
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
           
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
           
@property (nonatomic, readonly) double estimatedProgress;
           
@property (nonatomic, readonly) BOOL canGoBack;
           
@property (nonatomic, readonly) BOOL canGoForward;
           
- (nullable WKNavigation *)goBack;

- (nullable WKNavigation *)goForward;

- (nullable WKNavigation *)reload;
           
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
           

这几个包括了:

WKWebView初始化时的配置属性;初始化方法;加载url的方法;当前的加载状态;加载进度;前进/后退操作;解释执行JS语句的方法;

意思都比较清晰,熟悉就好;

WKWebView的使用:

使用之前先导入头文件:

#import <WebKit/WebKit.h>
           

接下来让我们看一段初始化的代码:

WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    
    // 设置偏好设置
    config.preferences = [[WKPreferences alloc] init];
    // 默认为0
    config.preferences.minimumFontSize = 10;
    // 默认认为YES
    config.preferences.javaScriptEnabled = YES;
    // 在iOS上默认为NO,表示不能自动通过窗口打开
    config.preferences.javaScriptCanOpenWindowsAutomatically = YES;
    
    // web内容处理池
    config.processPool = [[WKProcessPool alloc] init];
    
    // 通过JS与webview内容交互
    config.userContentController = [[WKUserContentController alloc] init];
    // 注入JS对象名称createdoctor,当JS通过createdoctor来调用时,
     [config.userContentController addScriptMessageHandler:self name:@"createdoctor"];
    
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0,ScreenWidth , ScreenHeight - 64) configuration:config];
    
    // 添加KVO监听
    [self.webView addObserver:self
                  forKeyPath:@"loading"
                     options:NSKeyValueObservingOptionNew
                     context:nil];
    [self.webView addObserver:self
                  forKeyPath:@"title"
                     options:NSKeyValueObservingOptionNew
                     context:nil];
    [self.webView addObserver:self
                  forKeyPath:@"estimatedProgress"
                     options:NSKeyValueObservingOptionNew
                     context:nil];
    
    self.webView.UIDelegate = self;
    self.webView.navigationDelegate = self;
    
    [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
    
    [self.view addSubview:self.webView];
           

这其中有两个类值得我们特别关注下:WKWebViewConfiguration和WKUserContentController;

我们注意到WKWebViewConfiguration对应的对象在设置相关属性之后,作为WKWebView初始化的配置项传入;WKUserContentController的实例作为注册JS对象的容器存在(有添加就有移除,api里方法都有);这两个类的API我就不列在这了,没几个方法,大家看下就行;

这段代码可以分为四个部分:

1.配置实例config的初始化,注意页面的一些设置都是在preferences的属性中设置的,config实例还有一个比较重要的属性是websiteDataStore(WKWebsiteDataStore),存储的信息类似UIWebView的缓存信息,这个类会在清理缓存的时候用到;

2.WKUserContentController实例注册JS对象;

3.WKWebView初始化及URL加载;

4.KVO监听加载状态,页面标题,加载进度 几个属性的变化;

1、3、4相信大家都比较清楚,主要是第2步,那接下来我就做下说明:

app webview调用执行JS,很简单,使用WKWebView,只需要用相应的实例调用方法即可:

NSString * js = @"alert('Objective-C call js to show alert');window.webkit.messageHandlers.createdoctor.postMessage('要告诉app医生已经创建成功 你可以进行界面切换了')";
    [self.webView evaluateJavaScript:js completionHandler:nil];
           

就像这样;

也可以提前注入一个js方法,在需要调用的时候执行相应方法即可,这样就不用每次都写一段js代码来解释执行了:

//js注入,注入一个测试方法。
    NSString *javaScriptSource = @"function userFunc(){window.webkit.messageHandlers.createdoctor.postMessage( {\"name\":\"HS\"})}";
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:javaScriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];// forMainFrameOnly:NO(全局窗口),yes(只限主窗口)
    [config.userContentController addUserScript:userScript];
           

但是实际的应用场景要稍复杂些,比如说前后端的传值(这里我先把app这里叫前端,JS端成为后端,这是相对来讲的,大家理解就行):

1.如果前端希望后端处理一些场景,但是还需要提供一下前端才有的数据,这是就需要在前端调用执行后端在页面中提供的js方法,进行传值,即“前->后”;

2.相应的,在后端处理了一些事物之后,或是需要主动触发前端的相关操作,就需要把具体的结果通过前端注册的JS对象,并调用方法,进行传值,即“后->前”;

前面提到的注册js对象,对应的就是“后-前”的这种场景,我们可以看下这个方法:

[config.userContentController addScriptMessageHandler:self name:@"createdoctor"];
           

Handler:是遵循WKScriptMessageHandler协议的实例对象,通过该协议的回调方法,回去服务端传过来的参数:

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
        NSLog(@"\nJS:\nname-%@;\nbody-%@\n",message.name,message.body);

}
           

这里指定为当前的视图控制器;

name:指定的值是一个字符串,这个字符串,或被webkit解释成一个对象,供后端使用,使用的格式形如:

window.webkit.messageHandlers.createdoctor.postMessage('哈哈哈')"

其中createdoctor就是我们注册时传入的name,可以注册多个JS对象(注意有注册就要有移除)供后端在不同的业务场景中调用,postMessage是一个方法,里边传的是参数;

若使用上边的代码那我们在离开这个ViewController的时候,在dealloc中就需要做几件事情:

一直提到要移除已经注册的JS对象,所以这里要调用WKUserContentController的实例方法

[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"createdoctor"];
           

同时还要移除KVO的监听;

还有一个(如果在意的话),就是清除缓存:

之前提到的WKWebsiteDataStore类是iOS9之后提供的,我们可以用它来清除webview存储的数据;

NSSet *websiteDataTypes = [NSSet setWithArray:@[
                                                                WKWebsiteDataTypeDiskCache,
                                                                WKWebsiteDataTypeOfflineWebApplicationCache,
                                                                WKWebsiteDataTypeMemoryCache,
                                                                WKWebsiteDataTypeLocalStorage,
                                                                WKWebsiteDataTypeCookies,
                                                                WKWebsiteDataTypeSessionStorage,
                                                                WKWebsiteDataTypeIndexedDBDatabases,
                                                                WKWebsiteDataTypeWebSQLDatabases
                                                                ]];
           

有很多类型是吧,区分不出来,就全删掉好了:

NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
        NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
        [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
                                                   modifiedSince:dateFrom completionHandler:^{
                                                       // code
                                                   }];
           

但是还会有这样的疑问,既然这个类是iOS9之后才有的,那iOS8中WKWebView的缓存如何清理啊,我在网上找了一段代码:

//wkwebview缓存清空
        NSString *libraryDir = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
                                                                   NSUserDomainMask, YES)[0];
        NSString *bundleId  =  [[[NSBundle mainBundle] infoDictionary]
                                objectForKey:@"CFBundleIdentifier"];
        NSString *webkitFolderInLib = [NSString stringWithFormat:@"%@/WebKit",libraryDir];
        NSString *webKitFolderInCaches = [NSString
                                          stringWithFormat:@"%@/Caches/%@/WebKit",libraryDir,bundleId];
        NSString *webKitFolderInCachesfs = [NSString
                                            stringWithFormat:@"%@/Caches/%@/fsCachedData",libraryDir,bundleId];
        
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:webKitFolderInCaches error:&error];
        [[NSFileManager defaultManager] removeItemAtPath:webkitFolderInLib error:&error];
        [[NSFileManager defaultManager] removeItemAtPath:webKitFolderInCachesfs error:&error];
           

这个我也没试过好不好用(一会再和大家讲为什么没试),实质就是去本地找缓存路径,然后清空指定路径的数据;

有了这些准备之后,接下来我们来实践一下,这里需要借助Safari提供的Web检查器进行JS的调试;

使用Safari进行web调试:

先看下如何使用Safari进行调试,大家看看这几张图就知道了:

iOS WebView中的JS交互
iOS WebView中的JS交互
iOS WebView中的JS交互

现在我用手机打开一个我项目中的url,就可以通过Safari进行调试了:

iOS WebView中的JS交互

在出现的网页检查器中刻意直接输入JS的代码,执行相关方法进行测试,比如我执行一个alert()方法:

iOS WebView中的JS交互

传入参数1的话,在我app中相应的webView上就会显示一个alert弹窗提示“1”;

注意:WKWebView对于这类alert有单独的代理方法进行处理,如:

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
           

需要在这个方法中,实现UIAlertController否则该弹窗不会显示;

app端-JS端:

app端给js端传值,只需调用evaluateJavaScript方法执行JS提供的方法即可;

当前webview中js提供了一个方法,如fnCommon,他有两个参数(我们传的是公参和私有参数,你用的话可以随便传点什么,这些方法实现时商定即可)

iOS WebView中的JS交互

在控制台也是可以调用到这个方法的:

iOS WebView中的JS交互

实际调用的话可以这样:

NSString * js = [NSString stringWithFormat:@"fnCommon(\'%@\',\'%@\')",pubStr,priStr];
    [self.webView evaluateJavaScript:js completionHandler:nil];
           

JS端拿到传递的参数,处理即可;

JS端-app端:

这个场景前面我们已经论述过了,这里直接给出控制台执行的示例:

iOS WebView中的JS交互

执行这句代码之后,app中的webview就会调用WKScriptMessageHandler协议的代理方法:

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
        NSLog(@"\nJS:\nname-%@;\nbody-%@\n",message.name,message.body);

}
           

name就是createdoctor;

body就是postMessage传入的字符串参数;

这里给出客户端的log,大家可以试一下;

iOS WebView中的JS交互

无法释放的问题:

前面我们已经讲过,在js端调用app端时需要注册JS对象:

[config.userContentController addScriptMessageHandler:self name:@"createdoctor"];
           

我们将代理设置为self,注册了对用的就是移除,我们是在dealloc中做的,但是实际调试过程中,我们发现退出界面后,dealloc方法并没有执行,这里貌似是self对象被强引用得不到释放造成的,这里我们可以通过自定义一个实现WKScriptMessageHandler协议的对象,如HQScriptMessageHandler:

HQScriptMessageHandler * messageHandle = [[HQScriptMessageHandler alloc]initWithDelegate:self];
    [config.userContentController addScriptMessageHandler:messageHandle name:@"createdoctor"];
           

在这个类中实现相应的回调方法,并处理即可,这样self就能正常释放,dealloc方法就会被调用了;

对象释放的问题:

在执行“window.webkit.messageHandlers.createdoctor.postMessage('要告诉app医生已经创建成功 你可以进行界面切换了') ”这句代码之后,虽然在WKScriptMessageHandler协议的代理方法中收到了正常的JS端传过来的参数,但是出现了crush:在被释放的对象并没有被初始化……

iOS WebView中的JS交互

………………这个malloc_error_break咋debug啊?(我只想说,我的内心是崩溃的>_<,这就是前面iOS8下清缓存的方式我没试的原因)

没办法的办法:

这个bug调试了一段时间,找了几个同业的牛人请教了一下,奈何人家写的code就没问题;在论坛上提了还没回复;最终我的解决方案是暂时使用UIWebView实现JS的交互,优先实现功能,这也是没办法的办法;

限于个人能力,没能将这个问题解决掉,希望有遇到相同情况的同学在看到我的文章之后,能指点一二,万分谢谢;

我自己也会继续尝试解决这个问题,有了进展我会更新上来,方便大家交流;

UIWebView API:

得,又回来看我们的老朋友了:

@property (nullable, nonatomic, assign) id <UIWebViewDelegate> delegate;

- (void)loadRequest:(NSURLRequest *)request;

- (void)reload;
- (void)stopLoading;

- (void)goBack;
- (void)goForward;

@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

           

在初始化webView视图之后,我们可以通过loadrequest方法载入相应的界面

NSString * url = @"http://www.baidu.com";
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    self.webView.delegate = self;
    [self.webView loadRequest:request];
           

load、reload、stoploading这个几个方法对应属性loading的状态值;

goback方法使用时需要判断属性canGoBack的值;goForward方式使用时需要判断属性canGoForward的值;这两个方法是处理webview内连接界面切换的;(对于跨域的链接需要手动打开)

看起来是不是和WKWebView差不多;

执行js代码使用如下方法:

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
           

其他属性就不看了,协议的代理方法又一个还是需要注意下:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
           

通过这个代理方法,也是可以实现原生和js交互的,在这个方法里面。可以拿到每一个URL,通过对URL的参数字段监测分析,可以实现JS调用OC代码;

用一个比较形象的词就是”url重定向“(姑且这样讲吧)

我们要用这种方式吗?当然不是,iOS7之后,苹果提供了一个新的库JavaScriptCore(对应头文件#import <JavaScriptCore/JavaScriptCore.h>),用来做JS交互,我们就用这个;

JavaScriptCore:

JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。使用起来也比较简单,深入了解的话推荐大家看看这篇文章

这里,我就直接放代码了,用起来都差不多;

app端=>JS端:

NSString * pubStr = [pubPara toJSONString];
    NSString * priStr = [priPara toJSONString];
    
    NSString * js = [NSString stringWithFormat:@"fnCommon(\'%@\',\'%@\')",pubStr,priStr];
    [self.webView stringByEvaluatingJavaScriptFromString:js];
           

调用webview方法,直接解释执行js代码即可;

或是参考上边文章中的这种方式

self.context = [[JSContext alloc] init];

    NSString *js = @"function add(a,b) {return a+b}";

    [self.context evaluateScript:js];

    JSValue *n = [self.context[@"add"] callWithArguments:@[@2, @3]];

    NSLog(@"---%@", @([n toInt32]));//---5
           

这种其实和WKWebView的js注入有点像;

JS端=>app端:

首先定义一个对象,并声明一个继承自JSExport协议的协议:

.h

#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>

@protocol HQJSProtocal <JSExport>

JSExportAs(postjsaction, -(void)postjsaction:(NSString *)name Scripts:(NSString *)action);
JSExportAs(alert, -(void)alert:(id)message);


@end

@protocol HQJSResultDelegate <NSObject>

-(void)receivejsaction:(NSString *)name Scripts:(NSString *)action;

@end

@interface HQJSInterface : NSObject<HQJSProtocal>

-(instancetype)initWithDelegate:(id<HQJSResultDelegate>)delegate;

@end
           

HQJSResultDelegate这个协议是用来回调传递HQJSProtocal协议方法接收到的数据的;

.m

#import "HQJSInterface.h"

@interface HQJSInterface()

@property (nonatomic , weak) id<HQJSResultDelegate> delegate;

@end

@implementation HQJSInterface
-(instancetype)initWithDelegate:(id<HQJSResultDelegate>)delegate{
    if (self = [super init]) {
        _delegate = delegate;
    }
    return self;
}

-(void)postjsaction:(NSString *)name Scripts:(NSString *)action{
    //nb
    if ([self.delegate respondsToSelector:@selector(receivejsaction:Scripts:)]) {
        [self.delegate receivejsaction:name Scripts:action];
    }
}
-(void)alert:(id)message{
    if ([message isKindOfClass:[NSString class]]) {
        NSLog(@"%@",message);

    }
}

@end
           

之后结合UIWebView使用:

- (void)webViewDidFinishLoad:(UIWebView *)webView{
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    HQJSInterface * dtp = [[HQJSInterface alloc]initWithDelegate:self];
    self.context[@"dtp"] = dtp;
}
           

获取上下文,注册交互对象,通过HQJSResultDelegate的协议方法,我们就可以得到js调用所传递的数据了;

总结:

这种js交互的实践,到是让我重新认识了一下js与原生,联想到了最近很火的ReactNative,移动端的开发对系统api及平台的依赖性是必然的,但是新的编程框架也在改变着一些东西,之前有位朋友说他已经连续几个月一直在用React这种方式编程了,不禁感叹技术的跟新如此之快,以至于稍不留神就又被抛出轨道的感觉,一开始的唠叨多少和这个也有些关系,敢于尝试新的东西,同时深入学习有所专攻,努力成长,迎接即将到来的2017。

参考文章:

http://www.jianshu.com/p/86a1b69bc9a6

http://www.jianshu.com/p/d19689e0ed83

http://www.tuicool.com/articles/qQRrMzY