使用Listview等控件加載數據時,第一時間想到的就是ObservableCollection,這個東西蠻好,如果新增、刪除、修改數據,都會自動更新UI。
可是,如果不需要增刪改,顯示大數據量,這個東西的加載性能怎么樣呢?
做個實驗。
1.准備數據,在本地磁盤上創建20000個文件,將其加載到ListView中。
Create file
var testPath = @"D:\TestLargeData\Test10000"; if (!Directory.Exists(testPath)) Directory.CreateDirectory(testPath); else { MessageBox.Show("test file has been created"); return; } for (int i = 0; i < 20000; i++) File.Create(Path.Combine(testPath, Path.GetRandomFileName()));
2. 使用ObserableCollection加載
ObservableCollection
#region data source 1 /// <summary> /// files and directories in current directory, display in listview /// </summary> ObservableCollection<FileItem> allFiles1 = new ObservableCollection<FileItem>(); /// <summary> /// files and directories in current directory, display in listview /// </summary> public ObservableCollection<FileItem> AllFiles1 { get { return allFiles1; } set { if (allFiles1 != value) allFiles1 = value; NotifyPropertyChanged("AllFiles1"); } } #endregion #region load data method---1 /// <summary> /// when current directory path change ,refresh listview /// </summary> public void Refresh1() { LoadLastFiles(); } /// <summary> /// loading last file task /// </summary> void LoadLastFiles() { var files = Directory.EnumerateFileSystemEntries(testPath); DateTime dtBegin = DateTime.Now; LogHelper.Log("1====loading begin:"); foreach (var pageFile in files) { Invoke(delegate { allFiles1.Add(GetFileItem(pageFile)); }); } LogHelper.Log("1====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("1====current ItemsCount:" + allFiles1.Count.ToString()); } #endregion
3.使用List加載
List
#region datasource 2 /// <summary> /// files and directories in current directory, display in listview /// </summary> List<FileItem> allFiles2 = new List<FileItem>(); /// <summary> /// files and directories in current directory, display in listview /// </summary> public List<FileItem> AllFiles2 { get { return allFiles2; } set { if (allFiles2 != value) allFiles2 = value; NotifyPropertyChanged("AllFiles2"); } } #endregion #region load data method---2 /// <summary> /// when current directory path change ,refresh listview /// </summary> public void Refresh2() { LoadLastFiles2(); } /// <summary> /// loading last file task /// </summary> void LoadLastFiles2() { var files = Directory.EnumerateFileSystemEntries(testPath); AllFiles2 = new List<FileItem>(); DateTime dtBegin = DateTime.Now; DateTime dtLastRefresh = DateTime.Now; LogHelper.Log("2====loading begin:"); foreach (string file in files) { AllFiles2.Add(GetFileItem(file)); NotifyPropertyChanged("AllFiles2"); } LogHelper.Log("2====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("2====current ItemsCount:" + AllFiles2.Count.ToString()); } #endregion
這次對比的結果是 :
04:23:13 235 | 1====loading begin:
04:23:13 850 | 1====loading ok:0.615
04:23:13 851 | 1====current ItemsCount:73338
04:23:15 458 | 2====loading begin:
04:23:15 961 | 2====loading ok:0.503
04:23:15 962 | 2====current ItemsCount:73338
對比發現,兩個差別不大的。
4. 上面的實驗,數據是在主線程加載的,實際的數據加載一般都是利用線程加載的,所以修改代碼如下:
5. ObserableCollection 加載放在線程中,需要Invoke了,如下:
ObservableCollection
#region load data method---1 /// <summary> /// when current directory path change ,refresh listview /// </summary> public void Refresh1() { //LoadLastFiles(); allFiles1.Clear(); Thread thread1 = new Thread(new ThreadStart(LoadLastFiles)); thread1.Start(); } /// <summary> /// loading last file task /// </summary> void LoadLastFiles() { var files = Directory.EnumerateFileSystemEntries(testPath); DateTime dtBegin = DateTime.Now; LogHelper.Log("1====loading begin:"); foreach (var pageFile in files) { Invoke(delegate { allFiles1.Add(GetFileItem(pageFile)); }); } LogHelper.Log("1====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("1====current ItemsCount:" + allFiles1.Count.ToString()); } #endregion
6.list 也放在線程中,這個需要傳遞Listview過來,刷新Items,如下
List
#region datasource 2 /// <summary> /// files and directories in current directory, display in listview /// </summary> List<FileItem> allFiles2 = new List<FileItem>(); /// <summary> /// files and directories in current directory, display in listview /// </summary> public List<FileItem> AllFiles2 { get { return allFiles2; } set { if (allFiles2 != value) allFiles2 = value; NotifyPropertyChanged("AllFiles2"); } } #endregion #region load data method---2 /// <summary> /// when current directory path change ,refresh listview /// </summary> public void Refresh2() { //LoadLastFiles2(); Thread thread2 = new Thread(new ThreadStart(LoadLastFiles2)); thread2.Start(); } /// <summary> /// loading last file task /// </summary> void LoadLastFiles2() { var files = Directory.EnumerateFileSystemEntries(testPath); allFiles2 = new List<FileItem>(); DateTime dtBegin = DateTime.Now; DateTime dtLastRefresh = DateTime.Now; LogHelper.Log("2====loading begin:"); foreach (string file in files) { allFiles2.Add(GetFileItem(file)); NotifyPropertyChanged("AllFiles2"); Invoke(delegate { this.tstLv.Items.Refresh(); }); dtLastRefresh = DateTime.Now; } NotifyPropertyChanged("AllFiles2"); LogHelper.Log("2====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("2====current ItemsCount:" + AllFiles2.Count.ToString()); } #endregion
經過多輪測試,發現list明顯速度比較慢
04:42:02 493 | 1====loading begin:
04:42:05 287 | 1====loading ok:2.7932793
04:42:05 288 | 1====current ItemsCount:73338
04:42:07 192 | 2====loading begin:
04:42:26 276 | 2====loading ok:19.0839082
04:42:26 277 | 2====current ItemsCount:73338
04:42:43 277 | 2====loading begin:
04:43:04 188 | 2====loading ok:20.9110909
04:43:04 189 | 2====current ItemsCount:73338
04:43:05 838 | 1====loading begin:
04:43:08 511 | 1====loading ok:2.6732673
04:43:08 512 | 1====current ItemsCount:73338
這一次,ObserableCollection 的優勢非常明顯。
7. 使用list時,每增加一個數據,就refresh一下,這里有點浪費了。
有兩個辦法:一是每加載若干數據(例如200個)refresh一次,而是每過0.1秒refresh一次。考慮到用戶的操作,0.1秒內用戶操作的可能性小的多,必將手沒那么快。
分時加載 list 優化的代碼如下:
List
void LoadLastFiles2() { var files = Directory.EnumerateFileSystemEntries(testPath); allFiles2 = new List<FileItem>(); DateTime dtBegin = DateTime.Now; DateTime dtLastRefresh = DateTime.Now; LogHelper.Log("2====loading begin:"); foreach (string file in files) { allFiles2.Add(GetFileItem(file)); NotifyPropertyChanged("AllFiles2"); if ((DateTime.Now - dtLastRefresh).TotalSeconds > 0.1) { Invoke(delegate { this.tstLv.Items.Refresh(); }); dtLastRefresh = DateTime.Now; } } NotifyPropertyChanged("AllFiles2"); LogHelper.Log("2====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("2====current ItemsCount:" + AllFiles2.Count.ToString()); }
這次對比,實在是差別太大了:
04:46:57 739 | 1====loading begin:
04:47:00 388 | 1====loading ok:2.650265
04:47:00 389 | 1====current ItemsCount:73338
04:47:03 612 | 2====loading begin:
04:47:03 853 | 2====loading ok:0.2410241
04:47:03 854 | 2====current ItemsCount:73338
04:47:20 351 | 2====loading begin:
04:47:20 641 | 2====loading ok:0.290029
04:47:20 662 | 2====current ItemsCount:73338
04:47:23 235 | 1====loading begin:
04:47:25 875 | 1====loading ok:2.640264
04:47:25 876 | 1====current ItemsCount:73338
看來分時加載,有點意思。
再來看看分數據量加載:
代碼:
List
void LoadLastFiles2() { var files = Directory.EnumerateFileSystemEntries(testPath); allFiles2 = new List<FileItem>(); DateTime dtBegin = DateTime.Now; DateTime dtLastRefresh = DateTime.Now; LogHelper.Log("2====loading begin:"); int i = 0; foreach (string file in files) { allFiles2.Add(GetFileItem(file)); NotifyPropertyChanged("AllFiles2"); if (i >= 100) { Invoke(delegate { this.tstLv.Items.Refresh(); }); dtLastRefresh = DateTime.Now; i = 0; } i++; } NotifyPropertyChanged("AllFiles2"); LogHelper.Log("2====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("2====current ItemsCount:" + AllFiles2.Count.ToString()); }
結果
04:54:03 480 | 1====loading begin:
04:54:06 056 | 1====loading ok:2.5762576
04:54:06 057 | 1====current ItemsCount:73338
04:54:07 112 | 2====loading begin:
04:54:07 429 | 2====loading ok:0.3170317
04:54:07 430 | 2====current ItemsCount:73338
04:54:24 489 | 2====loading begin:
04:54:24 789 | 2====loading ok:0.3010301
04:54:24 790 | 2====current ItemsCount:73338
04:54:26 486 | 1====loading begin:
04:54:29 176 | 1====loading ok:2.690269
04:54:29 177 | 1====current ItemsCount:73338
看來還是要 設定refresh的時機, 這樣子速度快了不少。
至於分數據還是分時,看實際的需要了,個人認為分時比較好。
分時的時間最好長一點,數據量分塊也大一點,減少刷新UI的時間,加載會快。
但是如果用戶同時 拖拽滾動條, 體驗就不好了,不流暢,滾動條會跳躍。
8. ObservableCollection 也有優化的方案,如下,采用延遲通知數據變化的方案。網上看到的,很抱歉,沒找到出處。
RangeObservableCollection
public class RangeObservableCollection<T>:ObservableCollection<T> { bool isDeferNotify = false; public void AddRange(IEnumerable<T> rangeData) { isDeferNotify = true; foreach (T data in rangeData) { Add(data); } isDeferNotify = false; } public void RemoveRange(IEnumerable<T> rangeData) { isDeferNotify = true; foreach (T data in rangeData) { Remove(data); } isDeferNotify = false; } protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { //if use DeferNotify, UI Operation is not fluent //if (!isDeferNotify) //{ base.OnCollectionChanged(e); // } } }
修改代碼如下:
Load1
void LoadLastFiles() { var files = Directory.EnumerateFileSystemEntries(testPath); DateTime dtBegin = DateTime.Now; LogHelper.Log("1====loading begin:"); int pageIndex = 0; int pageSize = 2000; bool isEndPage = false; while (!isEndPage) { var pageData = files.Skip(pageIndex * pageSize).Take(pageSize).Select(o=>GetFileItem(o)); if (pageData.Count() < pageSize) isEndPage = true; Invoke(delegate { allFiles1.AddRange(pageData); }); pageIndex++; } LogHelper.Log("1====loading ok:" + (DateTime.Now - dtBegin).TotalSeconds); LogHelper.Log("1====current ItemsCount:" + allFiles1.Count.ToString()); }
測試后發現,效果並不明顯:
05:05:08 989 | 1====loading begin:
05:05:13 189 | 1====loading ok:4.2
05:05:13 191 | 1====current ItemsCount:73338
所以,還是束之高閣吧。
嘮嘮叨叨這么多,其實呢,對於大數據加載,MS提供的ObservableCollection方案還是不錯的,但是呢,想要再快一點,還是自己來搞搞把。
另外呢,ObservableCollection為什么快,網上不少資料,研究吧。
歡迎拍磚!!
