談起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同步方案,所以採用了第二種方案。