Windows phone 應用開發[2]-數據緩存

今天把JDi/Server測試做完.終於有了時間來寫寫關於這個項目總結.關於我在博客上Post這些文章內容都是從實際項目應用而來.當然有些問題解決方案也是不斷被重複設計修改.期間也碰到諸多問題.也曾爲客戶端在UI設計和具體的實現倍感困惑過.下午在Product Ower UI原型設計討論會上. 設計團隊針對內部一個孵化SNS項目原型設計做了三套設計方案.從IPhone到Android 再到Windows phone.頓時不禁有一個疑問. 如果拋開市場定位 用戶羣體.和業務需求等等.單單從開發人員角度來說 什麼樣的APP才能稱之爲一個用戶能夠接受並樂於使用的呢?

在發表討論前 注意我這個命題成立條件[如上加粗字體].如果只看後半段.當然很多市場.運營 甚至是最終用戶都會否掉這個命題的成立條件.本篇暫且假設它是成立的.因此我在內部的Wiki上向IOS和Android WP團隊發起一個討論.把這個討論結果生成一個Wish List 按照優先級做了排列 如下:

App Wish List:

[1]:貼近用戶自身實用的功能

[2]: APP自身的穩定性

[3]: 良好的用戶交互體驗

[4]: 創意

……

 

從一個數據列表來以小見微來說明這個問題.:

在Windows phone中目前獲取動態數據列表數據方式.雲端/IsolataSTorage/網絡請求/Socket通信/SQLCED等.作爲常見客戶端而言.最多采用就是網絡請求的方式來從服務器端拉去數據.在採用 WebClient或HttpWebREquest方式或是雲端存儲 涉及網絡請求數據時.在Begin-End異步處理模型中應首先保證我們代碼塊做了正確的事.並能夠在出現異常時也能正常執行流程反饋在UI上. 一個APP健壯性在反饋編碼上最小層級就是代碼塊異常處理.

但是在異常處理中很多Dev都關注程序自身功能性Bug.客戶端明確定義是能夠處理自身以及和外界網絡發生交互時任何Exception.請求網絡中斷. 請求超時. 服務器端Server返回404 Not Found. 數據解析異常等等.這都需要客戶端處理並UI中有所呈現.代碼塊的完整異常處理是APP健壯性最小單位.

對於比較常見或是不可見的異常可以通過服務器端統一過濾編碼並明確返回客戶端提示.這樣做目的是爲了保證不把程序異常直接堆棧信息暴露給用戶. 並採用客戶端日誌記錄. 當然在客戶端異常存在某種條件下照成不可見的APP崩潰的情況. 所以在UnHandlerException()方法中當APP崩潰必須退出時也得給用戶一個友好的退出提示.

now.當順利拿到數據.通過最常用的ListBox來呈現.在DataTemplate數據模板中包含文字和圖片信息.在呈現文字信息之前加載時間內必須給出動態加載進度條. 但注意在用戶操作BackUpPress鍵時必要處理.

在數據呈現時.這裏必須提到ListBox自身性能問題.當通過綁定ItemSource數據源時,如果呈現ObserverCollection<T>沒有限定數量.會照成ListBox在UI操作發生延遲或是得不到及時響應.ObserverCollection<T>如果存在10000數據項.在後臺中Silverlight必須將其實例化10000個ListItem實例項.才能通過UI呈現出現.短時間APP內存使用劇增.出現性能瓶頸.

所以綁定時數據源ObserverCollection<T>應儘可能的小的數據量20-30.儘量減少每次更新代價. 如果無法避免類似在需要大型數據綁定時 可以考慮在後臺實現動態加載數據源方式.在數據呈現過程中請慎用ValueConverter轉換器.

ValueConverter轉換器慎用:

ValueConverter轉換器採用自定義代碼實現轉換數據格式或額外操作. 導致無法在實際元素呈現之前確定頁面佈局和數據緩存. 特別是在出現大數量時ValueConverter簡直就是延時器. 可能導致UI線程長時間阻塞得不到響應.大大降低用戶體驗.請慎用!

在動態加載實現上可以參考IPhone加載數據列表的方式.當用拖拽數據列表到達底部時則動態Loading數據.儘量保證UI可操作.不要讓用戶把時間浪費Loading等待上.

說到這想起奇藝客戶端不禁想吐槽一下[善意的:) 奇藝客戶端剛出第一個版本時 通過首頁進入到在Pivot樞軸呈現的同步劇場界面時[沒有截到圖]:

在Pivot控件中用戶左右移動PivotItem頁時總是提示用戶Loading界面. 要知道用戶沒有多少耐心去等待.而且用戶一發生PivotItem切換是都會加載數據過程.當你在完全斷開網絡連接情況.在打開客戶端時你會發現裏面沒有任何數據……這證明每次用戶操作數據都是即時的. 用戶體驗不太友好 用戶期望:

A:第一次可以允許進行加載數據.但當再次打開界面應具有可操作數據.

B:每次切換PivotItem樞軸頁加載等待時間這個Loading…太不合時宜了.如果已經加載過應存在客戶端中.當存在數據更新時才動態更新列表數據.

 

數據緩存對於數據列表價值主要體現在友好的用戶交互體驗上.所以一個健全的客戶端是無法缺少一個有效緩存系統支撐的.數據緩存在客戶端中主要解決兩個問題:

緩存解決的問題:

[1]:效率,緩存本身就是爲了提高性能,不要因爲緩存的原因反而減低了性能

[2]:數據的實時性,對於緩存數據的實時性,各種緩存設計都有自己的策略,比如設置過期時間、定時刷新等。具體採用哪種策略和具體的業務不無關係

 

數據列表緩存:

A:客戶端能夠存數據保證每次在斷開連接情況有數據可以操作.實現數據緩存

B:能夠實現數據列表以動態更新.提高UI用戶交互體驗

針對緩存需求設計緩存如下:

如上緩存設計則採用以單一過期時間作爲緩存策略.並把該緩存更新時間的選項暴露給用戶選擇. 用戶也可以清空本地客戶端緩存數據:

ok.從如上設計圖不難看出.當第一次發起請求時做了一方面把數據還回客戶端同時在客戶端建立數據緩存.當頁面再次加載時檢查緩存時間是否過期.如果已經過期則重新請求服務器拉取數據.並更新本地數據緩存數據.其實在設計這個緩存時.客戶端緩存更新需要暴露兩個接口.一個利用緩存時間策略有客戶端自動更新.當然作爲用戶也需要把更新列表和緩存的選項以刷新的方式暴露出來 例如在列表ApplicationBar下添加刷新操作:

但是總體來看這種更新緩存的方式我們來總結這種緩存設計的優缺點:

緩存設計優缺點:

A:緩存更新策略單一.更新緩存方式統一 編程容易控制.能滿足基本的客戶端數據緩存操作.

B:在緩存時間內存在無法獲取即時數據缺陷.

這個設計存在一個缺陷就是在緩存存儲時間內.服務器端更新的數據在用戶沒有操作刷新按鈕的情況下.無法即時獲取服務器端最新數據.客戶端只能通過不斷輪詢的方式來實現數據更新.這種方式主要因服務器沒有建立主動數據更新消息推送機制導致.更新間隔的頻率可以保持在30S內一次操作. 在Windows phone 中針對這種情況採用Push Notification推送通知的機制來完善這個缺陷:

在使用Windows phone Push Notification推送通知服務.服務器端建立一個WebService[WCF服務]當服務器端數據發生更新時則通過WCF 服務向雲端的推送通知服務發送一條數據更新的消息.由推送通知服務把消息響應給客戶端.通過消息處理程序客戶端解析數據更新消息後.重新連接服務器主動加載數據.並更新本地客戶端緩存.

推送通知具體工作流程如下:數字具體的工作步驟

但是這種方式在實際應用中也存在具體的幾個問題.在客戶端目的就是就是服務器端能夠在數據更新時建立一種主動向客戶端推送通知機制. 通知客戶端數據已經更新.由客戶端重新發起數據請求更新本地緩存.Push Notification推送服務完全能夠完成這項工作.但是在實際測試中依然發現一個問題. 其實在設計時我們忽略Windows phone 推送通知處理方式是批處理方式傳遞.

 處理的事務可能不是即時的。 推送通知的及時性將得不到保證,而且將由該推送通知服務決定何時將通知傳遞給客戶端;只能被動的通過數據更新方式通過監聽服務來觸發通知.

但是這種好處在建立主動更新的機制.

測試中發現.在服務器端把所有數據都做壓縮處理.也就是每次請求數據對服務器代價很低.如果沒有涉及大數量和即時更新要求.第一種輪詢緩存設計足以夠用.推送通知這種方式主要在建立一種主動更新的機制.把更新的操作交給應用程序後臺來做.但在必要的時候則完全越過推送通知方式 直接訪問WCF服務來檢測數據是否更新.也是可行的.

如上討論客戶端緩存實現設計兩種方式.各有利弊.針對這種方式 先實現輪詢的方式數據緩存的設計.實際項目中.當建立客戶端緩存時把數據列表的數據序列化後一Json文件的方式存在獨立存儲空間內.並對json文件進行管理.IsolateStroage開闢一塊獨立的區域.當我們一個APP設計多個模塊使用數據緩存.在同一個層級上管理多個Json文件就會發生混亂.

針對這種情況.可以採用以模塊爲分類在獨立存儲空間建立文件夾目錄.對應目錄下放着該模塊緩存所有數據Json文件.這樣對獨立存儲強制的分區的目的主要有兩個.一個便於文件管理和分類. 另外一個就是減少數據訪問查找範圍提高讀寫大文件時性能.

在BuyTicket模塊.當拿到數據文件並完成序列化成Json格式文件.執行存儲時格式是:

Cache Json File:

“[模塊目錄名稱]/[序列化Json文件名稱].Txt”

發現在編程中直接操作Json文件或目錄極容易出錯.我們把每一個建立緩存文件封裝成實體.然後真正文件操作前通過IsolateStorageSeting字典表於具體的CacheEntity緩存實體進行關聯.那麼編程操作就是是封裝的緩存實體CacheEntity對象.在以緩存時間單一策略下CacheEntity需要如下屬性:

  1. [DataContract]   
  2. public class CacheEntity   
  3. {   
  4. /// <summary>   
  5. /// 緩存起始時間   
  6. /// </summary>   
  7. [DataMember]   
  8. public string StartDate { get; set; }   
  9.  
  10. /// <summary>   
  11. /// 緩存週期   
  12. /// </summary>   
  13. [DataMember]   
  14. public string CacheDate { get; set; }   
  15.  
  16. /// <summary>   
  17. /// 唯一存儲Key   
  18. /// </summary>   
  19. [DataMember]   
  20. public string CacheKey { get; set; }   
  21.  
  22. /// <summary>   
  23. /// 存儲模塊   
  24. /// </summary>   
  25. [DataMember]   
  26. public string CacheDirName { get; set; }   
  27.  
  28. /// <summary>   
  29. /// 存儲文件名稱   
  30. /// </summary>   
  31. [DataMember]   
  32. public string CacheFileName { get; set; }//文件名稱   
  33.  
  34. /// <summary>   
  35. /// 緩存數據類型   
  36. /// </summary>   
  37. [DataMember]   
  38. public string CacheContext { get; set; }//緩存數據類型   

CacheEntity緩存實體類中StartData標識緩存開始的時間. DataCache則是用戶決定緩存週期.設置有默認值. 唯一存儲Key則用來標識在IsolateStorageSeting中標識CacheEntity.而緩存數據類型CaCheContext是在獲取Json文件後反序列化時需要指定源數據反序列類型.

有了CacheEntity封裝則可以在建立一個CacheManager容器來管理緩存數據. CacheManager中要實現緩存實體的CRUD 之外要設置緩存週期等 添加一個數據緩存:

  1. public class CacheManager   
  2. {   
  3. /// <summary>   
  4. /// 緩存是否過期   
  5. /// </summary>   
  6. /// <param name="getCacheEntity">Cache Entity</param>   
  7. /// <returns>Is out Of Date</returns>   
  8. public static bool CacheEntityIsOutDate(CacheEntity getCacheEntity)   
  9. {   
  10. bool isOutOfDate = false;   
  11. if (getCacheEntity != null)   
  12. {   
  13. DateTime currentDate = DateTime.Now;   
  14. TimeSpan getTimeSpan = currentDate - Convert.ToDateTime(getCacheEntity.StartDate);   
  15.  
  16. int compareValue = getTimeSpan.CompareTo(new TimeSpan(0, Convert.ToInt32(getCacheEntity.CacheDate), 0));   
  17. if (compareValue == -1)   
  18. isOutOfDate = false;//未過期   
  19. else   
  20. isOutOfDate = true;//過期   
  21. }   
  22. return isOutOfDate;   
  23. }   
  24.  
  25.  
  26. /// <summary>   
  27. /// 添加緩存   
  28. /// </summary>   
  29. /// <param name="getCacheEntity">cache Entity</param>   
  30. public static bool AddCacheEntity(CacheEntity getCacheEntity)   
  31. {   
  32. bool isCache = false; 34:  if (getCacheEntity != null)   
  33. isCache=UniversalCommon_operator.AddIsolateStorageObj(getCacheEntity.CacheKey,getCacheEntity);   
  34. return isCache;   
  35. }   

可以看到通過CacheManager能夠實現對緩存CacheEntity字典表管理方式.這樣大大簡化我們編程直接操作數據文件複雜性.有了CacheManager後還需要對底層Json文件進行管理定義一個FileManager類.在BuyTicket模塊添加一個TicketList數據緩存.這是需要在IsolateStorage獨立存儲創建BuyTicket文件夾並在該文件夾下建立一個TicketListJson.txt格式Json文件.添加文件操作:

  1. public class FileManager   
  2. {   
  3.  
  4.  
  5. /// <summary>   
  6. /// 創建文件目錄   
  7. /// </summary>   
  8. /// <param name="dirName">目錄名稱</param>   
  9. public static bool CreateDirectory(string dirName)   
  10. {   
  11. bool isCreateDir = false;   
  12. if (!string.IsNullOrEmpty(dirName))   
  13. {   
  14. using (IsolatedStorageFile getIsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())   
  15. {   
  16. if (!getIsolatedStorageFile.DirectoryExists(dirName))   
  17. {   
  18. //Not Exist And CreatDir   
  19. getIsolatedStorageFile.CreateDirectory(dirName);   
  20. isCreateDir = true;   
  21. }   
  22. }   
  23. }   
  24. return isCreateDir;   
  25. }   
  26.  
  27.  
  28. /// <summary>   
  29. /// 創建文件   
  30. /// </summary>   
  31. /// <param name="dirname">目錄名稱</param>   
  32. /// <param name="filename">文件名稱</param>   
  33. /// <param name="getDataStream">文件內容</param>   
  34. /// <returns>是否創建</returns>   
  35. public static bool CreateFile(string dirname, string filename, Stream getDataStream)   
  36. {   
  37. bool isCreateFile = false;   
  38. if (!string.IsNullOrEmpty(filename))   
  39. {   
  40.  
  41. #region No Directory   
  42. using (IsolatedStorageFile getIsolatedStorageFile = IsolatedStorageFile.GetUserStoreForApplication())   
  43. {   
  44. string filepath = filename + ".txt";   
  45. if (getIsolatedStorageFile.FileExists(filepath))   
  46. getIsolatedStorageFile.DeleteFile(filepath);//Exist To Delete   
  47.  
  48. //Create File   
  49. isCreateFile = true;   
  50. getIsolatedStorageFile.CreateFile(filepath);   
  51. getDataStream.Seek(0, SeekOrigin.Begin);   
  52.  
  53. //Write Data   
  54. using (StreamReader getReader = new StreamReader(getDataStream))   
  55. {   
  56. using (StreamWriter getWriter = new StreamWriter(getIsolatedStorageFile.OpenFile(filepath, FileMode.Open, FileAccess.Write)))   
  57. {   
  58. getWriter.Write(getReader.ReadToEnd());//Save Data   
  59. }   
  60. }   
  61. }   
  62. #endregion   
  63. }   
  64. return isCreateFile;   
  65. }   

這樣我們就是先對緩存和底層操作IsolateStorage獨立存儲空間Json文件進行管理. Now.在ListCache要加載這些數據並添加緩存中.在View_model操作如下:

  1. public void LoadDataCacheFilmTicketDate()   
  2. {   
  3. this.dataCacheTicketCol.Clear();   
  4.  
  5. //假設訪問網絡連接 獲得如下連接數據   
  6. this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "猩球崛起", TicketPrice = "80" });   
  7. this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "青蜂俠", TicketPrice = "180" });   
  8. this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "拯救大兵瑞恩", TicketPrice = "80" });   
  9. this.dataCacheTicketCol.Add(new FilmTicket() { FilmName = "Secret Men", TicketPrice = "80" });   
  10.  
  11. //添加緩存   
  12. #region 緩存存儲   
  13. if (dataCacheTicketCol.Count > 0)   
  14. {   
  15. List<FilmTicket> getTickelist = new List<FilmTicket>();   
  16. foreach (FilmTicket getFilmTicket in dataCacheTicketCol)   
  17. getTickelist.Add(getFilmTicket);   
  18.  
  19. if (getTickelist.Count > 0)   
  20. {   
  21. using (MemoryStream getStreamObj = new MemoryStream())   
  22. {   
  23. //Cache Store Entity   
  24. CacheEntity ReBackEntity = new CacheEntity()   
  25. {   
  26. StartDate = DateTime.Now.ToString(),   
  27. CacheKey = "FilmIndexList",   
  28. CacheDate = CacheManager.SettingCacheDate(),   
  29. CacheContext = typeof(List<FilmTicket>).ToString(),   
  30. CacheDirName = "BuyTicket",   
  31. CacheFileName = "FilmIndexList"   
  32. };   
  33. CacheManager.AddCacheEntity(ReBackEntity);   
  34.  
  35. //Store Cache File   
  36. Serialiser.PartSerialise(getStreamObj, getTickelist);   
  37. bool isCache = FileManager.CreateFile("BuyTicket""FilmIndexList", getStreamObj);   
  38.  
  39. //Store Cache Status   
  40. UniversalCommon_operator.AddIsolateStorageObj("CacheFilmIndexList"true);   
  41. }   
  42. }   
  43. }   
  44.  
  45. #endregion   

在添加數據緩存時.首先建立一個CacheEntity實體數據.分別設置緩存週期,開始時間默認爲現在. 存儲數據以及存儲數據類型List<FilmTicket>. 以便在反序列化通過反射方式來生成數據集合.並把該實體添加CacheManager管理容器中. 添加緩存後需要對底層Json執行存儲操作.並與CacheEntity進行關聯.關聯的方式是Key命名和CacheFileName一致.最後表示緩存存儲的狀態.

緩存建立成功.在下一次加載列表時會檢查獨立存儲空間緩存數據.如果沒有過期則加載獨立存儲中緩存數據. 加載緩存數據:

  1. #region 加載緩存   
  2. if (UniversalCommon_operator.IsolateStorageKeyIsExist("FilmIndexList"))   
  3. {   
  4. CacheEntity getcacheEntity = CacheManager.QueryCacheEntityObj("FilmIndexList");   
  5. if (getcacheEntity != null)   
  6. {   
  7. #region Cache Date Operator   
  8. if (CacheManager.CacheEntityIsOutDate(getcacheEntity))   
  9. getTicketIndex_ViewModel.LoadCurrentFilmList();//過期   
  10. else   
  11. {   
  12. //Read date from cache   
  13. string jsonStr = FileManager.ReadFile(getcacheEntity.CacheDirName, getcacheEntity.CacheFileName);   
  14. if (!string.IsNullOrEmpty(jsonStr))   
  15. {   
  16. //Assembly get Object Type   
  17. MemoryStream getJsonStream = new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(jsonStr));   
  18. Assembly currentAssembly = Assembly.Load("WelfareLife.Common");   
  19. Type currentType=currentAssembly.CreateInstance(getcacheEntity.CacheContext).GetType();   
  20.  
  21. List<FilmTicket> getTicketlist = Serialiser.PartDeSerialise(currentType, getJsonStream) as List<FilmTicket>;   
  22. if (getTicketlist.Count > 0)   
  23. {   
  24. //清空數據   
  25. this.getTicketIndex_ViewModel.filmTicketCollection.Clear();   
  26. getTicketlist.ForEach(x => getTicketIndex_ViewModel.filmTicketCollection.Add(x));   
  27. }   
  28. }   
  29. }   
  30. #endregion   
  31. }   
  32. }   
  33. #endregion 

首先通過CacheEntity獲取緩存Json格式數據.通過反射的方式獲取反序列化轉換數據類型.轉換成功後 更新ViewModle中ObserverCollection<T>集合數據.並反饋到UI上.這樣就成功獲取緩存中存儲數據.如果緩存已經過期.則重新請求服務器端數據.並更新本地緩存.這樣一個簡單以緩存週期爲單一策略的緩存構建完成.由於本片篇幅有限.關於推送通知的設計的緩存將不再本篇說明.如有要源碼請Email我.

如上從Windows phone APP穩定性和用戶交互體驗以一個數據列表方式從代碼塊異常處理,.數據性能.以及客戶端緩存系統構建3個角度.來說明這個問題.篇幅畢竟有限.本篇核心放在緩存系統構建方案設計和實現上.實際操作場景中.第一種輪詢的方式簡單實用.編程控制統一.而關於推送通知這種方式在服務器端交付時並無差異.但是相對比較複雜.對於比較頻繁或對服務器要求比較APP中則纔會體現與輪詢上優勢. 一個具有良好用戶體驗的客戶端對一個完善緩存系統是依賴的.當然我們其實做了四種方案.本文中只是拿了最爲特殊兩個方案進行比對.歡迎各位提供更好的解決方案.

本篇源碼見附件。

Windows phone應用開發:

Windows phone應用開發[1]-Text To speech

Windows phone應用開發[2]-數據緩存

相對於PC端數據緩存設計思路可選擇性在移動客戶端並不是特別多. 主要因爲移動客戶端能夠使用資源和用戶需求都遠低於Pc端Application.類似WP中能夠用來支持數據存儲SQLCE數據庫和IsolateStorage獨立存儲空間.等完全沒有Pc端應用程序開放性.從一個數據列表來說對緩存要求極爲簡單:

so.良好的用戶體驗 是建立在客戶端數據緩存基礎之上的……

可能這個排列對最終用戶而言存在爭議. 在Wiki中很多人提到創意應該走在APP最前端.也就是設計階段時應該考慮到.但是如果從開發角度而言.無論什麼樣的創意一旦從開發流程來執行時.並無任何差異.開發流程在每一個固定的MileStore里程碑中能夠持續交付功能.並能保證整個APP自身穩定性.再次基礎上建立良好的UI用戶交互體驗.整個APP開發使命也就算基本完成了.

如果對於Windows phone的APP滿足上面的WishList.需要注意哪些方面問題?

針對這個問題.開發者很容易陷入各種各樣開發細節之中.這些細節大多是他們眼前要面對或即將要解決的問題.而無法從這些細節的泥沼之中抽身出來從整個APP全局角度來思考這個問題.那針對如上APP需要 和自己理解.側重APP穩定性和用戶交互體驗.可以通過以下幾點來切入.

這些問題從Windows phone 一個數據列表說起.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章