summaryrefslogtreecommitdiff
path: root/chromium/docs/ui/android/bytecode_rewriting.md
blob: 6c2fb6c8c24b762aa484c2c8cd096f6a94fa4311 (plain)
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
# Bytecode Rewriting

## TL;DR

We modify the return type of AndroidX's `Fragment.getActivity()` method from `FragmentActivity`
to `Activity` to more easily add Fragments from multiple ClassLoaders into the same Fragment tree.

## Why?

In Java, two instances of the same class loaded from two different ClassLoaders aren't compatible
with each other at runtime. Because AndroidX libraries are bundled with each APK that use them, and
different APKs are loaded with different ClassLoaders, AndroidX classes from one APK cannot be used
with the same class from another APK. This causes problems for Fragment-based UIs in WebLayer, where
the implementation is in a different ClassLoader than the embedding app, so its Fragments cannot be
added to the embedding app's Fragment tree.

Note that this issue doesn't apply to Framework or standard library classes. Java ClassLoaders form
a tree, and if a ClassLoader can't find a particular class, it delegates to its parent.
The leaf ClassLoader used to load an app is responsible for loading the app's class files, while one
of its parents will load system-level classes. Because AndroidX classes get loaded by the
app-specific ClassLoader, different apps will load mutually incompatible versions, but a class like
Activity, which gets loaded from a parent ClassLoader, *will* be compatible between APKs at runtime,
because it ends up gets loaded from a common ClassLoader.

To get around this incompatibility, we can create a RemoteFragment that lives in the embedding app,
and a RemoteFragmentImpl that lives in another APK. The RemoteFragment can be added to the original
Fragment tree, and will forward all Fragment lifecycle events over an AIDL interface to
RemoteFragmentImpl. The fake Fragment in the secondary APK (RemoteFragmentImpl) can create a
FragmentController, which allows it to become the host of its own Fragment tree, and any UIs from
the secondary ClassLoaded can be added to this new Fragment tree that's been essentially grafted
onto the original.

This mostly works, but runs into issues when Fragments call `Fragment.getActivity()`, which they do
a lot. The getActivity implementation takes the Activity given to the FragmentController constructor
(via FragmentHostCallback), and casts it to a FragmentActivity before returning it. The original
Activity will typically be a FragmentActivity from the embedding app's ClassLoader, which means that
due to the aforementioned issues, this cast will fail when run in the secondary ClassLoader's
Fragment class because even though the Activity is a FragmentActivity, it's from the wrong
ClassLoader.

To fix this second issue, we modify the bytecode of `Fragment.getActivity()` in the AndroidX
prebuilt .aar files to return a plain Activity instead of a FragmentActivity. This allows us to
continue calling getActivity() as normal. Note that this does mean FragmentActivity-specific methods
can no longer be used in Fragments, but there were no uses of them in Chromium that couldn't be
trivially removed as of late 2020.

## How does it work?

The bytecode rewriting happens at build time by
[FragmentActivityReplacer](https://source.chromium.org/chromium/chromium/src/+/main:build/android/bytecode/java/org/chromium/bytecode/FragmentActivityReplacer.java),
which is specified as a bytecode rewriter via the `bytecode_rewriter_target` rule.  Compilation errors
related to this should get detected by
[compile_java.py](https://source.chromium.org/chromium/chromium/src/+/main:build/android/gyp/compile_java.py),
and print a message pointing users here, which is likely why you're reading this :)

If you need to apply FragmentActivityReplacer to a given target then add …

```
bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer"
```

… to the build configuration for that target.

If you still get a build or runtime error related to a FragmentActivity after adding in the
replacer, then the library may actually rely on the Activity being a FragmentActivity. If so, it
likely won't work with WebLayer as-is. If you know there are no plans to use the library in
WebLayer, you can try adding this instead:

```
bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer_single_androidx"
```

## How does this affect my code?

The goal is for these changes to be as transparent as possible; most code shouldn't run into issues.
However, if there's no way around calling a FragmentActivity method in your code, **and your
Fragment is in //chrome**, you could cast the Activity to a FragmentActivity as AndroidX used to do.
If your Fragment is in //components, FragmentActivity methods will likely not work directly, and may
need to be forwarded to an implementation in the original ClassLoader somehow.

The more important thing to note is that in a multi-ClassLoader world, `getActivity()` and
`getContext()` will typically return two different objects, so we need to be more careful about which
method we call, particularly for code in //components. `getActivity()` will return the Activity from
the original ClassLoader, and should be used to for calls like `.finish*()`, `.setTitle(), and
`.startActivity()` (which live in Activity anyway). When loading resources, you should default to
calling `getContext()`, as resources usually come from the same ClassLoader as the Fragment, and the
Context should be configured to load them correctly.

As a rule of thumb, prefer `getContext()` to `getActivity()`, unless you need to operate on the
Activity itself, or you know the resource or setting you need belongs to the original Activity.