I have the following parent method, that is used in all cases by various API levels:
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); } }
and setVoice21
does something like this:
@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
contains other code that requires API 21+ specifically TextToSpeech.Voice and Locale.Builder
When I run this code on a device < API 21 I'm getting the following error:
W/dalvikvm: VFY: unable to resolve exception class 6232 (Ljava/util/IllformedLocaleException;) W/dalvikvm: VFY: rejecting opcode 0x0d at 0x0168 W/dalvikvm: VFY: rejected Lcom/myapp/android/speech/MyTextToSpeech;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I W/dalvikvm: Verifier rejected class Lcom/myapp/android/speech/MyTextToSpeech;
E/AndroidRuntime: FATAL EXCEPTION: main java.lang.VerifyError: com/myapp/android/speech/MyTextToSpeech
If I remove the IllformedLocaleException and just replace it with a standard Exception, the app runs fine, despite the many other references to methods > API21 within setVoice21
To confuse me yet further, setVoice21
invokes the following class
@TargetApi(Build.VERSION_CODES.LOLLIPOP) private class TTSVoice { public void buildVoice() { try { // Do some API 21 stuff } catch (final IllformedLocaleException e) { } } }
This class is only referenced from setVoice21
, but I do not have to remove the reference to IllformedLocaleException here - I can leave it and the app runs fine.... Baffled.
Can anyone help me out as to why the IllformedLocaleException is causing this failure? Are Exceptions somehow handled differently?
I thank you in advance.
Note - I'm not sure that it is relevant, but I'm subclassing TextToSpeech in a standard way. I fear this may convolute the question, but just in case...
public class MyTextToSpeech extends TextToSpeech { public MyTextToSpeech(final Context context, final OnInitListener listener) { super(context, listener); } }
EDIT - The workaround provided by razzledazzle below, does allow the app to run without crashing, but I still remain non-the-wiser as to why such a step is necessary. I've never had to take such measures before when dealing with API versioning.
2 Answers
Answers 1
Resolved the issue by removing the IllformedLocaleException
class from the catch argument. This will still allow you to check for 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) { ... } } ... }
Answers 2
TL;DR: Exceptions are exceptional. Can't catch an exception whose type is not known.
The following is most speculation based on my limited knowledge about Java/Dalvik and common sense. Take it with a grain of salt. I found the method that spits out the failing log line and confirmed most of my mentioned speculations, see added links below.
Your problem seems to be that the classes are loaded at once, either the whole class is loaded or none of it. Verification is done first I guess to prevent some runtime checks (remember Android is resource constrained).
I used the following code:
@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; }
When the system was trying to create an instance of the class containing this method the following happened:
E/dalvikvm: Could not find class 'java.util.Locale$Builder', referenced from method com.test.TestFragment.setVoice21
Loading the Locale.Builder
class would be a ClassNotFoundException
.
W/dalvikvm: VFY: unable to resolve new-instance 5241 (Ljava/util/Locale$Builder;) in Lcom/test/TestFragment;
D/dalvikvm: VFY: replacing opcode 0x22 at 0x0000
Then on that non-existent class it would try to call the <init>
method which is prevented by replacing the OP_NEW_INSTANCE
with a OP_NOP
. I think this would have been survivable, as I see these all the time when using the support library. I think the assumption here is that if the class is not found then it must have been guarded with an SDK_INT
check. Also if it went through dexing/proguard and other stuff it must've been intentional and a ClassNotFoundException
is acceptable at runtime.
W/dalvikvm: VFY: unable to resolve exception class 5234 (Ljava/util/IllformedLocaleException;)
Another problematic class, notice this time it's an "exception class" which must be special. If you check the Java bytecode for this method via:
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 */
You can indeed see that the Exception table
and StackMapTable
and LocalVariableTable
all contain the problematic class, but not Locale$Builder
. This may be because the builder is not stored in a variable, but the point to take from here is that exceptions are handled specially and get more scrutiny than normal lines of code.
Using BakSmali on the APK via:
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
seems to reveal a similar pattern, here we can actually see the op-codes mentioned in the log. Notice that .catch
seems to be a special instruction, not an operation because it's preceded by a dot. I think this reinforces the scrutiny mentioned above: it's not a runtime operation, but it is required for the class to load the code contained within the methods.
W/dalvikvm: VFY: unable to find exception handler at addr 0xe
W/dalvikvm: VFY: rejected Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I
I guess this means that it was not able to reconstruct when to call which catch
block from the Exception table
and StackMapTable
because it couldn't find the class to determine the parent classes. This is confirmed in getCaughtExceptionType
where "unable to resolve exception class" directly leads to "unable to find exception handler" because it finds no common super-class for a non-existent exception, something like } catch (? ex) {
so it doesn't know what to catch.
W/dalvikvm: VFY: rejecting opcode 0x0d at 0x000e
W/dalvikvm: VFY: rejected Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I
I think at this point the verifier just gave up because it couldn't make sense of the OP_MOVE_EXCEPTION
. This is confirmed as that the getCaughtExceptionType
method is only used in one place, a switch. Breaking out of that we get "rejecting opcode" then it goto bail
s up the call stack to "rejected class". After bailing the error code was VERIFY_ERROR_GENERIC
which is mapped to VerifyError
. Couldn't find where the actual JNI Exception is thrown if it even works that way.
W/dalvikvm: Verifier rejected class Lcom/test/TestFragment;
Multiple rejections were filed against the setVoice21
method and hence the whole class must be rejected (this seems harsh to me, it's possible ART is different in this regard).
W/dalvikvm: Class init failed in newInstance call (Lcom/test/TestFragment;)
D/AndroidRuntime: Shutting down VM
W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0x41869da0)
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bumptech.glide.supportapp.v3, PID: 27649
java.lang.VerifyError: com/test/TestFragment
I guess this is similar to an ExceptionInInitializerError
in desktop Java which is thrown when a static { }
in class body or an static field initializer throws a RuntimeException
/Error
.
Why instanceof
works
Using razzledazzle's workaround changes those tables to include java/lang/Exception
, and moves the dependency to IllformedLocaleException
into the code to be executed at runtime:
0 14 17 Class java/lang/Exception 19: instanceof #34 // class java/util/IllformedLocaleException
and similarly the Smali:
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 instance-of v1, v0, Ljava/util/IllformedLocaleException;
E/dalvikvm: Could not find class 'java.util.IllformedLocaleException', referenced from method com.test.TestFragment.setVoice21
W/dalvikvm: VFY: unable to resolve instanceof 5234 (Ljava/util/IllformedLocaleException;) in Lcom/test/TestFragment;
Now, it's the same complaint as for Locale$Builder
above
D/dalvikvm: VFY: replacing opcode 0x20 at 0x000f
Replacing OP_INSTANCE_OF
with ?something?, it doesn't say :)
Another possible workaround
If you look at android.support.v4.view.ViewCompat*
classes you will notice that not all those classes are used on all versions. The correct one is chosen at runtime (search for static final ViewCompatImpl IMPL
in ViewCompat.java
) and only that is loaded. This ensures that even at class load time there won't be any weirdness due to missing classes and is performant. You can do a similar architecture to prevent that method from loading on earlier API levels.
0 comments:
Post a Comment