Dynamic feature module support
使用 0.27.1 版本尝试了一下,第一次的时候编译没有问题,但是如果只在 application module 里面使用了 booster 插件,还是扫不到 dynamic feature 的代码,看了下实现是直接把 BoosterAppTransform 的 scopes 改成了 FULL_PROJECT,而 BoosterFeatureTransform 的 scopes 则是FULL_WITH_FEATURE。后来再重新试了一下,发现BoosterAppTransform的 scopes 又改回 FULL_WITH_FEATURE,这时候又会出现之前的编译错误(com.android.build.api.transform.TransformException: com.android.tools.r8.utils.FeatureClassMapping$FeatureMappingException)。看了一下这个错误,是因为base.jar 里面包含了 dynamic-feature.jar 的类,所以在 transformDexWithDexSplitterForDebug 会出现类重复的编译错误。
之前说 Transform 使用了 FULL_WITH_FEATURE的 scopes 后,要打开了混淆之后才能扫到dynamic feature 的类,是因为下面的代码:
// if variantScope.consumesFeatureJars(), add streams of classes from features or
// dynamic-features.
// The main dex list calculation for the bundle also needs the feature classes for reference
// only
if (variantScope.consumesFeatureJars() || variantScope.getNeedsMainDexListForBundle()) {
transformManager.addStream(
OriginalStream.builder(project, "metadata-classes")
.addContentTypes(TransformManager.CONTENT_CLASS)
.addScope(InternalScope.FEATURES)
.setArtifactCollection(
variantScope.getArtifactCollection(
METADATA_VALUES, PROJECT, METADATA_CLASSES))
.build());
}
@Override
public boolean consumesFeatureJars() {
return getType().isBaseModule()
&& getVariantConfiguration().getBuildType().isMinifyEnabled()
&& globalScope.hasDynamicFeatures();
}
只有满足 variantScope.consumesFeatureJars() || variantScope.getNeedsMainDexListForBundle()条件才会把 dynamic feature 添加到 Transform 的输入,而条件一则需要打开混淆才为 true,条件二则需要打开 multi dex。因此在 debug 版本没有打开混淆的情况下,即使使用了FULL_WITH_FEATURE 的 scopes 也扫不到 dynamic feature 的类。
而 3.2.1 不会出现上面的编译错误,3.5.1 会,则是由于因为下面的代码:
3.2.1
// Add transform to create merged runtime classes if this is a feature or dynamic-feature.
// Merged runtime classes are needed if code minification is enabled in multi-apk project.
if (variantData.getType().isFeatureSplit()) {
createMergeClassesTransform(variantScope);
}
3.5.1
// Add transform to create merged runtime classes if this is a feature, a dynamic-feature,
// or a base module consuming feature jars. Merged runtime classes are needed if code
// minification is enabled in a project with features or dynamic-features.
if (variantData.getType().isFeatureSplit() || variantScope.consumesFeatureJars()) {
createMergeClassesTransform(variantScope);
}
对比两个版本的代码可以知道,3.2.1 只有在 dynamic feature module 上 才会使用 MergeClassesTransform,而 3.5.1 则在base module 也有可能使用。前面提到的 base.jar 就是由 MergeClassesTransform 生成的。看了下 MergeClassesTransform 的实现,它只会通过 getReferencedScopes (FULL_PROJECT)读取 class 输入,并打包成 jar 输出到其他目录,不影响 Transform 的流程(Regarding Streams, this is a no-op transform as it does not write any output to any stream) 。既然它的 scopes 是 FULL_PROJECT,那它生成的 base.jar 为什么会包含 dynamic feature module 的类呢。原因如下:
@NonNull
private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
Set<? super Scope> requestedScopes = transform.getReferencedScopes();
if (requestedScopes.isEmpty()) {
return ImmutableList.of();
}
List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());
Set<ContentType> requestedTypes = transform.getInputTypes();
for (TransformStream stream : streams) {
// streams may contain more than we need. In this case, we'll provide the whole
// stream as-is since it's not actually consumed.
// It'll be up to the TransformTask to make sure that the content of the stream is
// usable (for instance when a stream
// may contain two scopes, these scopes could be combined or not, impacting consumption)
Set<ContentType> availableTypes = stream.getContentTypes();
Set<? super Scope> availableScopes = stream.getScopes();
Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
availableTypes);
Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);
if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
streamMatches.add(stream);
}
}
return streamMatches;
}
这里是生成 Referenced streams 的地方,从注释可以知道,streams 可能会包含多于 Transform 所声明的,对应到我们这里的情况就是多出了 dynamic feature 的 stream。而MergeClassesTransform直接把所有的Referenced inputs (streams)都打到 jar 包里面去,所以 base.jar 就包含了 dynamic feature module 的代码。
针对上面的两个问题,我自己想到的解决办法是:
- debug 也打开 混淆,但是这时候所使用的Proguard配置文件里面关闭所有的优化项,这样应该就能把 dynamic feature 添加到输入 stream 中去,同时又不会去混淆文件影响 debug,或者也可以使用 multi dex(对于大一点的项目基本都要使用,但是只打开 multidex 而不打开混淆会有个比较奇怪的问题,就是 Transform 能扫描到 dynamic feature 的类,也能修改并输出,因为看输出的 jar 包里面的class 是有插桩代码的,但是 dynamic feature 最终产出的 apk 却不包含 插桩代码,这里面的编译流程还没搞清楚)
- Hook MergeClassesTransform,将 dynamic feature 的 input 排除掉,不打入到 base.jar,尝试了一下编译出来的包运行正常,但不知道有没有其他坑。。。
不过感觉上面的两个方法都不是很优雅,抛出来大家讨论一下看有没有更好的方法~
0.27.1-SNAPSHOT 用 官方 demo 测试过,可以扫描到 feature module 下的 classes
用官方 demo 测试 0.27.1-SNAPSHOT,测试结果还是和之前一样,测试 case 及 输出如下:
测试 case:
输出:
minifyEnabled = true,能扫到 dynamic module 的类,但是编译失败
minifyEnabled = false,不能扫到dynamic module 的类

这样可以看到 dynamic feature 模块下的 classes
./gradlew :app:bundleDebug --info
:app:bundleRelease 确实会构建失败,看起来像是 Android Gradle Plugin 的 bug
@Yang-yongwen 上述的问题你是采用的方案2解决的吗?