Java.lang.VerifyError IllformedLocaleException

У меня есть следующий родительский метод, который используется во всех случаях с помощью различных уровней API:

public int setVoice (@NonNull final String language, @NonNull final String region){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        return setVoice21(language, region);
    } else {
        return setVoiceDeprecated(language, region);
    }
}

и setVoice21 делает что-то вроде этого:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public int setVoice21 ( @NonNull final String language, @NonNull final String region){

    try {
        // try some API 21 stuff
    } catch (final IllformedLocaleException e) {
        e.printStackTrace();
        return setVoiceDeprecated(language, region);
    }

setVoice21 содержит другой код, для которого требуется API 21+, в частности TextToSpeech.Voice и Locale.Builder

Когда я запускаю этот код на устройстве < API 21 Я получаю следующую ошибку:

W/dalvikvm: VFY: невозможно разрешить класс исключения 6232 (Ljava/util/IllformedLocaleException;) W/dalvikvm: VFY: отклонение код операции 0x0d при 0x0168 Вт /dalvikvm: VFY: отклонено Lcom/MyApp/Android/речь/MyTextToSpeech;.setVoice21 (Ljava/lang/String; Ljava/lang/String;) я W/dalvikvm: Verifier отклонен класс Lcom/myapp/android/speech/MyTextToSpeech;

E/AndroidRuntime: FATAL EXCEPTION: main java.lang.VerifyError: ком/MyApp/Android/речь/MyTextToSpeech

Если я удалю IllformedLocaleException и просто заменим его стандартным исключением, приложение будет работать отлично, несмотря на многие другие ссылки на методы > API21 в setVoice21

Чтобы еще больше запутать меня, setVoice21 вызывает следующий класс

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private class TTSVoice {

    public void buildVoice() {

        try {
            // Do some API 21 stuff
        } catch (final IllformedLocaleException e) {
        }

    }
}

Этот класс ссылается только на setVoice21, но мне не нужно удалять ссылку на IllformedLocaleException здесь - я могу оставить его, и приложение работает нормально... Baffled.

Может ли кто-нибудь помочь мне выяснить, почему IllformedLocaleException вызывает этот сбой? Исключения как-то обрабатываются по-разному?

Я благодарю вас заранее.

Примечание. Я не уверен, что это релевантно, но я подклассифицирую TextToSpeech стандартным способом. Я боюсь, что это может свершить вопрос, но на всякий случай...

public class MyTextToSpeech extends TextToSpeech {

    public MyTextToSpeech(final Context context, final OnInitListener listener) {
        super(context, listener);
    }
}

EDIT - обходной путь предоставленный razzledazzle ниже, позволяет приложению запускаться без сбоев, но я по-прежнему остаюсь не- мудрее, почему такой шаг необходим. Мне никогда не приходилось принимать такие меры до того, как вы столкнулись с версиями API.

Ответ 1

TL; DR: Исключения являются исключительными. Не удается поймать исключение, тип которого неизвестен.

Следующее - это большинство предположений, основанных на моих ограниченных знаниях о Java/Dalvik и здравом смысле. Возьмите его с солью. Я нашел метод, который выплевывает неудачную строку журнала и подтвердил большинство моих упомянутых предположений, см. Дополнительные ссылки ниже.

Кажется, ваша проблема заключается в том, что классы загружаются сразу, либо весь класс загружен, либо ни один из них. Проверка выполняется сначала, я предполагаю, что для предотвращения некоторых проверок времени выполнения (помните, что Android ограничен ресурсами).

Я использовал следующий код:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public int setVoice21(@NonNull final String language, @NonNull final String region) {
    try {
        // try some API 21 stuff
        new Locale.Builder().build().getDisplayVariant();
    } catch (final IllformedLocaleException ex) {
        ex.printStackTrace();
    }
    return 0;
}

Когда система пыталась создать экземпляр класса, содержащего этот метод, произошло следующее:

E/dalvikvm: Не удалось найти класс 'java.util.Locale $Builder', на который ссылается метод com.test.TestFragment.setVoice21

Загрузка класса Locale.Builder будет ClassNotFoundException.

W/dalvikvm: VFY: невозможно разрешить новый экземпляр 5241 (Ljava/util/Locale $Builder;) в Lcom/test/TestFragment;
D/dalvikvm: VFY: замена кода операции 0x22 на 0x0000

Затем в этом несуществующем классе он попытается вызвать метод <init>, который предотвращается путем замены OP_NEW_INSTANCE с помощью OP_NOP. Я думаю, что это было бы выживаемо, поскольку я все время вижу это при использовании библиотеки поддержки. Я считаю, что предположение состоит в том, что если класс не найден, он должен быть защищен с помощью проверки SDK_INT. Кроме того, если он прошел через dexing/proguard и другие вещи, он, должно быть, был преднамеренным, а ClassNotFoundException допустим во время выполнения.

W/dalvikvm: VFY: невозможно разрешить класс исключения 5234 (Ljava/util/IllformedLocaleException;)

Еще один проблемный класс, обратите внимание на этот раз "класс исключения", который должен быть особенным. Если вы проверите байт-код Java для этого метода с помощью:

javap -verbose -l -private -c -s TestFragment.class > TestFragment.dis

public int setVoice21(java.lang.String, java.lang.String);
    ...
    Exception table:
       from    to  target type
           0    14    17   Class java/util/IllformedLocaleException
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         18      11     3    ex   Ljava/util/IllformedLocaleException;
          0      31     0  this   Lcom/test/TestFragment;
          0      31     1 language   Ljava/lang/String;
          0      31     2 region   Ljava/lang/String;
    StackMapTable: number_of_entries = 2
      frame_type = 81 /* same_locals_1_stack_item */
        stack = [ class java/util/IllformedLocaleException ]
      frame_type = 11 /* same */

Вы действительно можете видеть, что все Exception table и StackMapTable и LocalVariableTable содержат проблемный класс, но не Locale$Builder. Это может быть связано с тем, что строитель не хранится в переменной, но точка, которую нужно извлечь, заключается в том, что исключения обрабатываются специально и получают больше контроля, чем обычные строки кода.

Использование BakSmali на APK через:

apktool.bat d -r -f -o .\disassembled "app-debug.apk"

.method public setVoice21(Ljava/lang/String;Ljava/lang/String;)I
.prologue
:try_start_0
new-instance v1, Ljava/util/Locale$Builder;
invoke-direct {v1}, Ljava/util/Locale$Builder;-><init>()V
...
:try_end_0
.catch Ljava/util/IllformedLocaleException; {:try_start_0 .. :try_end_0} :catch_0
...
:catch_0
move-exception v0
.local v0, "ex":Ljava/util/IllformedLocaleException;
invoke-virtual {v0}, Ljava/util/IllformedLocaleException;->printStackTrace()V
Похоже, что

обнаруживает аналогичную структуру, здесь мы действительно можем увидеть op-коды, упомянутые в журнале. Обратите внимание, что .catch представляется специальной инструкцией, а не операцией, поскольку ей предшествует точка. Я думаю, что это усиливает внимание, упомянутое выше: это не операция времени выполнения, но требуется, чтобы класс загружал код, содержащийся в методах.

W/dalvikvm: VFY: невозможно найти обработчик исключений в addr 0xe
W/dalvikvm: VFY: отклонено Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String; Ljava/lang/String;) I

Я предполагаю, что это означает, что он не смог восстановить, когда вызывать блок catch из Exception table и StackMapTable, потому что он не мог найти класс для определения родительских классов. Это подтверждено в getCaughtExceptionType, где "неспособность разрешить класс исключений" напрямую приводит к "неспособности найти обработчик исключений", потому что не находит общего супер класс для несуществующего исключения, что-то вроде } catch (? ex) {, поэтому он не знает, что поймать.

W/dalvikvm: VFY: отклонение кода операции 0x0d на 0x000e
W/dalvikvm: VFY: отклонено Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String; Ljava/lang/String;) I

Я думаю, что в этот момент верификатор просто сдался, потому что он не мог понять OP_MOVE_EXCEPTION. Это подтверждается тем, что метод getCaughtExceptionType используется только в одном месте, переключатель. Из-за этого мы получаем "отказ от кода операции" , а затем goto bail до стека вызовов до "отклоненный класс" . После исправления кода ошибки был VERIFY_ERROR_GENERIC, который отображается на VerifyError. Не удалось найти, где вызывается фактическое исключение JNI, если оно даже работает таким образом.

W/dalvikvm: Verifier отклонил класс Lcom/test/TestFragment;

Множественные отклонения были поданы против метода setVoice21, и, следовательно, весь класс должен быть отклонен (это кажется мне суровым, возможно, в этом отношении АРТ отличается).

W/dalvikvm: сбой класса init в вызове newInstance (Lcom/test/TestFragment;)
D/AndroidRuntime: выключение виртуальной машины
W/dalvikvm: threadid = 1: выход с отключенным исключением (группа = 0x41869da0)
E/AndroidRuntime: FATAL EXCEPTION: главная
    Процесс: com.bumptech.glide.supportapp.v3, PID: 27649
    java.lang.VerifyError: com/test/TestFragment

Я предполагаю, что это похоже на ExceptionInInitializerError в настольном Java, который вызывается, когда тело static { } в классе класса или инициализатор статического поля генерирует RuntimeException/Error.

Почему instanceof работает

Использование обходного пути razzledazzle изменяет эти таблицы на java/lang/Exception и перемещает зависимость до IllformedLocaleException в код, который будет выполняться во время выполнения:

     0    14    17   Class java/lang/Exception
19: instanceof    #34                 // class java/util/IllformedLocaleException

и аналогичным образом Smali:

.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
instance-of v1, v0, Ljava/util/IllformedLocaleException;

E/dalvikvm: Не удалось найти класс 'java.util.IllformedLocaleException', на который ссылается метод com.test.TestFragment.setVoice21
W/dalvikvm: VFY: невозможно разрешить экземпляр 5234 (Ljava/util/IllformedLocaleException;) в Lcom/test/TestFragment;

Теперь, та же самая жалоба, что и для Locale$Builder выше

D/dalvikvm: VFY: замена кода операции 0x20 на 0x000f

Замена OP_INSTANCE_OF с? что-то?, он не говорит:)

Еще одно возможное обходное решение

Если вы посмотрите на классы android.support.v4.view.ViewCompat*, вы заметите, что не все эти классы используются во всех версиях. Правильный выбирается во время выполнения (поиск static final ViewCompatImpl IMPL в ViewCompat.java), и только это загружается. Это гарантирует, что даже во время загрузки класса не будет какой-либо странности из-за отсутствия классов и будет выполняться. Вы можете создать аналогичную архитектуру, чтобы предотвратить загрузку этого метода на более ранних уровнях API.

Ответ 2

Устранена проблема, удалив класс IllformedLocaleException из аргумента catch. Это все равно позволит вам проверить IllformedLocaleException.

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public int setVoice21 (@NonNull final String language, @NonNull final String region) {
    try {
        // try some API 21 stuff
        ...
    } catch (final Exception e) {
        e.printStackTrace();
        if (e instanceof IllformedLocaleException) {
            ...
        }
    }

    ...
}