从源码看include aar中布局,viewbing生成时类型固定为View

文章讲述了在使用Android的include标签引入AAR中的layout时,如何通过修改Gradle插件配置和自定义LayoutFileParser来确保生成的viewbinding类类型正确的问题及其解决方案。
摘要由CSDN通过智能技术生成

问题描述

当我们通过include 引入aar中的布局时,在其生成的对应viewbinding/DataBinding类中类型为view,而不是具体的viewbinding 类型,例如以下布局
其中@layout/test 为aar中的布局

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <include
      android:id="@+id/local"
      layout="@layout/local_test" />

    <include
      android:id="@+id/test"
      layout="@layout/test" />

  </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

生成的viewbinding类为

public final class ActivityMainBinding implements ViewBinding {
    @NonNull
    private final ConstraintLayout rootView;

    @NonNull
    public final LocalTestBinding local;

    @NonNull
    public final ConstraintLayout main;

    //其对应类型为View,
    @NonNull
    public final View test;

事实上我们期待其生成的类型为具体的viewBinding,在这里为TestBinding,即public final TestBinding test;

分析源码

从build输出的任务观察到可能是task: dataBindingGenBaseClassesDebug
因此,我们从TaskManager 进入找到改task的具体实现类DataBindingGenBaseClassesTask

protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) {
    val dataBindingEnabled = creationConfig.buildFeatures.dataBinding
    val viewBindingEnabled = creationConfig.buildFeatures.viewBinding
    if (!dataBindingEnabled && !viewBindingEnabled) {
        return
    }
    taskFactory.register(
        DataBindingMergeDependencyArtifactsTask.CreationAction(creationConfig))
    DataBindingBuilder.setDebugLogEnabled(logger.isDebugEnabled)

    //重点
    taskFactory.register(DataBindingGenBaseClassesTask.CreationAction(creationConfig))

    // DATA_BINDING_TRIGGER artifact is created for data binding only (not view binding)
    if (dataBindingEnabled) {
        if (creationConfig.services.projectOptions.get(BooleanOption.NON_TRANSITIVE_R_CLASS)
            && isKotlinKaptPluginApplied(project)) {
            val kotlinVersion = getProjectKotlinPluginKotlinVersion(project)
            if (kotlinVersion != null && kotlinVersion < KAPT_FIX_KOTLIN_VERSION) {
                // Before Kotlin version 1.5.20 there was an issue with KAPT resolving files
                // at configuration time. We only need this task as a workaround for it, if the
                // version is newer than 1.5.20 or KAPT isn't applied, we can skip it.
                taskFactory.register(
                    MergeRFilesForDataBindingTask.CreationAction(creationConfig))
            }
        }
        taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig))
        creationConfig.sources.java {
            it.addSource(
                TaskProviderBasedDirectoryEntryImpl(
                    name = "databinding_generated",
                    directoryProvider = creationConfig.artifacts.get(
                        InternalArtifactType.DATA_BINDING_TRIGGER
                    ),
                )
            )
        }
        setDataBindingAnnotationProcessorParams(creationConfig)
    }
}

@TaskAction
fun writeBaseClasses(inputChanges: InputChanges) {
    // TODO extend NewIncrementalTask when moved to new API so that we can remove the manual call to recordTaskAction

    recordTaskAction(analyticsService.get()) {
        // TODO figure out why worker execution makes the task flake.
        // Some files cannot be accessed even though they show up when directory listing is
        // invoked.
        // b/69652332
        val args = buildInputArgs(inputChanges)
        CodeGenerator(
            args,
            sourceOutFolder.get().asFile,
            Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
            encodeErrors,
            getRPackageProvider()).run()
    }
}

再看CodeGenerator.run()

override fun run() {
    try {
        initLogger()
        BaseDataBinder(
            LayoutInfoInput(args),
            getRPackage)
            .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
    } finally {
        clearLogger()
    }
}

再看BaseDataBinder.generateAll()

fun generateAll(writer : JavaFileWriter) {
    input.invalidatedClasses.forEach {
        writer.deleteFile(it)
    }

    val myLog = LayoutInfoLog()
    myLog.addAll(input.unchangedLog)

    val useAndroidX = input.args.useAndroidX
    val libTypes = LibTypes(useAndroidX = useAndroidX)

    // Sort the layout bindings to ensure deterministic order
    val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
        .groupBy(LayoutFileBundle::getFileName).toSortedMap()

        //for循环将layoutBindings 转化为对应的viewBinding或者dataBinding
    layoutBindings.forEach { layoutName, variations ->
        val layoutModel = BaseLayoutModel(variations, getRPackage)

        val javaFile: JavaFile
        val classInfo: GenClassInfoLog.GenClass
        //如果是dataBinding
        if (variations.first().isBindingData) {
            check(input.args.enableDataBinding) {
                "Data binding is not enabled but found data binding layouts: $variations"
            }

            val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
            javaFile = binderWriter.write()
            classInfo = binderWriter.generateClassInfo()
        } else {如果是viewBinding
            check(input.args.enableViewBinding) {
                "View binding is not enabled but found non-data binding layouts: $variations"
            }

            //重点,生成viewbinger
            val viewBinder = layoutModel.toViewBinder()
            //将viewbinder 转为为javaclassFile
            javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
            classInfo = viewBinder.generatedClassInfo()
        }

        writer.writeToFile(javaFile)
        myLog.classInfoLog.addMapping(layoutName, classInfo)

        variations.forEach {
            it.bindingTargetBundles.forEach { bundle ->
                if (bundle.isBinder) {
                    myLog.addDependency(layoutName, bundle.includedLayout)
                }
            }
        }
    }
    input.saveLog(myLog)
    // data binding will eat some errors to be able to report them later on. This is a good
    // time to report them after the processing is done.
    Scope.assertNoError()
}

再看layoutModel.toViewBinder()

fun BaseLayoutModel.toViewBinder(): ViewBinder {
    //获取生成类中R文件的包名
    val rClassName = ClassName.get(modulePackage, "R")

    fun BindingTargetBundle.toBinding(): ViewBinding {
        val idReference = id.parseXmlResourceReference().toResourceReference(rClassName, getRPackage)
        val (present, absent) = layoutConfigurationMembership(this)

        return ViewBinding(
            name = fieldName(this),
            type = parseLayoutClassName(fieldType, baseFileName),
            //在这里判断生成类型是viewBinding还是view
            //isBinder 是BaseLayoutModel 的内部属性,所以我们需要回头看layoutModel的生成
            form = if (isBinder) ViewBinding.Form.Binder else ViewBinding.Form.View,
            id = idReference,
            presentConfigurations = present,
            absentConfigurations = absent
        )
    }
    validateExplicitViewBindingTypes()
    val bindings = sortedTargets
        .filter { it.id != null }
        .filter { it.viewName != "merge" } // <merge> can have ID but it's ignored at runtime.
        .map { it.toBinding() }
    val rootNode = parseRootNode(rClassName, bindings)
    return ViewBinder(
        generatedTypeName = ClassName.get(bindingClassPackage, bindingClassName),
        layoutReference = ResourceReference(rClassName, "layout", baseFileName),
        bindings = bindings,
        rootNode = rootNode
    )
}

回头看layoutModel 的生成

val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
    .groupBy(LayoutFileBundle::getFileName).toSortedMap()

layoutBindings.forEach { layoutName, variations ->
    val layoutModel = BaseLayoutModel(variations, getRPackage)

其来源于 resourceBundle ,resourceBundle 是BaseDataBinder 的内部属性
是在初始化时为其进行了赋值

class BaseDataBinder(val input : LayoutInfoInput, val getRPackage: ((String, String) -> (String))?) {
    private val resourceBundle : ResourceBundle = ResourceBundle(
        input.packageName, input.args.useAndroidX)
    init {
        //读取固定目录下的xml文件,将其反序列化为LayoutFileBundle
        input.filesToConsider
            .forEach {
                it.inputStream().use {
                    val bundle = LayoutFileBundle.fromXML(it)
                    resourceBundle.addLayoutBundle(bundle, true)
                }
            }

            //添加日志信息
        resourceBundle.addDependencyLayouts(input.existingBindingClasses)
        //重点,进行有效性判断,同时在这里也判断了生成类中的属性是view还是Viewbinding
        resourceBundle.validateAndRegisterErrors()
    }

resourceBundle.validateAndRegisterErrors() 的相关逻辑这里不再列出,感兴趣自己具体看下,这里列举出最后的关键代码
可以看到,这里指定了具体的viewBinder还是view

if (target.isBinder()) {
    List<LayoutFileBundle> boundTo =
    mLayoutBundles.get(target.getIncludedLayout());
    String targetBinding = null;
    String targetBindingPackage = null;
    if (boundTo != null && !boundTo.isEmpty()) {
        targetBinding = boundTo.get(0).getFullBindingClass();
        targetBindingPackage = boundTo.get(0).getModulePackage();
    } else {
        IncludedLayout included = mDependencyBinders.getOrDefault(
            target.getIncludedLayout(), null);
        if (included != null) {
            targetBinding = included.interfaceQName;
            targetBindingPackage = included.modulePackage;
        }
    }
    if (targetBinding == null) {
        L.d("There is no binding for %s, reverting to plain layout",
                target.getIncludedLayout());
        if (target.getId() == null) {
            unboundIncludes.add(target);
        } else {
            target.setInterfaceType("android.view.View");
            target.mViewName = "android.view.View";
        }
    } else {
        target.setInterfaceType(targetBinding, targetBindingPackage);
    }
}
}

编写gradle 插件修复该问题

从上面分析的源码就可以看出,我们只要在执行dataBindingGenBaseClassesDebug任务之前,在初始化resourceBundle的固定目录下插入我们需要生成布局对应的LayoutFileBundle 的xml文件,就可以继续生成
正好生成类是公有方法

public final class LayoutFileParser {

   ...
    @Nullable
    public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
                                                           @NonNull final File outputFile, @NonNull final String pkg,
                                                           @NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup,
                                                           boolean isViewBindingEnabled, boolean isDataBindingEnabled)
   ...
    }

我们直接调用

//inputfile 对应aar中的layout布局
//dirFile
//outFile
//com.ligh.customplugin 包名

val bulder =  LayoutFileParser.parseXml(RelativizableFile.fromAbsoluteFile(input,dirFile),outFile,"com.ligh.customplugin",CustomLoomUp(),true,false)

//转化为string
val content = bulder.toXML()

//..按照固定目录自定义插入

运行自定义的插件,发现我们已经生成了 对应aar布局中的viewbinding文件,但是存在编译不通过,因为生成类中的R文件时我们指定包名中的R文件
但是并没有提供一个hook点来实现自定义R文件包名,此时我们需要自定义实现类转化viewBinder来指定R文件为aar中的R文件
即重写BaseLayoutModel.toViewBinder

fun BaseLayoutModel.toViewBinder(rPackage:String): ViewBinder {
    val rClassName = ClassName.get(rPackage, "R")

    fun ResourceBundle.BindingTargetBundle.toBinding(): ViewBinding {
        val idReference = id.parseXmlResourceReference().toResourceReference(rClassName, getRPackage)
        val (present, absent) = layoutConfigurationMembership(this)

        return ViewBinding(
            name = fieldName(this),
            type = parseLayoutClassName(fieldType, baseFileName),
            form = if (isBinder) ViewBinding.Form.Binder else ViewBinding.Form.View,
            id = idReference,
            presentConfigurations = present,
            absentConfigurations = absent
        )
    }
    validateExplicitViewBindingTypes()
    val bindings = sortedTargets
        .filter { it.id != null }
        .filter { it.viewName != "merge" } // <merge> can have ID but it's ignored at runtime.
        .map { it.toBinding() }
    val rootNode = parseRootNode(rClassName, bindings)
    return ViewBinder(
        generatedTypeName = ClassName.get(bindingClassPackage, bindingClassName),
        layoutReference = ResourceReference(rClassName, "layout", baseFileName),
        bindings = bindings,
        rootNode = rootNode
    )
}

自此,大功告成。aar的layout文件不再受viewBinding的约束
即使时include aar中的layout,也会生成其对应的布局

public final class ActivityMainBinding implements ViewBinding {
    @NonNull
    private final ConstraintLayout rootView;

    @NonNull
    public final LocalTestBinding local;

    @NonNull
    public final ConstraintLayout main;

    //其类型为具体的viewBinding
    @NonNull
    public final TestBinding test;

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值