Windows Phone App基於文件系統的圖片緩存方案


最近在做一個Windows Phone 8.1的應用,因為應用里面使用了很多圖片,所以打算將圖片文件緩存到文件系統里面,以便節省流量和提升體驗。

一般顯示圖片的做法大多只需要將對應的Uri地址綁定到對應控件的ImageSource屬性上即可,或者將Uri傳入BitmapImage對象,其會自動將資源異步下載好然后加載。

為了不將緩存的邏輯侵入實體模型層,所以打算在Uri->BitmapImage綁定上做文章,也就是利用IValueConverter接口構造值轉換器。

這里有一個問題就是,因為綁定的原因,IValueConverter接口只能是同步的方法,而圖片的下載的過程是一個異步的過程,所以我們需要一種解決方案實現一個異步的ValueConverter。

這個解決方案我思考了很久,最后在這里受到了啟發:http://stackoverflow.com/questions/15003827/async-implementation-of-ivalueconverter

Update: 順藤摸瓜找到了這個問題的答主的一個開源庫,主要是對async和await的拓展,推薦:https://github.com/StephenCleary/AsyncEx

具體而言,就是在同步的ValueConverter中構造一個Task-like的對象,然后將對應的屬性綁定到這個Task-like對象的一個屬性中去,然后Task-like待Task完成后更新對應的狀態。

這個解決方案實現很巧妙,同時也沒有對Model和ViewModel做任何的侵入,下面是我修改后用於圖片緩存的相關代碼:

 

首先是界面綁定所做的改動,大概如下,就是通過構造一個新的DataContext來實現綁定:

<Image DataContext="{Binding Image, Converter={StaticResource ImageConverter}}"
             Stretch="UniformToFill" Source="{Binding Result}" />

對應的ValueConverter類:

public class CacheImageValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            var image = value as Image;
            if (image == null || string.IsNullOrEmpty(image.Url))
            {
                var bitmap = new BitmapImage(new Uri("ms-appx:///assets/default.png"));
                var notifier = new TaskCompletionNotifier<BitmapImage>();
                notifier.SetTask(Task.FromResult(bitmap));
                return notifier;
            }
            else
            {
                var task = Task.Run(async () =>
                {
                    var cache = HiwedoContainer.Current.Resolve<ImageCache>();
                    var uri = await cache.GetImageSourceFromUrlAsync(image.Url);
                    return uri;
                });
                var notifier = new TaskCompletionNotifier<BitmapImage>();
                notifier.SetTask(task, c => new BitmapImage(c));
                return notifier;
            }
        }


        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

其次是Task-like的類。

public sealed class TaskCompletionNotifier<TResult> : INotifyPropertyChanged
    {
        private TResult _result;
        private IAsyncResult _task;

        public TaskCompletionNotifier()
        {
            this._result = default(TResult);
        }

        public void SetTask<T>(Task<T> task, Func<T, TResult> factoryFunc)
        {
            this._task = task;
            if (!task.IsCompleted)
            {
                var scheduler = (SynchronizationContext.Current == null)
                    ? TaskScheduler.Current
                    : TaskScheduler.FromCurrentSynchronizationContext();
                task.ContinueWith(t =>
                {
                    var propertyChanged = PropertyChanged;
                    if (propertyChanged != null)
                    {
                        this.OnPropertyChanged("IsCompleted");
                        if (t.IsFaulted)
                        {
                            InnerException = t.Exception;
                            this.OnPropertyChanged("ErrorMessage");
                        }
                        else
                        {
                            try
                            {
                                this._result = factoryFunc(task.Result);
                            }
                            catch (Exception ex)
                            {
                                Debug.WriteLine("Factory error: " + ex.Message);
                                this.InnerException = ex;
                                this.OnPropertyChanged("ErrorMessage");
                            }
                            this.OnPropertyChanged("Result");
                        }
                    }
                },
                    CancellationToken.None,
                    TaskContinuationOptions.ExecuteSynchronously,
                    scheduler);
            }
            else
            {
                this._result = factoryFunc(task.Result);
            }
        }

        public void SetTask(Task<TResult> task)
        {
            this._task = task;
            if (!task.IsCompleted)
            {
                var scheduler = (SynchronizationContext.Current == null)
                    ? TaskScheduler.Current
                    : TaskScheduler.FromCurrentSynchronizationContext();
                task.ContinueWith(t =>
                {
                    var propertyChanged = PropertyChanged;
                    if (propertyChanged != null)
                    {
                        this.OnPropertyChanged("IsCompleted");
                        if (t.IsFaulted)
                        {
                            InnerException = t.Exception;
                            this.OnPropertyChanged("ErrorMessage");
                        }
                        else
                        {
                            this._result = task.Result;
                            this.OnPropertyChanged("Result");
                        }
                    }
                },
                    CancellationToken.None,
                    TaskContinuationOptions.ExecuteSynchronously,
                    scheduler);
            }
            else
            {
                this._result = task.Result;
            }
        }

        public TResult Result
        {
            get { return this._result; }
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public Exception InnerException { get; set; }

        public string ErrorMessage
        {
            get { return (InnerException == null) ? null : InnerException.Message; }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

提供一個Task和Factory的函數重載是必須的,因為對於像BitmapImage這樣的類,其初始化需要在對應的UI線程中,不能直接將BitmapImage作為結果返回,所以在下面也可以看到,實際上Task返回的只是Uri,然后構造BitmapImage是在ContinueWith的方法中完成的。

最后就是用於圖片緩存的類,這個類比較簡單,就是如果判斷如果圖片已經存在,就直接從文件系統返回,如果不存在就先下載再返回對應的Uri。然后會有一個變量緩存所有已經緩存的文件名。

public class ImageCache
    {
        private const string ImageCacheFolder = "ImageCaches";
        private StorageFolder _cacheFolder;
        private IList<string> _cachedFileNames;

        public async Task<Uri> GetImageSourceFromUrlAsync(string url)
        {
            string fileName = url.Substring(url.LastIndexOf('/') + 1);
            if (this._cachedFileNames.Contains(fileName))
            {
                return new Uri("ms-appdata:///local/ImageCaches/" + fileName);
            }
            if (await DownloadAndSaveAsync(url, fileName))
            {
                _cachedFileNames.Add(fileName);
                return new Uri("ms-appdata:///local/ImageCaches/" + fileName);
            }
            Debug.WriteLine("Download image failed. " + url);
            return new Uri(url);
        }

        private async Task<bool> DownloadAndSaveAsync(string url, string filename)
        {
            try
            {
                var request = WebRequest.CreateHttp(url);
                request.Method = "GET";
                using (var response = await request.GetResponseAsync())
                {
                    using (var responseStream = response.GetResponseStream())
                    {
                        var file =
                            await this._cacheFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
                        using (var fs = await file.OpenStreamForWriteAsync())
                        {
                            await responseStream.CopyToAsync(fs);
                            Debug.WriteLine("Downloaded: " + url);
                            return true;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Error: " + ex.Message);
                return false;
            }
        }

        public async Task LoadCache()
        {
            var folders = await ApplicationData.Current.LocalFolder.GetFoldersAsync();
            foreach (var folder in folders)
            {
                if (folder.Name == ImageCacheFolder)
                {
                    this._cacheFolder = folder;
                    break;
                }
            }
            if (this._cacheFolder == null)
            {
                this._cacheFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(ImageCacheFolder);
            }
            this._cachedFileNames = (await this._cacheFolder.GetFilesAsync()).Select(c => c.Name).ToList();
        }

        public IList<string> CachedFileNames
        {
            get { return _cachedFileNames; }
            set { _cachedFileNames = value; }
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM