最近真夠忙的,瞎忙!好久沒寫部落格。不知道寫什麼,就寫些最近對使用者體驗這塊的一個小的見解吧。
無論大型或小型應用,靈活的緩衝可以說不僅大大減輕了伺服器的壓力,而且因為更快速的使用者體驗而方便了使用者。從事Android開發工作以來,個人認為Android應用可以說是作為小型應用,只是其中很多的開發時間花費在細節之上(UI互動方式、響應速度、效果、字型、顏色等等),其中90%乃至99的應用並不是需要即時更新的(即時通訊類的除外:QQ),而且詬病於蝸牛般的移動網速,3G也是狗屎(至少中國目前是這樣的),與伺服器的資料互動是能少則少,這樣使用者體驗才更好。
這是我公司產品(感興趣的朋友可以到這裡下載,提有用建議有回報的哦http://www.hulutan.net/ 目前仍在不斷完善,後邊還有驚喜)的,其實這個列表基本上可以全部用快取資料來處理,即便你有新遊戲發布,我不信你一個上百個應用出世。
採用緩衝,可以進一步大大緩解資料互動的壓力,又能提供一定的離線瀏覽。下邊我簡略列舉一下緩衝管理的適用環境:
1. 提供網路服務的應用
2. 資料更新不需要即時更新,哪怕是3-5分鐘的延遲也是可以採用緩衝機制。
3. 緩衝的到期時間是可以接受的(類似網易的新聞閱讀,支援離線離線閱讀)
這樣所帶來的好處:
1. 減小伺服器的壓力
2. 提高用戶端的響應速度(本機資料提取嘛)
3. 一定程度上支援離線瀏覽(可以參考網易的那個新聞應用,個人感覺離線閱讀做得非常棒。)
一、緩衝管理的方法
緩衝管理的原理很簡:通過時間的設定來判斷是否讀取緩衝還是重新下載;斷網下就沒什麼好說的,直接去緩衝即可。
裡面會有一些細節的處理,後面會詳細闡述。基於這個原理,目前個人用過的兩種比較常見的緩衝管理方法是:資料庫和檔案(txt)。
二、資料庫(SQLite)緩衝方式
這種方法是在下載完資料檔案後,把檔案的相關資訊如url,路經,下載時間,到期時間等存放到資料庫,當然我個人建議把url作為唯一的標識。下次下載的時候根據url先從資料庫中查詢,如果查詢到目前時間並未到期,就根據路徑讀取本地檔案,從而實現緩衝的效果。
從實現上我們可以看到這種方法可以靈活存放檔案的屬性,進而提供了很大的擴充性,可以為其它的功能提供一定的支援。
從操作上需要建立資料庫,每次查詢資料庫,如果到期還需要更新資料庫,清理緩衝的時候還需要刪除資料庫資料,稍顯麻煩,而資料庫操作不當又容易出現一系列的效能,ANR問題,指標錯誤問題,實現的時候要謹慎,具體作的話,但也只是增加一個工具類或方法的事情。
還有一個問題,緩衝的資料庫是存放在/data/data/<package>/databases/目錄下,是佔用記憶體空間的,如果緩衝累計,容易浪費記憶體,需要及時清理緩衝。
當然這種方法從目前一些應用的實用上看,我沒有發現什麼問題,估計使用的量還比較少吧。
本文本人不太喜歡資料庫,原因操作麻煩,尤其是要自己寫建表那些語句,你懂的。我側重檔案快取方式。
三、檔案快取方式
這種方法,使用File.lastModified()方法得到檔案的最後修改時間,與目前時間判斷是否到期,從而實現緩衝效果。
實現上只能使用這一個屬性,沒有為其它的功能提供支援人員的可能。操作上倒是簡單,比較時間即可,而且取的資料也就是檔案裡的JSON資料而已。本身處理也不容易帶來其它問題,代價低廉。
四、檔案法緩衝方式的兩點說明
1. 不同類型的檔案的緩衝時間不一樣。
籠統的說,不變檔案的緩衝時間是永久,變化檔案的緩衝時間是最大忍受不變時間。說白點,圖片檔案內容是不變的,一般存在SD卡上直到被清理,我們是可以永遠讀取緩衝的。設定檔內容是可能更新的,需要設定一個可接受的緩衝時間。
2. 不同環境下的緩衝時間標準不一樣。
無網路環境下,我們只能讀取快取檔案,為了應用有東西顯示,沒有什麼到期之說了。
WiFi網路環境下,緩衝時間可以設定短一點,一是網速較快,而是流量不要錢。
3G流量環境下,緩衝時間可以設定長一點,節省流量,就是節省金錢,而且使用者體驗也更好。
GPS就別說更新什麼的,已經夠慢的了。緩衝時間能多長就多長把。
當然,作為一款好的應用,不會死定一種情況,針對於不同網路變換不同形式的緩衝功能是必須有的。而且這個時間根據自己的實際情況來設定:資料的更新頻率,資料的重要性等。
五、何時重新整理
開發人員一方面希望盡量讀取緩衝,使用者一方面希望即時重新整理,但是響應速度越快越好,流量消耗越少越好(關於這塊,的確開發中我沒怎麼想到,畢竟介面就是這麼多,現在公司的產品幾乎點一下就訪問一下,而且還有些雞肋多餘的功能。慢慢修改哈哈),是一個矛盾。
其實何時重新整理我也不知道,這裡我提供兩點建議:
1. 資料的最長多長時間不變,對應用無大的影響。
比如,你的資料更新時間為4小時,則緩衝時間設定為1~2小時比較合適。也就是更新時間/緩衝時間=2,但使用者個人修改、網站編輯人員等一些人為的更新就另說。一天使用者總會看到更新,即便有延遲也好,視你產品的用途了;如果你覺得你是資訊類應用,再減少,2~4小時,如果你覺得資料比較重要或者比較受歡迎,使用者會經常把玩,再減少,1~2小時,依次類推。
當然類似這個介面的資料我認為更新時間能多長就多長了,儘可能長。如果你拿後邊那個有多少資料會變動來搪塞。我會告訴你:這個只是一個引導性的介面,你有多少款遊戲跟使用者半毛錢關係都沒有,10億也跟他沒關,他只要確定這裡能找到他要找的 湯姆貓 就行。否則你又失去了一個使用者。
2. 提供重新整理按鈕。
必要時候或最保險的方法使在相關介面提供一個重新整理按鈕,或者當下流行的下拉式清單重新整理方式。為緩衝,為載入失敗提供一次重新來過的機會。畢竟喝骨頭湯的時候,我也不介意碗旁多雙筷子。
總而言之,一切使用者至上,為了更好的使用者體驗,方法也會層出不窮。期待更好的辦法
關鍵代碼:
package com.hulutan.gamestore.cache;import java.io.File;import java.io.IOException;import android.os.Environment;import android.util.Log;import com.hulutan.gamestore.Constants;import com.hulutan.gamestore.GameStoreApplication;import com.hulutan.gamestore.util.EncryptUtils;import com.hulutan.gamestore.util.FileUtils;import com.hulutan.gamestore.util.NetworkUtils;import com.hulutan.gamestore.util.NetworkUtils.NetworkState;import com.hulutan.gamestore.util.StringUtils;/** * 緩衝工具類 * @author naibo-liao * @時間: 2013-1-4下午02:30:52 */public class ConfigCacheUtil { private static final String TAG=ConfigCacheUtil.class.getName(); /** 1秒逾時時間 */ public static final int CONFIG_CACHE_SHORT_TIMEOUT=1000 * 60 * 5; // 5 分鐘 /** 5分鐘逾時時間 */ public static final int CONFIG_CACHE_MEDIUM_TIMEOUT=1000 * 3600 * 2; // 2小時 /** 中長緩衝時間 */ public static final int CONFIG_CACHE_ML_TIMEOUT=1000 * 60 * 60 * 24 * 1; // 1天 /** 最大緩衝時間 */ public static final int CONFIG_CACHE_MAX_TIMEOUT=1000 * 60 * 60 * 24 * 7; // 7天 /** * CONFIG_CACHE_MODEL_LONG : 長時間(7天)緩衝模式 <br> * CONFIG_CACHE_MODEL_ML : 中長時間(12小時)緩衝模式<br> * CONFIG_CACHE_MODEL_MEDIUM: 中等時間(2小時)緩衝模式 <br> * CONFIG_CACHE_MODEL_SHORT : 短時間(5分鐘)緩衝模式 */ public enum ConfigCacheModel { CONFIG_CACHE_MODEL_SHORT, CONFIG_CACHE_MODEL_MEDIUM, CONFIG_CACHE_MODEL_ML, CONFIG_CACHE_MODEL_LONG; } /** * 擷取緩衝 * @param url 訪問網路的URL * @return 快取資料 */ public static String getUrlCache(String url, ConfigCacheModel model) { if(url == null) { return null; } String result=null; String path=Constants.ENVIROMENT_DIR_CACHE + StringUtils.replaceUrlWithPlus(EncryptUtils.encryptToMD5(url)); File file=new File(path); if(file.exists() && file.isFile()) { long expiredTime=System.currentTimeMillis() - file.lastModified(); Log.d(TAG, file.getAbsolutePath() + " expiredTime:" + expiredTime / 60000 + "min"); // 1。如果系統時間是不正確的 // 2。當網路是無效的,你只能讀緩衝 if(NetworkUtils.getNetworkState(GameStoreApplication.getInstance().getContext()) != NetworkState.NETWORN_NONE) { if(expiredTime < 0) { return null; } if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_SHORT) { if(expiredTime > CONFIG_CACHE_SHORT_TIMEOUT) { return null; } } else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_MEDIUM) { if(expiredTime > CONFIG_CACHE_MEDIUM_TIMEOUT) { return null; } } else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_ML) { if(expiredTime > CONFIG_CACHE_ML_TIMEOUT) { return null; } } else if(model == ConfigCacheModel.CONFIG_CACHE_MODEL_LONG) { if(expiredTime > CONFIG_CACHE_MEDIUM_TIMEOUT) { return null; } } else { if(expiredTime > CONFIG_CACHE_MAX_TIMEOUT) { return null; } } } try { result=FileUtils.readTextFile(file); } catch(IOException e) { e.printStackTrace(); } } return result; } /** * 設定緩衝 * @param data * @param url */ public static void setUrlCache(String data, String url) { if(Constants.ENVIROMENT_DIR_CACHE == null) { return; } File dir=new File(Constants.ENVIROMENT_DIR_CACHE); if(!dir.exists() && Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { dir.mkdirs(); } File file=new File(Constants.ENVIROMENT_DIR_CACHE + StringUtils.replaceUrlWithPlus(EncryptUtils.encryptToMD5(url))); try { // 建立快取資料到磁碟,就是建立檔案 FileUtils.writeTextFile(file, data); } catch(IOException e) { Log.d(TAG, "write " + file.getAbsolutePath() + " data failed!"); e.printStackTrace(); } catch(Exception e) { e.printStackTrace(); } } /** * 刪除曆史快取檔案 * @param cacheFile */ public static void clearCache(File cacheFile) { if(cacheFile == null) { if(Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { try { File cacheDir=new File(Environment.getExternalStorageDirectory().getPath() + "/hulutan/cache/"); if(cacheDir.exists()) { clearCache(cacheDir); } } catch(Exception e) { e.printStackTrace(); } } } else if(cacheFile.isFile()) { cacheFile.delete(); } else if(cacheFile.isDirectory()) { File[] childFiles=cacheFile.listFiles(); for(int i=0; i < childFiles.length; i++) { clearCache(childFiles[i]); } } }}
擷取緩衝:
String cacheConfigString=ConfigCacheUtil.getUrlCache(Net.API_HELP, ConfigCacheModel.CONFIG_CACHE_MODEL_LONG); if(cacheConfigString != null) { //do something }
設定緩衝:
ConfigCacheUtil.setUrlCache(data, Net.API_HELP);