295 lines
12 KiB
Markdown
295 lines
12 KiB
Markdown
|
|
# Class Verification Failures
|
||
|
|
|
||
|
|
[TOC]
|
||
|
|
|
||
|
|
## This document is obsolete
|
||
|
|
|
||
|
|
While class verification failures still exist, our Java optimizer, R8, has
|
||
|
|
solved this problem for us. Developers should not have to worry about this
|
||
|
|
problem unless there is a bug in R8. See [this bug](http://b/138781768) for where
|
||
|
|
they implemented this solution for us.
|
||
|
|
|
||
|
|
## What's this all about?
|
||
|
|
|
||
|
|
This document aims to explain class verification on Android, how this can affect
|
||
|
|
app performance, how to identify problems, and chromium-specific solutions. For
|
||
|
|
simplicity, this document focuses on how class verification is implemented by
|
||
|
|
ART, the virtual machine which replaced Dalvik starting in Android Lollipop.
|
||
|
|
|
||
|
|
## What is class verification?
|
||
|
|
|
||
|
|
The Java language requires any virtual machine to _verify_ the class files it
|
||
|
|
loads and executes. Generally, verification is extra work the virtual machine is
|
||
|
|
responsible for doing, on top of the work of loading the class and performing
|
||
|
|
[class initialization][1].
|
||
|
|
|
||
|
|
A class may fail verification for a wide variety of reasons, but in practice
|
||
|
|
it's usually because the class's code refers to unknown classes or methods. An
|
||
|
|
example case might look like:
|
||
|
|
|
||
|
|
```java
|
||
|
|
public class WindowHelper {
|
||
|
|
// ...
|
||
|
|
public boolean isWideColorGamut() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||
|
|
return mWindow.isWideColorGamut();
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Why does that fail?
|
||
|
|
|
||
|
|
In this example, `WindowHelper` is a helper class intended to help callers
|
||
|
|
figure out wide color gamut support, even on pre-OMR1 devices. However, this
|
||
|
|
class will fail class verification on pre-OMR1 devices, because it refers to
|
||
|
|
[`Window#isWideColorGamut()`][2] (new-in-OMR1), which appears to be an undefined
|
||
|
|
method.
|
||
|
|
|
||
|
|
### Huh? But we have an SDK check!
|
||
|
|
|
||
|
|
SDK checks are completely irrelevant for class verification. Although readers
|
||
|
|
can see we'll never call the new-in-OMR1 API unless we're on >= OMR1 devices,
|
||
|
|
the Oreo version of ART doesn't know `isWideColorGamut()` was added in next
|
||
|
|
year's release. From ART's perspective, we may as well be calling
|
||
|
|
`methodWhichDoesNotExist()`, which would clearly be unsafe.
|
||
|
|
|
||
|
|
All the SDK check does is protect us from crashing at runtime if we call this
|
||
|
|
method on Oreo or below.
|
||
|
|
|
||
|
|
### Class verification on ART
|
||
|
|
|
||
|
|
While the above is a mostly general description of class verification, it's
|
||
|
|
important to understand how the Android runtime handles this.
|
||
|
|
|
||
|
|
Since class verification is extra work, ART has an optimization called **AOT
|
||
|
|
("ahead-of-time") verification**¹. Immediately after installing an app, ART will
|
||
|
|
scan the dex files and verify as many classes as it can. If a class fails
|
||
|
|
verification, this is usually a "soft failure" (hard failures are uncommon), and
|
||
|
|
ART marks the class with the status `RetryVerificationAtRuntime`.
|
||
|
|
|
||
|
|
`RetryVerificationAtRuntime`, as the name suggests, means ART must try again to
|
||
|
|
verify the class at runtime. ART does so the first time you access the class
|
||
|
|
(right before class initialization/`<clinit>()` method). However, depending on
|
||
|
|
the class, this verification step can be very expensive (we've observed cases
|
||
|
|
which take [several milliseconds][3]). Since apps tend to initialize most of
|
||
|
|
their classes during startup, verification significantly increases startup time.
|
||
|
|
|
||
|
|
Another minor cost to failing class verification is that ART cannot optimize
|
||
|
|
classes which fail verification, so **all** methods in the class will perform
|
||
|
|
slower at runtime, even after the verification step.
|
||
|
|
|
||
|
|
*** aside
|
||
|
|
¹ AOT _verification_ should not be confused with AOT _compilation_ (another ART
|
||
|
|
feature). Unlike compilation, AOT verification happens during install time for
|
||
|
|
every application, whereas recent versions of ART aim to apply AOT compilation
|
||
|
|
selectively to optimize space.
|
||
|
|
***
|
||
|
|
|
||
|
|
## Chromium's solution
|
||
|
|
|
||
|
|
**Note:** This section is no longer relevant as R8 has fixed this for us. We intend
|
||
|
|
to remove these ApiHelperFor classes - see [this bug](https://crbug.com/1302156).
|
||
|
|
|
||
|
|
In Chromium, we try to avoid doing class verification at runtime by
|
||
|
|
manually out-of-lining all Android API usage like so:
|
||
|
|
|
||
|
|
```java
|
||
|
|
public class ApiHelperForOMR1 {
|
||
|
|
public static boolean isWideColorGamut(Window window) {
|
||
|
|
return window.isWideColorGamut();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class WindowHelper {
|
||
|
|
// ...
|
||
|
|
public boolean isWideColorGamut() {
|
||
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||
|
|
return ApiHelperForOMR1.isWideColorGamut(mWindow);
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This pushes the class verification failure out of `WindowHelper` and into the
|
||
|
|
new `ApiHelperForOMR1` class. There's no magic here: `ApiHelperForOMR1` will
|
||
|
|
fail class verification on Oreo and below, for the same reason `WindowHelper`
|
||
|
|
did previously.
|
||
|
|
|
||
|
|
The key is that, while `WindowHelper` is used on all API levels, it only calls
|
||
|
|
into `ApiHelperForOMR1` on OMR1 and above. Because we never use
|
||
|
|
`ApiHelperForOMR1` on Oreo and below, we never load and initialize the class,
|
||
|
|
and thanks to ART's lazy runtime class verification, we never actually retry
|
||
|
|
verification. **Note:** `list_class_verification_failures.py` will still list
|
||
|
|
`ApiHelperFor*` classes in its output, although these don't cause performance
|
||
|
|
issues.
|
||
|
|
|
||
|
|
### Creating ApiHelperFor\* classes
|
||
|
|
|
||
|
|
There are several examples throughout the code base, but such classes should
|
||
|
|
look as follows:
|
||
|
|
|
||
|
|
```java
|
||
|
|
/**
|
||
|
|
* Utility class to use new APIs that were added in O_MR1 (API level 27).
|
||
|
|
* These need to exist in a separate class so that Android framework can successfully verify
|
||
|
|
* classes without encountering the new APIs.
|
||
|
|
*/
|
||
|
|
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
||
|
|
public class ApiHelperForOMR1 {
|
||
|
|
private ApiHelperForOMR1() {}
|
||
|
|
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
* `@RequiresApi(Build.VERSION_CODES.O_MR1)`: this tells Android Lint it's OK to
|
||
|
|
use OMR1 APIs since this class is only used on OMR1 and above. Substitute
|
||
|
|
`O_MR1` for the [appropriate constant][4], depending when the APIs were
|
||
|
|
introduced.
|
||
|
|
* Don't put any `SDK_INT` checks inside this class, because it must only be
|
||
|
|
called on >= OMR1.
|
||
|
|
* R8 is smart enough not to inline methods where doing so would introduce
|
||
|
|
verification failures (b/138781768)
|
||
|
|
|
||
|
|
### Out-of-lining if your method has a new type in its signature
|
||
|
|
|
||
|
|
Sometimes you'll run into a situation where a class **needs** to have a method
|
||
|
|
which either accepts a parameter which is a new type or returns a new type
|
||
|
|
(e.g., externally-facing code, such as WebView's glue layer). Even though it's
|
||
|
|
impossible to write such a class without referring to the new type, it's still
|
||
|
|
possible to avoid failing class verification. ART has a useful optimization: if
|
||
|
|
your class only moves a value between registers (i.e., it doesn't call any
|
||
|
|
methods or fields on the value), then ART will not check for the existence of
|
||
|
|
that value's type. This means you can write your class like so:
|
||
|
|
|
||
|
|
```java
|
||
|
|
public class FooBar {
|
||
|
|
// FooBar needs to have the getNewTypeInAndroidP method, but it would be
|
||
|
|
// expensive to fail verification. This method will only be called on >= P
|
||
|
|
// but other methods on the class will be used on lower OS versions (and
|
||
|
|
// also can't be factored into another class).
|
||
|
|
public NewTypeInAndroidP getNewTypeInAndroidP() {
|
||
|
|
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
|
||
|
|
// Stores a NewTypeInAndroidP in the return register, but doesn't do
|
||
|
|
// anything else with it
|
||
|
|
return ApiHelperForP.getNewTypeInAndroidP();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
|
||
|
|
@VerifiesOnP
|
||
|
|
@RequiresApi(Build.VERSION_CODES.P)
|
||
|
|
public class ApiHelperForP {
|
||
|
|
public static NewTypeInAndroidP getNewTypeInAndroidP() {
|
||
|
|
return new NewTypeInAndroidP();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Note:** this only works in ART (L+), not Dalvik (KitKat and earlier).
|
||
|
|
|
||
|
|
## Investigating class verification failures
|
||
|
|
|
||
|
|
Class verification is generally surprising and nonintuitive. Fortunately, the
|
||
|
|
ART team have provided tools to investigate errors (and the chromium team has
|
||
|
|
built helpful wrappers).
|
||
|
|
|
||
|
|
### Listing failing classes
|
||
|
|
|
||
|
|
The main starting point is to figure out which classes fail verification (those
|
||
|
|
which ART marks as `RetryVerificationAtRuntime`). This can be done for **any
|
||
|
|
Android app** (it doesn't have to be from the chromium project) like so:
|
||
|
|
|
||
|
|
```shell
|
||
|
|
# Install the app first. Using Chrome as an example.
|
||
|
|
autoninja -C out/Default chrome_public_apk
|
||
|
|
out/Default/bin/chrome_public_apk install
|
||
|
|
|
||
|
|
# List all classes marked as 'RetryVerificationAtRuntime'
|
||
|
|
build/android/list_class_verification_failures.py --package="org.chromium.chrome"
|
||
|
|
W 0.000s Main Skipping deobfuscation because no map file was provided.
|
||
|
|
first.failing.Class
|
||
|
|
second.failing.Class
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
"Skipping deobfuscation because no map file was provided" is a warning, since
|
||
|
|
many Android applications (including Chrome's release builds) are built with
|
||
|
|
proguard (or similar tools) to obfuscate Java classes and shrink code. Although
|
||
|
|
it's safe to ignore this warning if you don't obfuscate Java code, the script
|
||
|
|
knows how to deobfuscate classes for you (useful for `is_debug = true` or
|
||
|
|
`is_java_debug = true`):
|
||
|
|
|
||
|
|
```shell
|
||
|
|
build/android/list_class_verification_failures.py --package="org.chromium.chrome" \
|
||
|
|
--mapping=<path/to/file.mapping> # ex. out/Release/apks/ChromePublic.apk.mapping
|
||
|
|
android.support.design.widget.AppBarLayout
|
||
|
|
android.support.design.widget.TextInputLayout
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
Googlers can also download mappings for [official
|
||
|
|
builds](http://go/webview-official-builds).
|
||
|
|
|
||
|
|
### Understanding the reason for the failure
|
||
|
|
|
||
|
|
ART team also provide tooling for this. You can configure ART on a rooted device
|
||
|
|
to log all class verification failures (during installation), at which point the
|
||
|
|
cause is much clearer:
|
||
|
|
|
||
|
|
```shell
|
||
|
|
# Enable ART logging (requires root). Note the 2 pairs of quotes!
|
||
|
|
adb root
|
||
|
|
adb shell setprop dalvik.vm.dex2oat-flags '"--runtime-arg -verbose:verifier"'
|
||
|
|
|
||
|
|
# Restart Android services to pick up the settings
|
||
|
|
adb shell stop && adb shell start
|
||
|
|
|
||
|
|
# Optional: clear logs which aren't relevant
|
||
|
|
adb logcat -c
|
||
|
|
|
||
|
|
# Install the app and check for ART logs
|
||
|
|
adb install -d -r out/Default/apks/ChromePublic.apk
|
||
|
|
adb logcat | grep 'dex2oat'
|
||
|
|
...
|
||
|
|
... I dex2oat : Soft verification failures in boolean org.chromium.content.browser.selection.SelectionPopupControllerImpl.b(android.view.ActionMode, android.view.Menu)
|
||
|
|
... I dex2oat : boolean org.chromium.content.browser.selection.SelectionPopupControllerImpl.b(android.view.ActionMode, android.view.Menu): [0xF0] couldn't find method android.view.textclassifier.TextClassification.getActions ()Ljava/util/List;
|
||
|
|
... I dex2oat : boolean org.chromium.content.browser.selection.SelectionPopupControllerImpl.b(android.view.ActionMode, android.view.Menu): [0xFA] couldn't find method android.view.textclassifier.TextClassification.getActions ()Ljava/util/List;
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
*** note
|
||
|
|
**Note:** you may want to avoid `adb` wrapper scripts (ex.
|
||
|
|
`out/Default/bin/chrome_public_apk install`). These scripts cache the package
|
||
|
|
manager state to optimize away idempotent installs. However in this case, we
|
||
|
|
**do** want to trigger idempotent installs, because we want to re-trigger AOT
|
||
|
|
verification.
|
||
|
|
***
|
||
|
|
|
||
|
|
In the above example, `SelectionPopupControllerImpl` fails verification on Oreo
|
||
|
|
(API 26) because it refers to [`TextClassification.getActions()`][5], which was
|
||
|
|
added in Pie (API 28). If `SelectionPopupControllerImpl` is used on pre-Pie
|
||
|
|
devices, then `TextClassification.getActions()` must be out-of-lined.
|
||
|
|
|
||
|
|
## See also
|
||
|
|
|
||
|
|
* Bugs or questions? Contact ntfschr@chromium.org
|
||
|
|
* ART team's Google I/O talks: [2014](https://youtu.be/EBlTzQsUoOw) and later
|
||
|
|
years
|
||
|
|
* Analysis of class verification in Chrome and WebView (Google-only
|
||
|
|
[doc](http://go/class-verification-chromium-analysis))
|
||
|
|
* Presentation on class verification in Chrome and WebView (Google-only
|
||
|
|
[slide deck](http://go/class-verification-chromium-slides))
|
||
|
|
|
||
|
|
[1]: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.5
|
||
|
|
[2]: https://developer.android.com/reference/android/view/Window.html#isWideColorGamut()
|
||
|
|
[3]: https://bugs.chromium.org/p/chromium/issues/detail?id=838702
|
||
|
|
[4]: https://developer.android.com/reference/android/os/Build.VERSION_CODES
|
||
|
|
[5]: https://developer.android.com/reference/android/view/textclassifier/TextClassification.html#getActions()
|