内存高效的图像处理

时间:2012-09-13 11:37:14

标签: c# winforms image

我有一个WinForms应用程序需要加载一堆图像,并在应用程序的生命周期内将它们存储在内存中。

我目前这样做的方法是将每个图像加载为位图,但不出所料,这会占用大量内存。

是否有更高效的内存加载方式?

2 个答案:

答案 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 :(得分:1)

您应该使用基于LRU的缓存来存储图像,但不能全部存储图像。如果需要尚未加载的图像,则应用程序可以加载它。