1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275
  276
  277
  278
  279
  280
  281
  282
  283
  284
  285
  286
  287
  288
  289
  290
  291
  292
  293
  294
  295
  296
  297
  298
  299
  300
  301
  302
  303
  304
  305
  306
  307
  308
  309
  310
  311
  312
  313
  314
  315
  316
  317
  318
  319
  320
  321
  322
  323
  324
  325
  326
  327
  328
  329
  330
  331
  332
  333
  334
  335
  336
  337
  338
  339
  340
  341
  342
  343
  344
  345
  346
  347
  348
  349
  350
  351
  352
  353
  354
  355
  356
  357
  358
  359
  360
  361
  362
  363
  364
  365
  366
  367
  368
  369
  370
  371
  372
  373
  374
  375
  376
  377
  378
  379
  380
  381

docs / android_isolated_splits.md [blame]

# Isolated Splits

This doc aims to explain the ins and outs of using Isolated Splits on Android.

For an overview of apk splits and how to use them in Chrome, see
[android_dynamic_feature_modules.md].

[TOC]

## About

### What are Isolated Splits?

Isolated Splits is an opt-in feature (via [android:isolatedSplits] manifest
entry) that cause all feature splits in an application to have separate
`Context` objects, rather than being merged together into a single Application
`Context`. The `Context` objects have distict `ClassLoader` and `Resources`
instances. They are loaded on-demand instead of eagerly on launch.

With Isolated Splits, each feature split is loaded in its own ClassLoader, with
the parent split set as the parent ClassLoader.

[android:isolatedSplits]: https://developer.android.com/reference/android/R.attr#isolatedSplits

### Why use Isolated Splits?

The more DEX that is loaded on start-up, the more RAM and time it takes for
application code to start running. Loading less code on start-up is particularly
helpful for Chrome, since Chrome tends to spawn a lot of processes, and because
renderer processes require almost no DEX.

### What Splits Exist in Chrome?

Chrome's splits look like:

```
base.apk <-- chrome.apk <-- image_editor.apk
                        <-- feedv2.apk
                        <-- ...
```

* The browser process loads the `chrome` split on start-up, and other splits are
  loaded on-demand.
* Renderer and GPU processes do not load any feature splits.
  * The `chrome` split exists to minimize the amount of DEX loaded by renderer
    processes. However, it also enables faster browser process start-up by
    allowing DEX to be loaded concurrently with other start-up tasks.

### How are Isolated Splits Loaded?

There are two ways:
1) They can be loaded by Android Framework when handling an intent.
   * E.g.: If a feature split defines an Activity in its manifest, Android
     will create the split's Context and associate the Activity with it.
2) They can be loaded explicitly via [BundleUtils.createIsolatedSplitContext()].
   * The most common way to load in this way is through declaring a
     `ModuleInterface`, as described in [android_dynamic_feature_modules.md].

[BundleUtils.createIsolatedSplitContext()]: https://source.chromium.org/search?q=func:createIsolatedSplitContext&ss=chromium
[android_dynamic_feature_modules.md]: android_dynamic_feature_modules.md

## OS Support for Isolated Splits

Initial support was added in Android O. On earlier Android versions, all
feature splits are loaded during process start-up and merged into the
Application Context.

## OS Bugs

### Base ClassLoader used for Services in Splits (Android Pre-S)

Service Contexts are created with the base split's ClassLoader rather than the
split's ClassLoader.

Fixed in Android S. Bug: [b/169196314] (Googler only).

**Work-around:**

We use [SplitCompatService] (and siblings) to put a minimal service class in the
base split. They forward all calls to an implementation class, which can live
in the `chrome` split (or other splits). We also have a [compile-time check] to
enforce that no Service subclasses exist outside of the base split.

[b/169196314]: https://issuetracker.google.com/169196314
[SplitCompatService]: https://source.chromium.org/search?q=symbol:SplitCompatService&ss=chromium
[compile-time check]: https://source.chromium.org/chromium/chromium/src/+/main:build/android/gyp/create_app_bundle.py;l=446;drc=c4dd266492ad1e242161b415ac5a1d9fccd7a041

### Corrupted .odex (Android O MR1)

Android O MR1 has a bug where `bg-dexopt-job` (runs during maintenance windows)
breaks optimized dex files for Isolated Splits. The corrupt `.odex` files cause
extremely slow startup times.

**Work-around:**

We [preemptively run] `dexopt` so that `bg-dexopt-job` decides there is no work
to do. We trigger this from [PackageReplacedBroadcastReceiver] so that it
happens whenever Chrome is updated rather than when the user launches Chrome.

[preemptively run]: https://source.chromium.org/search?q=symbol:DexFixer.needsDexCompile&ss=chromium
[PackageReplacedBroadcastReceiver]: https://source.chromium.org/search?q=symbol:PackageReplacedBroadcastReceiver&ss=chromium

### Conflicting ClassLoaders #1

Tracked by [b/172602571], sometimes a split's parent ClassLoader is different
from the Application's ClassLoader. This manifests as odd-looking
`ClassCastExceptions` where `"TypeA cannot be cast to TypeA"` (since the two
`TypeAs` are from different ClassLoaders).

Tracked by UMA `Android.IsolatedSplits.ClassLoaderReplaced`. Occurs < 0.05% of
the time.

**Work-around:**

On Android O, there is no work-around. We just [detect and crash early].

Android P added [AppComponentFactory], which offers a hook that we use to
[detect and fix] ClassLoader mixups. The ClassLoader mixup also needs to be
corrected for `ContextImpl` instances, which we do via
[ChromeBaseAppCompatActivity.attachBaseContext()].

[b/172602571]: https://issuetracker.google.com/172602571
[detect and crash early]: https://source.chromium.org/search?q=crbug.com%2F1146745&ss=chromium
[AppComponentFactory]: https://developer.android.com/reference/android/app/AppComponentFactory
[detect and fix]: https://source.chromium.org/search?q=f:splitcompatappcomponentfactory&ss=chromium
[ChromeBaseAppCompatActivity.attachBaseContext()]: https://source.chromium.org/search?q=BundleUtils\.checkContextClassLoader&ss=chromium

### Conflicting ClassLoaders #2

Tracked by [b/172602571], when a new split language split or feature split is
installed, the ClassLoaders for non-base splits are recreated. Any reference to
a class from the previous ClassLoader (e.g. due to native code holding
references to them) will result in `ClassCastExceptions` where
`"TypeA cannot be cast to TypeA"`.

**Work-around:**

There is no work-around. This is a source of crashes. We could potentially
mitigate by restarting chrome when a split is installed.

### System.loadLibrary() Broken for Libraries in Splits

Tracked by [b/171269960], Android is not adding the apk split to the associated
ClassLoader's `nativeSearchPath`.  This means that `libfoo.so` within an
isolated split is not found by a call to `System.loadLibrary("foo")`.

**Work-around:**

Load libraries via `System.load()` instead.

```java
System.load(BundleUtils.getNativeLibraryPath("foo", "mysplitsname"));
```

[b/171269960]: https://issuetracker.google.com/171269960

### System.loadLibrary() Unusable from Split if Library depends on Another Loaded by Base Split

Also tracked by [b/171269960], maybe related to linker namespaces. If a split
tries to load `libfeature.so`, and `libfeature.so` has a `DT_NEEDED` entry for
`libbase.so`, and `libbase.so` is loaded by the base split, then the load will
fail.

**Work-around:**

Have base split load libraries from within splits. Proxy all JNI calls through
a class that exists in the base split.

### System.loadLibrary() Broken for Libraries in Splits on System Image

Also tracked by [b/171269960], Android's linker config (`ld.config.txt`) sets
`permitted_paths="/data:/mnt/expand"`, and then adds the app's `.apk` to an
allowlist. This allowlist does not contain apk splits, so library loading is
blocked by `permitted_paths` when the splits live on the `/system` partition.

**Work-around:**

Use compressed system image stubs (`.apk.gz` and `-Stub.apk`) so that Chrome is
extracted to the `/data` partition upon boot.

### Too Many Splits Break App Zygote

Starting with Android Q / TriChrome, Chrome uses an [Application Zygote]. As
part of initialization, Chrome's `ApplicationInfo` object is serialized into a
fixed size buffer. Each installed split increases the size of the
`ApplicationInfo` object, and can push it over the buffer's limit.

**Work-around:**

Do not add too many splits, and monitor the size of our `ApplicationInfo` object
([crbug/1298496]).

[crbug/1298496]: https://bugs.chromium.org/p/chromium/issues/detail?id=1298496
[Application Zygote]: https://developer.android.com/reference/android/app/ZygotePreload

### AppComponentFactory does not Hook Split ClassLoaders

`AppComponentFactory#instantiateClassLoader()` is meant to allow apps to hook
`ClassLoader` creation. The hook is called for the base split, but not for other
isolated splits. Tracked by [b/265583114]. There is no work-around.

[b/265583114]: https://issuetracker.google.com/265583114

### Incorrect Handling of Shared Libraries

Tracked by [b/265589431]. If an APK split has `<uses-library>` in its manifest,
the classloader for the split is meant to have that library added to it by the
framework. However, Android does not add the library to the classpath when a
split is dynamically installed, but instead adds it to the classpath of the base
split's classloader upon subsequent app launches.

**Work-around:**

 * Always add `<uses-library>` to the base split.

[b/265589431]: https://issuetracker.google.com/265589431

## Other Quirks & Subtleties

### System Image APKs

When distributing Chrome on Android system images, we generate a single `.apk`
file that contains all splits merged together (or rather, all splits whose
`AndroidManifest.xml` contain `<dist:fusing dist:include="true" />`). We do this
for simplicity; Android supports apk splits on the system image.

You can build Chrome's system `.apk` via:
```sh
out/Release/bin/trichrome_chrome_bundle build-bundle-apks --output-apks SystemChrome.apks --build-mode system
unzip SystemChrome.apks system/system.apk
```

Shipping a single `.apk` file simplifies distribution, but eliminates all the
benefits of Isolated Splits.

### Chrome's Application ClassLoader

A lot of Chrome's code uses the `ContextUtils.getApplicationContext()` as a
Context object. Rather than auditing all usages and replacing applicable ones
with the `chrome` split's Context, we [use reflection] to change the
Application instance's ClassLoader to point to the `chrome` split's ClassLoader.

[use reflection]: https://source.chromium.org/search?q=f:SplitChromeApplication%20replaceClassLoader&ss=chromium

### ContentProviders

Unlike other application components, ContentProviders are created on start-up
even when they are not the reason the process is being created. If a
ContentProvider were to be declared in a split, its split's Context would need
to be loaded during process creation, eliminating any benefit.

**Work-around:**

We declare all ContentProviders in the base split's `AndroidManifest.xml` and
enforce this with a [compile-time check]. ContentProviders that would pull in
significant amounts of code use [SplitCompatContentProvider] to delegate to a
helper class living within a split.

[compile-time check]: https://source.chromium.org/search?q=symbol:_MaybeCheckServicesAndProvidersPresentInBase&ss=chromium
[SplitCompatContentProvider]: https://source.chromium.org/search?q=symbol:SplitCompatContentProvider&ss=chromium

### JNI and ClassLoaders

When you call from native->Java (via `@CalledByNative`), there are two APIs
that Chrome could use to resolve the target class:

1) JNI API: [JNIEnv::FindClass()]
2) Java Reflection API:`ClassLoader.loadClass())`

Chrome uses #2. For methods within feature splits, `generate_jni()` targets
use `split_name = "foo"` to make the generated JNI code use the split's
ClassLoader.

[JNIEnv::FindClass()]: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp16027

### Accessing Android Resources

When resources live in a split, they must be accessed through a Context object
associated with that split. However:

* Bug: Chrome's build system [improperly handles ID conflicts] between splits.
* Bug: Splash screens [fail to load] for activities in Isolated Splits (unless
  associated resources are defined in the base split).
* Quirk: `RemoteViews`, notification icons, and other Android features that
  access resources by Package ID require resources to be in the base split when
  Isolated Splits are enabled.

**Work-around:**

Chrome [stores all Android resources in the base split]. There is [a crbug] to
track moving resources into splits, but it may prove too challenging.

[stores all Android resources in the base split]: https://source.chromium.org/search?q=recursive_resource_deps%5C%20%3D%5C%20true
[improperly handles ID conflicts]: https://crbug.com/1133898
[fail to load]: https://issuetracker.google.com/171743801
[a crbug]: https://crbug.com/1165782

### Inflating Layouts

Layouts should be inflated with an Activity Context so that
configuration-specific resources and themes are used. If layouts contain
references to View classes from different feature splits than the Activity's,
then the views' split ClassLoaders must be used.

**Work-around:**

Use the `ContextWrapper` created via: [BundleUtils.createContextForInflation()]

[BundleUtils.createContextForInflation()]: https://source.chromium.org/search?q=symbol:BundleUtils.createContextForInflation&ss=chromium

### onRestoreInstanceState with Classes From Splits

When Android kills an app, it normally calls `onSaveInstanceState()` to allow
the app to first save state. The saved state includes the class names of active
Fragments, RecyclerViews, and potentially other classes from splits. Upon
re-launch, these class names are used to reflectively instantiate instances.
`FragmentManager` uses the ClassLoader of the Activity to instantiate them,
and `RecyclerView` uses the ClassLoader associated with the `Bundle` object.
The reflection fails if the active Activity resides in a different spilt from
the reflectively instantiated classes.

**Work-around:**

Chrome stores the list of all splits that have been used for inflation during
[`onSaveInstanceState`] and then uses [a custom ClassLoader] to look within them
for classes that do not exist in the application's ClassLoader. The custom
ClassLoader is passed to `Bundle` instances in
`ChromeBaseAppCompatActivity.onRestoreInstanceState()`.

Having Android Framework call `Bundle.setClassLoader()` is tracked in
[b/260574161].

[`onSaveInstanceState`]: https://source.chromium.org/search?q=symbol:ChromeBaseAppCompatActivity.onSaveInstanceState&ss=chromium
[a custom ClassLoader]: https://source.chromium.org/search?q=symbol:ChromeBaseAppCompatActivity.getClassLoader&ss=chromium
[b/260574161]: https://issuetracker.google.com/260574161

### Calling Methods Across a Split Boundary

Due to having different ClassLoaders, package-private methods don't work across
the boundary, even though they will compile.

**Work around:**

Make any method public that you wish to call in another module, even if it's in
the same package.

### Proguarding Splits

"Proguarding" is the build step that performs whole-program optimization of Java
code, and "R8" is the program Chrome uses to do this. R8 currently supports
mapping input `.jar` files to output feature splits. If two feature splits share
a common GN `dep`, then its associated `.jar` will be promoted to the parent
split (or to the base split) by our [proguard.py] wrapper script.

This scheme means that if a single class from a large library is needed by, or
promoted to, the base split, then every class needed from that library by
feature splits will also remain in the base split. The feature request to have
R8 move code into deeper splits on a per-class basis is [b/225876019] (Googler
only).

[proguard.py]: https://source.chromium.org/search?q=symbol:_DeDupeInputJars%20f:proguard.py&ss=chromium
[b/225876019]: https://issuetracker.google.com/225876019

### Metadata in Splits

Metadata is queried on a per-app basis (not a per-split basis). E.g.:

```java
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
Bundle b = ai.metaData;
```

This bundle contains merged values from all fully-installed apk splits.

## Other Resources

 * [go/isolated-splits-dev-guide] (Googlers only).
 * [go/clank-isolated-splits-architecture] (Googlers only).

[go/isolated-splits-dev-guide]: http://go/isolated-splits-dev-guide
[go/clank-isolated-splits-architecture]: http://go/clank-isolated-splits-architecture