在 Android 上突然丢失了很多 类 4

Suddenly missing lots of classes on Android 4

多年来,我的应用在 Android 4.0.3 及更高版本上一直 运行 正常。随着最近的一次小更新,它突然开始在 Android 4.x 设备上崩溃。当我在 Android 4 上调试它时,它可以正常构建和安装,但控制台会收到很多以

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

然后我的应用程序中的许多 class 出现类似错误。该应用程序会一直运行,直到它需要使用这些缺失的 classes 之一,然后崩溃。

我在Android4上看过几篇关于64K方法限制的类似问题的帖子,可以想象,在最近的更新中,我恰好跨过了64K的门槛。所以我尝试根据 these instructions 设置 multidex,但它没有什么区别,我不知道如何判断它是否有效。当我构建一个 APK 文件并检查其内容时,我只看到一个 classes.dex 文件。这可能意味着我的 multidex 设置不工作,或者我没有超过 64K 的方法,我的问题是别的。

我的第二个理论是 Android 6 中添加的 MidiDeviceInfo class 的使用导致 Android 4 出现问题。但是,我正在使用版本检查和 @TargetApi(24) 注释忽略 Android 4-5 上的 MIDI 代码。而且Android5上没有出现这个问题,奇怪的是每次都是第一个报错

这些理论中的一个听起来是否正确,或者听起来像是其他问题?

顺便问一下,"unable to find class referenced in signature" 到底是什么意思——方法签名?我确实有一些方法使用了 MidiDeviceInfo 类型的参数,但我用 @TargetApi(24) 注释了所有这些方法,编译时没有任何问题。

更新

感谢 homerman 的提示,我可以看出方法的数量不是问题。这使我回到 MIDI 兼容性,但我正在查看与以前版本的差异,但我没有看到 MIDI 代码有任何变化。我还应该寻找什么?

更新 2

自上一个版本以来,我找不到与差异相关的任何内容,所以我从我的 VCS 检查了上一个版本,运行 它 运行 很好。但是我在那个版本的 logcat 中注意到 MIDI classes 的所有相同错误。所以看起来这些并不是真正的问题的一部分。我想这正是 Android 在遇到更新的 classes 时所做的。我一直比较上一个好版本和当前版本之间的 logcats,然后我看到了不同之处:我最近向我的 classes 之一添加了一个 OnScrollChangeListener。这产生了另一批丢失的 class 错误,这些错误不在上一个应用程序版本中。当我删除该新功能时,该应用程序再次在 Android 4 上正常运行。

问题来了:我有一个这样的 class 定义:

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);
    }

}

显然 class 定义中 View.OnScrollChangeListener 的存在导致一切混乱。有没有办法声明仅针对受支持的 Android 版本?

显然 Android 5 优雅地忽略了这个不受支持的引用,但 Android 4 没有。

View.OnScrollChangeListener接口是在API第23层加入的。也就是说你的class定义已经有问题了;如果这个 class 在 23 之前加载,你会崩溃。

解决方案是创建一个公共父级(可能是一个接口)和该父级的两个具体实现:一个用于 23 岁之前,一个用于 23 岁以上。然后根据currently-运行设备使用一个工厂得到合适的实现。

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
}

然后你可以有类似的东西

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

android 支持库经常这样做,物有所值。这是来自 ActionBarDrawerToggle 的示例,它根据 API 版本在两个实现之间切换:

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 {
    ...
}

Ben P 的回答让我走上了正确的道路,但我对将我的 class 转换为界面感到困惑,所以这是我想出的:

// this was the original class and I added scrollViewDidScroll() to keep all my related code in one class
public class DocumentViewer extends RelativeLayout {

    if (Build.VERSION.SDK_INT >= 23) {
        if (this instanceof DocumentViewerExtras) {
            // assign the listener on supported devices
            DocumentViewerExtras thisExtras = (DocumentViewerExtras)this;
            textView.setOnScrollChangeListener(thisExtras);
        }
    }

    public void scrollViewDidScroll(final View scrollView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        // do something with the scroll results
    }

}

// this is a new class that adds OnScrollChangeListener on supported devices
@RequiresApi(23)
public class DocumentViewerExtras extends DocumentViewer implements View.OnScrollChangeListener {

    public DocumentViewerExtras(Fragment fragment) {
        super(fragment);
    }

    @Override
    public void onScrollChange(final View scrollView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        this.scrollViewDidScroll(scrollView, scrollX, scrollY, oldScrollX, oldScrollY);
    }

}

// this method instantiates the best available class
public DocumentViewer getDocumentViewer(Fragment fragment) {
    if (Build.VERSION.SDK_INT >= 23) {
        return new DocumentViewerExtras(fragment);
    } else {
        return new DocumentViewer(fragment);
    }
}

// this is how I get a new instance of the class
DocumentViewer documentViewer = getDocumentViewer(this);