2012-09-13 45 views
1

我有一個WinForms應用程序,需要加載一堆圖像,並在應用程序的整個生命週期內將它們存儲在內存中。內存高效的圖像handlilng

我目前這樣做的方式是將每個圖像作爲位圖加載,但毫不奇怪,這是吃了一大堆內存。

是否有更高效的內存加載方式?

+0

一堆圖像有多少張圖像?還有他們有多大,以及它將運行在哪種機器上(最終用戶桌面,高端圖形服務器等)? – ilivewithian

+0

可能有幾百個,大小不一。大多數會很小,有些可能是1Mb的光盤。 –

+0

該應用程序適用於一般桌面使用。 –

回答

1

根據你實際想要做的事情,你可能並不需要同時需要它們都在內存中。

這是我爲惰性加載數據所需的通用類。請注意,IApplicationDataSession只是一個用於從我們的數據庫中提取數據的類。但是,你可以用自己喜歡的任意替換此:

namespace Foo.Applications.DataHelpers 
{ 
    /// <summary> 
    /// This delegate allows the calling class to define a method for converting from the data type 
    /// that was queried to an alternate type. 
    /// </summary> 
    /// <typeparam name="S"></typeparam> 
    /// <typeparam name="T"></typeparam> 
    /// <param name="dataToConvert"></param> 
    /// <returns></returns> 
    public delegate S DataConverter<S, T>(T dataToConvert); 

    /// <summary> 
    /// Used for passing data to a new thread. 
    /// </summary> 
    internal class DataLoadRequest 
    { 
    public int Skip { get; private set; } 
    public int Take { get; private set; } 

    public DataLoadRequest(int skip, int take) 
    { 
     Skip = skip; 
     Take = take; 
    } 
    } 
    /// <summary> 
    /// This class allows for lazy loading of an arbitrary range of data. It also 
    /// allows for a transform from its current type to another type based on a conversion 
    /// delegate. 
    /// </summary> 
    /// <typeparam name="T">The entity type which should be queried.</typeparam> 
    /// <typeparam name="S">The object which T is converted to using the converter. If this is the same type 
    /// as T, then the converter can be null. </typeparam> 
    public class LazyDataLoader<S, T> where S : class 
    { 
    private S[] _cachedData; 
    private readonly int _defaultChunkSize; 
    private readonly Expression<Func<T, bool>> _whereClause; 
    private readonly IApplicationDataSession _dataSession; 


    private ManualResetEvent _donePreloadingDataEvent; 

    private readonly DataConverter<S, T> _converter; 

    private int? _totalRecords; 

    public LazyDataLoader(Expression<Func<T, bool>> whereClause, IApplicationDataSession dataSession, DataConverter<S, T> converter = null, int defaultChunkSize = 30, int? totalRecords = null) 
    { 
     _defaultChunkSize = defaultChunkSize; 
     _whereClause = whereClause; 
     _dataSession = dataSession; 

     _totalRecords = totalRecords; 

     if (converter == null && typeof(S) != typeof(T)) 
     { 
     throw new ArgumentException("if a converter is not given, then S must be the same type as T."); 
     } 

     _converter = converter ?? DefaultDataConverter; 
    } 

    /// <summary> 
    /// The total number of items that exist either in memory or in the DB. 
    /// </summary> 
    /// <returns></returns> 
    public int TotalItems() 
    { 
     return CachedData.Length; 
    } 

    /// <summary> 
    /// Get a subset of the items. 
    /// </summary> 
    /// <param name="index">the index from which to start</param> 
    /// <param name="take">the number of items to pull out.</param> 
    /// <returns></returns> 
    public S[] GetRange(int index, int take) 
    { 
     //don't start preloading new data until the previous data has been preloaded. 
     //TODO allow for preloading multiple ranges at the same time. 
     if (_donePreloadingDataEvent != null) 
     { 
     _donePreloadingDataEvent.WaitOne(); 
     } 

     if (index > TotalItems()) 
     { 
     throw new ArgumentException(string.Format("Index of: {0} was greater than total items: {1}", index, TotalItems())); 
     } 

     if (take == 0) 
     { 
     return new S[0]; 
     } 
     else if (take < 0) 
     { 
     throw new ArgumentException(string.Format("For index {0}, take had a value of {1}.", index, take)); 
     } 

     // if we're at the end of the data set, don't pick a range that's out of bounds. 
     var numberToTake = Math.Min(take, TotalItems() - index); 
     var range = CachedData.ToList().GetRange(index, numberToTake); 

     //if all values are already cached, return them 
     var firstDefaultIndex = range.FindIndex(s => s == default(S)); 
     if (firstDefaultIndex == -1) 
     { 
     return range.ToArray(); 
     } 

     LoadAsNeeded(range, index); 


     return range.ToArray(); 
    } 

    /// <summary> 
    /// This can be called to prepare a range of data prior to usage. 
    /// </summary> 
    /// <param name="index"></param> 
    /// <param name="take"></param> 
    public void PreloadRangeAsync(int index, int take) 
    { 
     if (index >= TotalItems()) 
     { 
     return; 
     } 

     //don't start preloading new data until the previous data has been preloaded. 
     //TODO allow for preloading multiple ranges at the same time. 
     if (_donePreloadingDataEvent != null) 
     { 
     _donePreloadingDataEvent.WaitOne(); 
     } 

     // if we're at the end of the data set, don't pick a range that's out of bounds. 
     var numberToTake = Math.Min(take, TotalItems() - index); 
     var range = CachedData.ToList().GetRange(index, numberToTake); 

     //if all values are already cached, return them 
     var firstDefaultIndex = range.FindIndex(s => s == default(S)); 
     if (firstDefaultIndex == -1) 
     { 
     return; 
     } 

     _donePreloadingDataEvent = new ManualResetEvent(false); 

     ThreadPool.QueueUserWorkItem(AsyncDataLoadingHandler, new DataLoadRequest(index, numberToTake)); 
    } 

    /// <summary> 
    /// The number of items currently in memory 
    /// </summary> 
    /// <returns></returns> 
    public int TotalCachedItems() 
    { 
     return CachedData.Count(cd => cd != default(S)); 
    } 

    private void AsyncDataLoadingHandler(object state) 
    { 
     try 
     { 
     var dataLoadRequest = (DataLoadRequest)state; 
     var results = Load(dataLoadRequest.Skip, dataLoadRequest.Take); 
     AddData(results, dataLoadRequest.Skip); 
     } 
     finally 
     { 
     _donePreloadingDataEvent.Set(); 
     } 
    } 

    private void AddData(T[] dataToAdd, int startingIndex) 
    { 
     if (dataToAdd.Length + startingIndex > _totalRecords) 
     { 
     throw new Exception("Error when loading new data into array. The index would be out of bounds."); 
     } 

     lock (CachedData) 
     { 
     Convert(dataToAdd).CopyTo(CachedData, startingIndex); 
     } 
    } 

    private T[] Load(int skip, int take) 
    { 
     var pageInfo = new PageInfo(skip, take); 
     var queryData = new QueryData<T>(_whereClause, pageInfo: pageInfo, includeChildren: true); 
     var results = _dataSession.Query(queryData); 

     _totalRecords = _totalRecords ?? results.TotalRecords; 

     return results.Results.ToArray(); 
    } 

    private void LoadAsNeeded(List<S> range, int startingIndex) 
    { 
     var firstDefaultIndex = range.FindIndex(s => s == default(S)); 
     if (firstDefaultIndex == -1) 
     { 
     return; 
     } 

     var i = firstDefaultIndex; 
     while (i < range.Count) 
     { 
     // continue looping through items until you hit a default 
     if (range[i] == default(S)) 
     { 
      var numSequentialDefaults = GetLengthOfSequentialDefaults(range.GetRange(i, range.Count - i)); 
      var newValues = Load(i + startingIndex, numSequentialDefaults); 

      //loop through the new values and assign them where necessary 
      var j = 0; 
      while (j < newValues.Length) 
      { 
      var convertedValue = Convert(newValues[j]); 
      range[j + i] = convertedValue; 
      CachedData[startingIndex + j + i] = convertedValue; 
      j++; 
      } 
      i += numSequentialDefaults; 
     } 
     else 
     { 
      i += range.TakeWhile(s => s != default(S)).Count(); 
     } 
     } 
    } 

    private S[] CachedData 
    { 
     get 
     { 
     if (_cachedData == null) 
     { 
      // this is necessary so we can instantiate the cache with the correct size. 
      var initialData = Load(0, _defaultChunkSize); 

      _cachedData = new S[_totalRecords.Value]; 
      AddData(initialData, 0); 
     } 

     return _cachedData; 
     } 
    } 

    private static int GetLengthOfSequentialDefaults(IEnumerable<S> range) 
    { 
     return range.TakeWhile(s => s == default(S)).Count(); 
    } 

    private S[] Convert(T[] valuesToConvert) 
    { 
     return valuesToConvert.Select(Convert).ToArray(); 
    } 

    private S Convert(T valueToConvert) 
    { 
     return _converter(valueToConvert); 
    } 

    /// <summary> 
    /// This converter is used when conversion isn't actually necessary (S == T). It simply casts T to S. 
    /// </summary> 
    private static readonly DataConverter<S, T> DefaultDataConverter = dataToConvert => (S)(object)dataToConvert; 
    } 

} 

我敢肯定有在這裏有些東西可以改進的;這只是我在一個下午做的事情,但迄今爲止它對我很有幫助。您可能需要添加卸載最近未使用的數據的功能。

+1

PNG會使它們在光盤上更小,但它是否會使它們在內存中更小? –

1

您應該使用基於LRU的緩存來存儲圖像,但不是全部。如果需要尚未加載的圖像,則應用程序可以加載它。