Tuesday, September 3, 2013

Smoothing performance on Fragment transitions

Suppose you're doing a pretty standard Fragment replacement with a custom animation:

getSupportFragmentManager()
    .beginTransaction()
    .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
    .replace(android.R.id.content, new MyFragment())
    .commit();

You may notice that the performance can be a bit rough, not as smooth as you'd like. A common way to improve Android animation performance is to use hardware layers.  Normally you'd add it to the animation directly but with fragments you don't get access to it unless you take advantage of Fragment.onCreateAnimation()*.  Here's how it looks:

public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    Animation animation = super.onCreateAnimation(transit, enter, nextAnim);

    // HW layer support only exists on API 11+
    if (Build.VERSION.SDK_INT >= 11) {
        if (animation == null && nextAnim != 0) {
            animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);
        }

        if (animation != null) {
            getView().setLayerType(View.LAYER_TYPE_HARDWARE, null);

            animation.setAnimationListener(new AnimationListener() {
                public void onAnimationEnd(Animation animation) {
                    getView().setLayerType(View.LAYER_TYPE_NONE, null);
                }

                // ...other AnimationListener methods go here...
            });
        }
    }

    return animation;
}

Now the animation should be a lot more smooth!  In my own code, I've overridden this method in a base Fragment from which all others extend so that I always get this feature (though if you're more particular you could only apply it to certain Fragments).

* If you're not using the support library, then you'll be overriding Fragment.onCreateAnimator() and using animator-based classes.

Tuesday, August 20, 2013

Joda Time's Memory Issue in Android

I've recently gotten fed up with how nuts the built-in calendar library is for Java/Android.  It's incredibly easy to make mistakes and it's unintuitive at best, so I've finally decided to take the plunge and switch to Joda time.

Joda is like a dream come true* except for one fairly extreme memory issue that I ran into.  After adding it to the app we started to see two huge memory sinks show up in MAT: a JarFile and a ZipFile that in our app took up a combined 4MB!



Joda's JAR is only half a meg, so how come these things took up so much space?  Why didn't any other JARs take up this space?  What's even stranger is that the amount of memory used seemed to scale based on the number of resources I had in the application; a simple test app only used up an extra 700kb, but Expedia's resource-heavy app took up the above.

It turns out the problem is ClassLoader.getResourceAsStream().  Joda time includes the olson timezone database in the JAR itself and loads the TZ data dynamically through getResourceAsStream().  For some reason getResourceAsStream() does some rather extreme caching and takes up a ton of memory if you use it**.

Thankfully there's a fairly simple solution.  You can actually implement any timezone Provider you want, circumventing the normal JAR-based ZoneInfoProvider.  Just make sure that your implementation has a default constructor and setup your system properties thus:

System.setProperty("org.joda.time.DateTimeZone.Provider",
    AssetZoneInfoProvider.class.getCanonicalName());

As such, I imported all of the TZ data (compiled, from the JAR) into my project's /assets/ directory.  Then I took the source for ZoneInfoProvider and reworked it so that openResource() uses the AssetManager to retrieve data.  I hooked it up and voila - no more excessive memory usage!  As an added bonus, this makes it a lot easier to update your TZ data without relying on a new version of Joda time.

As an epilogue, if someone can explain why getResourceAsStream() causes the sadness it does I'd be interested to know.  I tried looking into it for a bit but gave up because it wasn't like I would be able to change the system code anyways.

* Seriously: if you deal with dates, times, or some combination thereof at all, you will be doing yourself a favor by switching to Joda time.

** What initially tipped me off was a Jackson XML post about the same problem: https://github.com/FasterXML/jackson-core/pull/49

Wednesday, July 3, 2013

Don't Override ListView.getAdapter()

A few days we ran into a bug.  When we were updating the underlying data for the particular Adapter, the ListView would blow up with this exception:

E/AndroidRuntime(1809): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(16908298, class com.expedia.bookings.widget.ItinListView) with Adapter(class android.widget.HeaderViewListAdapter)]

The thing is, we weren't updating from a background thread.  That exception is thrown when the # of items the ListView thinks the adapter has and the # it actually has are out of sync, but I couldn't see how that was happening.  What was going on?

It turned out to be me overriding ListView.getAdapter().  There's some code I've been working with recently which has a custom ListView; inside of it is a custom Adapter.  I wanted access to that Adapter (but not the ListAdapter wrappers that are sometimes added in the case of header/footer views), so I overrided ListView.getAdapter() and had it return the custom Adapter.

However, the ListView uses getAdapter() sometimes to get the count for the # of items it has.  By bypassing the wrapper ListAdapter, the count was sometimes wrong (since it wasn't accounting for header/footer Views).

The moral of the story is: don't override ListView.getAdapter().

Thursday, June 27, 2013

How to Correctly Format Date/Time Strings on Android

One aspect of internationalization is to correctly format your date/time strings.  Different countries use very different formats and it's easy to incorrectly format your strings for your international users.

Your first foray into formatting date/times is probably through java.text.DateFormat (via SimpleDateFormat):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(cal.getTime());
// date == "2013-12-20"

While this works great for formatting parameters (for, say, a web service), it's terrible for localization.  Your international users won't be using the same date/time format you're using and it won't pick up user preferences (e.g., date order or 12-hour vs 24-hour).

A more correct way of doing it is to use android.text.format.DateFormat (not to be confused with the previous DateFormat).  There are some methods here that return formatters defined by the system's locale, like getDateFormat() and getTimeFormat() (among others):

Calendar cal = new GregorianCalendar(2013, 11, 20);
DateFormat df = android.text.format.DateFormat.getDateFormat(this); 
String date = df.format(cal.getTime());
// date == "12/20/2013"

The problem with these formatters is that they are inflexible; what if you don't want to show a year on a date?  What if you want to include the day of the week?  There are only limited circumstances where these formatters are good enough.

The best solution is to use DateUtils.  It has two powerful methods - formatDateTime() and formatDateRange() - which take in flags to determine which fields to include.  It automatically formats to the user's locale and preferences without you having to worry about it.

DateUtils.formatDateTime() formats a single point in time.  Here's a few examples:

Calendar cal = new GregorianCalendar(2013, 11, 20);
String date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
// date == "12/20/2013"
date = DateUtils.formatDateTime(this, cal.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME);
// date == "00:00, 12/20/2013"

DateUtils.formatDateRange() formats a range, like "Jan 5 - Feb 12".  Why might you want to use this instead of just concatenating two calls to formatDateTime()?  Besides being easier, it can optimize output in certain circumstances by reducing redundant field usage, like months/years when they don't change throughout the range:

Calendar cal1 = new GregorianCalendar(2013, 11, 20);
Calendar cal2 = new GregorianCalendar(2013, 11, 25);
Calendar cal3 = new GregorianCalendar(2014, 0, 5);
String date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal2.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20 - 24"
date = DateUtils.formatDateRange(this, cal1.getTimeInMillis(), cal3.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE);
// date == "December 20, 2013 - January 4, 2014"

One thing to watch out for with formatDateRange() is where it cuts off the day.  You may notice in the example above that the date ranges seem to be off by one day; that's because it cuts off at midnight.  If you add a millisecond it should properly format the range.

If you want your application to abide by the locale's formatting rules while still having control over what information to show, DateUtils is your place to go.  Be sure to read through all the different formatting flags so you can wield the most power with this tool.

(One final note - the example code above shows output in my locale.  In your locale it may differ - this is on purpose, of course!)

Tuesday, May 14, 2013

Sentinels of the Multiverse Randomizer App

My latest side project is a randomizer application for the board game Sentinels of the Multiverse.

You can grab the app here: https://play.google.com/store/apps/details?id=com.idunnolol.sotm

You can check out the source here: https://github.com/dlew/android-sotm

I have to admit that writing it was a gigantic waste of time (the benefit is minuscule in comparison to the time it took to write).  However, it was a  ton of fun to make; there's something very liberating about writing an app from scratch with no baggage to worry about.  It was also liberating to write an app that uses modern APIs with complete disregard to backwards compatibility.  As such, it's only available on ICS+.

In other news, I will be at Google I/O again this week.  In particular I will be helping man Expedia's booth in the Android sandbox; feel free to come by and say hello.

Tuesday, April 30, 2013

Before & After

I attend weekly trivia nights with friends.  One of the categories, Before & After, has piqued my interest.  It works like this: two movies are described, and you have to name both movies.  What makes it a bit easier is that the two movies share a common combining word.  Here are a few examples:

  • Men in Black Swan
  • Sling Blade Runner
  • Law Abiding Citizen Kane
  • My Fair Lady and the Tramp
  • Batman & Robin Hood
  • A Walk to Remember the Titans

I find the answers to be fairly amusing, so I wrote a program that generates before & afters.

You can find the source code here.

I've pumped out a spreadsheet of results (for movies) here.

I used a word list to discover words in common between the start/end of movie names.  For finding the common word, I generated both a forwards and a backwards trie of the first/last word of each movie name.  Tries are pretty much the best for simple text searches.

Ultimately, the program is general enough that it could work on anything.  The main reason I focused on movies was because IMDB is good enough to dump their ratings data, which helped immensely for sussing out good vs. bad results.  I found the best way to rank results was a combination of the # of voters (showing popularity of both items) and the difference in their rating (in that combining two items with vastly different ratings is hilarious).

There was one problem I could not overcome (since I was putting relatively minimal effort into this venture), which is determining when a word was part of a compound word or not.  For example, I would want to match "ball" in "basketball" because "basketball" is a compound word, but I wouldn't want to match "all" in "hall".  Solving this problem would a word list with more data than the word itself, so I just skipped it.

Tuesday, April 9, 2013

Memory Management: A Case Study

During the development of the most recent version of Expedia's Android app we ran into a variety of memory problems.  I thought I'd share some details about the problems we ran into and how we solved them.

Memory Analyzer Tool (MAT)

If you're not familiar with Eclipse's MAT you should be.  MAT should be your first stop for dealing with memory problems.  Not only is it useful for finding out what's hogging memory, it can also be used to find memory leaks.

An important note about using MAT - almost all memory problems I've run into relate to Bitmaps.  In versions of Android previous to 3.x, Bitmaps were stored in native memory (rather than the heap) and thus not viewable in MAT.  This makes it much harder to debug memory problems on 2.x devices, so you might want to stick to 4.x devices for investigating memory issues.

High-Resolution Bitmaps

Bitmaps in memory are uncompressed (even if they were compressed in your APK).  This can cause serious issues if you have high-resolution assets.  For example, we had tablet backgrounds which were the full resolution of the N10 (2560x1600).  That's fine with the N10's beefy heap size, but when scaled down to smaller devices (like the N7) the scaled image would take up 1/8th of the app's memory!  Imagine doing that multiple times and you can see why we ran into memory issues.

We solved this problem one of two ways: first, we reduced the resolution of the asset.  Some of these backgrounds were blurred, meaning we could lower the resolution greatly without losing quality.  When we couldn't lower the resolution without quality suffering greatly, we would sometimes replace the normal asset with a backup color (when we detected low memory situations).

BitmapFactory.decodeResource()

When you're decoding resources yourself (rather than using something like ImageView.setImageResource()), there are a couple things to look out for.

If you're decoding the same resource over and over again, you might want to cache it.  This problem hit us because we had an ImageView in a list show a default asset before we load an image from the network.  If you scrolled through a list quickly, and each one is loading its own default asset, you could create a burst of memory usage that runs you out of memory.  By caching this default, we not only saved memory but also improved performance.

Another issue we ran into with decodeResource() involves device density and BitmapFactory.Options.  If you are using them you should know that the default is to not auto-scale the asset at all.  So if you've only got an xhdpi version of the asset on an mdpi device, you're going to load them in at xhdpi size, taking up more memory than you expected.  So make sure to set that if you're manually loading assets with Options.  (If you're not passing BitmapFactory.Options you don't need to worry, as the default decodeResources() handles density for you.)

LruCache

Having an in-memory LruCache of Bitmaps loaded from the network is all the rage these days. However, it can easily lull you into a false sense of security.  Just because you've limited your Bitmap cache doesn't mean you'll never run out of memory!  If your LruCache is 1/4th the size of available memory, but your app (at one point) needs all of it not to crash... well, you've got a problem.

Things got better for us once we started manually clearing the LruCache at key moments.  Having a DiskLruCache helps a lot if you do this, as you can clear the memory cache but then quickly reload images if you need to without going to the network.

Resources and sPreloadedDrawables

This is a red herring!  You may see in MAT that Resources is taking up a whole ton of memory with a variable called sPreloadedDrawables.  Unfortunately, you can't avoid sPreloadedDrawables; it's a pre-defined set of system resources that are preloaded into memory for every app.  Don't spend any time thinking about them.

Recommendation

Overall, what you should do is find an old phone and use it as your primary development device.  That way you know from the start that your app runs on all phones.  It's much harder to discover these limitations later on.

For example, the Nexus 4 (my primary device) has a whopping 192MB of memory.  Even with all xhdpi assets that thing is difficult to run out of memory on a standard app.  Compare that to what QA was using - devices with 24MB to 32MB.  It's a very different game when you're trying to design a beautiful app to run in 24MB on hdpi!