編輯:關於Android編程
在上一文中,我們提到retrofit 2.0中如何實現非持久化cookie的兩種方案,但並未做過深的解釋。現在我們重點關注JavaNetCookieJar實現非持久化cookie背後的原理。
話不多說,步入正題。
首先來看上文中提到的非持久化cookie的實現:
public void setCookies(OkHttpClient.Builder builder) { CookieManager cookieManager = new CookieManager(); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); builder.cookieJar(new JavaNetCookieJar(cookieManager)); }
現在我們就以這段代碼為起點來研究為什麼這幾行代碼就實現了非持久化cookie呢?不難先發現此處實現cookie的關鍵就是cookieJar(),我們就以該方法切入。
首先來看該方法的源碼:
//設置cookie處理器,如果沒設置,則使用CookieJar.NO_COOKIES作為默認的處理器
public Builder cookieJar(CookieJar cookieJar) {
if (cookieJar == null) throw new NullPointerException("cookieJar == null");
this.cookieJar = cookieJar;
return this;
}
通過該方法我們知道這裡的關鍵就在於CookieJar接口,來看看這個接口的定義:
public interface CookieJar {
//默認的cookie處理器,不接受任何cookie
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List cookies) {
}
@Override public List loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
//根絕cookie處理策略,保存響應中的cookie
void saveFromResponse(HttpUrl url, List cookies);
//為請求添加cookie
List loadForRequest(HttpUrl url);
}
該接口非常簡單,提供了保存和加載cookie的兩個方法
loadForRequest(HttpUrl url)及
saveFromResponse(HttpUrl url,List
除此之外,該接口中存在一個默認的實現NO_COOKIES。
接下來,我們來看看該接口的另外一個實現類JavaNetCookieJar,它本質上只是java.net.CookieHandler的代理類。同樣,來看一下JavaNetCookieJar中的源碼:
public final class JavaNetCookieJar implements CookieJar {
private final CookieHandler cookieHandler;
public JavaNetCookieJar(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
}
@Override public void saveFromResponse(HttpUrl url, List cookies) {
if (cookieHandler != null) {
List cookieStrings = new ArrayList<>();
for (Cookie cookie : cookies) {//遍歷cookie集合
cookieStrings.add(cookie.toString());
}
Map> multimap = Collections.singletonMap("Set-Cookie", cookieStrings);
try {
//具體的保存工作交給cookieHandler去處理
cookieHandler.put(url.uri(), multimap);
} catch (IOException e) {
Internal.logger.log(WARNING, "Saving cookies failed for " + url.resolve("/..."), e);
}
}
}
@Override public List loadForRequest(HttpUrl url) {
// The RI passes all headers. We don't have 'em, so we don't pass 'em!
Map> headers = Collections.emptyMap();
Map> cookieHeaders;
try {
//從cookieHandler中取出cookie集合。
cookieHeaders = cookieHandler.get(url.uri(), headers);
} catch (IOException e) {
Internal.logger.log(WARNING, "Loading cookies failed for " + url.resolve("/..."), e);
return Collections.emptyList();
}
List cookies = null;
for (Map.Entry> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
for (String header : entry.getValue()) {
if (cookies == null) cookies = new ArrayList<>();
cookies.addAll(decodeHeaderAsJavaNetCookies(url, header));
}
}
}
return cookies != null
? Collections.unmodifiableList(cookies)
: Collections.emptyList();
}
//將請求Header轉為OkHttp中HttpCookie
private List decodeHeaderAsJavaNetCookies(HttpUrl url, String header) {
List result = new ArrayList<>();
for (int pos = 0, limit = header.length(), pairEnd; pos < limit; pos = pairEnd + 1) {
//具體轉換過程在此不做展示
}
return result;
}
}
上面的代碼非常簡單,其核心無非在於通過CookieHandler實現對cookie的保存和取值。既然,真正的工作類是CookieHandler,那我們就重點關注CookieHandler.
public abstract class CookieHandler {
private static CookieHandler cookieHandler;
public synchronized static CookieHandler getDefault() {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.GET_COOKIEHANDLER_PERMISSION);
}
return cookieHandler;
}
public synchronized static void setDefault(CookieHandler cHandler) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SecurityConstants.SET_COOKIEHANDLER_PERMISSION);
}
cookieHandler = cHandler;
}
public abstract Map>
get(URI uri, Map> requestHeaders)
throws IOException;
public abstract void
put(URI uri, Map> responseHeaders)
throws IOException;
}
我們發現CookieHandler是個抽象類,其實現類就是我們上面在非持久化Cookie中用到的CookieManager,沒辦法,我們在此轉移焦點至CookieManager:
public class CookieManager extends CookieHandler
{
private CookiePolicy policyCallback;//cookie處理策略
private CookieStore cookieJar = null;//cookie存儲
public CookieManager() {
this(null, null);
}
//在這裡我們可以指定cookie存儲方式及cookie處理策略
public CookieManager(CookieStore store,
CookiePolicy cookiePolicy)
{
// use default cookie policy if not specify one
policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER
: cookiePolicy;
// 如果沒有指定持久化方式,則默認使用內存持久化。
if (store == null) {
cookieJar = new InMemoryCookieStore();
} else {
cookieJar = store;
}
}
//指定cookie處理策略
public void setCookiePolicy(CookiePolicy cookiePolicy) {
if (cookiePolicy != null) policyCallback = cookiePolicy;
}
public CookieStore getCookieStore() {
return cookieJar;
}
//獲取cookie
public Map>
get(URI uri, Map> requestHeaders)
throws IOException
{
//... 省略多行代碼
Map> cookieMap =
new java.util.HashMap>();
//... 省略多行代碼
for (HttpCookie cookie : cookieJar.get(uri)) {
//... 省略多行代碼
}
// apply sort rule (RFC 2965 sec. 3.3.4)
List cookieHeader = sortByPath(cookies);
cookieMap.put("Cookie", cookieHeader);
return Collections.unmodifiableMap(cookieMap);
}
//保存cookie
public void
put(URI uri, Map> responseHeaders)
throws IOException
{
//... 省略多行代碼
PlatformLogger logger = PlatformLogger.getLogger("java.net.CookieManager");
for (String headerKey : responseHeaders.keySet()) {
// RFC 2965 3.2.2, key must be 'Set-Cookie2'
// we also accept 'Set-Cookie' here for backward compatibility
if (headerKey == null
|| !(headerKey.equalsIgnoreCase("Set-Cookie2")
|| headerKey.equalsIgnoreCase("Set-Cookie")
)
)
{
continue;
}
for (String headerValue : responseHeaders.get(headerKey)) {
try {
List cookies;
try {
cookies = HttpCookie.parse(headerValue);
} catch (IllegalArgumentException e) {
// Bogus header, make an empty list and log the error
cookies = java.util.Collections.emptyList();
if (logger.isLoggable(PlatformLogger.Level.SEVERE)) {
logger.severe("Invalid cookie for " + uri + ": " + headerValue);
}
}
for (HttpCookie cookie : cookies) {
if (cookie.getPath() == null) {
//... 省略多行代碼
cookie.setPath(path);
}
if (cookie.getDomain() == null) {
//... 省略多行代碼
cookie.setDomain(host);
}
//... 省略多行代碼
if (shouldAcceptInternal(uri, cookie)) {
cookieJar.add(uri, cookie);
}
}
} catch (IllegalArgumentException e) {
// invalid set-cookie header string
// no-op
}
}
}
}
}
在CacheManager中,需要重點關注的便是policyCallback和cookieJar兩個成員變量。分別來看看這二者做了什麼?
cookie處理策略
cookie的處理策略由CookiePolicy接口確定,該接口中預置了三種處理策略:ACCEPT_ALL,ACCEPT_NONE及ACCEPT_SERVER,分別用於接受所有cookie,不接受cookie,只接受初始服務器的cookie。
cookie存儲
cookie的存儲則由CookieStore接口負責,該接口目前存在唯一的實現類InMemoryCookieStore,通過該實現類的名字也大概猜的出它將cookie存在內存中了,同樣我們簡單的浏覽一下其源碼:
class InMemoryCookieStore implements CookieStore {
private List cookieJar = null;
private Map> domainIndex = null;
private Map> uriIndex = null;
// use ReentrantLock instead of syncronized for scalability
private ReentrantLock lock = null;
/**
* The default ctor
*/
public InMemoryCookieStore() {
cookieJar = new ArrayList();
domainIndex = new HashMap>();
uriIndex = new HashMap>();
lock = new ReentrantLock(false);
}
//保存cookie
public void add(URI uri, HttpCookie cookie) {
// pre-condition : argument can't be null
if (cookie == null) {
throw new NullPointerException("cookie is null");
}
lock.lock();
try {
// 首先移除已經存在的cookie
cookieJar.remove(cookie);
// 緩存時間不為0
if (cookie.getMaxAge() != 0) {
cookieJar.add(cookie);
// 保存到域索引中
if (cookie.getDomain() != null) {
addIndex(domainIndex, cookie.getDomain(), cookie);
}
//保存到url索引中
if (uri != null) {
// add it to uri index, too
addIndex(uriIndex, getEffectiveURI(uri), cookie);
}
}
} finally {
lock.unlock();
}
}
}
到現在我們已經弄明白非持久化Cookie的原理,這裡用一張類圖來概括一下:
<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxociAvPg0KPGgxIGlkPQ=="持久化cookie實現">從設計的來說,這裡也充分體現了面向抽象編程,單一職責理念等。對這些設計原則不明白的可以看我以前寫的文章。
持久化Cookie實現
自定義InDiskCookieStore實現持久化Cookie
通過上面的分析,我們最終知道非持久化Cookie的存儲靠InMemoryCookieStore實現,除此之外,我們發現CookieManager中也可以指定另外的CookieStore的實現。這樣看來,我們只需要自定義InDiskCookieStore實現CookieStore,並將其設置給CookieManager就可以實現Cookie的持久化了。接下來,我們仿照inMemoryCookieStore來實現自己的InDiskCookieStore的即可。
不難發現這裡的重點在於如何持久化Cookie到本地。我們可以從響應頭中讀取set-cookie(可能有多個),然後拼接存儲在相關的配置文件中,也可以直接序列化HttpCookie對象到本地。這裡我們采用序列化的方式實現,對Java序列化和反序列化不熟的同學可以自行補充。
查看HttpCookie發現,該類並不支持序列化,哪該怎麼辦呢?最簡單的方式是為該類創建輔助類DiskHttpCookie,用來實現序列化和反序列化的過程:
public class DiskHttpCookie implements Serializable {
private static final long serialVersionUID = -6370968478600008500L;
private transient final HttpCookie mCookie;
private transient HttpCookie mClientCookie;
public DiskHttpCookie(HttpCookie cookie) {
mCookie = cookie;
}
public HttpCookie getCookie() {
HttpCookie cookie = mCookie;
if (mClientCookie != null) {
cookie = mClientCookie;
}
return cookie;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(mCookie.getName());
out.writeObject(mCookie.getValue());
out.writeObject(mCookie.getComment());
out.writeObject(mCookie.getCommentURL());
out.writeObject(mCookie.getDomain());
out.writeLong(mCookie.getMaxAge());
out.writeObject(mCookie.getPath());
out.writeObject(mCookie.getPortlist());
out.writeBoolean(mCookie.getSecure());
out.writeBoolean(mCookie.getDiscard());
out.writeInt(mCookie.getVersion());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
String name = (String) in.readObject();
String value = (String) in.readObject();
mClientCookie = new HttpCookie(name, value);
mClientCookie.setComment((String) in.readObject());
mClientCookie.setCommentURL((String) in.readObject());
mClientCookie.setDomain((String) in.readObject());
mClientCookie.setMaxAge(in.readLong());
mClientCookie.setPath((String) in.readObject());
mClientCookie.setPortlist((String) in.readObject());
mClientCookie.setSecure(in.readBoolean());
mClientCookie.setDiscard(in.readBoolean());
mClientCookie.setVersion(in.readInt());
}
}
在DiskHttpCookie中,通過writeObject()和readObject()方法實現自定義序列化的過程。下面我們正式來看InDiskCookieStore的實現,此次我們參考了async-http-client的相關源碼:
public class InDiskCookieStore implements CookieStore {
private static final String LOG_TAG = "InDiskCookieStore";
private static final String COOKIE_PREFS = "CookiePrefsFile";
private static final String COOKIE_NAME_PREFIX = "cookie_";
//在內存中,緩存cookie,加快訪問速度
private final HashMap> cookies;
//存儲cookie到本地
private final SharedPreferences cookiePrefs;
private ReentrantLock mLock = null;
public InDiskCookieStore(Context context) {
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
mLock = new ReentrantLock(false);
cookies = new HashMap<>();
initMemoryCookie();
}
private void initMemoryCookie() {
// 加載本地cookie到內存中
Map prefsMap = cookiePrefs.getAll();
for (Map.Entry entry : prefsMap.entrySet()) {
if (((String) entry.getValue()) != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {
String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
for (String name : cookieNames) {
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null) {
HttpCookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null) {
if (!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap());
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
@Override
public void add(URI uri, HttpCookie cookie) {
mLock.lock();
try {
String name = getCookieToken(uri, cookie);
if (!cookie.hasExpired()) {
if (!cookies.containsKey(uri.getHost()))
cookies.put(uri.getHost(), new ConcurrentHashMap());
cookies.get(uri.getHost()).put(name, cookie);
} else {//清除過期cookie
if (cookies.containsKey(uri.toString()))
cookies.get(uri.getHost()).remove(name);
}
// Save cookie into persistent store
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new DiskHttpCookie(cookie)));
prefsWriter.commit();
} finally {
mLock.unlock();
}
}
protected String getCookieToken(URI uri, HttpCookie cookie) {
return cookie.getName() + cookie.getDomain();
}
@Override
public List get(URI uri) {
mLock.lock();
ArrayList ret = new ArrayList();
if (cookies.containsKey(uri.getHost()))
ret.addAll(cookies.get(uri.getHost()).values());
mLock.unlock();
return ret;
}
@Override
public boolean removeAll() {
mLock.lock();
try {
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
prefsWriter.clear();
prefsWriter.commit();
cookies.clear();
} finally {
mLock.unlock();
}
return true;
}
@Override
public boolean remove(URI uri, HttpCookie cookie) {
mLock.lock();
try {
String name = getCookieToken(uri, cookie);
if (cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {
cookies.get(uri.getHost()).remove(name);
SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {
prefsWriter.remove(COOKIE_NAME_PREFIX + name);
}
prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));
prefsWriter.commit();
return true;
} else {
return false;
}
} finally {
mLock.unlock();
}
}
@Override
public List getCookies() {
mLock.lock();
ArrayList ret = new ArrayList();
for (String key : cookies.keySet())
ret.addAll(cookies.get(key).values());
mLock.unlock();
return ret;
}
@Override
public List getURIs() {
mLock.lock();
ArrayList ret = new ArrayList();
for (String key : cookies.keySet())
try {
ret.add(new URI(key));
} catch (URISyntaxException e) {
e.printStackTrace();
}
mLock.unlock();
return ret;
}
//序列化DiskHttpCookie,並將其轉為字符串
protected String encodeCookie(DiskHttpCookie cookie) {
if (cookie == null)
return null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(cookie);
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
//通過cookie字符串反序列化為HttpCookie對象
protected HttpCookie decodeCookie(String cookieString) {
byte[] bytes = hexStringToByteArray(cookieString);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
cookie = ((DiskHttpCookie) objectInputStream.readObject()).getCookie();
} catch (IOException e) {
Log.d(LOG_TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
//字節轉16進制
protected String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase(Locale.US);
}
//16進制轉字節
protected byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}
此類一方面將cookie暫存在內存中,以便實現快速的訪問,另一方面會將cookie序列化在本地。在每次創建InDiskCookieStore時,會用本地的cookie初始化到緩存中。
那麼如何使用呢?其實用和我們使用JavaNetCookieJar非常相似:
public void setCookies(OkHttpClient.Builder builder) {
CookieManager cookieManager = new java.net.CookieManager(new InDiskCookieStore(this),CookiePolicy.ACCEPT_ORIGINAL_SERVER);
builder.cookieJar(new JavaNetCookieJar(cookieManager));
}
通過這種方式,無需做其他的變化就實現了持久化Cookie。
自定義攔截器實現持久化Cookie
通過自定義攔截器實現Cookie持久化的思路我們已經在上一篇文章說過,就不多做解釋了,直接來看代碼即可:
首先定義響應攔截器,該攔截器實現從response獲取set-cookie字段的值,並將其保存在本地。
public class SaveCookiesInterceptor implements Interceptor {
private static final String COOKIE_PREF = "cookies_prefs";
private Context mContext;
public SaveCookiesInterceptor(Context context) {
mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
//set-cookie可能為多個
if (!response.headers("set-cookie").isEmpty()) {
List cookies = response.headers("set-cookie");
String cookie = encodeCookie(cookies);
saveCookie(request.url().toString(),request.url().host(),cookie);
}
return response;
}
//整合cookie為唯一字符串
private String encodeCookie(List cookies) {
StringBuilder sb = new StringBuilder();
Set set=new HashSet<>();
for (String cookie : cookies) {
String[] arr = cookie.split(";");
for (String s : arr) {
if(set.contains(s))continue;
set.add(s);
}
}
Iterator ite = set.iterator();
while (ite.hasNext()) {
String cookie = ite.next();
sb.append(cookie).append(";");
}
int last = sb.lastIndexOf(";");
if (sb.length() - 1 == last) {
sb.deleteCharAt(last);
}
return sb.toString();
}
//保存cookie到本地,這裡我們分別為該url和host設置相同的cookie,其中host可選
//這樣能使得該cookie的應用范圍更廣
private void saveCookie(String url,String domain,String cookies) {
SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
if (TextUtils.isEmpty(url)) {
throw new NullPointerException("url is null.");
}else{
editor.putString(url, cookies);
}
if (!TextUtils.isEmpty(domain)) {
editor.putString(domain, cookies);
}
editor.apply();
}
}
其次定義請求攔截器,如果該請求存在cookie,則為其添加到Header的Cookie中,代碼如下:
public class AddCookiesInterceptor implements Interceptor {
private static final String COOKIE_PREF = "cookies_prefs";
private Context mContext;
public AddCookiesInterceptor(Context context) {
mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
String cookie = getCookie(request.url().toString(), request.url().host());
if (!TextUtils.isEmpty(cookie)) {
builder.addHeader("Cookie", cookie);
}
return chain.proceed(builder.build());
}
private String getCookie(String url, String domain) {
SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);
if (!TextUtils.isEmpty(url)&&sp.contains(url)&&!TextUtils.isEmpty(sp.getString(url,""))) {
return sp.getString(url, "");
}
if (!TextUtils.isEmpty(domain)&&sp.contains(domain) && !TextUtils.isEmpty(sp.getString(domain, ""))) {
return sp.getString(domain, "");
}
return null;
}
}
最後我們將這兩個攔截器設置到OkHttpClient即可:
public void setCookies(OkHttpClient.Builder builder) {
builder.addInterceptor(new AddCookiesInterceptor(this));
builder.addInterceptor(new SaveCookiesInterceptor(this));
}
這樣,我們也實現了Cookie的持久化,再次證明了OkHttp的攔截器機制是如此方便。
和上面的方案相比,該方案需要額外添加兩個攔截器。對某些有代碼潔癖的人來說,可能並不喜歡這種方案。
自定義CookieJar實現持久化Cookie
到現在我們已經實現了持久化Cookie的兩種方案。自定義CookieJar來實現Cookie持久化和以上兩種方案並無太大的區別,結合上一篇文章中通過CookieJar實現非持久化Cookie,你也可以很容易的搞定持久化Cookie,故不做詳細的說明了。
總結
今天我們從頭到位了解了Cookie持久化技術,並對不同的方案做了簡單的說明,希望對各位有所幫助。
先上圖:說在前面:View的事件分發簡單記憶方法::dispathTouchEvent----->onTouchEvent------->onClick如上圖
前段時間項目有需要要顯示彩色探測點的二維碼,網上搜了下,有基於java,c等的源碼。網上的java源碼直接照搬有些問題,自己改了改。不說廢話,先上結果圖。原理其實很簡單,
Binder概述Binder是一種Android實現的IPC,用於進程間通信。通常Linux系統提供好幾種進程間通信的方式,比如1) Message Queue :把進程
大約在15年下半年開始,熱補丁方案開始大量湧現,一時間熱補丁修復技術在 Android 圈非常火爆,比較有代表性的開源實現有 Dexposed、AndFix、Nuwa 以