Macro "Application Level" Memory ManagementFigure 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.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. 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. 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 connections A form that shows the database logon user interface Saving 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 connections A form that shows the database logon user interface to the user Main 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 screen Common bitmaps that are being used in the user interface Details 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 record Custom 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 data Font objects used for drawing labels on a chart Cached background bitmaps An 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.
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. Listing 8.1. Ways of Deferred Loading, Caching, and Releasing Graphics Resourcesusing 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 |