Let's say you've got an application which needs to display a bunch of pictures. They don't all have to be displayed at the same time, but there's a lot of them to deal with. How do you keep the memory requirements for your application down but don't skimp on functionality and have easy to maintain code? And, as always, one of the objectives is to make sure there's not a huge performance hit either.
Here's where the Proxy pattern comes in. It's a very simple pattern, and you've probably used it without knowing. The basic thought is that instead of loading up a picture object, you load up a picture proxy object. This proxy object holds a reference to Picture, as well as a FolderItem of the picture on disk. So now you don't need to keep the picture in memory until you absolutely have to -- and when that time comes, you can use the FolderItem to load the picture. Since you have a lot of pictures to load, and pictures in memory are much larger than FolderItems in memory, this pattern will help your memory footprint. If you don't believe me, check out the empirical stats at the end of the post.
Let's take a look at how a proxy object might work. We'll continue with the example of a picture proxy object, but keep in mind that a proxy can be any sort of object.
[rbcode]
class PictureProxy
Private Dim mPicture as Picture
Private Dim mFile as FolderItem
Sub Constructor( file as FolderItem )
mFile = file
mPicture = nil
End Sub
Function Picture() as Picture
if mPicture = nil then
// We need to load the picture
mPicture = mFile.OpenAsPicture
end if
return mPicture
End Function
End Class
[/rbcode]
That's it -- that's the entire proxy pattern. It's entire job is to provide a way to delay loading of a resource or time intensive operation until you absolutely have to. So you can use this sort of pattern to defer loading pictures or sounds, or you can use it to defer loading huge databases until the user needs them, etc. So using the proxy object is simple.
Let's say you have a function on App.Open that loads up a bunch of pictures that are used thru the entire app at different points in time. Instead of storing a Picture object for each of them and using OpenAsPicture right off the bat, you'd use a PictureProxy object instead, like this:
[rbcode]
Sub App.Open()
mWallpaper = new PictureProxy( GetTheWallpaperFile() )
mButtonImages.Append( new PictureProxy( GetButtonImage( 0 ) ) )
mButtonImages.Append( new PictureProxy( GetButtonImage( 1 ) ) )
mButtonImages.Append( new PictureProxy( GetButtonImage( 2 ) ) )
// Etc.
End Sub
[/rbcode]
Now you can use all of these pictures by simply calling the Picture method on a PictureProxy object.
[rbcode]
someGraphicsObject.DrawPicture( mProxy.Picture, 0, 0 )
[/rbcode]
If it's the first time the Picture method is called, then the proxy loads the picture up and returns it. Otherwise it returns the cached copy of the image.
So let's take a peek at what sort of performance gains there are when using proxies properly. I made a small application that loads all the picture files (with a .bmp or .jpg extension) into a listbox (using proxy objects) and then displays them in a canvas when the listbox's index change. First, I tested code that would load all the images up as Pictures and stores them in the CellTag for each row. Then I tested using proxies in the same fashion.
The test data was a set of 467 pictures that take up 100 MB of space on the disk. They're all JPG files and the average size is about 3MB of uncompressed bitmap data (1024x768 images).
The non-proxy implementation never finished loading -- I had to force quit it. It had run for two minutes, was taking up 30 MB of actual memory (peaked at 230 MB of actual memory) and had over 1 GB tied up in virtual memory. I should note that after a certain point in time, the OS drops the actual memory back down to something reasonable and just works solely in VM, which is why the actual looks so small.
So then I tried the proxy implementation -- it took up 9 MB of actual RAM, it peaked at 9 MB of RAM and was using 4 MB of virtual memory. Quite the savings! But keep in mind, if you were to try to load every single proxy picture (by arrowing thru the listbox, for example), then eventually it would have a memory footprint just like the non-proxy version. So while a proxy may seem like an amazing gain (and it is), it won't help you save space if every proxied object gets loaded. Just something to keep in mind.
You may be wondering why I made the PictureProxy class the way I did instead of using operator overloading. It's certainly possible to implement the Operator_Convert method to convert a PictureProxy into a Picture automatically and take the place of the Picture method. Then you could use it in place of a Picture and not have to bother with a method call.
I decided not to do that because it would be very easy to forget that you're working with a proxy and using it as a Picture causes a load to happen. If you make it easy to forget that there are side effects, then it's very easy to misuse a proxy object. However, in this case, there are very few things you can do with a Picture that don't require the picture to be loaded -- so it would be perfectly safe to do the operator overloading. I did it this way mostly for example purposes, you're welcome to do it either way (of course) -- it's just something I wanted you to be aware of.
As you can see, using proxies is a very easy thing to do, and it can provide a very elegant solution to your memory requirements in some circumstances. It's also useful for delaying lengthy operations until they're absolutely needed. So go forth and proxy!
A few months ago, I was trying to come up with something similar which would also automatically unload the cached data after a certain point. But the approach I was taking was an entirely different class and embedded file structure (kinda like a ZIP archive).
Your solution is much more elegant. =)
Based on your article, I modified the PictureProxy to include the cache with a default of 16 items before the older accessed items begin to be freed. Here is my new code.
Class PictureProxy
Private Dim mPicture As Picture
Private Dim mFile As FolderItem
Private Dim mCache As Dictionary
Private Dim mLastAccess As Double
Sub Constructor(file as FolderItem)
mFile = file
mPicture = Nil
mCache = cacheReference
End Sub
Private Function CacheReference() As Dictionary
Static pCache As Dictionary
If pCache Is Nil Then pCache = New Dictionary
Return pCache
End Function
Function CacheSize() As Integer
Return SizeOfCache
End Function
Sub CacheSize(size As Integer)
Dim x As Integer
x = SizeOfCache
Call SizeOfCache( size )
If x > size Then
FlushExpired
End If
End Sub
Sub ClearCache()
mCache.Clear
End Sub
Private Sub FlushExpired()
Dim oldest, current As PictureProxy
Dim k, kMax As Integer
Do Until (mCache.Count current.mLastAccess Then oldest = current
Next
oldest.mPicture = Nil
mCache.Remove( oldest )
Loop
End Sub
Function Picture() As Picture
Dim now As New Date
If mPicture = Nil Then
// We need to load the picture
mPicture = mFile.OpenAsPicture
mCache.Value( Me ) = now
FlushExpired
End If
mLastAccess = now.TotalSeconds
Return mPicture
End Function
Private Function SizeOfCache(newSize As Integer = 0) As Integer
Static pCacheSize As Integer = 16
If newSize > 0 Then pCacheSize = newSize
Return pCacheSize
End Function
End Class
Nice! Good use of statics too. :-)
hi
thanks
i m intersted of this subject but i need your help
i tried to download the small application above but i cant execute it or open it becase windows doesnt recognize the file type
please could u send me the source and which language to execute
also i m grateful if u can help me in the folowing problem
i want to open a microsoft winword document contains an image and if i click on the image the image disapheres if i click again the image appears
thanks alot
abdallah from jordan
The source code requires you to have a copy of REALbasic. It's a binary file format, so it won't do you much good unless you download a demo of RB 5.5 from the website.
Unfortunately, I don't have any experience in mucking with Word documents. I know that RB can control Word itself (and thru that, its documents), but as for reading in .doc files into data structures that are useful, I've never tried it (though I am sure the format is at least partially documented via OpenOffice).
Sorry!