Chapter 8

Making Your Application Fast and Responsive

In This Chapter

  • Applying nonfunctional requirements to Android apps
  • Constructing your app to perform well
  • Making your application appear responsive

An app that is successful in gaining user acceptance must meet two kinds of requirements: functional and nonfunctional. Functional requirements describe what the app must do (the functions it must perform). A wayfinding app, for example, must allow the user to plot a route from one location to another. As another example, the Tic-Tac-Toe application must show you a board and enable you to place Xs and Os on it. These functional requirements describe what is needed to “build the right app.”

While most developers realize that their apps must implement these functional requirements, they don't focus enough effort on meeting nonfunctional requirements, which mandate how the app must do what it does. Nonfunctional requirements provide guidelines on how to “build the app right.”

In this chapter, we focus on nonfunctional requirements and identify three that apply especially to Android apps: performance, battery conservation, and responsiveness. We then show you ways in which to meet these nonfunctional requirements.

Becoming Familiar with Nonfunctional Requirements

It's a statement worth repeating: Nonfunctional requirements (NFR) — also known as quality requirements or design requirements — provide guidelines on how to build an app right. For example, you might consider it appropriate to require a wayfinding app to plot a route in fewer than 30 seconds, or for a computer playing a Tic-Tac-Toe application to mimic a human in order to retain the user's interest.

The list below outlines the important principles about NFR:

  • Nonfunctional requirements describe how to implement an app. For example, if security isn't a requirement, you can decide to store all app data in unencrypted files and on the SD card. If security is an issue, however, you might encrypt the files and store them in the app's private file area so that their content isn't easily accessible and they're deleted when the app is uninstalled.
  • NFR most commonly occur in the following categories: availability, performance, scalability, usability, security, modifiability, and maintainability. (Cost is also often an important nonfunctional requirement category.) When you build an app, you have to decide which nonfunctional requirements apply to your app. You have to also refine your NFR so that they're “testable.” For example, for a performance NFR, you should be able to measure the speed of your application at the places where the NFR applies (such as during the screen display).
  • Certain nonfunctional requirements are especially important for mobile devices. These requirements are performance, battery conservation, and responsiveness, all discussed in the following sections.
  • image Do not attempt to meet all possible nonfunctional requirements in an app. It's extra work to implement features in ways that users don't value. For this reason, you should also define (and, wherever applicable, quantify) to what extent you want to meet the NFR.
  • image NFR fulfillment always involves trade-offs. In other words, meeting a single nonfunctional requirement often negatively affects the meeting of another. For example, performance and security are often at cross-purposes. For example, you pay a performance overhead for the increased security of encryption and decryption of stored data. The trick is to find the right balance.

Designing Your App at Multiple Levels

When you design an app, you actually design it at multiple levels. At the highest level of design are “architectural” decisions (such as the use of the Android framework, or even the choice of Java as the development language). You have little choice in making certain architectural decisions, such as the use of the Android framework and Java to develop Android apps. Google provides the highest level of support for Java, and so developing apps in Java is significantly easier than using C or C++. Thus the architectural decision to use Java is a no-brainer. In other architectural decisions, however, you have choices, such as the type of encryption to use to secure your data wherever it is stored, retrieved, or transmitted. Architectural decisions are the most difficult to change after you've started building the software. Our translation is that you can't do much about architectural decisions after you make them; so try to make good decisions in the first place.

After the architecture is set (yes, like cement), the next level of design is in the partitioning of your application into classes, the allocation of methods to classes, and then, as a last step, the mapping of the design to the architecture of the first step. We cover this process in detail in Chapter 7. Needless to say, this design, while not quite as hard to change, is still difficult to modify after it has been decided upon.

The third level of design is at the choice of algorithms (say, for sorting) and data structures (using a hash table to find a value in a contact list, such as a phone number, corresponding to a key, say, the name of the contact). Abstracting the data structures and algorithms inside classes or methods so that they are compartmentalized really helps when you try to change out algorithms and data structures.

The final level of design lies in your choice of low-level coding practices. Code is relatively easy to change; all you need is a good editor, a few hours to hack your way through the existing code, and strong coffee to keep you awake throughout the process.

Optimizing Application Performance

In this section, we address app performance in Android, again using the sample Tic-Tac-Toe application.

Architectural choices with respect to performance include elements such as whether you're using SQLite or files (SQLite is slower for many things, however, it's faster if you're retrieving bits of already stored data), building screens using 2D graphics or widgets, or accessing the network every time your app needs data or storing certain data locally, for example. The following list describes the requirements-driven architectural decisions we made in the Tic-Tac-Toe application, and our reasons:

  • Programming language: We use Java because its built-in support and easy app development outweigh any performance gains we might make by writing native (C or C++) code, especially considering that our app has no out-of-the-ordinary need for high performance.
  • Data storage: We store user preferences using the built-in preference classes because of their programming convenience. We use SQLite to store login and password information because the number of times login information is retrieved (a task that SQLite excels in, like all relational databases) will most likely be much higher than the number of registered users added (a task that SQLite is slower in doing, like all relational databases).
  • Remote data transfer format: Though we show both an XML and a JSON example, the preferred format is likely to be JSON because of its lower overhead.
  • Graphics: We chose raw 2D graphics rather than widgets for the board for speed purposes. (Note that the Tic-Tac-Toe application has simple enough graphics that widgets would have worked as well, as the button-based implementation in Chapter 6 clearly illustrates.)

We also made a couple of deliberate decisions regarding data structures and algorithms in Tic-Tac-Toe. The primary data structure decision is to represent the grid as a two-dimensional array and encapsulate it inside a Grid class so that we can change this implementation if it turns out to be slow. (Note that the design process shown in Chapter 7 independently arrives at the conclusion that the grid should be its own class — this connection should serve as a validation for the design process.) The primary algorithm decision is to have the machine randomly pick empty squares to play. Note that this scheme is also abstracted (inside GameSession.androidTakes ATurn()), so if you want to change the way the machine plays, you have to modify only this lone method.

Your coding practices should improve application performance by doing less computation. You can do many things to bring about this result:

  • Save intermediate results in variables and then reuse them, especially in loops. This simple example from GameSession.androidTakesA Turn() demonstrates this technique:
    …
    activeGame.play(pickedX=picked.x(), pickedY=picked.y());
    gameView.placeSymbol(pickedX, pickedY);
    …
  • Avoid internal getters and setters. Access member variables directly within the class instead of using the getters and setters. This avoids the overhead of an additional method call. You can see this process in the GameGrid class, where the locations in the two-dimensional array member variable grid are accessed directly instead of using the accessor methods setValueAtLocation(…) and getValueAt Location(…), which is how the grid is accessed outside the class by client classes such as Board and Game.
  • Avoid creating unnecessary objects. Remember that Java Strings (though appearing to be elementary data types) are objects, so limit the number of strings you create as well. We illustrate this tactic in the Symbol class in Tic-Tac-Toe, where we used the Singleton pattern so that only one instance of an X, O, and Blank symbol is ever created, and we defined symbols as enumerated types rather than as strings. Related to this concept is the use of constants (variables declared using static final —see the next item for more on this).
  • Use static final for constants. If constants are simply declared as static, the definitions can be overridden and are therefore treated as variables. Thus, the Java virtual machine (JVM) has to perform extra work to figure out the correct reference and value of the constant. If the constant is declared using static final, the compiler knows to substitute the value of the constant where it sees uses of the constant in the code so that no runtime overhead is incurred. For an example, see the declarations shown in the following GameSession.java class:
    private static final int ANDROIDTIMEOUTBASE=500;
    private static final int ANDROIDTIMEOUTSEED=2000;
  • Know the framework libraries well, and use them wherever possible rather than write your own code. Because the library-implemented code is usually optimized (say, using assembler code), it's more efficient than equivalent code written by you, even after the compiler has tried to optimize it. The link http://developer.android.com/guide/practices/design/performance.html on the Android website gives the examples String.indexOf(…), System.arraycopy(…), and related methods that can be as much as ten times faster than handwritten code optimized by the compiler.

An excellent set of old-but-gold techniques around low-level coding practices for efficiency is Jon Bentley's rules for writing efficient programs. We've been able to find summaries of these techniques at various places on the web by simply entering the keywords Jon Bentley writing efficient programs in a search engine. For an example, see www.crowl.org/lawrence/programming/Bentley82.html.

Using the Profiler for Code Optimization

You can expend a lot of time optimizing code, only to see no real impact on the performance of your program. To make your optimization efforts pay off, develop the habit of profiling, instrumenting your app's code in order to understand in which methods your program is spending most of its execution time. The Eclipse IDE with the Android SDK installed provides a useful way to do it, as shown below:

  1. Open Eclipse and make the Devices view visible by choosing WindowimageShow ViewimageOther and then selecting Devices inside the Android collection.
  2. Start the Tic-Tac-Toe app on either a device or an emulator. The Devices view starts to show activity, as shown in Figure 8-1.

    Figure 8-1: Devices view in Eclipse shows a running program.

    images

  3. Log in to Tic-Tac-Toe and navigate to the Options screen, as shown in Figure 8-2.

    Figure 8-2: Starting a new game in Tic Tac-Toe.

    images

  4. Return to Devices view, select the line com.wiley.fordummies.androidsdk.tictactoe, and begin profiling by clicking the icon to the left of the Stop sign. (Or, hover the mouse over the icons until you see the Start Method Profiling tooltip.)
  5. Play four or five games, stretching out every game as far as possible, and then stop the profiling by clicking the same icon as in Step 4. This time, however, notice that it has turned black.

The profiler window opens (see Figure 8-3).

Figure 8-3: The Traceview profiler in Eclipse.

images

Now things get interesting. The bottom pane of the profiler window (refer to Figure 8-3) shows a list of methods, ordered by their contribution to overall execution time. As you review this list of methods, you start to come across methods from Tic-Tac-Toe. You will see that Board.onDraw(…) and Board.getBitMapForSymbol(…) are contributing large chunks of time. Drill into these methods by clicking the arrow to the left, as shown in Figure 8-4, and notice that Board.getBitMapForSymbol(…) is the true culprit.

…
for(int i = 0; i < GameGrid.SIZE; i++){
    for(int j = 0; j < GameGrid.SIZE; j++){
        Bitmap symSelected = getBitmapForSymbol(grid.getValueAtLocation(i, j));
        offsetX = (int)(((width - symSelected.getWidth())/2) + (i * width));
        offsetY = (int)(((height - symSelected.getHeight())/2) + (j * height));
        canvas.drawBitmap(symSelected, offsetX, offsetY, ditherPaint);
     }
}
…

Figure 8-4: Drilling down in the profiler. Look first at Board.onDraw (…). You can see that getBitMapForSymbol (…) is being called in a loop from Board.onDraw (…).

images

This example explains why onDraw(…) is spending a lot of time in getBitMapForSymbol(…). An examination of the latter function shows that it's calling getResources() and BitmapFactory.decodeResources(…) for every square on the board, even though the values returned by these methods are invariant (the same) for every call (and for every instance of the game):

public Bitmap getBitmapForSymbol(Symbol aSymbol){
    Resources res = getResources();
    Bitmap symX = BitmapFactory.decodeResource(res, R.drawable.x);
    Bitmap symO = BitmapFactory.decodeResource(res, R.drawable.o);
    Bitmap symBlank = BitmapFactory.decodeResource(res, R.drawable.blank);

    Bitmap symSelected = symBlank;
    if (aSymbol == Symbol.SymbolXCreate())
        symSelected = symX;
    else if (aSymbol == Symbol.SymbolOCreate())
        symSelected = symO;
    return symSelected;
}

To quickly perform some optimization, you declare a set of private static variables:

static Bitmap symX=null, symO=null, symBlank=null;
static boolean bitMapsInitialized=false;

You also modify the code of getBitMapForSymbol(…) so that it gets the bitmaps only once per application run:

public Bitmap getBitmapForSymbol(Symbol aSymbol){
    if (!bitMapsInitialized){
        Resources res = getResources();
        symX = BitmapFactory.decodeResource(res, R.drawable.x);
        symO = BitmapFactory.decodeResource(res, R.drawable.o);
        symBlank = BitmapFactory.decodeResource(res, R.drawable.blank);
        bitMapsInitialized=true;
     }
     Bitmap symSelected = symBlank;
     if (aSymbol == Symbol.SymbolXCreate())
         symSelected = symX;
     else if (aSymbol == Symbol.SymbolOCreate())
         symSelected = symO;
     return symSelected;
}

Voilà! When you profile again, you see that Board.onDraw(…) and getBitmapForSymbol(…) are no longer the top contributors to app execution time. Now you can move on to the new methods on the list that contribute to execution time.

image Though this section explains how to use the profiling tools to judiciously “micro-optimize” application performance, note that your app will run on multiple hardware platforms, with different versions of the Dalvik virtual machine running on different processors at different speeds, with different computational add-ons (such as a video co-processor) that affect the speed of your app on each system. If performance on a specific platform is especially important to you, profile and optimize for that platform.

Maximizing Battery Life

If you own a smartphone or tablet device, you know that battery power is the scarcest resource. The elements that use the largest amount of battery power are the processor, the radio, and the display, so conserving battery power boils down to minimizing computation, minimizing network activity, and not forcing the display to run continuously or at full brightness. Minimizing computation essentially means following the techniques described in the previous section on improving performance, so in this section, we focus on reducing network activity.

On a smartphone, two major applications use the network:

  • Data services: For example, file downloads, web browsing, and video and audio streaming
  • Location services: For example, GPS and 3G and 4G networks

We address both types in the following sections.

Minimizing data services

With respect to data services, the first thing to keep in mind is that trying to make a connection and send or receive data when the connection is poor or non-existent wastes a considerable amount of power. Therefore, you can simply test the network connection in your code before trying to send or receive data. The method hasNetworkConnection() in the Help class in Tic-Tac-Toe shows how to test the various networks available on the device:

private boolean hasNetworkConnection(){
    ConnectivityManager connectivityManager =
        (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo =
        connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
    boolean isConnected = true;
    boolean isWifiAvailable = networkInfo.isAvailable();
    boolean isWifiConnected = networkInfo.isConnected();
    networkInfo =
        connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
    boolean isMobileAvailable = networkInfo.isAvailable();
    boolean isMobileConnnected = networkInfo.isConnected();
    isConnected = (isMobileAvailable&&isMobileConnnected)||
                  (isWifiAvailable&&isWifiConnected);
    return(isConnected);
}

Reducing network use during data transfers is the next step. You can't do much directly about network use because the need for the data transfers is determined by the user. (In other words, users who want to watch YouTube will simply use YouTube.)

However, sometimes you have a choice in how you access that data. For example, several service providers (including Google) provide their data services in XML as well as JSON format. If you have this choice, pick the more compact representation — in this case, JSON — because less data must be transferred in order to receive the same information. In the Tic-Tac-Toe file GeoLocation.java, getNameFromLocation(…) shows you how to get back XML data, and getGeoPointFromName(…) shows you how to get JSON data back. As you can see, the amount of processing required is about the same, but with all factors consistent, JSON data is more compact and uses less network bandwidth.

Minimizing location services

In this section we cover the power management aspects of using location services (using location services is described in Chapter 10). You have a couple of different ways to reduce the amount of battery power used by a smartphone or tablet when trying to find location information. One is to use the last known location rather than repeatedly try to determine the current location over the network when it's slow or down or the provider is unavailable. The following code snippet from the method getBestCurrent Location(…) from the Tic-Tac-Toe class GeoLocation.java shows you how to do it:

public Location getBestCurrentLocation(){
    Location myLocation=null;
    myLocation = manager.getLastKnownLocation(bestProvider);
    if (myLocation == null){
        myLocation = manager.getLastKnownLocation(“network”);
    }
    if (myLocation != null){
        System.out.println(“GeoLocation is >”+myLocation.toString()+”<”);
        thisLocation = myLocation;
    }
    return thisLocation;
}

Another approach is to use a less expensive location provider when possible. You can pick the location provider directly, or you can specify the criteria you care about and have Android give you the appropriate location provider. In Tic-Tac-Toe, we have not done this; instead, the code chooses the best provider available, regardless of cost. Take a look at this constructor for the GeoLocation class:

public GeoLocation(Context theContext){
    thisContext = theContext;
    manager =
      (LocationManager) thisContext.getSystemService(Context.LOCATION_SERVICE);
    Criteria criteria = new Criteria();
    bestProvider = manager.getBestProvider(criteria, true);
    registerForLocationUpdates();
}

The following code snippet shows how you would write this method differently if you're looking for the cheapest provider:

public GeoLocation(Context theContext){
    Criteria criteria = new Criteria();
    criteria.setAccuracy(Criteria.ACCURACY_COARSE);
    criteria.setPowerRequirement(Criteria.POWER_LOW);
    LocationManager manager =
        (LocationManager)getSystemService(thisContext.LOCATION_SERVICE);
    String cheapestProvider = myLocationManager.getBestProvider(c, true);
    registerForLocationUpdates();
}

Be sure to unregister for location updates when your activity is paused, or else these updates will continue to waste battery power, even when the application isn't running.

Certain apps might need to lock the screen and prevent it from dimming or turning off while your application is running (for example, an app that is providing turn-by-turn driving directions). For this task, you use a wake lock. We don't describe wake locks in detail because they're beyond the scope of this book, but check out these two pages for more information:

Be sure to disable wake locks when your activity is paused. As with location updates, wake locks can continue after onPause().

Ensuring Responsiveness in Your Apps

One nonfunctional requirement (NFR) of special concern in mobile apps is responsiveness. Mobile app developers must ensure that their apps don't even appear to freeze, become sluggish, or fail to respond to user input, for example.

image Ensuring responsiveness isn't the same as optimizing performance.

Though your app might operate as fast as computationally possible, it must appear to be controllable by the user, even when it's actively working. For example, refreshing a web page might take a long time because the network or the server providing the page is slow. Obviously, your app can do nothing about speeding the refresh, but whenever this type of operation takes place, your app must not freeze and it should, for example, allow the user to abandon the sluggish activity.

xs

image The primary technique to achieve responsiveness is threading. Essentially, the idea is to move from the main thread of the operation that's likely to take a long time and execute its operations on separate (additional) threads using the thread packages in Java. The following simple example shows you how a Java thread can be used to load an image from a network:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork(); // user written method
            // do something with the image
             …
        }
     }).start();
}

Another use of threads is in handling areas in your application where proper operation requires a waiting period. Two examples are in Tic-Tac-Toe. The first is in the SplashScreen activity, whose onCreate(…) method is listed here:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.splash);
    // Launch a new thread for the splash screen
    Thread splashThread = new Thread() {
        @Override public void run() {
            try {
                int elapsedTime = 0;
                while(active && (elapsedTime < splashTime)) {
                    sleep(sleepTime);
                    if(active) elapsedTime = elapsedTime + timeIncrement;
                }
            } catch(InterruptedException e) {
            // do nothing
            } finally {
                finish();
                startActivity(
                new Intent(“com.wiley.fordummies.androidsdk.tictactoe.Login”));
            }
          }
       };
       splashThread.start();
}

In this code snippet, the splash screen is displayed and a separate thread is launched that sleeps for a while and then launches the Login activity. Because the main thread isn't put to sleep, it remains responsive so that the user can exit the splash screen by touching it. A touch event invokes the following onTouch method, which lets the app exit the splash screen:

public boolean onTouchEvent(MotionEvent event) {
   if (event.getAction() == MotionEvent.ACTION_DOWN) {
       active = false;
   }
   return true;
}

We also use threading to implement responsiveness in the implementation of machine play in Tic-Tac-Toe. Take a look at the following method scheduleAndroidsTurn(…) in the GameSession class:

private void scheduleAndroidsTurn() {
    System.out.println(“Thread ID in scheduleAndroidsTurn:” +
                       Thread.currentThread().getId());
    board.disableInput();
    if(!testMode){
        Random randomNumber = new Random();
        Handler handler = new Handler();
        handler.postDelayed(
            new Runnable() {
                public void run() {
                    androidTakesATurn();
                }
             },
             ANDROIDTIMEOUTBASE + randomNumber.nextInt(ANDROIDTIMEOUTSEED));
  }else{
      androidTakesATurn();
  }
}

The machine pretends to think for a random period and then makes its move. While the computer is “thinking,” the user is prevented from making a move. However, while input to the Tic-Tac-Toe board is prevented, other user actions should not be (such as allowing the user to exit the game by choosing MenuimageEnd Game). You implement this behavior as described in this list:

  • board.disableInput(): Prevents user input from registering on the game board.
  • Handler.postDelayed(…): Posts a delayed callback to the method androidTakesATurn() with a randomly generated delay. Thus, androidTakesATurn() is called after the random delay (during which the board isn't clickable). This method plays the computer's move and then enables the board. During this process, the main application thread (the user interface thread) is still active and can accept user input, including the directive to end the game, which we just mentioned.

Certain Android framework components automatically launch long-lived operations in separate threads (so that you don't have to do it). Take a look at the extracts of the activity HelpWithWebView:

public class HelpWithWebView extends Activity implements OnClickListener {
    protected void onCreate(Bundle savedInstanceState) {
        String URL=null;
        super.onCreate(savedInstanceState);
        setContentView(R.layout.helpwithwebview);
        WebView helpInWebView=null;
        helpInWebView = (WebView) findViewById(R.id.helpwithwebview);
        View buttonExit = findViewById(R.id.button_exit);
        buttonExit.setOnClickListener(this);
        Bundle extras = getIntent().getExtras();
        if(extras !=null){
            URL = extras.getString(“URL”);
        }
        helpInWebView.loadUrl(URL);
     }
…
}

helpInWebView.loadUrl(URL) launches a separate thread to load the URL so that the application remains responsive while the page is being loaded.

Understanding the SDK Components Used in This Chapter

This section relates additional details about the components (packages and classes) of the Android framework and its add-ons that provide the functionality covered in this chapter. We don't go into great detail because Google provides web pages that are more comprehensive than we could ever be, but we give you an idea of its components and describe what they can do.

The Android thread model and components

When an Android application is launched, a thread (known as the main thread or the user interface (UI) thread) is created. The Android user interface isn't thread-safe, so if separate threads are launched, they shouldn't perform user interface operations. (Although the Android runtime throws exceptions in most cases, in cases where your app manages to avoid throwing exceptions, you see some strange behavior.) Thus, you have to break your long-lived operation into two tasks:

  • The intense, long-lived computation is performed.
  • The result of the computation is shown in the user interface.

    The Android framework offers four ways to access the UI thread:

  • Activity.runOnUiThread(Runnable myRunnable) runs the specified runnable object on the UI thread. (See an example of this in GameSessionTest.java in the TicTacToeProject-Test.)
  • View.post(Runnable myRunnable) causes the runnable to be added to the message queue to be run by the UI thread as it processes all its messages.
  • View.postDelayed(Runnable, long) causes the runnable to be added to the message queue after a specified period.
  • The Handler class lets you perform the preceding post(…) and postDelayed(…) operations when you don't have access to an active View. (See an example of this in GameSession. java in the TicTacToeProject.)

image Look for more details about threading at http://developer.android.com/resources/articles/painless-threading.html and on the pages linked to it.

Power management components

Even though we don't describe the power management functionality in the Android SDK because it's beyond the scope of this book, we include a short section about the Android power manager class, which lets you set wake locks that can, for example, maintain full screen brightness while your application is running.

The main class is PowerManager, which is documented at http://developer.android.com/reference/android/os/PowerManager.html. Acquiring an instance of this class allows you to set wake locks, put the device to sleep, check screen activity, and determine when user activity last took place.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.69.85