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.
setVoice21()
directly? – CataphoresissetVoice
. Hence the question – Rubberneck