前言:
一段时间了,手头的工作照常进行中,之前有好几个主题一直想写,但又搁置了,主要是主题有点大,看样子路还是要一步一步走的;
前几天,收到了一份面试通知,一家比较大的互联网企业吧,想想自己也没什么面试经验,总想着要换个环境,也想换个角度审视一下自己,所以就去了,真心是没准备什么,临行前把几个常用算法翻出来看了看……这里就说说我的体会吧(可能会占一定篇幅,谁让这是我的地盘呢)
到了之后,人力的同学带我上楼,先是笔试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:
接下来可以看下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进行调试,大家看看这几张图就知道了:
现在我用手机打开一个我项目中的url,就可以通过Safari进行调试了:
在出现的网页检查器中刻意直接输入JS的代码,执行相关方法进行测试,比如我执行一个alert()方法:
传入参数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,他有两个参数(我们传的是公参和私有参数,你用的话可以随便传点什么,这些方法实现时商定即可)
在控制台也是可以调用到这个方法的:
实际调用的话可以这样:
NSString * js = [NSString stringWithFormat:@"fnCommon(\'%@\',\'%@\')",pubStr,priStr];
[self.webView evaluateJavaScript:js completionHandler:nil];
JS端拿到传递的参数,处理即可;
JS端-app端:
这个场景前面我们已经论述过了,这里直接给出控制台执行的示例:
执行这句代码之后,app中的webview就会调用WKScriptMessageHandler协议的代理方法:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
NSLog(@"\nJS:\nname-%@;\nbody-%@\n",message.name,message.body);
}
name就是createdoctor;
body就是postMessage传入的字符串参数;
这里给出客户端的log,大家可以试一下;
无法释放的问题:
前面我们已经讲过,在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:在被释放的对象并没有被初始化……
………………这个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