Created
December 17, 2012 22:38
-
-
Save garuma/4323075 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Adding usage example will be great.