Macro "Application Level" Memory Management Figure 8.1 shows schematically how an application's performance will degrade with increasing use of memory. As you can see, a desktop application has a much wider range of memory usage in which its performance behavior remains fairly good. Past a memory usage threshold performance starts to degrade markedly as memory starts getting swapped in and out of a page file and the garbage collector makes more frequent efforts to free up and compact memory; still the performance degrades much less rapidly than on a mobile device. In contrast, mobile devices have a smaller range of memory usage in which good performance is maintained. Past a critical threshold a mobile application's performance will degrade drastically as the garbage collector runs almost continually to try to recover ever-decreasing amounts of free memory. The diagram conveys that there is a comfortable range of memory utilization where application will perform well. As long as memory consumption stays below this critical threshold, all is generally well. However, if the critical memory usage threshold is crossed, the application's performance will deteriorate rapidly as the garbage collector is forced to run ever more frequently and invasively try to recover memory for your application's usage.Figure 8.1. Application performance trends with increased memory consumption. [View full size image]
I'm Writing Native Code. Is This Still an Issue for Me? Unlike managed-code runtimes (.NET Compact Framework, Java, and so on), native code development does not use garbage collection. However, this does not mean that you are off the hook if you are writing native code; on the contrary, the design burden is often greater! On desktop computers, native code applications benefit implicitly from the operating system's ability to page memory in and out of RAM and onto a disk-based page file. This results in sluggish but still functioning applications. If your mobile device application memory usage exceeds available memory capacity, your application will just run out of memory and fail. This means that for native code development for mobile devices the onus is on the developer to preemptively avoid these low-memory situations. Additionally, native code algorithms that needlessly allocate and free memory because of inefficient design will have to contend with memory-fragmentation issues that also decrease application performance. Whereas managed-code run-times can compact memory during garbage collection to decrease memory fragmentation, native code has no such built-in facility. The bottom line is that you still need to design carefully and do your own memory management. | As a mobile application designer, your job is not to find a way to utilize all the free memory and push the application right up to but not beyond the threshold where performance will deteriorate rapidly. Instead, you should aim to maintain a safe margin of free memory so that your application can continue to run efficiently over a wide rage of situations. The reason for this is that your application is typically not in full control of all the memory on the device; memory is shared by the operating system and different applications the user may be running. If any application allocates and holds unnecessary memory, all of the applications' performances will suffer. Take no more than you need, and give back what you do not need close at hand.As with other aspects of mobile device design, coming up with the right memory model to meet your needs is a matter of experimentation and experience. You will have to experiment with different approaches and tune your model based on the resources available on your mobile device and the requirements of your application. Learning to engineer in the kind of flexibility that gives you the capacity for experimentation is important to successful mobile application development.
Won't This Problem Just Go Away with the "Next Generation" of Mobile Devices? Extra memory costs money, generates heat, consumes power, and takes up additional space. All of these factors will improve over time, but they will not vanish. Even with the predictions of Moore's law continuing to provide us with successive generations of more powerful computing at lower costs there will be a strong incentive to keep mobile devices cheap, small, and running a long time on a single battery charge.Most mobile devices today already have an ample amount of memory to perform a wide variety of tasks, but this memory needs to be used with prudence. All too often, poor desktop design habits are brought to devices. This results in undisciplined memory allocation in algorithms and poor memory management that keeps resources in memory when they are no longer useful.Earlier in this book, an analogy was made comparing today's desktop computers to large houses in the country and mobile devices to smart-looking but small apartments in the city. This mental image of "finite size" is an important one to keep in mind. You can accomplish almost everything you want on a mobile device, but you cannot do it all at once or keep everything in memory at the same time. You need a well-classified set of rules about how to bring things into your apartment and when to throw things out.Future mobile devices will be more powerful and have more memory, but more will also be asked of them. Good memory management strategies will still be essential for building great applications for these devices. | It is wasteful to keep things you do not need in memory because these things take up room that could better be used by the active parts of your application. On the other hand, throwing everything out the moment you do not need it immediately is equally foolish.A metaphor: If you live in a small apartment and find you need a stapler to staple your papers together, you go to the store and buy a stapler. This takes time and resourcesnot a lot, but enough. If you throw the stapler out as soon as you do not immediately need it, you have slightly more space in your apartment (after you've taken out the garbage containing the discarded stapler) but no stapler. The next time you need to staple things together, you need to take the time and expense of going back to the store and buying another stapler. To do this would be foolish; if there is a reasonable chance you will need a stapler in the upcoming year or two, you keep it around. However, if you can guarantee that you will never need a stapler again (perhaps you have decided to use paper clips from now on), you should throw it out no matter how little room it takes up. Who needs the clutter of useless objects?There is a reasonable balance that needs to be found. Because a stapler is a pretty useful thing, not too big, and used fairly often, you keep it around. What about a large steam-cleaning machine for carpets? You could go out and buy one, spend a lot of resources to get it, and then keep it around; if you live in small place and do not steam clean your carpets too often, however, this would not be the wisest solution. Instead you go and rent one, do your work, and then get rid of it. Some things you plan to keep and some things you get rid of when you are done with them.The tricky part comes when objects fall somewhere in between. These objects are not individually too big or expensive, each takes up only a moderate amount of space, and they are potentially useful at some point in the future. These are like clothes you almost never wear. These are items that you should throw out but keep around just in case you change your mind. Keeping a few of them around is no problem, but so many things fall into this category that you can easily fill your whole apartment with moderately useful objects and leave no room for other things you use more often. You can end up with an apartment full of clothes you rarely wear and boxes of stuff you almost never use. This is okay if you have a giant house with lots of storage, but an absolute mess when you are living in an apartment. Desktop = big house. Mobile device = apartment.Clearly there are rules about what should be kept around and what should be thrown out. Thankfully these rules are generally based on common sense and just need to be applied systematically. That is what having an explicit application memory model is about: applying common sense in a disciplined way to keep your mobile application running efficiently.As with any kind of engineering problem, a fundamental tension needs to be solved by a balanced solution. The reluctance to dispose of an object you may use again at some time in the future must be balanced with the benefits of having working room for your application to perform well in.What kind of information belongs in your macro memory model? It is helpful to sort the application data and resources you work with into two top-level buckets: (1) the objects and resources that your application needs to efficiently run, and (2) the actual user information that your application is working with.Necessary application overhead This includes the resources your application needs to keep the user interface and other aspects of your application going. Examples are open connections to databases, open files, threads, graphics, and user interface objects such as brushes, pens, controls on forms, and the forms themselves. These are all incidental to the data that the user is actually working with but are vital to the application actually interacting with the user or with external sources of information.User data This is actual data the user is interested in working with. It is the portion of that data that is being held in memory as opposed to being stored in some database or file on the device or externally. For example, if a mobile application is intended to let the user navigate the streets of London, the user data is the information about the streets of the city that is presently loaded into memory. If the application tracks inventory, the user data is the loaded inventory data. If the application is a chess game, the user data is the in-memory representation of the state of the chessboard.Here again the concept of state machines will be handy. Consider having two top-level state machines for your application, one to manage each kind of data listed above. It is useful to separate of your application's overhead data from the user's data and to have a different machine that manages each.
Is a Bitmap "User" Data or "Application Overhead" Data? Because bitmap data represents images loaded into memory and ready for use in an application's graphics, it can take up a significant amount of space. Bitmap data for large images can be costly to keep in memory so managing when it comes into memory and when it is discarded is worth giving some thought to.Some kinds of bitmaps are user data. For example, if your application is a real estate application that loads photos of houses into memory, each house photo is unique and corresponds to the user data associated with a given property. Just like the street location of a house or the price of a house, it is data that is loaded per data item. The same would go for medical images that belong to patient records; each image is associated with user data.Some kinds of bitmaps are "application overhead." Bitmaps used to make fancy-looking buttons, cached background images, and the predrawn pieces of a graphical chart are all application overhead because they are not specifically associated with the data the user is working with. A generic bitmap of a human body that can be used by a medical application to indicate locations of injuries would fall into the same camp. These are common resources not specific to any user data. They are generic resources that depend on the mode the application is in. Some forms may use these bitmaps and others may not; the image data can be loaded or discarded based on the mode of the application and is not dependent on the set of data the user is working with.Some kinds of bitmaps could fall into either camp. If you have a real estate application that can show three kinds of floor-plan bitmaps based on the user's loaded data or if your medial application has six different body-type images corresponding to different patients' sex, size, and weight, these could be considered either application overhead or user data. Similarly, if your game has bitmaps for all the characters on the screen but these bitmaps are changed to look different as the user advances to different levels, the bitmaps represent cases where a credible argument can be made for them either being user data or application overhead. In these cases, you will have to choose to place the resources in the memory model that makes the most sense for your application. Put these kinds of images into whichever memory management bucket best meets your needs. |
Managing an Application's "Overhead" Data As noted previously, "application overhead" is data that the user does not directly interact with and so it is not "user data." It represents the resources needed for the application to function effectively so users can see and manipulate their data. Managing the objects and resources that your application needs to run effectively can usually be done by means of a simple state machine. As your application goes through different states, different resources are useful to have around. When entering a new state, needed resources can be created and stored in application memory. Similarly, when exiting an application state, resources not immediately needed in the next state can be discarded and the space they use can be returned to the free memory pool. Often these states correspond to user forms that are currently displayed or tabs on a form that the user flips through and brings to the foreground. When designing your application's state model, it is often useful to identify discrete user interface modes and use them as the basis for building your state model.Let's take a look at a simple database-oriented application. This application stores and works with medical data for patients. The data is stored in a database and is loaded on a per-patient basis with password security required to load or save a patient's data. This kind of an application could be broken up into five different discrete states where the user is presented with different user interfaces for accomplishing tasks. These states are as follows:Loading data from database This screen enables the device's user to authenticate access to the database and load a specific patient's data. Application overhead required includes the following:Database connectionsA form that shows the database logon user interfaceSaving data to database This screen enables the user to authenticate access to the database and save a specific patient's data. Application overhead required includes the following:Database connectionsA form that shows the database logon user interface to the userMain screen of application This screen displays a patient's case history that has been loaded from the database and enables the device's user to view and navigate this data. Application overhead required includes the following:The form for the main screenCommon bitmaps that are being used in the user interfaceDetails screen for working with and editing specific data This screen is displayed when the user needs to edit the details of some of the patient's data or enter new data. Application overhead required includes the following:The form for editing a loaded recordCustom controls on the form that allow for specific data-entry needs (for example, a custom control with validating logic for entering blood pressure data or a custom control for editing medication dosage information)Charting screen for displaying sets of data points This screen is for displaying graphical information relating to some aspect of the patient's medical history. For example, charts could be drawn that show blood pressure over time or blood white cell counts to monitor infections. Application overhead required includes the following:Graphics pens and brushes used for drawing chart dataFont objects used for drawing labels on a chartCached background bitmapsAn offscreen bitmap for drawing the graph image onto before copying it onto the screen Some of these states share common resources. For example, a black graphics pen resource for drawing lines or an appropriately sized font or bitmap image may be useful in several of the states above. The need for a database connection is shared by two of the states listed above. Also some objects may not be required by each of the states but may be very time-consuming to create; you may want to test the performance behavior of caching these kinds of objects.You can easily create a state machine to manage these kinds of resources. All that needs to be done when entering a new state is to determine which objects it is necessary to create if they are not already in existence and which objects should be released if they have been allocated in a previous state. Having all of this logic managed by a single state machine rather than distributed throughout your application makes this information very easy to maintain. A state machine also makes it easy to experiment with different optimizations and tune your application's performance.
Using IDisposable and Freeing Expensive Nonmanaged Resources in the .NET Framework When an object is no longer needed, your application code should discard it by removing any variable references that point to the object. When the garbage collector is next run (usually based on the need to allocate memory for the application), the memory is reclaimed when the runtime discovers that no live references exist to the object. This generally works well but means that the cleanup of objects can be deferred indefinitely if no pressing need causes a garbage collection to occur. Because we do not know when a given object is going to be garbage collected, the memory lifetime of the object is said to be "nondeterministic." Nondeterministic cleanup is a common challenge for garbage-collected memory management systems. It is generally not a good idea to write application logic that runs when the object's finalizer (destructor) is run because your application does not have explicit control on when the code will be run.Problems can arise when the discarded managed object also represents an "expensive unmanaged resource," such as a database connection, a file handle, or a graphics resource of some sort. Until the object gets cleaned up by the garbage collection, the unmanaged resource it represents is still held. This can be costly to system performance both for the client device as well as for any server resources the client may be holding.To address this problem, the .NET Framework has a design pattern that allows code to explicitly release the underlying resource held by an object. Objects that represent expensive resources all have a Dispose() method. This method is defined in the IDisposable interface; classes that support deterministic reclamation of their resources implement this interface and thus have the Dispose() method as part of their class.If an object has a Dispose() method, you should always call it when your application has finished its use of the object. Dispose() should be called when you are about to remove any variable references to it and leave it for the garbage collector to clean up. This will guarantee that the expensive resource that the object represents is immediately released. As with other aspects of memory management, it is a good idea to do this in desktop applications but vital that it be done in mobile device applications because there are fewer resources (such as operating system handles) to go around!Similarly, if you are designing a class that represents an expensive resource, you should implement the IDisposable interface to allow code that uses the class to deterministically release the resources the class holds. Review the .NET Framework documentation on IDisposable.Dispose() for details on how to implement this properly.Note: C# has added a special keyword to its language that can aid in calling .Dispose() when the scope of a resource's usage is bounded within a block of code in a function. Instead of needing to explicitly call .Dispose(), the variable can be declared with the using keyword. For example:[View full width]using(System.Drawing.Graphics myGfx = System.Drawing.Graphics
.FromImage(myImage)) { //Do work with myGfx... } //myGfx.Dispose() is automatically called here... When declared this way, Dispose() is automatically called when the variable goes out of scope. The using keyword can be a nice convenience when the lifetime of a resource is well defined within a block of code. |
Managing the Amount of User Data in Memory User data represents the actual data the application's user is viewing or manipulating. Managing the amount and lifetime of user data kept in memory can be more complex than managing application overhead data because the kinds of data that can be held in memory can vary based on the structure and purpose of your application. The user data held by a chess game is different in structure from user data that holds the medical history of a patient. A chess board's state can be held in a predetermined size array of integer values. A patient's medical history may have no defined size boundary; it may consist of sets of measurements, text notes, images, links to additional data, and almost a limitless amount of related information.Managing the state of a chessboard is usually a matter of deciding when to load the array of data into memory, when to save it, and when to discard the data. This can be handled by a fairly simple state machine designed for the task.Managing the memory used to hold the complex medical records of patients can be considerably more complex. These records can be of arbitrary size, and this probably means you will need to come up with a windowing model for this data where the device's user is given a partial view onto the full set of data but not all of the data needs to be kept in memory at one time. The mobile device's user is given the illusion that all the data is kept in memory when in actuality data is swapped in and out as needed. This can be a simple machine, but it can also be a very sophisticated machine depending on the kinds of data managed and the resources available to manage that data.For management of potentially complex user data, a good approach is to build a well-encapsulated class that manages the state that is kept in memory at any given time. This class is in charge of loading new data and discarding old data when it is no longer needed. It manages the outside world's access to the user data and creates the illusion of infinite memory capacity. Other application code accessing the data from outside the encapsulation class should not need to have any knowledge of the internal state of this user data management class. Having a specific class tasked managing all of the user's data allows you a great deal of design flexibility. Some benefits of this approach are as follows:The ability to automatically manage the amount of loaded data If you are finding that your application is thrashing under memory pressure, you can narrow the size of the window of data that is kept in memory at any time without needing to redesign your whole application. Because nothing outside of the class is aware of what is being cached in memory and what needs to be reloaded, you have more design flexibility to tune this algorithm.The ability to have different implementations for different device classes If you are targeting multiple classes of devices, you can tune for each device's memory and storage limitations. A mobile phone and a PDA may have different memory characteristics or different abilities to load data on demand. These device differences may require you to approach data caching differently. Having this logic in one well-defined place enables you to do this much more easily.
Using a Load-on-Demand Model There are two basic strategies for allocating objects:When entering a new application state, create all the objects that may be needed by that state. This has the virtue of simplicity. When your application enters a state, simply call a function that ensures all of the needed objects are available and in a ready-to-use state. If you are sure that the all objects you create will be needed by the application in the immediate future, this is a fine strategy. The problem with this strategy is that as your application evolves and your design changes it is easy to end up with a lot of excess baggage. Old objects that are not actually needed but are created and loaded anyway are a waste of valuable resources. Be careful when you decide to batch-create a set of objects because your application may evolve to the point where you are creating unused overhead and you will pay a performance tax for this.Defer the creation of any object until a proven need for it arises. This model is a slightly more complex to design but ends up being more efficient in many cases because you only create objects when they are needed. This model is often deferred to as a using a "class factory," "resource dispenser," or "lazy loading."The code sample in Listing 8.1 shows two ways of doing deferred creation and caching of globally used graphical resources. There are two modes of object creation:Batch creation of grouped resources The following code creates an array list that holds four bitmaps. These bitmaps represent frames of an animated image; as such they are all loaded together and placed into an indexed array that can be easily accessed. Program code needing access to this collection of images would call GraphicsGlobals.PlayerBitmapsCollection();. If the array of bitmaps is already loaded into memory, the function returns the cached object without delay. Otherwise, the individual bitmap resources are loaded into the array list and returned. When the application enters a state where these in-memory bitmaps are not needed, the application's code can call GraphicsGlobals.g_PlayerBitmapsCollection_CleanUp(); to release the bitmap resources and the array list. The system resources for the bitmaps will be freed immediately and the managed memory for these objects will be reclaimed as necessary during garbage collection.Individual creation of drawing resources For resources whose usages are not lumped together like the bitmap images above, it is often useful to create a caching access function to control access to the resource. The first time the function to request a resource is called (for example, GraphicsGlobals.g_GetBlackPen();), the function creates the instance of the resource. Subsequent calls return the cached instance. For commonly used resources, this is often much more efficient than continually creating and destroying instances of the resource each time it is needed by some separate piece of code. In the following code, I have made the assumption that all of these resources should be released at the same time and written a function (GraphicsGlobals.g_CleanUpDrawingResources();) that frees any of the cached resources that are created. When the application enters a state where these resources are not needed, this function should be called.
Some Common Sense About Caching If you are going to be using the same resource repeatedly inside a function, it is not efficient to call the caching access function (or property) each time you need the resource. In these cases, it is better to call the function once and store it in a local variable for the duration of its required use. You get the benefit of using a globally cached resource rather than needing to create and destroy the resource in your function as well the efficiency of not needing to make unnecessary function calls inside your algorithm. Common sense should dictate when this is a good approach. |
Listing 8.1. Ways of Deferred Loading, Caching, and Releasing Graphics Resources
using system; public class GraphicsGlobals { private static System.Drawing.Bitmap s_Player_Bitmap1; private static System.Drawing.Bitmap s_Player_Bitmap2; private static System.Drawing.Bitmap s_Player_Bitmap3; private static System.Drawing.Bitmap s_Player_Bitmap4; private static System.Collections.ArrayList s_colPlayerBitmaps; //--------------------------------------------------- //Releases all the resources //--------------------------------------------------- public static void g_PlayerBitmapsCollection_CleanUp() { //If we don't have any bitmaps loaded, there is nothing to clean up if(s_colPlayerBitmaps == null) {return;} //Tell each of these objects to free up //whatever nonmanaged resources they are //holding. s_Player_Bitmap1.Dispose(); s_Player_Bitmap2.Dispose(); s_Player_Bitmap3.Dispose(); s_Player_Bitmap4.Dispose(); //Clear each of these variables, so they do not keep //the objects in memory s_Player_Bitmap1 = null; s_Player_Bitmap2 = null; s_Player_Bitmap3 = null; s_Player_Bitmap4 = null; //Get rid of the array list s_colPlayerBitmaps = null; } //Function: Returns Collection of Bitmaps public static System.Collections.ArrayList g_PlayerBitmapsCollection() { //--------------------------------------------------- //If we have already loaded these, just return them //--------------------------------------------------- if(s_colPlayerBitmaps != null) {return s_colPlayerBitmaps;} //Load the bitmaps as resources from our executable binary System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); System.Reflection.AssemblyName thisAssemblyName = thisAssembly.GetName(); string assemblyName = thisAssemblyName.Name; //Load the bitmaps s_Player_Bitmap1 = new System.Drawing.Bitmap( thisAssembly.GetManifestResourceStream(assemblyName + ".Hank_RightRun1.bmp")); s_Player_Bitmap2 = new System.Drawing.Bitmap( thisAssembly.GetManifestResourceStream(assemblyName + ".Hank_RightRun2.bmp")); s_Player_Bitmap3 = new System.Drawing.Bitmap( thisAssembly.GetManifestResourceStream(assemblyName + ".Hank_LeftRun1.bmp")); s_Player_Bitmap4 = new System.Drawing.Bitmap( thisAssembly.GetManifestResourceStream(assemblyName + ".Hank_LeftRun2.bmp")); //Add them to the collection s_colPlayerBitmaps = new System.Collections.ArrayList(); s_colPlayerBitmaps.Add(s_Player_Bitmap1); s_colPlayerBitmaps.Add(s_Player_Bitmap2); s_colPlayerBitmaps.Add(s_Player_Bitmap3); s_colPlayerBitmaps.Add(s_Player_Bitmap4); //Return the collection return s_colPlayerBitmaps; } private static System.Drawing.Pen s_blackPen; private static System.Drawing.Pen s_whitePen; private static System.Drawing.Imaging.ImageAttributes s_ImageAttribute; private static System.Drawing.Font s_boldFont; //--------------------------------------- //Called to release any drawing resources we //may have cached //--------------------------------------- private static void g_CleanUpDrawingResources() { //Clean up the black pen, if we've got one if(s_blackPen != null) {s_blackPen.Dispose(); s_blackPen = null;} //Clean up the white pen, if we've got one if(s_whitePen != null) {s_whitePen.Dispose(); s_whitePen = null;} //Clean up the ImageAttribute, if we've got one //Note: This type does not have a Dispose() method //because all of its data is managed-data if(s_ImageAttribute != null) {s_ImageAttribute = null;} //Clean up the bold font, if we've got one if(s_boldFont != null) {s_boldFont.Dispose(); s_boldFont = null;} } //------------------------------------- //This function allows us access to the //cached black pen //------------------------------------- private static System.Drawing.Pen g_GetBlackPen() { //If the pen does not exist yet, create it if(s_blackPen == null) { s_blackPen = new System.Drawing.Pen( System.Drawing.Color.Black); } //Return the black pen return s_blackPen; } //------------------------------------- //This function allows us access to the //cached white pen //------------------------------------- private static System.Drawing.Pen g_GetWhitePen() { //If the pen does not exist yet, create it if(s_whitePen == null) {s_whitePen = new System.Drawing.Pen( System.Drawing.Color.White);} //Return the white pen return s_whitePen; } //------------------------------------- //This function allows us access to the //cached bold font //------------------------------------- private static System.Drawing.Font g_GetBoldFont() { //If the pen does not exist yet, create it if(s_boldFont == null) { s_boldFont = new System.Drawing.Font( System.Drawing.FontFamily.GenericSerif, 10, System.Drawing.FontStyle.Bold); } //Return the bold font return s_boldFont; } //----------------------------------------------- //This function allows us access to the //cached imageAttributes we use for bitmaps //with transparency //----------------------------------------------- private static System.Drawing.Imaging.ImageAttributes g_GetTransparencyImageAttribute() { //If it does not exist, create it if(s_ImageAttribute == null) { //Create an image attribute s_ImageAttribute = new System.Drawing.Imaging.ImageAttributes(); s_ImageAttribute.SetColorKey(System.Drawing.Color.White, System.Drawing.Color.White); } //Return it return s_ImageAttribute; } } //End class
|