前面说过,只要退出登录,App就会闪退。这是因为我们在退出登录时清除WKWebView的cookie时引发了崩溃。
苹果应该在iOS11.3上修改了nonPersistentDataStore创建的DataStore的实现。
我们先回归一下WKWebView下cookie的前世今生。cookie本质上是h5用来保存登录、用户名、以及一些特定时间内有效的信息的机制。这个东西有几个显而易见的好处:
1、本地化的存储一些信息,可以减少h5请求的次数。提升h5的性能,降低服务端压力;
2、cookie作为h5请求request的一部分,会把一些客户端的固定信息直接传给服务端,避免h5因为某些信息又需要发送网络请求;
3、cookie是域名隔离的,有一定的安全性;
正因为有这么多的好处,所以,很多时候h5大量的依赖了cookie机制。甚至用它保存了一些敏感信息。这些信息的泄露会对用户的安全造成重大威胁。所以苹果在iOS8引入WKWebView时,没有给原生暴露任何操控cookie的API(事实上,不仅cookie,其他任何和缓存、持久化相关的接口都没有)。我的理解是:苹果有意识将cookie对开发者进行了屏蔽,就是不希望原生开发者对cookie进行干预。而cookie的安全性完全由h5来保证。讲道理这么做无可厚非。但是有个东西很可怕----那就是用户习惯。在原来老的UIWebView下,以及其他的浏览器中熟悉了各种cookie操作的开发者肯定不会就此罢休。各种携带cookie的“黑”操作层出不穷。所以,从iOS9开始,苹果暴露了WKWebView数据存储的类----WKWebsiteDataStore。顾名思义,这个类暴露了一些获取(清除)特定缓存的能力,这里包括了cookie。下面稍微介绍一下这些能力
可以获取dataStore中所有的数据类型。这里有:
WKWebsiteDataTypeDiskCache,//硬盘缓存
WKWebsiteDataTypeOfflineWebApplicationCache,//离线应用缓存
WKWebsiteDataTypeMemoryCache,//内存缓存
WKWebsiteDataTypeLocalStorage,//localStorage,cookie的一个兄弟
WKWebsiteDataTypeCookies,//cookie
WKWebsiteDataTypeSessionStorage,//session
WKWebsiteDataTypeIndexedDBDatabases,//索引数据库
WKWebsiteDataTypeWebSQLDatabases//数据库
这里可以看出,与h5相关的缓存都暴露给了原生。
-(void)fetchDataRecordsOfTypes:(NSSet*)dataTypes completionHandler:(void(^)(NSArray *))completionHandler;
-(void)removeDataOfTypes:(NSSet*)dataTypes forDataRecords:(NSArray *)dataRecords completionHandler:(void (^)(void))completionHandler;
移除某些特定的数据类型。
- (void)removeDataOfTypes:(NSSet *)websiteDataTypes modifiedSince:(NSDate *)date completionHandler:(void (^)(void))completionHandler;
移除指定时期的特定的数据类型。
从以上可以看出,苹果暴露了各种缓存的获取方法和移除方法。但是没有给设置方法。而且cookie并不突出。和其他兄弟一样。所以苹果的本意还是不希望原生直接干预cookie的设置。
但是从iOS11开始,一切都变了。苹果彻底放开了cookie的操作权限。它在WKWebsiteDataStore中暴露了一个
WKHTTPCookieStore类型的属性。专门用来管理cookie。cookie摇身一变,从嫔妃变成了皇后。位置特殊了。通过这个属性,原生可以自由的移除cookie和设置cookie了(是不是和以前的NSHttpCookieStorage一样了?)。
这种设计带来了一个问题?现在在iOS8、9、10、11都存在的情况下,我们如何清除cookie。iOS8没有暴露接口,用户量也不大了,我们不做考虑。iOS9,10只有一种方式。iOS11有两种(注意苹果暴露了WKHTTPCookieStore的属性来操作cookie,但是老的API并没有废弃,哪怕是和cookie相关的那部分也没有废弃)。我想看到这个问题,脑子比较正常的人都会想:
iOS11采用新的API就行了啊。
但总有脑子不正常的人,比如我。我原本是想这么干的。但是后来一想,老的API没有废弃,那就肯定可以用,那我继续用它呗。但是有个问题,iOS11既然暴露了新的接口,这是不是意味着老的API可能清除cookie会失败呢?或者后续哪个版本万一失败呢?所以,我用了一种比较“保险”的方案。那就是先用老的API去清除,然后用新的API去校验。伪代码可以这么来看:
old api fetch cookie:
finish{
old api remove cookie:
finish{
new api get cookie:
finish{
if cookie still exist
new api clear cookie
}
}
}
逻辑上没有毛病!!!我到目前为止仍然认为逻辑上没有毛病。
下面开始清除。在清除的时候我们发现WKWebsiteDataStore有两个类方法:
/* @abstract Returns the default data store. */
+ (WKWebsiteDataStore *)defaultDataStore;
/** @abstract Returns a new non-persistent data store.
@discussion If a WKWebView is associated with a non-persistent data store, no data will
be written to the file system. This is useful for implementing "private browsing" in a web view.
*/
+ (WKWebsiteDataStore *)nonPersistentDataStore;
字面意思很清楚,第二个是非持久的dataStore, 那第一个就是持久的。我的做法是分别去清除这两类dataStore中的cookie。需求做完没有毛病,一直很健康,退出登录时cookie被清除的寸草不生。我暗自欣慰。
但是iOS11.3推出后,登陆的同事反馈,闪退。瞎了我的钛合金狗眼。为啥,再经历了mac系统升级、xcode升级、手机操作系统升级等一系列变态操作后,真相来了:
我们上述方法在清除nonPersistentDataStore时当执行到用iOS11的API去校验时就挂了。崩溃的栈显示WKWebsiteDataStore正在析构。WTF!!!!
问题的原因大概就清晰了:
defaultDataStore不挂,原因是它类似于单例,代码执行过程中不会析构!
nonPersistentDataStore 以前不挂,现在开始挂,说明以前苹果对它的实现也是单例,而现在它应该不是单例了。我们创建它的实例是在函数内部,当出了方法作用域后它就会析构了。执行iOS11 API检验时它正在析构,向一个正在析构的对象发消息它是会挂的。
现在我们回头来看nonPersistentDataStore的说明:
@abstract Returns a new non-persistent data store.
@discussion If a WKWebView is associated with a non-persistent data store, no data will
be written to the file system. This is useful for implementing "private browsing" in a web view.
和前面持久化的dataStore不同的是,nonPersistentDataStore用于实现私密的浏览器,即该浏览器的数据与其他WebView不共享。苹果本质的意愿应该是一个WKWebView和一个nonPersistentDataStore的绑定。以前的策略应该是多个WKWebView可以和一个non-persistent的datastore绑定。只要它和普通的datastore隔离即可。(当然,以上都是通过现象的推测。如果有人有确定说明,欢迎指正)
至此,这个坑介绍完毕!我们来总结一下这里我们的失误:
第一、对nonPersistentDataStore缺少比较全面的理解,如果理解了它的用途,其实可以判定当前我们的App是没有使用场景的;
第二、iOS11和iOS9、10的API最好不要嵌套使用。其实我们当时做的时候就发现老的API的效果是ok的。只不过不放心,用一种莫须有的心态增加了一个逻辑。其实这个逻辑即使有必要,也要经过验证,证明老的API确实无法完成再去完善,毕竟cookie即使清除不了,也不是天塌下来的事。后续完善即可。这种前期画蛇添足的思维方式还是要多改变!!!
完!