谈起Cookie,若是没有了解过它,可能会望文生畏。作过WebView开发的人可能会对它比较了解。Android的Cookie是由系统去管理的,其特色是会被持久化成一个db文件,保存在/data/data/{packageName}/app_webview/Cookies
中(不一样系统、不一样浏览器实现可能不同,但大致如此)。一般,网站的登陆信息是使用Cookie来保存的,若是App也是使用Cookie来实现鉴权,那么在WebView和App之间就须要创建一套Cookie同步机制。html
尽管考拉的鉴权机制不是使用Cookie来实现的,但咱们也遇到了相似的需求,使用WebView打开一个特定的url,这个url的响应会写入指定的Cookie,而后url通过一次302重定向,通过url拦截后打开一个App页面,并把url响应中携带的Cookie带到这个App页面。java
若是App和WebView处于同一个进程,那么实现起来是比较简单的,能够参考这篇文章,代码不作过多解释,以okhttp
为例:android
import android.webkit.CookieManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
/**
* Provides a synchronization point between the webview cookie store and okhttp3.OkHttpClient cookie store
*/
public final class WebviewCookieHandler implements CookieJar {
private CookieManager webviewCookieManager = CookieManager.getInstance();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
String urlString = url.toString();
for (Cookie cookie : cookies) {
webviewCookieManager.setCookie(urlString, cookie.toString());
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
String urlString = url.toString();
String cookiesString = webviewCookieManager.getCookie(urlString);
if (cookiesString != null && !cookiesString.isEmpty()) {
//We can split on the ';' char as the cookie manager only returns cookies
//that match the url and haven't expired, so the cookie attributes aren't included
String[] cookieHeaders = cookiesString.split(";");
List<Cookie> cookies = new ArrayList<>(cookieHeaders.length);
for (String header : cookieHeaders) {
cookies.add(Cookie.parse(url, header));
}
return cookies;
}
return Collections.emptyList();
}
}
复制代码
代码来自 gist.github.com/justinthoma…。git
可是若是App和WebView处于不一样的进程,事情就没那么简单了。因为不一样进程之间数据是不共享的,进程之间的Cookie同步就成了一个问题。随后的测试发现,App的多进程间是共享同一个Cookies文件的,但进程之间的Cookie数据不必定可以实时同步。咱们遇到的问题是,WebView进程访问携带了特定Cookie的url后,这些Cookie并无同步到主进程。因而,带着层层疑问,咱们开始了进程间同步Cookie的猜测实验。考虑一下两个进程间可能致使Cookie数据不一致的地方(如下假设App在A进程,WebView在B进程):github
CookieManager.getInstance().setAcceptCookie(true)
保证A进程可以读取到Cookie;下面咱们一一分析上述7种状况,并加以条件进行测试。须要说明的是,为了避免影响每次实验的结果,都须要在加载url以前,清空/data/data
目录下的Cookie文件。web
WebView访问一个url,B进程的WebView写入Cookie之后,没有当即写入Cookies.db持久化,致使A进程读取不到最新的Cookie。chrome
WebView在加载url时,服务端返回须要写入的Cookie可使用Chrome Inspect来查看。针对WebView的Cookie持久化时机,咱们能够作一个简单的实验。浏览器
实验步骤:
一、使用WebView加载url;
二、加载完成后(调用WebViewClient.onPageFinished()
),拿到Cookie文件,查看是否有写入Cookie。缓存
以https://m.baidu.com
为例,未加载WebView组件以前,咱们能够找一台root过的手机,查看/data/data/{packageName}
目录下是没有app_webview
目录的。bash
加载url之后,可使用chrome inspect查看Cookie信息,m.baidu.com
会生成如下Cookie:
此时再次访问上述目录,能够发现app_webview目录已经存在了,而且生成了Cookie文件。说明在第一次打开WebView加载完https://m.baidu.com
的时候就已经生成了Cookie而且持久化,
为了进一步证明,咱们导出/data/data/{packageName}/app_webview/Cookies
文件,并查看是否包含上面的Cookie,来证明Cookie是否有被持久化。
结果显而易见——Cookie在WebView加载完成url之后几乎是当即持久化的,咱们的第一个猜测不成立。
因为Cookie是和WebView挂钩的,可能须要在A进程建立一个WebView来让Cookie在进程间同步。
咱们知道,WebView的Cookie是交由系统去管理的[^1],WebView在实例化过程当中可能对Cookie进行必定的操做。若是没有实例化WebView,是否是Cookie就同步不过来呢?基于这个猜测,咱们进行第二次实验。
实验步骤:
一、B进程加载https://m.baidu.com
后,在B进程使用CookieManager
查看m.baidu.com
的Cookie;
二、A进程实例化WebView,不加载,而后在A进程使用CookieManager
查看m.baidu.com
的Cookie;
三、B进程再次使用WebView加载https://m.taobao.com
,在B进程查看m.taobao.com
的Cookie;
四、A进程再次实例化WebView,不加载,在A进程查看m.taobao.com
的Cookie。
咱们看到一个有趣的现象:
首次实例化A进程的WebView时,能够拿到B进程以前写入的Cookie。但当B进程再次写入其余Cookie时,此时再实例化A进程的WebView却取不到了。这个过程可能说明了只有在第一次实例化WebView的时候才会去同步持久化的Cookie,当Cookie再次更新时,别的进程读取不到更新后的Cookie数据。第二个猜测不成立。
setAcceptCookie(true)
A进程须要调用
CookieManager.getInstance().setAcceptCookie(true)
保证A进程可以读取到Cookie。
既然须要使用到Cookie,而进程是否默认容许记录Cookie是个未知的行为,索性咱们能够测试一下,强制让进程容许记录Cookie。可使用以下代码:
CookieManage.getInstance().setAcceptCookie(true);
复制代码
实验步骤:
一、在Application启动的时候调用CookieManage.getInstance().setAcceptCookie(true);
二、重复猜测二的实验步骤,观察A进程和B进程的Cookie同步状况。 三、在Application启动的时候调用CookieManage.getInstance().setAcceptCookie(false);
四、再次重复猜测二的步骤。
不管是否设置容许记录Cookie,测试结果和猜测二的结果同样,图就不贴了,说明Cookie在进程间的同步和是否容许记录Cookie无关。第三个猜测不成立。
B进程的Cookie可能失效了,致使A进程读取不到Cookie。
B进程的Cookie可能失效了,致使A进程读取不到Cookie。出现这个猜测的缘由是咱们使用chrome inspect查看Cookie时,它显示的时间的确是过时了的,好比刚才访问的https://m.baidu.com
,
有一条Cookie的时间表示为2019-04-28T05:38:12.000Z
,可是注意到时间最后的字母Z
,它表示的是GMT/UTC时间里的GMT+0时区[^2]。转换成北京时间(GMT+8)后,就是下午1点38分。
说明这条Cookie仍是有效的,排除了因为Cookie失效致使A进程访问不到的可能。另外,在Android中,即便Cookie已经失效,也可以经过CookieManager.getInstance().getCookie(url)
取得,而且该方法返回一个字符串,不包含Cookie的Expires
字段。第四个猜测不成立。
A进程和B进程的Cookie文件根本不是同一个,致使数据没法同步。
A进程和B进程的Cookie文件根本不是同一个,致使数据没法同步。通过上面的猜测和实验,其实能够说明这个猜测是不成立的,若是进程读取的Cookie文件不是同一个的话,那么在B进程访问https://m.baidu.com
后,A进程不可能拿到B进程的WebView写入的Cookie,测试二的结论说明了这一点。为了让事实更具备说服力,仍是以实验说明这一点。
实验步骤:
一、B进程访问https://m.baidu.com
;
二、保存Cookie文件的最后修改时间;
三、A进程再次访问https://m.baidu.com
(或者别的url也能够);
四、查看Cookie文件的最后修改时间并与步骤二的进行比对。
咱们分别在14:06的B进程和14:08的A进程访问了https://m.baidu.com
,结果以下:
说明App里的不一样进程使用的是同一个Cookie文件进行读取和写入。第五个猜测不成立。
A进程建立了WebView而且访问了同域的url,而后覆盖了B进程以前已经持久化的Cookie
由第五个猜测的实验结果可知,不一样进程间是使用同一个Cookie文件进行持久化。若是A进程和B进程都容许写Cookie,那么进程间就可能产生Cookie覆盖的现象。咱们能够测试一下。
实验步骤:
一、使用B进程WebView打开https://m.baidu.com
,记录当前的Cookie文件;
二、使用A进程WebView打开https://m.baidu.com
,记录当前的Cookie文件;
三、对第一步和第二步的Cookie文件进行对比。
https://m.baidu.com
)
https://m.baidu.com
)
从图中能够看到,B进程访问url后的Cookie和A进程访问url后的Cookie数据几乎是一致的,只有一列不同——last_access_utc
。咱们猜想这个字段表示上一次成功读取/写入该Cookie的时间(没有找到相关的文档介绍),但至少说明Cookies这个文件发生了覆盖,也就是说,App里的不一样进程对同一个域访问,可能会形成Cookie覆盖。
即使如此,到目前为止,尚未可以解释B进程的部分Cookie在A进程获取不到的现象。
Cookie是经过CookieManager管理的,CookieManager是个单例,单个进程可能只会读取一次Cookies.db,而后缓存在内存中。
Android中全部与Cookie的操做都与CookieManager有关,上面的几种猜测都没有考虑到CookieManager的问题,CookieManager是一个单例,一旦建立,除非进程被清除,不然便不会销毁。若是说CookieManager只有在建立时才读取一次Cookies.db文件,后面对Cookie的读取优先使用内存中的缓存,那么上面的现象即可以解释得通了。仍是经过实验来验证。
实验步骤:
一、A进程未初始化CookieManager的状况下,使用进程B访问https://m.baidu.com
,Cookie持久化后,而后分别在初始化A进程的CookieManager先后,查看A进程的Cookie状况;接着再使用进程B访问https://m.taobao.com
,Cookie持久化后,再次查看A进程的Cookie状况。
二、A进程未初始化CookieManager的状况下,使用进程B访问https://m.baidu.com
和https://m.taobao.com
,Cookie持久化后,初始化A进程的CookieManager,并查看A进程的Cookie状况。
结果证明了猜测!CookieManager在未初始化时取不到m.baidu.com
的Cookie,一旦初始化了CookieManager,则可以取到m.baidu.com
的Cookie。但步骤二再一次说明,只要初始化了CookieManager,那么该进程的Cookie再也取不到其余进程更新后的Cookie信息。
至此,多进程下Cookie同步问题的猜测所有验证完毕了,能够得出的结论是——Cookie在多进程间的获取只和第一次初始化CookieManager有关系,一旦CookieManager实例建立,则须要重启进程才能同步进程间的Cookie。
回到本文遇到的问题,既然问题的缘由已经找到了,那么确定有解决办法。一种不完美的方案是先启动B进程并加载url,等到加载完成即将跳转到App页面的时候通知主进程初始化CookieManager,这样即可以取到url中指定的Cookie信息。这种方案的缺点是再次访问这个url写入新的指定Cookie时不会当即同步到主进程,须要等到App重启主进程之后才会同步;另一种解决方案是把WebView和App都放在主进程便可。本文最终因为没有可以完美解决多进程Cookie同步方案,所以采用了第二种方案。