在做图片相关的应用的时候,经常需要用大图片的缓存,默认的Image控件不支持缓存的支持,本文自定义一个支持图片缓存的控件
当图片的地址是网络图片时候
根据Url判断该图片是否存在本地,如果存在,则直接从本地读取,如果不存在,则通过Http请求下载该图片,保存到本地,然后读取到Image控件中
当图片为本地地址的时候,直接从本地读取,设置到Image控件中
1、在定义可缓存图片控件之前,先封装一下文件存储的帮助类
using System; using System.IO; using System.IO.IsolatedStorage; using System.Text; using System.Threading.Tasks; using System.Windows; using Windows.ApplicationModel; using Windows.Storage; using Newtonsoft.Json; using XTuOne.Common.Helpers; namespace XTuOne.Utility.Helpers { public class StorageHelper : IStorageHelper { #region 单例 public static IStorageHelper Instance { get; private set; } public static object LockObject; static StorageHelper() { Instance = new StorageHelper(); LockObject = new object(); } private StorageHelper() { } #endregion #region 同步读写方法 public Stream ReadFile(string filePath) { lock (LockObject) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { if (!sf.FileExists(filePath)) { throw new FileNotFoundException(string.Format("没有找到文件:{0}", filePath)); } using (var fs = sf.OpenFile(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var stream = new MemoryStream(); fs.CopyTo(stream); stream.Seek(0, SeekOrigin.Begin); return stream; } } } } public string CreateFile(Stream stream, string filePath, bool replace = false) { lock (LockObject) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { var directory = Path.GetDirectoryName(filePath); if (directory != null && !sf.DirectoryExists(directory)) { //如果目录不存在,则创建 sf.CreateDirectory(directory); } if (FileExist(filePath)) { if (!replace) { return filePath; } sf.DeleteFile(filePath); } //如果不存在或者存在且替换 using (var fs = sf.CreateFile(filePath)) { stream.CopyTo(fs); } } } return filePath; } public string CreateFile(byte[] data, string filePath, bool replace = false) { lock (LockObject) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { var directory = Path.GetDirectoryName(filePath); if (directory != null && !sf.DirectoryExists(directory)) { //如果目录不存在,则创建 sf.CreateDirectory(directory); } if (FileExist(filePath)) { if (!replace) { return filePath; } sf.DeleteFile(filePath); } //如果不存在或者存在且替换 using (var fs = new IsolatedStorageFileStream(filePath, FileMode.OpenOrCreate, sf)) { fs.Write(data, 0, data.Length); } } } return filePath; } public string ReadAllText(string fileName) { using (var reader = new StreamReader(ReadFile(fileName))) { return reader.ReadToEnd(); } } public string WriteAllText(string fileName, string text, bool replace) { return CreateFile(Encoding.UTF8.GetBytes(text), fileName, replace); } #endregion #region 异步读写方法 public async Task<Stream> ReadFileAsync(string filePath) { var storageFile = await GetStorageFileAsync(filePath); return await storageFile.OpenStreamForReadAsync(); } public async Task<string> CreateFileAsync(Stream stream, string filePath, bool replace = false) { var storageFile = await GetStorageFileAsync(filePath); if (storageFile != null) { if (FileExist(filePath)) { if (replace) { //替换先删除 await storageFile.DeleteAsync(StorageDeleteOption.PermanentDelete); } else { return filePath; } } storageFile = await GetStorageFileAsync(filePath); var destStream = await storageFile.OpenStreamForWriteAsync(); await stream.CopyToAsync(destStream); } return filePath; } public async Task<string> CreateFileAsync(byte[] data, string filePath, bool replace = false) { var storageFile = await GetStorageFileAsync(filePath); if (storageFile != null) { if (FileExist(filePath)) { if (replace) { //替换先删除 await storageFile.DeleteAsync(StorageDeleteOption.PermanentDelete); } else { return filePath; } } storageFile = await GetStorageFileAsync(filePath); var destStream = await storageFile.OpenStreamForWriteAsync(); await destStream.WriteAsync(data, 0, data.Length); } return filePath; } public async Task<string> ReadAllTextAsync(string fileName) { using (var reader = new StreamReader(await ReadFileAsync(fileName))) { return await reader.ReadToEndAsync(); } } public async Task<string> WriteAllTextAsync(string fileName, string text, bool replace) { return await CreateFileAsync(Encoding.UTF8.GetBytes(text), fileName, replace); } #endregion #region 普通方法:判断文件(文件夹)存在,创建(删除)文件夹,获取文件(文件夹) public bool FileExist(string fileName) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { return sf.FileExists(fileName); } } public bool DirectoryExist(string directory) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { return sf.DirectoryExists(directory); } } public void DeleteFile(string fileName) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { if (sf.FileExists(fileName)) { sf.DeleteFile(fileName); } } } public void CreateDirectory(string directory) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { if (sf.DirectoryExists(directory)) { sf.DeleteDirectory(directory); } } } public void DeleteDirectory(string directory, bool isDeleteAll) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { if (sf.DirectoryExists(directory)) { if (isDeleteAll) { var files = GetFiles(directory); foreach (var file in files) { DeleteFile(file); } var directories = GetDirectories(directory); foreach (var s in directories) { DeleteDirectory(s, true); } } sf.DeleteDirectory(directory); } } } public string[] GetFiles(string directory) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { return sf.GetFileNames(directory); } } /// <summary> /// 获取本地文件夹中的文件 /// </summary> public string[] GetDirectories(string directory) { using (var sf = IsolatedStorageFile.GetUserStoreForApplication()) { return sf.GetDirectoryNames(directory); } } #endregion #region 拷贝文件(从安装包到本地) /// <summary> /// 从安装包拷贝文件到本地 /// </summary> public async Task CopyPackageFileToLocalAsync(string source, string target = null, bool replace = false) { using (var stream = GetResourceStream(source)) { await CreateFileAsync(stream, target ?? source, replace); } } /// <summary> /// 从安装包拷贝路径到本地 /// </summary> public async Task CopyPackageFolderToLocalAsync(string source, string target = null, bool replace = false) { target = target ?? source; var packagePath = Package.Current.InstalledLocation; var folder = await GetStorageFolderAsync(packagePath, source); //拷贝文件 var files = await folder.GetFilesAsync(); foreach (var storageFile in files) { var fileName = storageFile.Name; using (var stream = await storageFile.OpenStreamForReadAsync()) { await CreateFileAsync(stream, target + fileName, replace); } } //拷贝子文件夹(递归) var folders = await folder.GetFoldersAsync(); foreach (var storageFolder in folders) { await CopyPackageFolderToLocalAsync(source + storageFolder.Name + "/", target + storageFolder.Name + "/", replace); } } #endregion #region 从安装包(安装路径)中读取(同步) public Stream GetResourceStream(string file) { //引用安装路径的文件的时候不以'/'开头 file = file.TrimStart('/'); return Application.GetResourceStream(new Uri(file, UriKind.Relative)).Stream; } #endregion #region 序列化 public void Serialize<T>(string fileName, T obj, bool replace) { var json = JsonConvert.SerializeObject(obj); WriteAllText(fileName, json, replace); } T IStorageHelper.DeSerialize<T>(string fileName) { var json = ReadAllText(fileName); return JsonConvert.DeserializeObject<T>(json); } public async Task SerializeAsync<T>(string fileName, T obj, bool replace) { var json = JsonConvert.SerializeObject(obj); await WriteAllTextAsync(fileName, json, replace); } public async Task<T> DeSerializeAsync<T>(string fileName) { var json = await ReadAllTextAsync(fileName); return JsonConvert.DeserializeObject<T>(json); } #endregion #region 辅助方法 /// <summary> /// 根据路劲获取StorageFolder /// </summary> private async Task<StorageFolder> GetStorageFolderAsync(StorageFolder folder, string directory) { var directories = directory.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); foreach (var s in directories) { folder = await folder.CreateFolderAsync(s, CreationCollisionOption.OpenIfExists); } return folder; } /// <summary> /// 根据文件名异步获取本地文件夹StorageFolder(如果路径不存在,则创建路径) /// </summary> private async static Task<StorageFolder> GetStorageFolderAsync(string filePath) { var localFolder = ApplicationData.Current.LocalFolder; var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory)) { var directories = directory.Split(new[] {'\', '/'}, StringSplitOptions.RemoveEmptyEntries); foreach (var s in directories) { localFolder = await localFolder.CreateFolderAsync(s, CreationCollisionOption.OpenIfExists); } } return localFolder; } /// <summary> /// 根据路径得到StoreageFile /// </summary> private async static Task<StorageFile> GetStorageFileAsync(string filePath) { var folder = await GetStorageFolderAsync(filePath); var fileName = Path.GetFileName(filePath); if (fileName != null) { return await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists); } return null; } #endregion } }
图片的写入和读取都使用了线程锁,在最后说明
注意:上面的异步方法是线程不安全的,在多线程的情况下,当文件被一个线程写入的时候,另一个线程调用读的方法会抛出异常 Access Deny,访问被阻止
实现了StorageHelper,下面是CacheableImage的实现,支持占位图片,加载失败图片,配置保存路径
2、自定义可缓存图片控件的实现
<UserControl x:Class="XTuOne.Controls.CacheableImage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" d:DesignHeight="480" d:DesignWidth="480"> <Grid x:Name="LayoutRoot"> <Image x:Name="Image" Stretch="Fill"></Image> </Grid> </UserControl>
using System; using System.IO; using System.Net; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using XTuOne.Utility.Helpers; namespace XTuOne.Controls { /// <summary> /// 支持本地缓存的图片空间 /// </summary> public partial class CacheableImage { public CacheableImage() { InitializeComponent(); } public static readonly DependencyProperty CachedDirectoryProperty = DependencyProperty.Register( "CachedDirectory", typeof (string), typeof (CacheableImage), new PropertyMetadata("/ImageCached/")); public static readonly DependencyProperty FaildImageUrlProperty = DependencyProperty.Register( "FaildImageUrl", typeof(Uri), typeof(CacheableImage), new PropertyMetadata(default(string))); public static readonly DependencyProperty LoadingImageUrlProperty = DependencyProperty.Register( "LoadingImageUrl", typeof(Uri), typeof(CacheableImage), new PropertyMetadata(default(string))); public static readonly DependencyProperty StretchProperty = DependencyProperty.Register( "Stretch", typeof (Stretch), typeof (CacheableImage), new PropertyMetadata(default(Stretch), StretchPropertyChangedCallback)); private static void StretchPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var cachedImage = (CacheableImage)dependencyObject; var stretch = (Stretch)dependencyPropertyChangedEventArgs.NewValue; if (cachedImage.Image != null) { cachedImage.Image.Stretch = stretch; } } public Stretch Stretch { get { return (Stretch) GetValue(StretchProperty); } set { SetValue(StretchProperty, value); } } /// <summary> /// 加载失败的图片 /// </summary> public Uri FaildImageUrl { get { return (Uri)GetValue(FaildImageUrlProperty); } set { SetValue(FaildImageUrlProperty, value); } } /// <summary> /// 加载中显示的图片(需要进行网络请求时) /// </summary> public Uri LoadingImageUrl { get { return (Uri)GetValue(LoadingImageUrlProperty); } set { SetValue(LoadingImageUrlProperty, value); } } /// <summary> /// 缓存到本地的目录 /// </summary> public string CachedDirectory { get { return (string) GetValue(CachedDirectoryProperty); } set { SetValue(CachedDirectoryProperty, value); } } public static readonly DependencyProperty ImageUrlProperty = DependencyProperty.Register( "ImageUrl", typeof (string), typeof (CacheableImage), new PropertyMetadata(default(string), ImageUrlPropertyChangedCallback)); public string ImageUrl { get { return (string)GetValue(ImageUrlProperty); } set { SetValue(ImageUrlProperty, value); } } private static async void ImageUrlPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var cachedImage = (CacheableImage) dependencyObject; var imageUrl = (string)dependencyPropertyChangedEventArgs.NewValue; if (string.IsNullOrEmpty(imageUrl)) { return; } if (imageUrl.StartsWith("http://") || imageUrl.Equals("https://")) { var fileName = cachedImage.CachedDirectory + Uri.EscapeDataString(imageUrl); //网络图片,判断是否存在 if (!StorageHelper.Instance.FileExist(fileName)) { try { if (cachedImage.LoadingImageUrl != null) { cachedImage.Image.Source = new BitmapImage(cachedImage.LoadingImageUrl); } //请求 var request = WebRequest.CreateHttp(imageUrl); request.AllowReadStreamBuffering = true; var response = await request.GetResponseAsync(); var stream = response.GetResponseStream(); await Task.Delay(1000); //保存到本地 StorageHelper.Instance.CreateFile(stream, fileName); } catch (Exception e) { //请求失败 if (cachedImage.FaildImageUrl != null) { cachedImage.Image.Source = new BitmapImage(cachedImage.FaildImageUrl); } return; } } //读取图片文件 var imageStream = StorageHelper.Instance.ReadFile(fileName); var bitmapImage = new BitmapImage(); bitmapImage.SetSource(imageStream); cachedImage.Image.Source = bitmapImage; } else { //本地图片 var bitmapImage = new BitmapImage(new Uri(imageUrl, UriKind.Relative)); cachedImage.Image.Source = bitmapImage; } } public static readonly DependencyProperty ImageStreamProperty = DependencyProperty.Register( "ImageStream", typeof (Stream), typeof (CacheableImage), new PropertyMetadata(default(Stream), ImageStreamPropertyChangedCallback)); private static void ImageStreamPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var cachedImage = (CacheableImage) dependencyObject; var imageStream = (Stream) dependencyPropertyChangedEventArgs.NewValue; var bitmapImage = new BitmapImage(); bitmapImage.SetSource(imageStream); cachedImage.Image.Source = bitmapImage; } /// <summary> /// 支持直接传递流进来 /// </summary> public Stream ImageStream { get { return (Stream) GetValue(ImageStreamProperty); } set { SetValue(ImageStreamProperty, value); } } } }
为了保证线程安全,这里图片的保存没有用到异步,因为如果有很多图片进行请求的时候,可能会线程请求异常,像上面说的情况
上面的StorageHelper在读取和写入的时候都加了线程锁(其实不应该在读取文件的时候加锁的),是为了保证,在写入的过程中,读取文件出现无权访问的问题
暂时没有找到方法支持线程安全,如果你有更好的方案,可以给我留言