sample-child插件pluginCompileOnly公共库 sample-base-lib , 打包时并 dependOn公共 sample-base插件,DataBinding使用会报错
大神您好! 我们项目目前在使用多插件单进程这种场景。在使用过程中,出现运行时报databinding找不到标签错误:java.lang.RuntimeException: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0,目前不知道什么原因,所以求助与您。我用你提供的Demo复现出来了。 测试Demo地址:https://github.com/lilidejing/feature-shadow-test-databinding/tree/feature-shadow-test-databinding
demo测试,在project/sample/source/sample-plugin目录下一共有3个插件,分别是sample-app、sample-child、sample-base,还有一个公共库sample-base-lib。 复现步骤: 1,打包插件:打包插件时是打包的 sample-app插件,它里面有脚本会自动将上面3个插件打成一个zip插件包。 2,打包宿主:直接打包project/sample/source/sample-host项目即可 3,运行宿主,默认界面会选中sample-child,点击启动插件,进入界面即会崩溃报错。
sample-child的报错界面,里面有说明3种databinding使用方式和报错情况。第三种使用方式不会报错,但返回的databinding也为null: https://github.com/lilidejing/feature-shadow-test-databinding/blob/feature-shadow-test-databinding/projects/sample/source/sample-plugin/sample-child/src/main/java/com/test/TestActivitySkip3.java
有一种情况,三种databinding使用方式都不会报错:
1,把sample-child的gradle文件下面两句代码注释掉: pluginCompileOnly project(":sample-base-lib") normalImplementation project(":sample-base-lib")
2,把 sample-app 的gradle文件中打包packagePlugin 脚本中的sampleChild {} 中的dependsOn = ['sample-base']注释掉
但是我们项目不能这么做,因为需要用到dependsOn这个属性
以上就是我提供的信息,抽空帮忙看下什么原因,谢谢!
最开始说的报错信息如下: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.tencent.shadow.sample.host/com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity}: java.lang.RuntimeException: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0 2025-09-04 17:01:59.026 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3432) 2025-09-04 17:01:59.027 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3596) 2025-09-04 17:01:59.027 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 2025-09-04 17:01:59.028 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 2025-09-04 17:01:59.028 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 2025-09-04 17:01:59.028 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2067) 2025-09-04 17:01:59.029 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.os.Handler.dispatchMessage(Handler.java:106) 2025-09-04 17:01:59.029 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.os.Looper.loop(Looper.java:223) 2025-09-04 17:01:59.030 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.ActivityThread.main(ActivityThread.java:7705) 2025-09-04 17:01:59.030 7380-7380 DEBUG com.tencent.shadow.sample.host E at java.lang.reflect.Method.invoke(Native Method) 2025-09-04 17:01:59.031 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 2025-09-04 17:01:59.031 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952) 2025-09-04 17:01:59.031 7380-7380 DEBUG com.tencent.shadow.sample.host E Caused by: java.lang.RuntimeException: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0 2025-09-04 17:01:59.032 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.tencent.shadow.core.loader.delegates.ShadowActivityDelegate.onCreate(ShadowActivityDelegate.kt:159) 2025-09-04 17:01:59.032 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.tencent.shadow.core.runtime.container.PluginContainerActivity.onCreate(PluginContainerActivity.java:84) 2025-09-04 17:01:59.033 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.Activity.performCreate(Activity.java:7994) 2025-09-04 17:01:59.033 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.Activity.performCreate(Activity.java:7978) 2025-09-04 17:01:59.034 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1310) 2025-09-04 17:01:59.034 7380-7380 DEBUG com.tencent.shadow.sample.host E at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3405) 2025-09-04 17:01:59.034 7380-7380 DEBUG com.tencent.shadow.sample.host E ... 11 more 2025-09-04 17:01:59.035 7380-7380 DEBUG com.tencent.shadow.sample.host E Caused by: java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0 2025-09-04 17:01:59.035 7380-7380 DEBUG com.tencent.shadow.sample.host E at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:185) 2025-09-04 17:01:59.036 7380-7380 DEBUG com.tencent.shadow.sample.host E at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:152) 2025-09-04 17:01:59.036 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.test.TestActivitySkip3.onCreate(TestActivitySkip3.java:41) 2025-09-04 17:01:59.036 7380-7380 DEBUG com.tencent.shadow.sample.host E at com.tencent.shadow.core.loader.delegates.ShadowActivityDelegate.onCreate(ShadowActivityDelegate.kt:156) 2025-09-04 17:01:59.037 7380-7380 DEBUG com.tencent.shadow.sample.host E ... 16 more 2025-09-04 17:01:59.037 7380-7380 DEBUG com.tencent.shadow.sample.host E Back traces ends.
dependsOn属性只是将这些插件的ClassLoader的parent关系指定一下,允许它们跨ClassLoader加载类。但一个Android app,也就是一个插件,它实际上还包含资源文件,so文件等。这些其他文件dependsOn属性是没有任何处理的。
资源相关的接口一般都是从一个context对象上发起的,这个context肯定会对应一个插件,那它其实只能加载到自己打包的资源。 所以你应该检查一下那些找不到的资源ID是哪个插件里面的,它的context是哪个插件。
更要注意的是,那些XML资源本身是支持继承的,如果要继承的两个文件处于两个不同的插件中,肯定是不行的了。
但上面说的也只是现有代码的能力。如果你更进一步研究Android系统的资源加载机制,应该能发现它本来是有共享资源的设计的。所以才有资源ID分区的设计。所以理论上是能实现跨apk共享资源的。
多插件的场景更复杂,也更需要你在使用前了解插件框架的方方面面。不能拿这个框架当黑盒用。
dependsOn属性只是将这些插件的ClassLoader的parent关系指定一下,允许它们跨ClassLoader加载类。但一个Android app,也就是一个插件,它实际上还包含资源文件,so文件等。这些其他文件dependsOn属性是没有任何处理的。
资源相关的接口一般都是从一个context对象上发起的,这个context肯定会对应一个插件,那它其实只能加载到自己打包的资源。 所以你应该检查一下那些找不到的资源ID是哪个插件里面的,它的context是哪个插件。
更要注意的是,那些XML资源本身是支持继承的,如果要继承的两个文件处于两个不同的插件中,肯定是不行的了。
但上面说的也只是现有代码的能力。如果你更进一步研究Android系统的资源加载机制,应该能发现它本来是有共享资源的设计的。所以才有资源ID分区的设计。所以理论上是能实现跨apk共享资源的。
多插件的场景更复杂,也更需要你在使用前了解插件框架的方方面面。不能拿这个框架当黑盒用。
感谢您的回复!非常感谢!
我用shadow之前有去做过大量的了解,也了解dependOn的机制,现在遇到的问题比较疑惑。
如果dependsOn属性只是将这些插件的ClassLoader的parent关系指定一下,允许它们跨ClassLoader加载类,但是以下现象让我感到疑惑。
我将 sample-base和 sample-base-lib的 gradle文件中buildFeatures { buildConfig = true viewBinding = true dataBinding = true } 改为 buildFeatures { buildConfig = true viewBinding = false dataBinding = false },即禁用databinding。
然后sample-child的gradle文件不禁用databinding,继续使用databinding。
运行启动sample-child的databinding界面会报下面的错误:
关键错误信息:
java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/databinding/DataBinderMapperImpl;
com.tencent.shadow.sample.host E at androidx.databinding.DataBindingUtil.
日志显示运行时 在sample-base-plugin-debug.apk、和sample-runtime-debug.apk 中找不到DataBinderMapperImpl,我认为是正常的,因为他们中没有声明dataBinding = true。 但为什么不继续到 sample-child-plugin-debug.apk (sample-child生成的apk)中去找DataBinderMapperImpl这个类了?
所以上面那个问题, java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0,是不是因为, Tag: layout/layout_activity_skip2_0 是注册在sample-child-plugin-debug.apk (sample-child生成的apk)中生成的DataBinderMapperImpl中。 然后运行时,却去 sample-base-plugin-debug.apk和sample-runtime-debug.apk 中的DataBinderMapperImpl去寻找,所以找不到?
我理解的如果到依赖的公共插件找不到DataBinderMapperImpl的话,应该到当前插件中去寻找才对。
谢谢!
你似乎没有关心是哪个classloader找不到类。比如3个类依次依赖,A->B->C,但又处于3个不同的classloader中。那你知道加载A的过程吗?加载A肯定也需要加载B和C吧?那加载它们3个的是同一个classloader吗?如果都是双亲委派的逻辑,都会搜索哪个classloader呢?你可能需要理清这个问题。
应该注意到类总是用当前类的classloader去加载其他类。
你说的这个情况可能就是B类在用B的classloader查找C,但实际上C和A在同一个classloader中,而B不会去A的classloader中查找类。
建议debug断点到pluginclassloader中观察加载过程。
谢谢回复! 已找到最开始问题java.lang.IllegalArgumentException: View is not a binding layout. Tag: layout/layout_activity_skip2_0的根本原因。
我简单描述下场景: 1,插件之间的关系:插件A 依赖 插件B ,即插件A dependOn 插件B。, 2,布局文件layout_activity_skip2所在位置:在插件A中 3,插件A和插件B都允许使用dataBinding。 4,当编译时,插件A会生成一个类:androidx.databinding.DataBinderMapperImpl,里面会存放Tag: layout/layout_activity_skip2_0。 插件B也会生成一个类:androidx.databinding.DataBinderMapperImpl,里面是未存放 Tag: layout/layout_activity_skip2_0。
原因: 因为存在dependOn的关系,所以运行时classLoader会用插件B的specialClassLoader去加载androidx.databinding.DataBinderMapperImpl,加载成功。 但实际上 加载的 androidx.databinding.DataBinderMapperImpl是插件B编译时生成的,里面不存在Tag: layout/layout_activity_skip2_0这个标签,所以就报了 View is not a binding layout. Tag: layout/layout_activity_skip2_0这个错误。
接下来我们这边打算想一下解决方案,在现有的机制下,dependOn场景 如何让插件A能正常使用DataBinding,或者改用ViewBinding。 不知道您那边有没有好的建议?
谢谢!
参考源码: `override fun loadClass(className: String, resolve: Boolean): Class<> { var clazz: Class<>? = findLoadedClass(className)
if (clazz == null) {
if (specialClassLoader == null) {
// 没有依赖其他插件 -> 正常双亲委派
return super.loadClass(className, resolve)
}
// runtime 类,强制走宿主 loader
if (className.subStringBeforeDot() == "com.tencent.shadow.core.runtime") {
return loaderClassLoader.loadClass(className)
}
// 在白名单里 -> 走宿主 loader
if (className.inPackage(allHostWhiteTrie)) {
return super.loadClass(className, resolve)
}
// 先尝试 specialClassLoader
try {
clazz = specialClassLoader.loadClass(className)
} catch (e: ClassNotFoundException) {
// ignore
}
if (clazz == null) {
clazz = findClass(className) // 从插件自己的 dex 里找
}
}
return clazz
}`
如果是因为多个插件有重名的DataBinderMapperImpl,而实际上它们又是不同的类。然后这些插件还有继承关系。为了让它们加载不冲突,我们应该是需要给这个类在不同插件里改名吧。可以试试用添加一个transform,在编译后改名。可以参考shadow的transform实现。
如果是因为多个插件有重名的DataBinderMapperImpl,而实际上它们又是不同的类。然后这些插件还有继承关系。为了让它们加载不冲突,我们应该是需要给这个类在不同插件里改名吧。可以试试用添加一个transform,在编译后改名。可以参考shadow的transform实现。
很不错的建议,谢谢! 实际上binding = DataBindingUtil.bind(findViewById(R.id.root)); 这句执行时,会到DataBindingUtil中的静态变量sMapper(DataBinderMapperImpl)中去找对应的tag,它一定会去找DataBinderMapperImpl对象,所以重命名DataBinderMapperImpl没有用。 所以实际上是 DataBindingUtil 这个类也存在 多个插件有重名的情况、又是不同的类、然后这些插件还有继承关系 。 但是 DataBindingUtil 属于系统的API,插件编译时不会编译到build目录下,用transform的方案无法重命名DataBindingUtil 。
如果万一要用transform方案去做 ,是不是要参考你的代理Activity方案transform去实现一个DataBindingUtil 代理才行?
DataBindingUtil 部分源码参考: `package androidx.databinding;
/**
- Utility class to create {@link ViewDataBinding} from layouts. */ public class DataBindingUtil { private static DataBinderMapper sMapper = new DataBinderMapperImpl(); private static DataBindingComponent sDefaultComponent = null; `
那也不一定非得改名。在PluginClassloader里改加载逻辑也是可以的吧,这些重名的类就不要走类似双亲委派的逻辑了。
1,我们最后的处理方案是在 PluginClassloader里改加载逻辑,让DataBindingUtil 相关的类不走 类似双亲委派的逻辑了。可以解决以上DataBinding问题。 2,但是dependOn这种场景,又遇到了几个新问题。 这些新问题都通过在 PluginClassloader里改加载逻辑,改为 指定类不走 类似双亲委派的逻辑可以解决。
问题来了: 因为这种情况后续可能会遇到很多,所以我想问的是,如果插件A 依赖 插件B ,即插件A dependOn 插件B 这种场景,我的PluginClassLoader能否都改为:插件A优先加载插件A自己的类,如果找不到,再走之前的逻辑,它会再加载 插件B的类 ?这样会有什么问题没?是否存在风险?
我们的PluginClassLoader类的loadClass方法最终具体修改代码如下,帮忙看下是否存在风险:
` override fun loadClass(className: String, resolve: Boolean): Class<*> {
var clazz: Class<*>? = findLoadedClass(className)
if (clazz == null) {
//specialClassLoader 为null 表示该classLoader依赖了其他的插件classLoader,需要遵循双亲委派
if (specialClassLoader == null) {
// return super.loadClass(className, resolve)
// ======================== START: 最終修正版 ========================
// 我們採用「自己優先」策略
// 這是為了防止被依賴的插件 B 的同名類別污染
var suppressed: ClassNotFoundException? = null
try {
// 1. 優先在自己的 dex 中尋找 (findClass)
clazz = findClass(className)!!
} catch (e: ClassNotFoundException) {
suppressed = e
//在自己的 dex 中沒有找到
}
if (clazz == null) {
try {
// 2. 如果自己沒有(例如它是一個純粹的 runtime 基礎類別),再退回到正常的依賴插件搜尋邏輯
clazz = super.loadClass(className, resolve)
} catch (e: ClassNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && suppressed != null) {
e.addSuppressed(suppressed)
}
throw e
}
}
return clazz!!
// ========================= END: 最終修正版 =========================
}
//插件依赖跟loader一起打包的runtime类,如ShadowActivity,从loader的ClassLoader加载
if (className.subStringBeforeDot() == "com.tencent.shadow.core.runtime") {
return loaderClassLoader.loadClass(className)
}
//包名在白名单中的类按双亲委派逻辑,从宿主中加载
if (className.inPackage(allHostWhiteTrie)) {
val classLoader = super.loadClass(className, resolve)
return classLoader
}
var suppressed: ClassNotFoundException? = null
try {
//正常的ClassLoader这里是parent.loadClass,插件用specialClassLoader以跳过parent
clazz = specialClassLoader.loadClass(className)!!
} catch (e: ClassNotFoundException) {
suppressed = e
}
if (clazz == null) {
try {
clazz = findClass(className)!!
} catch (e: ClassNotFoundException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
e.addSuppressed(suppressed)
}
throw e
}
}
}
return clazz
}
` 其他附加参考信息(以下问题通过以上修改方案可以解决):============DataBinding问题通过修改 PluginClassloader之后,项目中又遇到的其他问题参考:=========== 插件之间的关系:插件A 依赖 插件B ,即插件A dependOn 插件B。 1,如果插件A 和 插件B都依赖androidx.appcompat:appcompat:1.6.0 这个库。 如果插件A 启动的界面Activity都继承使用 AppCompactActivity,则会报下面几种错误: 1,java.lang.RuntimeException: java.lang.IllegalStateException: This app has been built with an incorrect configuration. Please configure your build for VectorDrawableCompat. 2,java.lang.RuntimeException: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity. 3,Caused by: java.lang.RuntimeException: android.view.InflateException: Binary XML file line #28 in com.seuic.kysy:layout/activity_home: Binary XML file line #28 in com.seuic.kysy:layout/activity_home: Error inflating class TextView 2,如果插件A 或者 插件A依赖的三方库 的AndroidManifest文件 有定义 androidx.core.content.FileProvider,运行时会报如下错误: 1,java.lang.RuntimeException: partKey==ky-plugin-integration-foundation className==androidx.core.content.FileProvider authorities==com.seuic.kysy.integration.fileprovider 2,java.lang.RuntimeException: partKey==ky-plugin-integration-foundation className==com.kye.pda.utilcode.util.UtilsFileProvider authorities==com.seuic.kysy.utilcode.fileprovider(插件A依赖的三方库报错) 3,如果依赖了viewModel相关,运行时插件A还会报下面的错误: java.lang.NoSuchFieldError: No static field Companion of type Landroidx/lifecycle/ViewModelProvider$Factory$Companion; in class Landroidx/lifecycle/ViewModelProvider$Factory; or its superclasses (declaration of 'androidx.lifecycle.ViewModelProvider$Factory' appears in ...
以上问题的共同点,都是插件A和插件B都有相同的类,A插件使用了B插件的PluginClassLoader 加载的该类,且都似乎涉及到在校验资源文件那一步抛异常了。
Shadow现有实现中的dependOn特性只是为了满足最初业务需求而抽象出的通用设计。如果它不能满足你的需求,你随便怎么改都行的。Shadow并没有对ClassLoader有什么特殊的改动,所以你修改loadClass的逻辑和这个项目就没什么关系了。作为开源项目,我们要交流的是开源项目的代码相关的问题。
Shadow现有实现中的dependOn特性只是为了满足最初业务需求而抽象出的通用设计。如果它不能满足你的需求,你随便怎么改都行的。Shadow并没有对ClassLoader有什么特殊的改动,所以你修改loadClass的逻辑和这个项目就没什么关系了。作为开源项目,我们要交流的是开源项目的代码相关的问题。
我们目前将 PluginClassLoader 的 loadClass 逻辑做了调整: 当走 dependsOn 逻辑时,不再采用双亲委派机制,而是优先从自身加载类。这个修改确实解决了之前提到的那些问题。
我们使用dependsOn的初衷是这样的: 在一个 plugin.zip 中包含多个子插件的场景下,业务插件通过 dependsOn 依赖公共插件。 所有业务插件共同使用的 组件库、系统库或第三方库,统一由公共插件以 implementation 方式依赖; 而业务插件本身仅以 compileOnly 的方式依赖这些库。
这样做的目的,是为了减少插件整体的包体积,避免多个业务插件重复打入相同的依赖库。
我想知道作者您设计dependsOn的初衷是否和我们想象的一样?