記UWP開發——多線程操作/並發操作中的坑
一切都要從新版風車動漫UWP的圖片緩存功能說起。
起因便是風車動漫官網的番劇更新都很慢,所以圖片更新也非常慢。在開發新版的過程中,我很簡單就想到了圖片多次重復下載導致的資源浪費問題。
所以我給app加了一個緩存機制:
創建一個用戶控件CoverView,將首頁GridView.ItemTemplate裏的Image全部換成CoverView
CoverView一旦接到ImageUrl的修改,就會自動向後臺的PictureHelper申請指定Url的圖片
PictureHelper會先判斷本地是否有這個Url的圖片,沒有的話從風車動漫官網下載一份,保存到本地,然後返回給CoverView
關鍵就是PictureHelper的GetImageAsync方法
本地緩存圖片的代碼片段:
//緩存文件名以MD5的形式保存在本地 string name = StringHelper.MD5Encrypt16(Url); if (imageFolder == null) imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists); StorageFile file; IRandomAccessStream stream = null; if (File.Exists(imageFolder.Path + "\\" + name)) { file = await imageFolder.GetFileAsync(name); stream = await file.OpenReadAsync(); } //文件不存在or文件為空,通過http下載 if (stream == null || stream.Size == 0) { file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting); stream= await file.OpenAsync(FileAccessMode.ReadWrite); IBuffer buffer = await HttpHelper.GetBufferAsync(Url); await stream.WriteAsync(buffer); } //...
嗯...一切都看似很美好....
但是運行之後,發現了一個很嚴重的偶發Exception
查閱google良久後,得知了發生這個問題的原因:
主頁GridView一次性加載了幾十個Item後,幾十個Item中的CoverView同時調用了PictureHelper的GetImageAsync方法
幾十個PictureHelper的GetImageAsync方法又同時訪問緩存文件夾,導致了非常嚴重的IO鎖死問題,進而引發了大量的UnauthorizedAccessException
有=又查閱了許久之後,終於找到了解決方法:
SemaphoreSlim異步鎖
使用方法如下:
private static SemaphoreSlim asyncLock = new SemaphoreSlim(1);//1:信號容量,即最多幾個異步線程一起執行,保守起見設為1 public async static Task<WriteableBitmap> GetImageAsync(string Url) { if (Url == null) return null; try { await asyncLock.WaitAsync(); //緩存文件名以MD5的形式保存在本地 string name = StringHelper.MD5Encrypt16(Url); if (imageFolder == null) imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists); StorageFile file; IRandomAccessStream stream = null; if (File.Exists(imageFolder.Path + "\\" + name)) { file = await imageFolder.GetFileAsync(name); stream = await file.OpenReadAsync(); } //文件不存在or文件為空,通過http下載 if (stream == null || stream.Size == 0) { file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting); stream = await file.OpenAsync(FileAccessMode.ReadWrite); IBuffer buffer = await HttpHelper.GetBufferAsync(Url); await stream.WriteAsync(buffer); } //... } catch(Exception error) { Debug.WriteLine("Cache image error:" + error.Message); return null; } finally { asyncLock.Release(); } }
成功解決了並發訪問IO的問題
但是在接下來的Stream轉WriteableBitmap的過程中,問題又來了....
這個問題比較好解決
BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream); WriteableBitmap bitmap = null; await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate { bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight); stream.Seek(0); await bitmap.SetSourceAsync(stream); }); stream.Dispose(); return bitmap;
使用UI線程來跑就ok了
然後!問題又來了
WriteableBitmap到被return為止,都很正常
但是到接下來,我在CoverView裏做其他一些bitmap的操作時,出現了下面這個問題
又找了好久,最後回到bitmap的PixelBuffer一看,擦,全是空的?
雖然bitmap成功的new了出來,PixelHeight/Width啥的都有了,當時UI線程中的SetSourceAsync壓根沒執行完,所以出現了內存保護的神奇問題
明明await了啊?
最後使用這樣一個奇技淫巧,最終成功完成
BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream); WriteableBitmap bitmap = null; TaskCompletionSource<bool> task = new TaskCompletionSource<bool>(); await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate { bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight); stream.Seek(0); await bitmap.SetSourceAsync(stream); task.SetResult(true); }); await task.Task;
關於TaskCompletionSource,請參閱
https://www.cnblogs.com/loyieking/p/9209476.html
最後總算是完成了....
public async static Task<WriteableBitmap> GetImageAsync(string Url) { if (Url == null) return null; try { await asyncLock.WaitAsync(); //緩存文件名以MD5的形式保存在本地 string name = StringHelper.MD5Encrypt16(Url); if (imageFolder == null) imageFolder = await cacheFolder.CreateFolderAsync("imagecache", CreationCollisionOption.OpenIfExists); StorageFile file; IRandomAccessStream stream = null; if (File.Exists(imageFolder.Path + "\\" + name)) { file = await imageFolder.GetFileAsync(name); stream = await file.OpenReadAsync(); } //文件不存在or文件為空,通過http下載 if (stream == null || stream.Size == 0) { file = await imageFolder.CreateFileAsync(name, CreationCollisionOption.ReplaceExisting); stream = await file.OpenAsync(FileAccessMode.ReadWrite); IBuffer buffer = await HttpHelper.GetBufferAsync(Url); await stream.WriteAsync(buffer); } BitmapDecoder bitmapDecoder = await BitmapDecoder.CreateAsync(stream); WriteableBitmap bitmap = null; TaskCompletionSource<bool> task = new TaskCompletionSource<bool>(); await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async delegate { bitmap = new WriteableBitmap((int)bitmapDecoder.PixelWidth, (int)bitmapDecoder.PixelHeight); stream.Seek(0); await bitmap.SetSourceAsync(stream); task.SetResult(true); }); await task.Task; stream.Dispose(); return bitmap; } catch(Exception error) { Debug.WriteLine("Cache image error:" + error.Message); return null; } finally { asyncLock.Release(); } }
記UWP開發——多線程操作/並發操作中的坑