Search code examples
javaandroiddalvik

Suddenly missing lots of classes on Android 4


My app has been running fine on Android 4.0.3 and above for years. With a recent minor update, it suddenly started crashing on Android 4.x devices. When I debug it on Android 4, it builds and installs fine, but the console gets lots of errors starting with

W/dalvikvm: VFY: unable to find class referenced in signature (Landroid/media/midi/MidiDeviceInfo;)
I/dalvikvm: Failed resolving Lcom/arlomedia/myapp/App$4; interface 113 'Landroid/media/midi/MidiManager$OnDeviceOpenedListener;'
W/dalvikvm: Link of class 'Lcom/arlomedia/myapp/App$4;' failed
E/dalvikvm: Could not find class 'com.arlomedia.myapp.App$4', referenced from method com.arlomedia.myapp.App.addMidiPorts
W/dalvikvm: VFY: unable to resolve new-instance 447 (Lcom/arlomedia/myapp/App$4;) in Lcom/arlomedia/myapp/App;
D/dalvikvm: VFY: replacing opcode 0x22 at 0x0007

and followed by similar errors for many of the classes in my app. The app runs until it needs to use one of these missing classes, then crashes.

I've read several posts with similar issues related to the 64K method limit on Android 4, and it is conceivable that in the recent update, I happened to cross the 64K threshold. So I tried to set up multidex according to these instructions, but it doesn't make a difference and I don't know how to tell if it's working. When I build an APK file and inspect its contents, I only see one classes.dex file. This could either mean my multidex setup isn't working, or I don't have more than 64K methods and my problem is something else.

My second theory is that the use of the MidiDeviceInfo class, which was added in Android 6, is causing a problem on Android 4. However, I'm using version checks and @TargetApi(24) annotations to ignore the MIDI code on Android 4-5. And this problem doesn't occur on Android 5. Yet it is curious that this is the first error to pop up every time.

Does one of these theories sound right, or does it sound like some other problem?

By the way, what exactly does "unable to find class referenced in signature" mean -- a method signature? I did have some methods that used arguments of the MidiDeviceInfo type, but I annotated all those with @TargetApi(24) and I don't have any problems compiling.

Update

Thanks to homerman's tip, I can see that the number of methods isn't the issue. This leads me back to the MIDI compatibility, but I'm looking at diffs from the previous version and I don't see any changes to the MIDI code. What else should I be looking for?

Update 2

I couldn't find anything that seemed relevant in the diffs since the last version, so I checked out the last version from my VCS and ran it and it ran fine. But I noticed in the logcat for that version all the same errors for the MIDI classes. So it seems that those aren't really part of the problem. I guess that's just what Android does when it runs into newer classes. I kept comparing the logcats between the last good version and the current version and then I saw the difference: I recently added an OnScrollChangeListener to one of my classes. That generated another batch of missing class errors that weren't in the last app version. And when I remove that new functionality, the app runs fine on Android 4 again.

So here is the problem: I have a class definition like this:

public class DocumentViewer extends RelativeLayout implements View.OnScrollChangeListener {

    if (Build.VERSION.SDK_INT >= 23) {
        textView.setOnScrollChangeListener(this);
    }

    @Override
    public void onScrollChange(final View scrollView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        Log.d("onScrollChange", "scroll changed: " + scrollY);
    }

}

Apparently the presence of View.OnScrollChangeListener in the class definition is what's messing everything up. Is there a way to declare that only for the supported Android versions?

And apparently Android 5 gracefully ignores this unsupported reference, but Android 4 doesn't.


Solution

  • The View.OnScrollChangeListener interface was added in API level 23. That means that your class definition is already a problem; if this class is ever loaded pre-23, you'll crash.

    The solution is to create a common parent (probably an interface) and two concrete implementations of this parent: one for pre-23 and one for 23+. Then use a factory to get the appropriate implementation based on the currently-running device.

    public interface DocumentViewer {
        // interface methods
    }
    
    public class DocumentViewPre23 extends RelativeLayout implements DocumentViewer {
        // interface methods
    }
    
    public class DocumentViewer23 extends RelativeLayout implements DocumentViewer, View.OnScrollChangeListener {
        // interface methods
    }
    

    And then you could have something like

    public static DocumentViewer getDocumentViewer() {
        if (Build.VERSION.SDK_INT >= 23) {
            return new DocumentViewer23();
        } else {
            return new DocumentViewerPre23();
        }
    }
    

    The android support library does this quite often, for what it's worth. Here's an example from ActionBarDrawerToggle that switches between two implementations based on the API version:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        mActivityImpl = new JellybeanMr2Delegate(activity);
    } else {
        mActivityImpl = new IcsDelegate(activity);
    }
    
    public interface Delegate {
        ...
    }
    
    @RequiresApi(18)
    private static class JellybeanMr2Delegate implements Delegate {
        ...
    }
    
    private static class IcsDelegate implements Delegate {
        ...
    }