Skip to content

Instantly share code, notes, and snippets.

@garuma
Created December 17, 2012 22:38
Show Gist options
  • Save garuma/4323075 to your computer and use it in GitHub Desktop.
Save garuma/4323075 to your computer and use it in GitHub Desktop.
using System;
using Android.Graphics;
namespace Foo
{
public class BitmapCache
{
DiskCache diskCache;
LRUCache<string, Bitmap> memCache;
BitmapCache (DiskCache diskCache)
{
this.diskCache = diskCache;
this.memCache = new LRUCache<string, Bitmap> (10);
}
public static BitmapCache CreateCache (Android.Content.Context ctx, string cacheName, string version = "1.0")
{
return new BitmapCache (DiskCache.CreateCache (ctx, cacheName, version));
}
public void AddOrUpdate (string key, Bitmap bmp, TimeSpan duration)
{
diskCache.AddOrUpdate (key, bmp, duration);
memCache.Put (key, bmp);
}
public bool TryGet (string key, out Bitmap bmp)
{
if ((bmp = memCache.Get (key)) != null)
return true;
return diskCache.TryGet (key, out bmp);
}
}
}
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Bitmap = Android.Graphics.Bitmap;
using Env = Android.OS.Environment;
namespace Foo
{
public class DiskCache
{
enum JournalOp {
Created = 'c',
Modified = 'm',
Deleted = 'd'
}
const string JournalFileName = ".journal";
const string Magic = "MONOID";
readonly Encoding encoding = Encoding.UTF8;
string basePath;
string journalPath;
string version;
Timer cleanupTimer;
struct CacheEntry
{
public DateTime Origin;
public TimeSpan TimeToLive;
public CacheEntry (DateTime o, TimeSpan ttl)
{
Origin = o;
TimeToLive = ttl;
}
}
Dictionary<string, CacheEntry> entries = new Dictionary<string, CacheEntry> ();
public DiskCache (string basePath, string version)
{
this.basePath = basePath;
if (!Directory.Exists (basePath))
Directory.CreateDirectory (basePath);
this.journalPath = Path.Combine (basePath, JournalFileName);
this.version = version;
try {
InitializeWithJournal ();
} catch {
Directory.Delete (basePath, true);
Directory.CreateDirectory (basePath);
}
ThreadPool.QueueUserWorkItem (CleanCallback);
}
public static DiskCache CreateCache (Android.Content.Context ctx, string cacheName, string version = "1.0")
{
/*string cachePath = Env.ExternalStorageState == Env.MediaMounted
|| !Env.IsExternalStorageRemovable ? ctx.ExternalCacheDir.AbsolutePath : ctx.CacheDir.AbsolutePath;*/
string cachePath = ctx.CacheDir.AbsolutePath;
return new DiskCache (Path.Combine (cachePath, cacheName), version);
}
void InitializeWithJournal ()
{
if (!File.Exists (journalPath)) {
using (var writer = new StreamWriter (journalPath, false, encoding)) {
writer.WriteLine (Magic);
writer.WriteLine (version);
}
return;
}
string line = null;
using (var reader = new StreamReader (journalPath, encoding)) {
if (!EnsureHeader (reader))
throw new InvalidOperationException ("Invalid header");
while ((line = reader.ReadLine ()) != null) {
try {
var op = ParseOp (line);
string key;
DateTime origin;
TimeSpan duration;
switch (op) {
case JournalOp.Created:
ParseEntry (line, out key, out origin, out duration);
entries.Add (key, new CacheEntry (origin, duration));
break;
case JournalOp.Modified:
ParseEntry (line, out key, out origin, out duration);
entries[key] = new CacheEntry (origin, duration);
break;
case JournalOp.Deleted:
ParseEntry (line, out key);
entries.Remove (key);
break;
}
} catch {
break;
}
}
}
}
void CleanCallback (object state)
{
KeyValuePair<string, CacheEntry>[] kvps;
lock (entries) {
var now = DateTime.UtcNow;
kvps = entries.Where (kvp => kvp.Value.Origin + kvp.Value.TimeToLive < now).Take (10).ToArray ();
Android.Util.Log.Info ("DiskCacher", "Removing {0} elements from the cache", kvps.Length);
foreach (var kvp in kvps) {
entries.Remove (kvp.Key);
try {
File.Delete (Path.Combine (basePath, kvp.Key));
} catch {}
}
}
}
bool EnsureHeader (StreamReader reader)
{
var m = reader.ReadLine ();
var v = reader.ReadLine ();
return m == Magic && v == version;
}
JournalOp ParseOp (string line)
{
return (JournalOp)line[0];
}
void ParseEntry (string line, out string key)
{
key = line.Substring (2);
}
void ParseEntry (string line, out string key, out DateTime origin, out TimeSpan duration)
{
key = null;
origin = DateTime.MinValue;
duration = TimeSpan.MinValue;
var parts = line.Substring (2).Split (' ');
if (parts.Length != 3)
throw new InvalidOperationException ("Invalid entry");
key = parts[0];
long dateTime, timespan;
if (!long.TryParse (parts[1], out dateTime))
throw new InvalidOperationException ("Corrupted origin");
else
origin = new DateTime (dateTime);
if (!long.TryParse (parts[2], out timespan))
throw new InvalidOperationException ("Corrupted duration");
else
duration = TimeSpan.FromMilliseconds (timespan);
}
public void AddOrUpdate (string key, Bitmap bmp, TimeSpan duration)
{
key = SanitizeKey (key);
lock (entries) {
bool existed = entries.ContainsKey (key);
using (var stream = new BufferedStream (File.OpenWrite (Path.Combine (basePath, key))))
bmp.Compress (Bitmap.CompressFormat.Png, 100, stream);
AppendToJournal (existed ? JournalOp.Modified : JournalOp.Created,
key,
DateTime.UtcNow,
duration);
entries[key] = new CacheEntry (DateTime.UtcNow, duration);
}
}
public bool TryGet (string key, out Bitmap bmp)
{
key = SanitizeKey (key);
lock (entries) {
bmp = null;
if (!entries.ContainsKey (key))
return false;
try {
bmp = Android.Graphics.BitmapFactory.DecodeFile (Path.Combine (basePath, key));
} catch {
return false;
}
return true;
}
}
void AppendToJournal (JournalOp op, string key)
{
using (var writer = new StreamWriter (journalPath, true, encoding)) {
writer.Write ((char)op);
writer.Write (' ');
writer.Write (key);
writer.WriteLine ();
}
}
void AppendToJournal (JournalOp op, string key, DateTime origin, TimeSpan ttl)
{
using (var writer = new StreamWriter (journalPath, true, encoding)) {
writer.Write ((char)op);
writer.Write (' ');
writer.Write (key);
writer.Write (' ');
writer.Write (origin.Ticks);
writer.Write (' ');
writer.Write ((long)ttl.TotalMilliseconds);
writer.WriteLine ();
}
}
string SanitizeKey (string key)
{
return new string (key
.Where (c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
.ToArray ());
}
}
}
// Based on Sys.Web code
using System;
using System.Collections.Generic;
namespace Foo
{
public class LRUCache<TKey, TValue>
{
int capacity;
LinkedList<ListValueEntry<TKey, TValue>> list;
Dictionary<TKey, LinkedListNode<ListValueEntry<TKey, TValue>>> lookup;
LinkedListNode<ListValueEntry<TKey, TValue>> openNode;
public LRUCache (int capacity)
{
this.capacity = capacity;
this.list = new LinkedList<ListValueEntry<TKey, TValue>>();
this.lookup = new Dictionary<TKey, LinkedListNode<ListValueEntry<TKey, TValue>>> (capacity + 1);
this.openNode = new LinkedListNode<ListValueEntry<TKey, TValue>>(new ListValueEntry<TKey, TValue> (default(TKey), default(TValue)));
}
public void Put (TKey key, TValue value)
{
if (Get(key) == null) {
this.openNode.Value.ItemKey = key;
this.openNode.Value.ItemValue = value;
this.list.AddFirst (this.openNode);
this.lookup.Add (key, this.openNode);
if (this.list.Count > this.capacity) {
// last node is to be removed and saved for the next addition to the cache
this.openNode = this.list.Last;
// remove from list & dictionary
this.list.RemoveLast();
this.lookup.Remove(this.openNode.Value.ItemKey);
ClearValue (this.openNode.Value.ItemValue);
} else {
// still filling the cache, create a new open node for the next time
this.openNode = new LinkedListNode<ListValueEntry<TKey, TValue>>(new ListValueEntry<TKey, TValue>(default(TKey), default(TValue)));
}
}
}
void ClearValue (TValue value)
{
var bmp = value as Android.Graphics.Bitmap;
if (bmp != null) {
bmp.Recycle ();
return;
}
var disposable = this.openNode.Value.ItemValue as IDisposable;
if (disposable != null)
disposable.Dispose ();
}
public TValue Get (TKey key)
{
LinkedListNode<ListValueEntry<TKey, TValue>> node = null;
if (!this.lookup.TryGetValue (key, out node))
return default (TValue);
this.list.Remove (node);
this.list.AddFirst (node);
return node.Value.ItemValue;
}
class ListValueEntry<K, V> where K : TKey
where V : TValue
{
internal V ItemValue;
internal K ItemKey;
internal ListValueEntry(K key, V value)
{
this.ItemKey = key;
this.ItemValue = value;
}
}
}
}
@dehghani-mehdi
Copy link

Adding usage example will be great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment