记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(); } }