Shadow icon indicating copy to clipboard operation
Shadow copied to clipboard

sample-child插件pluginCompileOnly公共库 sample-base-lib , 打包时并 dependOn公共 sample-base插件,DataBinding使用会报错

Open lilidejing opened this issue 5 months ago • 10 comments

大神您好! 我们项目目前在使用多插件单进程这种场景。在使用过程中,出现运行时报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.

lilidejing avatar Sep 04 '25 09:09 lilidejing

dependsOn属性只是将这些插件的ClassLoader的parent关系指定一下,允许它们跨ClassLoader加载类。但一个Android app,也就是一个插件,它实际上还包含资源文件,so文件等。这些其他文件dependsOn属性是没有任何处理的。

资源相关的接口一般都是从一个context对象上发起的,这个context肯定会对应一个插件,那它其实只能加载到自己打包的资源。 所以你应该检查一下那些找不到的资源ID是哪个插件里面的,它的context是哪个插件。

更要注意的是,那些XML资源本身是支持继承的,如果要继承的两个文件处于两个不同的插件中,肯定是不行的了。

但上面说的也只是现有代码的能力。如果你更进一步研究Android系统的资源加载机制,应该能发现它本来是有共享资源的设计的。所以才有资源ID分区的设计。所以理论上是能实现跨apk共享资源的。

多插件的场景更复杂,也更需要你在使用前了解插件框架的方方面面。不能拿这个框架当黑盒用。

shifujun avatar Sep 04 '25 09:09 shifujun

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.(DataBindingUtil.java:32) com.tencent.shadow.sample.host E at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:152) ........ Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.databinding.DataBinderMapperImpl" on path: DexPathList[[zip file "/data/user/0/com.tencent.shadow.sample.host/files/ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/3dba257252bf35015b60107419dc9ec0/plugin-debug.zip/sample-base-plugin-debug.apk"],nativeLibraryDirectories=[/data/user/0/com.tencent.shadow.sample.host/files/ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/lib/C850F6D5-3341-4E9F-8F8A-DDFB176A6D8B_lib, /system/lib64, /system_ext/lib64]] java.lang.ClassNotFoundException: Didn't find class "androidx.databinding.DataBinderMapperImpl" on path: DexPathList[[zip file "/data/user/0/com.tencent.shadow.sample.host/files/ShadowPluginManager/UnpackedPlugin/test-dynamic-manager/3dba257252bf35015b60107419dc9ec0/plugin-debug.zip/sample-runtime-debug.apk"],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64]] com.tencent.shadow.sample.host E at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:207) 2025-09-04 19:56:22.900 11471-11471 DEBUG com.tencent.shadow.sample.host E at java.lang.ClassLoader.loadClass(ClassLoader.java:379) 2025-09-04 19:56:22.901 11471-11471 DEBUG com.tencent.shadow.sample.host E at java.lang.ClassLoader.loadClass(ClassLoader.java:312) 2025-09-04 19:56:22.901 11471-11471 DEBUG com.tencent.shadow.sample.host E at com.tencent.shadow.core.loader.classloaders.PluginClassLoader.loadClass(PluginClassLoader.kt:102)

日志显示运行时 在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的话,应该到当前插件中去寻找才对。

谢谢!

lilidejing avatar Sep 04 '25 12:09 lilidejing

你似乎没有关心是哪个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中观察加载过程。

shifujun avatar Sep 04 '25 12:09 shifujun

谢谢回复! 已找到最开始问题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

}`

lilidejing avatar Sep 05 '25 02:09 lilidejing

如果是因为多个插件有重名的DataBinderMapperImpl,而实际上它们又是不同的类。然后这些插件还有继承关系。为了让它们加载不冲突,我们应该是需要给这个类在不同插件里改名吧。可以试试用添加一个transform,在编译后改名。可以参考shadow的transform实现。

shifujun avatar Sep 05 '25 03:09 shifujun

如果是因为多个插件有重名的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; `

lilidejing avatar Sep 05 '25 10:09 lilidejing

那也不一定非得改名。在PluginClassloader里改加载逻辑也是可以的吧,这些重名的类就不要走类似双亲委派的逻辑了。

shifujun avatar Sep 05 '25 10:09 shifujun

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 插件B1,如果插件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 加载的该类,且都似乎涉及到在校验资源文件那一步抛异常了。

lilidejing avatar Sep 15 '25 03:09 lilidejing

Shadow现有实现中的dependOn特性只是为了满足最初业务需求而抽象出的通用设计。如果它不能满足你的需求,你随便怎么改都行的。Shadow并没有对ClassLoader有什么特殊的改动,所以你修改loadClass的逻辑和这个项目就没什么关系了。作为开源项目,我们要交流的是开源项目的代码相关的问题。

shifujun avatar Sep 15 '25 04:09 shifujun

Shadow现有实现中的dependOn特性只是为了满足最初业务需求而抽象出的通用设计。如果它不能满足你的需求,你随便怎么改都行的。Shadow并没有对ClassLoader有什么特殊的改动,所以你修改loadClass的逻辑和这个项目就没什么关系了。作为开源项目,我们要交流的是开源项目的代码相关的问题。

我们目前将 PluginClassLoader 的 loadClass 逻辑做了调整: 当走 dependsOn 逻辑时,不再采用双亲委派机制,而是优先从自身加载类。这个修改确实解决了之前提到的那些问题。

我们使用dependsOn的初衷是这样的: 在一个 plugin.zip 中包含多个子插件的场景下,业务插件通过 dependsOn 依赖公共插件。 所有业务插件共同使用的 组件库、系统库或第三方库,统一由公共插件以 implementation 方式依赖; 而业务插件本身仅以 compileOnly 的方式依赖这些库。

这样做的目的,是为了减少插件整体的包体积,避免多个业务插件重复打入相同的依赖库。

我想知道作者您设计dependsOn的初衷是否和我们想象的一样?

lilidejing avatar Nov 13 '25 03:11 lilidejing