編輯:關於Android編程
在上一篇博客中僅僅是簡單的講解了OkHttp的緩存問題,主要是通過http協議裡面的control-cache控制緩存,而且是僅僅只能是Get請求才能緩存,如果Post請求OkHttp會讓response返回null,同時報504錯誤,也就是沒緩存。okhttp為什麼要這樣做呢?通過查看緩存的文件,我們可以發現,OkHttp緩存的是整個http請求的信息,所以這就和http協議有關系了。在RESTful API裡面,我們把Get請求理解為從服務端查詢數據,Post請求理解為更新服務端數據,而http協議裡面緩存通常只適用於idempotent request,也就是Get請求,為什麼只適應Get請求?我們都知道Get請求url結合提交參數是唯一標示,而Post請求的參數是在http的body體裡面,是可變的,無法成為唯一的標示。但是,我們在項目中基本上每一個接口都要提交基本參數,一般用的都是Post請求。Get請求還不太安全,請求的路徑大小還有限制。既然OkHttp有限制。那麼我們可以自己手動緩存。
既然要手動緩存,那麼我們就要來看看android裡面手動緩存有哪些。主要有兩種方式,一種是sqlite緩存,一種是文件緩存。
sqlite緩存
目前有很多第三方sqlite框架,比如可以結合GreenDao來做緩存,一個緩存對應一個表。把url路經,下載時間,過期時間等信息都存放到數據庫。然後把url做為請求的唯一標示,在有網的情況下,判斷當前請求url緩存是否存在,存在就要移除數據庫裡面的緩存,然後緩存新的緩存,在沒有網絡的情況下,判斷緩存是否過期,然後進行數據庫操作。從這裡我們可以看出,數據庫操作還是比較頻繁的,一不留神,就會出現應用性能問題,ANR問題,指針問題。而且android數據庫是放在/data/data/<包名>/databases/目錄下,它會占用應用內存的,一但緩存很多的話,就要及時去清理緩存,很麻煩。
文件緩存
為什麼說文件緩存更好呢?如果SD存在的話,我們可以把緩存放在SD的/data/data/<包名>/cache目錄下,不存在SD的話,再放在/data/data/<包名>下面。即使內存再多,也不會影響應用的內置應用空間。文件緩存一般都會通過DiskLruCache實現,DiskLruCache是硬盤緩存,即使應用進程結束了,緩存還是存在的。當應用卸載時,改目錄的數據也會清除掉,不會留下殘余數據。DiskLruCache緩存,沒有什麼過期時間之說,只要它存在文件裡面,我們就可以隨時去讀取它。下面我們就用DiskLruCache對Retrofit+OkHttp的響應體進行緩存。這裡我們只緩存json數據。
獲取DiskLruCache對象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
不能直接通過new的方法創建,要通過調用DiskLruCache.open()這個方法獲取,有四個參數,File指的是緩存的存儲路徑,一般優先存儲於SD卡的 /sdcard/Android/data/<包名>/cache 路徑下,如果SD卡不存在,再存在/data/data/<包名>/cache 這個路徑下,判斷代碼如下
private File getDiskCacheDir(Context context, String uniqueName)
{
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable())
{
//如果SD卡存在通過getExternalCacheDir()獲取路徑,
cachePath = context.getExternalCacheDir().getPath();
} else
{
//如果SD卡不存在通過getCacheDir()獲取路徑,
cachePath = context.getCacheDir().getPath();
}
//放在路徑 /.../data//cache/uniqueName
return new File(cachePath + File.separator + uniqueName);
}
appVersion指的是版本號,可以指應用的版本號,valueCount指的就是一個key對應多少個文件,一般我們指定1個文件,一對一使得後面更好獲取。maxSize指的是緩存的最大大小,一般傳入5M或者10M就夠了。
寫入緩存
首先我們先獲取一個DiskLruCache.Editor對象,代碼如下
public DiskLruCache.Editor editor(String key)
{
try
{
key = Utils.hashKeyForDisk(key);
//wirte DIRTY
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
//edit maybe null :the entry is editing
if (edit == null)
{
Log.w(TAG, "the entry spcified key:" + key + " is editing by other . ");
}
return edit;
} catch (IOException e)
{
e.printStackTrace();
}
return null;
}
首先進行的是Utils.hashKeyForDisk(key),也就是通過MD5生成唯一的請求標示,這樣就可以通過key來獲取DiskLruCache.Editor實例。獲取到實例後就可以獲取到OutputStream,然後通過BufferedWriter寫入,如下代碼
public void put(String key, String value)
{
DiskLruCache.Editor edit = null;
BufferedWriter bw = null;
try
{
edit = editor(key);
if (edit == null) return;
OutputStream os = edit.newOutputStream(0);
bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write(value);
edit.commit();//write CLEAN
} catch (IOException e)
{
e.printStackTrace();
try
{
//s
edit.abort();//write REMOVE
} catch (IOException e1)
{
e1.printStackTrace();
}
} finally
{
try
{
if (bw != null)
bw.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
讀取緩存
首先是通過key獲取DiskLruCache.Snapshot實例,然後得到InputStream,如下代碼
public InputStream get(String key)
{
try
{
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key));
if (snapshot == null) //not find entry , or entry.readable = false
{
Log.e(TAG, "not find entry , or entry.readable = false");
return null;
}
//write READ
return snapshot.getInputStream(0);
} catch (IOException e)
{
e.printStackTrace();
return null;
}
}
然後就是InputStreamReader讀取,如下代碼
public String getAsString(String key) {
InputStream inputStream = null;
inputStream = get(key);
if (inputStream == null) return null;
String str = null;
try {
str = Util.readFully(new InputStreamReader(inputStream, Util.UTF_8));
} catch (IOException e) {
e.printStackTrace();
try {
inputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return str;
}
static String readFully(Reader reader) throws IOException
{
try
{
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1)
{
writer.write(buffer, 0, count);
}
return writer.toString();
} finally
{
reader.close();
}
}
然後就是刪除操作
public boolean remove(String key)
{
try
{
key = Utils.hashKeyForDisk(key);
return mDiskLruCache.remove(key);
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
直接remove掉就ok了。
從Github裡面搜索DiskLruCache,可以看到鴻洋大神的base-diskcache框架,它主要是把diskcache封裝成和AsimpleCache框架一樣,挺好用的。
使用方法如下(來源於base-diskcache框架)
存
put(String key, Bitmap bitmap)
put(String key, byte[] value)
put(String key, String value)
put(String key, JSONObject jsonObject)
put(String key, JSONArray jsonArray)
put(String key, Serializable value)
put(String key, Drawable value)
editor(String key).newOutputStream(0);//原有的方式
取
String getAsString(String key);
JSONObject getAsJson(String key)
JSONArray getAsJSONArray(String key)
T getAsSerializable(String key)
Bitmap getAsBitmap(String key)
byte[] getAsBytes(String key)
Drawable getAsDrawable(String key)
InputStream get(String key);//原有的用法
這裡我只是保存響應的json,只用到
put(String key, String value)
和
String getAsString(String key);
兩個方法,至於key使用請求參數生成的MD5做為唯一的標示。
下面就使用這個DiskLruCache封裝進行手動緩存,DiskLruCache的源碼和封裝代碼可以去鴻洋的github上下載。
基於上一篇博客的HRetrofitNetHelper對象。進行代碼修改,修改點如下
去除OkHttp的cache緩存配置 去除mUrlInterceptor的攔截器 改在call的onresponse裡面進行操作 enqueueCall方法配置成鏈式編程配置然後再貼上全部的代碼,注意幾個修改點就好了。
public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger {
public static HRetrofitNetHelper mInstance;
public Retrofit mRetrofit;
public OkHttpClient mOkHttpClient;
public HttpLoggingInterceptor mHttpLogInterceptor;
private BasicParamsInterceptor mBaseParamsInterceptor;
private Context mContext;
public Gson mGson;
//DiskLruCache封裝的幫助類,
private DiskLruCacheHelper diskLruCacheHelper;
public static final String BASE_URL = "http://192.168.1.102:8080/GoachWeb/";
private Action1 onNextAction;
private HRetrofitNetHelper(Context context){
this.mContext = context ;
createSubscriberByAction();
mGson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
mHttpLogInterceptor = new HttpLoggingInterceptor(this);
mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
Map tempParams = getBaseParams();
mBaseParamsInterceptor = new BasicParamsInterceptor.Builder()
.addParamsMap(tempParams)
.build();
try {
//創建DiskLruCacheHelper 對象
diskLruCacheHelper = new DiskLruCacheHelper(mContext);
} catch (IOException e) {
e.printStackTrace();
}
//這裡去除了緩存配置和mUrlInterceptor的配置
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(12, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(mHttpLogInterceptor)
.addInterceptor(mBaseParamsInterceptor)
.build();
mRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(mGson))
.client(mOkHttpClient)
.build();
}
public static HRetrofitNetHelper getInstance(Context context){
if(mInstance==null){
synchronized (HRetrofitNetHelper.class){
if(mInstance==null){
mInstance = new HRetrofitNetHelper(context);
}
}
}
return mInstance;
}
public T getAPIService(Class service) {
return mRetrofit.create(service);
}
/*這裡改成鏈式編程,默認是不緩存。在不緩存的情況下,只需配置Call>實例,也就是調用上面getAPIService方法獲取的實例。然後就是retrofitCallBack回調接口,如果需要緩存的情況,那麼就要再配置isCache為true,然後配置Type(主要是Gson解析泛型會報錯Java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to,所以需再傳遞這個參數進行解析),最後調用start方法進行請求*/
public static final class enqueueCall{
boolean isCache;
Type clazz;
Call call;
RetrofitCallBack retrofitCallBack;
HRetrofitNetHelper mRetrofitNetHelper;
private Context mContext;
public Gson mGson;
private DiskLruCacheHelper diskLruCacheHelper;
public enqueueCall(HRetrofitNetHelper retrofitNetHelper){
isCache = false;
this.mRetrofitNetHelper = retrofitNetHelper;
this.mContext = retrofitNetHelper.mContext;
this.mGson = retrofitNetHelper.mGson;
this.diskLruCacheHelper = retrofitNetHelper.diskLruCacheHelper;
}
public enqueueCall call(Call> call){
this.call = call ;
return this;
}
public enqueueCall clazz(Type clazz){
this.clazz = clazz ;
return this;
}
public enqueueCall retrofitCallBack(RetrofitCallBack retrofitCallBack){
this.retrofitCallBack = retrofitCallBack ;
return this;
}
public enqueueCall isCache(boolean isCache){
this.isCache = isCache ;
return this;
}
public enqueueCall start(){
call.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
//獲取請求Request
Request request = call.request();
//獲取請求的url
String requestUrl = call.request().url().toString();
//去獲取返回數據
BaseResp resp = response.body() ;
//去獲取RequestBody
RequestBody requestBody = request.body();
//緩存格式為utf-8
Charset charset = Charset.forName("UTF-8");
//去獲取要保存的key
String key="";
//如果是Post請求,要通過Buffer去讀取body體裡面的參數
if(method.equals("POST")){
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(Charset.forName("UTF-8"));
}
Buffer buffer = new Buffer();
try {
requestBody.writeTo(buffer);
} catch (IOException e) {
e.printStackTrace();
}
key = buffer.readString(charset);
buffer.close();
}else{
//如果不是Post請求,比如Get請求,那麼久通過url做為唯一標識
key = requestUrl;
}
Log.d("zgx","response==========key"+key);
//處理特殊接口,如果是登錄接口進行彈框提示
if(!TextUtils.isEmpty(requestUrl)){
if(requestUrl.contains("LoginDataServlet")) {
if (Looper.myLooper() == null) {
Looper.prepare();
}
mRetrofitNetHelper.createObservable("現在請求的是登錄接口");
}
}
//分為有網和沒網的情況下
//如果有網
if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
//如果返回數據為null
if(resp==null){
//回調失敗接口
if(retrofitCallBack!=null)
retrofitCallBack.onFailure("暫無數據");
}else{
//如果是接口返回2000或者2001或者2002,進行彈框提示
if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {
Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();
}
//如果接口返回200,並且http請求code返回200,說明請求成功
if (resp.getResultCode() == 200&&response.code()==200) {
if(retrofitCallBack!=null){
//需要緩存數據
String cacheResponse = mGson.toJson(resp);
//判斷下當前是否存在key緩存的數據,如果存在移除掉, if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(diskLruCacheHelper.getAsString(key)))
diskLruCacheHelper.remove(key);
//當需要緩存的數據不為空的時候,並且需要緩存的時候,通過diskLruCacheHelper進行緩存 if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(cacheResponse)&&isCache){
Log.d("zgx","response========cacheResponse"+cacheResponse);
diskLruCacheHelper.put(key,cacheResponse);
}
//然後就是回調成功接口
retrofitCallBack.onSuccess(resp);
}
} else {
//這個是請求失敗,那麼就回調失敗接口
// ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);
if(retrofitCallBack!=null)
retrofitCallBack.onFailure(resp.getErrMsg());
}
}
return;
}
//沒有網絡的情況下,去獲取key對應的緩存
String json = diskLruCacheHelper.getAsString(key);
//如果緩存不存在,那麼久回調失敗接口
if(json==null){
Toast.makeText(mContext, "沒有緩存!", Toast.LENGTH_SHORT).show();
if(retrofitCallBack!=null){
retrofitCallBack.onFailure("沒有緩存!");
}
}else{
//判斷是否配置clazz,一定要先配置,要不然Gson解析出錯
if(clazz==null){
throw new IllegalArgumentException("請先配置clazz");
}
//解析緩存數據,然後進行回調成功接口
resp = mGson.fromJson(json,clazz);
if(retrofitCallBack!=null){
retrofitCallBack.onSuccess(resp);
}
}
}
@Override
public void onFailure(Call> call, Throwable t) {
// ToastMaker.makeToast(mContext, "網絡錯誤,請重試!", Toast.LENGTH_SHORT);
if(retrofitCallBack!=null){
retrofitCallBack.onFailure(t.toString());
}
}
});
return this;
}
}
//.....省略,和上篇博客代碼一樣
//這裡我們改成通過diskLruCacheHelper封裝的類進行刪除緩存
public void clearCache() throws IOException {
diskLruCacheHelper.delete();
}
}
主要修改的地方,上面基本上都注釋到了,這裡沒有做緩存的過期時間,有網的情況下,還是保持數據的實時性,沒網的情況下才會去讀取緩存。
ILoginService.class
public interface ILoginService {
@FormUrlEncoded
@POST("LoginDataServlet")
Call> userLogin(@Field("username") String username, @Field("password") String password);
}
INewsService.class
public interface INewsService {
@FormUrlEncoded
@POST("NewsDataServlet")
Call>> userNews(@Field("userId") String userId);
}
這裡主要是測試這兩個接口
登錄請求修改代碼如下
首先實現回調接口
//傳入成功回調的BaseResp的泛型T為RegisterBean
implements HRetrofitNetHelper.RetrofitCallBack
然後是Call請求配置
final Call> repos = loginService.userLogin(username,password);
new HRetrofitNetHelper
.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)//repos指的是retrofitNetHelper.getAPIService返回的API
.retrofitCallBack(this)//配置回調接口
.isCache(true)//設置需要緩存
.clazz(new TypeToken>(){}.getType())//Gson解析緩存需要
.start();//真正開始發起請求
然後實現兩個回調方法
@Override
public void onSuccess(BaseResp baseResp) {
Date date = baseResp.getResponseTime();
if(baseResp.getData().getErrorCode()==1){
Toast.makeText(getBaseContext(),"登錄成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(getBaseContext(),"用戶不存在",Toast.LENGTH_SHORT).show();
}
mDialog.dismiss();
}
@Override
public void onFailure(String error) {
Log.d("zgx","onFailure======"+error);
mDialog.dismiss();
}
如果新聞頁也要緩存,那麼代碼同理修改如下。
private void loadData(){
INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);
Log.d("zgx","mUserId====="+mUserId);
final Call>> repos = newService.userNews(mUserId);
new HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)
.retrofitCallBack(this)
.isCache(true)
.clazz(new TypeToken>>(){}.getType())
.start();
}
這樣就緩存了登錄接口的數據和新聞頁面的數據。
下面就來測試下,只緩存登錄接口。測試結果為有網的情況下,根據上面代碼知道登錄成功會彈出登錄成功的Toast,並且會生成緩存文件,沒有網絡的情況下會去讀取緩存,並且還是會彈出Toast提示,登錄失敗不彈。效果如下
接下來我們再看下沒有緩存的效果,代碼只要修改不配置<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;">
HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)
.retrofitCallBack(this)
.start();
然後就來看效果,有網的情況下應該為登錄成功,沒網的情況下,提示沒有緩存,效果如下
Get請求效果同理。同樣可以得到這樣的效果,感興趣的可以去試下。
最後配置3個權限
總體感覺Retrofit+OkHttp框架用起來還是很方便的。特別是響應式編程,用的特別爽。還有就是Retrofit的源碼設計的特別完美。不過在這裡,用RxAndroid用的還是比較少,相信以後會用的越來越多,而且現在谷歌的agera響應式編程也出來了。
1.創建HelloWorld項目任何編程語言寫出的第一個程序毫無疑問都會是Hello World,這已經是自20世紀70年代一直流傳下來的傳統,在編程界已成為永恆的經典,
先給大家說下項目需求:TextView顯示一段文字,格式為:白雪公主(姓名,字數不確定)向您發來了2(消息個數,不確定)條消息這段文字中名字和數字的長度是不確定的,還要求
在上篇文章中我們提到在Android開發中經常用到xml文件,當然跟服務器打交道,大部分還是喜歡用Json數據。Json的定義:一種輕量級的數據交換格式,具有良好的可讀和
NinePatch圖片以*.9.png結尾,和普通圖片的區別是四周多了一個邊框如上圖所示,左邊那條黑色線代表圖片垂直拉伸的區域,上邊的那條黑色線代表水平拉伸區域,右邊的黑