Chasing a Cunning Android bug

August 06, 2012

Most software bugs are small, pesky problems with an obvious cause and a simple solution. But every once in a while I encounter a bug who is more advanced: a bug that hides in the shadows of other bugs, avoiding detection by its coffee-drinking predator. This week I had the opportunity to stalk an advanced bug that was hiding in the development version of Square Card Reader.

Symptoms

We never saw the bug on Android 4.0 (Ice Cream Sandwich). But it was easy to reproduce on my Android 2.2 (Froyo) test device. Clicking 'Sign In' consistently failed with this exception:

java.lang.IllegalStateException: Could not find a method signIn(View) in the
activity class com.squareup.ui.ReaderLandingActivity for onClick handler on
view class android.widget.Button with id 'landing_sign_in_button'
    at android.view.View$1.onClick(View.java:2059)
    at android.view.View.performClick(View.java:2408)
    at android.view.View$PerformClick.run(View.java:8816)
    at android.os.Handler.handleCallback(Handler.java:587)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:123)
    at android.app.ActivityThread.main(ActivityThread.java:4627)
Caused by: java.lang.NoSuchMethodException
    at java.lang.Class.getDeclaredMethods(Native Method)
    at java.lang.ClassCache.getDeclaredPublicMethods(ClassCache.java:166)
    at java.lang.ClassCache.getDeclaredMethods(ClassCache.java:179)
    at java.lang.ClassCache.findAllMethods(ClassCache.java:249)
    at java.lang.ClassCache.getFullListOfMethods(ClassCache.java:223)
    at java.lang.ClassCache.getAllPublicMethods(ClassCache.java:204)
    at java.lang.Class.getMethod(Class.java:984)
    at android.view.View$1.onClick(View.java:2052)
    ... 11 more

The Android framework was looking for signIn(View) because we'd registered an onClick handler in our XML layout:

<Button
    android:id="@+id/landing_sign_in_button"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:onClick="signIn"
    android:text="@string/sign_in"
    />

But this shouldn't have failed! We properly declared that method in our ReaderLandingActivity:

public void signIn(View v) {
  startActivity(new Intent(this, ReaderLoginActivity.class));
}

Something was going wrong in Dalvik, Android's application runtime. It wasn't seeing a method that I knew was there.

APIs from the future

To share code and structure, all of Square's activities extend from a common superclass activity. It declares a couple of methods involving types not introduced until Honeycomb:

  public final void onActionModeStarted(android.view.ActionMode mode) {
    ...
  }

  public final void onActionModeFinished(android.view.ActionMode mode) {
    ...
  }

This is generally okay. As long as we don't call the method, Dalvik should ignore the missing classes.

Dalvik did bulk reflection back in Froyo

Suppose you're calling ReaderLandingActivity.class.getMethod("signIn", View.class).

  • In Ice Cream Sandwich, Dalvik calls some C code that walks through the .dex file, finds the signIn method, and returns it as a Method object.
  • In Froyo, Dalvik calls some C code that walks through the .dex file and finds all methods on ReaderLandingActivity. Then Java takes over and searches for the signIn method in that list.

Dalvik did too much class initialization back in Froyo

Just because I'm looking up a method doesn't mean I'll ever invoke it. For example, Guice just checks for an annotation and ignores it if unfound. Class initialization is expensive and should be put off until necessary.

But in Froyo, Dalvik eagerly initialized each method's return type and its parameter types. If any of those types failed to initialize, we have a problem.

Squash the bug

We ask for one method, signIn. Dalvik loads all methods on ReaderLandingActivity and its supertypes. That includes onActionModeStarted, which takes a parameter that didn't exist on Froyo. Dalvik cannot initialize the missing class so the failure cascades and causes our method lookup to fail.

This was made especially difficult to find because of another bug. When a class fails to initialize during getMethods(), Dalvik swallows the missing class failure and throws a less useful NoSuchMethodException.

We worked around the problem by declaring click handlers in code rather than XML. Though it's more verbose, it also avoids potentially inefficient reflection calls on those devices where reflection is the slowest.

Dalvik has steadily improved in quality and efficiency from release to release. But it means we need to be extra careful when we're testing and optimizing our code for older devices.

Jesse Wilson
Maker of Android apps and libraries in Waterloo, Canada.

Comments

Get support help at squareup.com/support. We'll delete off-topic comments.