Windows phone 應用開發[2]-資料快取

l_serein發表於2013-01-22

轉自:http://www.cnblogs.com/chenkai/archive/2011/11/09/2242597.html


今天把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]: 創意

……

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

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

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

這些問題從Windows phone 一個資料列表說起.

<1>從列表說起

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

2011-11-08_111911

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

2011-11-08_152058

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

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

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

2011-11-08_1616552011-11-08_162756

在資料呈現時.這裡必須提到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樞軸呈現的同步劇場介面時[沒有截到圖]:

Img313999102

在Pivot控制元件中使用者左右移動PivotItem頁時總是提示使用者Loading介面. 要知道使用者沒有多少耐心去等待.而且使用者一發生PivotItem切換是都會載入資料過程.當你在完全斷開網路連線情況.在開啟客戶端時你會發現裡面沒有任何資料……這證明每次使用者運算元據都是即時的. 使用者體驗不太友好 使用者期望:

A:第一次可以允許進行載入資料.但當再次開啟介面應具有可運算元據.

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

so.良好的使用者體驗 是建立在客戶端資料快取基礎之上的……

<2>Windows phoen 中構建資料快取

資料快取對於資料列表價值主要體現在友好的使用者互動體驗上.所以一個健全的客戶端是無法缺少一個有效快取系統支撐的.資料快取在客戶端中主要解決兩個問題:

快取解決的問題:

[1]:效率,快取本身就是為了提高效能,不要因為快取的原因反而減低了效能

[2]:資料的實時性,對於快取資料的實時性,各種快取設計都有自己的策略,比如設定過期時間、定時重新整理等。具體採用哪種策略和具體的業務不無關係

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

資料列表快取:

A:客戶端能夠存資料保證每次在斷開連線情況有資料可以操作.實現資料快取

B:能夠實現資料列表以動態更新.提高UI使用者互動體驗

針對快取需求設計快取如下:

2011-11-08_191655

如上快取設計則採用以單一過期時間作為快取策略.並把該快取更新時間的選項暴露給使用者選擇. 使用者也可以清空本地客戶端快取資料:

2011-11-08_1919462011-11-08_191955

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

2011-11-08_161655

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

快取設計優缺點:

A:快取更新策略單一.更新快取方式統一 程式設計容易控制.能滿足基本的客戶端資料快取操作.

B:在快取時間記憶體在無法獲取即時資料缺陷.

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

2011-11-09_112554

在使用Windows phone Push Notification推送通知服務.伺服器端建立一個WebService[WCF服務]當伺服器端資料發生更新時則通過WCF 服務向雲端的推送通知服務傳送一條資料更新的訊息.由推送通知服務把訊息響應給客戶端.通過訊息處理程式客戶端解析資料更新訊息後.重新連線伺服器主動載入資料.並更新本地客戶端快取.

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

2011-11-09_114435

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

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

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

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

<3>客戶端快取設計實現

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

針對這種情況.可以採用以模組為分類在獨立儲存空間建立資料夾目錄.對應目錄下放著該模組快取所有資料Json檔案.這樣對獨立儲存強制的分割槽的目的主要有兩個.一個便於檔案管理和分類. 另外一個就是減少資料訪問查詢範圍提高讀寫大檔案時效能.

2011-11-09_140428

在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; }//快取資料型別
 39:  }

CacheEntity快取實體類中StartData標識快取開始的時間. DataCache則是使用者決定快取週期.設定有預設值. 唯一儲存Key則用來標識在IsolateStorageSeting中標識CacheEntity.而快取資料型別CaCheContext是在獲取Json檔案後反序列化時需要指定源資料反序列型別.

有了CacheEntity封裝則可以在建立一個CacheManager容器來管理快取資料. CacheManager中要實現快取實體的CRUD 之外要設定快取週期等 新增一個資料快取:

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

可以看到通過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:  }
 66: }

這樣我們就是先對快取和底層操作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
 46:  }

在新增資料快取時.首先建立一個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我.

<4>總結

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

本篇原始碼: /Files/chenkai/CommonLibrary_Client.rar


相關文章