Мое приложение уже много лет отлично работает на 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$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
а затем аналогичные ошибки для многих классов в моем приложении. Приложение работает до тех пор, пока ему не понадобится использовать один из этих недостающих классов, а затем вылетает.
Я прочитал несколько сообщений с аналогичными проблемами, связанными с ограничением метода в 64 КБ на Android 4, и вполне возможно, что в недавнем обновлении я перешагнул порог в 64 КБ. Итак, я попытался настроить multidex в соответствии с эти инструкции, но это не имеет значения, и я не знаю, как определить, работает ли он. Когда я создаю файл APK и проверяю его содержимое, я вижу только один файл classes.dex. Это может означать, что моя установка multidex не работает, или у меня не более 64K методов, и моя проблема в другом.
Моя вторая теория заключается в том, что использование класса MidiDeviceInfo, который был добавлен в Android 6, вызывает проблему на Android 4. Однако я использую проверки версий и аннотации @TargetApi(24), чтобы игнорировать MIDI-код на Android 4-5. И эта проблема не возникает на Android 5. И все же любопытно, что это первая ошибка, которая появляется каждый раз.
Правильна ли одна из этих теорий или похожа на другую проблему?
Кстати, что именно означает «невозможно найти класс, на который есть ссылка в сигнатуре» - сигнатуру метода? У меня было несколько методов, которые использовали аргументы типа MidiDeviceInfo, но я аннотировал все они с помощью @TargetApi(24), и у меня нет проблем с компиляцией.
Обновлять
Благодаря совету Хомермана я вижу, что проблема не в количестве методов. Это возвращает меня к совместимости с MIDI, но я смотрю на различия по сравнению с предыдущей версией и не вижу никаких изменений в коде MIDI. Что еще мне следует искать?
Обновление 2
Я не смог найти ничего, что казалось бы актуальным в различиях со времени последней версии, поэтому я проверил последнюю версию из моей VCS и запустил ее, и она работала нормально. Но я заметил в logcat для этой версии все те же ошибки для классов MIDI. Так что, похоже, это не совсем часть проблемы. Думаю, именно это и делает Android, когда сталкивается с новыми классами. Я продолжал сравнивать logcats между последней хорошей версией и текущей версией, а затем увидел разницу: недавно я добавил OnScrollChangeListener в один из моих классов. Это сгенерировало еще одну партию отсутствующих ошибок класса, которых не было в последней версии приложения. И когда я удалю эту новую функциональность, приложение снова будет нормально работать на Android 4.
Итак, вот проблема: у меня есть такое определение класса:
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);
}
}
Очевидно, присутствие View.OnScrollChangeListener в определении класса - вот что все испортило. Есть ли способ объявить это только для поддерживаемых версий Android?
И, по-видимому, Android 5 изящно игнорирует эту неподдерживаемую ссылку, а Android 4 - нет.
О, круто. Согласно программе просмотра DEX, приложение определяет 23792 метода и ссылается на 31693 метода. Так что это далеко не предел.
Вы обновляли какие-либо зависимости?
есть ли шанс, что это связано с ProGuard? Если вы используете ProGuard для обфускации, возможно, попробуйте протестировать не обфусцированную сборку и посмотреть, исчезнет ли проблема.
вы упомянули «недавнее незначительное обновление», которое, похоже, вызвало такое поведение. что обновлялось?
@Chintan Shah, я не обновлял зависимости.
@homerman, при отладке не запутываю. Я собираюсь добавить обновление с дополнительной информацией о версиях приложения.




Интерфейс View.OnScrollChangeListener был добавлен на уровне API 23. Это означает, что определение вашего класса уже является проблемой; если этот класс когда-либо загружается до 23-х, вы рухнете.
Решение состоит в том, чтобы создать общего родителя (возможно, интерфейса) и двух конкретных реализаций этого родителя: одну для до 23 и одну для 23+. Затем используйте фабрику, чтобы получить соответствующую реализацию на основе работающего в данный момент устройства.
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 { ... }
Ответ Бена Пи привел меня к правильному пути, но я запутался в преобразовании моего класса в интерфейс, поэтому вот что я придумал:
// 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);
я бы, вероятно, сначала профилировал APK, чтобы увидеть, где он на самом деле стоит относительно лимита DEX в 64k - developer.android.com/studio/build/apk-analyzer#view_dex_fil es