起因 之前由于业务上需要,需要按照不同的配置项给同一个项目打包出两个不同的产物(有点类似于渠道包,不过比渠道包复杂得多)。
示例 假如有一个项目,你需要从源码中获取某个标识符,来确定打包的渠道,你需要在源码编译时候去决定打包的内容。当然,具体场景肯定没这么简单,是包含了大量具体的复杂逻辑执行的。于是你创建一个项目,包含了如下的代码:
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 public class BaseIdentifierRetrieverImpl implements IIdentifierRetriever { @Override public OverseasIdentifier retrieverID () { return new OverseasIdentifier (); } }
但随后看了一下 MainActivity
的 smali
产物,恍然大悟:
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
会先进行编译,编译的时候由于 BaseIdentifierRetrieverImpl
的 retrieveID
方法返回了 DomesticIdentifier
,所以 MainActivity
中的 invokevirtual
也是调用的这个方法。然后才编译的 :overlay-test-src_overseas
并替换原有同名产物,但此时 MainActivity
位于 OverlayTestDomesticLib
中,已经编译好了,不会再修改。
看来这种方法行不通,于是我的代码就顺理成章地合入了。
取消方法覆写返回子类 有人可能会说了,主播主播,你这不就是因为 domestic
的 BaseIdentifierRetrieverImpl
返回类型返回了 IIdentifier
的具体子类吗,你直接声明返回 IIdentifier
不就可以了吗?诶,你还真是个小机灵鬼,这确实可以,只需要进行如下修改:
1 2 3 4 5 6 public class BaseIdentifierRetrieverImpl implements IIdentifierRetriever { @Override public IIdentifier retrieverID () { return new DomesticIdentifier (); } }
代码立马就可以跑了,但,这真是你想要的吗?某些业务确实是明确知道会返回这一个子类,如果只是返回的超类,则还需要额外增加类型判断。并且,随着业务复杂度的上升,可能不仅仅有方法调用,万一到时候有常量调用,被编译器内联优化了怎么办呢?这些未知性对于业务来说算得上是一个潜在的风险,不应冒这种风险来做一个不必要的改动。
渠道包行为对比 没写过渠道包,了解不多,一直从事的系统应用开发,后续看心情更新吧。