前言:
一段時間了,手頭的工作照常進行中,之前有好幾個主題一直想寫,但又擱置了,主要是主題有點大,看樣子路還是要一步一步走的;
前幾天,收到了一份面試通知,一家比較大的網際網路企業吧,想想自己也沒什麼面試經驗,總想着要換個環境,也想換個角度審視一下自己,是以就去了,真心是沒準備什麼,臨行前把幾個常用算法翻出來看了看……這裡就說說我的體會吧(可能會占一定篇幅,誰讓這是我的地盤呢)
到了之後,人力的同學帶我上樓,先是筆試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