Android.bp 代码 overlay 的一些问题

起因

之前由于业务上需要,需要按照不同的配置项给同一个项目打包出两个不同的产物(有点类似于渠道包,不过比渠道包复杂得多)。

示例

假如有一个项目,你需要从源码中获取某个标识符,来确定打包的渠道,你需要在源码编译时候去决定打包的内容。当然,具体场景肯定没这么简单,是包含了大量具体的复杂逻辑执行的。于是你创建一个项目,包含了如下的代码:

1
2
3
4
5
6
7
8
9
10
├── src
│   └── com
│   └── gitofleonardo
│   └── overlaytest
│   ├── BaseIdentifierRetrieverImpl.java
│   ├── DomesticIdentifier.java
│   ├── IdentifierRetrieverImpl.java
│   ├── IIdentifier.java
│   ├── IIdentifierRetriever.java
│   └── MainActivity.java

IIdentifier 为一个标识符接口,用来返回具体的标识符:

1
2
3
public interface IIdentifier {
String getIdentifier();
}

IIdentifierRetriever 为用来获取这个标识符实现的接口:

1
2
3
public interface IIdentifierRetriever {
IIdentifier retrieverID();
}

然后,有一个用于国内渠道的标识符实现:

1
2
3
4
5
6
public class DomesticIdentifier implements IIdentifier {
@Override
public String getIdentifier() {
return "DomesticID";
}
}

并且配备了相应的 retriever 来获取这个标识符实例:

1
2
3
4
5
6
public class BaseIdentifierRetrieverImpl implements IIdentifierRetriever {
@Override
public DomesticIdentifier retrieverID() {
return new DomesticIdentifier();
}
}

最后,IdentifierRetrieverImpl 是你在使用时的具体实现:

1
2
public class IdentifierRetrieverImpl extends BaseIdentifierRetrieverImpl {
}

你需要在 MainActivity 中通过 IdentifierRetrieverImpl 来获取当前打包的标识符:

1
2
3
IdentifierRetrieverImpl retriever = new IdentifierRetrieverImpl();
String data = retriever.retrieverID().getIdentifier();
Log.i(TAG, data);

这样一来,你在使用的时候,就不需要关心 IdentifierRetrieverImpl 内部具体是怎么实现的,不需要关心编译的是什么 target

你只需要配置 Android.bp 来添加编译 target 就可以了。

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
filegroup {
name : "overlay-test-src",
srcs : [
"src/**/*.java",
"src/**/*.kt",
],
}
android_library {
name : "OverlayTestResLib",
srcs : [],
resource_dirs : ["res"],
static_libs : [
"com.google.android.material_material",
],
manifest : "AndroidManifest.xml",
sdk_version : "current",
min_sdk_version : min_overlaytest_sdk_version,
}

android_library {
name : "OverlayTestLib",
srcs : [
":overlay-test-src",
],
static_libs : [
"OverlayTestResLib",
],
platform_apis : true,
min_sdk_version : min_overlaytest_sdk_version,
sdk_version : "current"
}

android_app {
name : "HhvvgOverlayTest",
static_libs : [
"OverlayTestLib",
],
optimize: {
enabled: false,
},
sdk_version : "current",
min_sdk_version : min_overlaytest_sdk_version,
target_sdk_version: "current",
system_ext_specific: true,
}

通过 make HhvvgOverlayTest -j$(nproc) 即可得到产物。

添加海外产物

这时候,由于业务需要,你需要添加海外的标识符,以供海外的版本使用。这时候,你将国内跟海外的拆分成两个不同的 build target

1
2
3
4
5
6
7
8
9
10
11
12
├── src_build_domestic
│   └── com
│   └── gitofleonardo
│   └── overlaytest
│   ├── BaseIdentifierRetrieverImpl.java
│   └── DomesticIdentifier.java
└── src_build_overseas
└── com
└── gitofleonardo
└── overlaytest
├── BaseIdentifierRetrieverImpl.java
└── OverseasIdentifier.java

各自的 BaseIdentifierImpl 分别返回各自的 IIdentifier 类型。对 Android.bp稍加修改,即可实现这种需求:

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
filegroup {
name : "overlay-test-src_domestic",
srcs : [
"src_build_domestic/**/*.java",
"src_build_domestic/**/*.kt",
],
}

filegroup {
name : "overlay-test-src_overseas",
srcs : [
"src_build_overseas/**/*.java",
"src_build_overseas/**/*.kt",
],
}

//-------------------- Domestic target -----------------------//
android_library {
name : "OverlayTestDomesticLib",
srcs : [
":overlay-test-src",
":overlay-test-src_domestic",
],
static_libs : [
"OverlayTestResLib",
],
platform_apis : true,
min_sdk_version : min_overlaytest_sdk_version,
sdk_version : "current"
}

android_app {
name : "HhvvgOverlayTestDomestic",
static_libs : [
"OverlayTestDomesticLib",
],
optimize: {
enabled: false,
},
sdk_version : "current",
min_sdk_version : min_overlaytest_sdk_version,
target_sdk_version: "current",
system_ext_specific: true,
}

//-------------------- Overseas target -----------------------//
android_library {
name : "OverlayTestOverseasLib",
srcs : [
":overlay-test-src",
":overlay-test-src_overseas",
],
static_libs : [
"OverlayTestResLib",
],
platform_apis : true,
min_sdk_version : min_overlaytest_sdk_version,
sdk_version : "current"
}

android_app {
name : "HhvvgOverlayTestOverseas",
static_libs : [
"OverlayTestOverseasLib",
],
optimize: {
enabled: false,
},
sdk_version : "current",
min_sdk_version : min_overlaytest_sdk_version,
target_sdk_version: "current",
system_ext_specific: true,
}

一切都很顺利,运行起来功能正常没有问题。

1
2
3
4
5
// target HhvvgOverlayTestDomestic log output
MainActivity com.gitofleonardo.overlaytest I DomesticID

// target HhvvgOverlayTestOverseas log output
MainActivity com.gitofleonardo.overlaytest I OverseasID

于是你高高兴兴去找组长 review 合入,但组长说这个目录是不是有点太复杂了,可不可以简化下,直接在编译命令入手,让另一个 target 去合并入原本的 target 源码中,并亲自给你改了一版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android_app {
name : "HhvvgOverlayTestOverseas",
static_libs : [
"OverlayTestDomesticLib",
],
srcs : [
":overlay-test-src_overseas",
],
optimize: {
enabled: false,
},
sdk_version : "current",
min_sdk_version : min_overlaytest_sdk_version,
target_sdk_version: "current",
system_ext_specific: true,
}

这样就只需要给 overlay-test-src_overseas 新建目录就可以了,编译的时候由 overlay-test-src_overseas 去覆盖原本的 OverlayTestDomesticLib 中的源码。编译成功了,但是同时运行也报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Process: com.gitofleonardo.overlaytest, PID: 2727
java.lang.NoSuchMethodError: No virtual method retrieverID()Lcom/gitofleonardo/overlaytest/DomesticIdentifier; in class Lcom/gitofleonardo/overlaytest/IdentifierRetrieverImpl; or its super classes (declaration of 'com.gitofleonardo.overlaytest.IdentifierRetrieverImpl' appears in /data/app/~~UtmhUdACU8iRHNhrZ-8jzQ==/com.gitofleonardo.overlaytest-mtnkYfyBNu-ke2cIpPxowA==/base.apk)
at com.gitofleonardo.overlaytest.MainActivity.onCreate(MainActivity.java:23)
at android.app.Activity.performCreate(Activity.java:9079)
at android.app.Activity.performCreate(Activity.java:9057)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1531)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4188)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4393)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222)
at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133)
at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2773)
at android.os.Handler.dispatchMessage(Handler.java:109)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.app.ActivityThread.main(ActivityThread.java:8934)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)

居然报 no virtual method,编译的时候不是好好的吗?于是抱着怀疑的心情反编译看了下产物。

1
2
3
IdentifierRetrieverImpl retriever = new IdentifierRetrieverImpl();
String data = retriever.retrieverID().getIdentifier();
Log.i(TAG, data);

这不是跟源码里的一模一样吗,怎么会报错呢。我们来看一下具体报错信息:No virtual method retrieverID()Lcom/gitofleonardo/overlaytest/DomesticIdentifier; in class Lcom/gitofleonardo/overlaytest/IdentifierRetrieverImpl;,不对啊,我们编译的不是 overseas 版本的吗,为什么他会去调用 DomesticIdentifier retrieverID() 方法呢?看一下 BaseIdentifierRetrieverImpl,确实没错啊:

1
2
3
4
5
6
7
8
/* loaded from: classes.dex */
public class BaseIdentifierRetrieverImpl implements IIdentifierRetriever {
/* JADX DEBUG: Method merged with bridge method: retrieverID()Lcom/gitofleonardo/overlaytest/IIdentifier; */
@Override // com.gitofleonardo.overlaytest.IIdentifierRetriever
public OverseasIdentifier retrieverID() {
return new OverseasIdentifier();
}
}

但随后看了一下 MainActivitysmali 产物,恍然大悟:

1
2
.local v0, "retriever":Lcom/gitofleonardo/overlaytest/IdentifierRetrieverImpl;
invoke-virtual {v0}, Lcom/gitofleonardo/overlaytest/IdentifierRetrieverImpl;->retrieverID()Lcom/gitofleonardo/overlaytest/DomesticIdentifier;

反编译的源码中看不出来是调用了 DomesticIdentifier retrieverID(),但是 smali 中确实就是调用的这个方法。还记得编译 HhvvgOverlayTestOverseas 的源码依赖是怎么写的吗:

1
2
3
4
5
6
static_libs : [
"OverlayTestDomesticLib",
],
srcs : [
":overlay-test-src_overseas",
],

没错,上面由于 HhvvgOverlayTestOverseas 依赖的是 OverlayTestDomesticLib,所以 OverlayTestDomesticLib 会先进行编译,编译的时候由于 BaseIdentifierRetrieverImplretrieveID 方法返回了 DomesticIdentifier,所以 MainActivity 中的 invokevirtual 也是调用的这个方法。然后才编译的 :overlay-test-src_overseas 并替换原有同名产物,但此时 MainActivity 位于 OverlayTestDomesticLib 中,已经编译好了,不会再修改。

看来这种方法行不通,于是我的代码就顺理成章地合入了。

取消方法覆写返回子类

有人可能会说了,主播主播,你这不就是因为 domesticBaseIdentifierRetrieverImpl 返回类型返回了 IIdentifier 的具体子类吗,你直接声明返回 IIdentifier 不就可以了吗?诶,你还真是个小机灵鬼,这确实可以,只需要进行如下修改:

1
2
3
4
5
6
public class BaseIdentifierRetrieverImpl implements IIdentifierRetriever {
@Override
public IIdentifier retrieverID() {
return new DomesticIdentifier();
}
}

代码立马就可以跑了,但,这真是你想要的吗?某些业务确实是明确知道会返回这一个子类,如果只是返回的超类,则还需要额外增加类型判断。并且,随着业务复杂度的上升,可能不仅仅有方法调用,万一到时候有常量调用,被编译器内联优化了怎么办呢?这些未知性对于业务来说算得上是一个潜在的风险,不应冒这种风险来做一个不必要的改动。

渠道包行为对比

没写过渠道包,了解不多,一直从事的系统应用开发,后续看心情更新吧。

作者

Hhvvg

发布于

2025-03-16

更新于

2025-03-20

许可协议