58同城迁移到AndroidX实践及Jetifier源码分析

本文介绍了58同城迁移到AndroidX实践过程及对Jetifier源码分析。

《春宵》

春宵一刻值千金,花有清香月有阴。

歌管楼台声细细,秋千院落夜沉沉。

-宋代,苏轼

前言

AndroidX是谷歌在2018年IO大会上推出的,是对support库的整理后的产物,用于取代support库,解决使用support库必须保持统一的版本及命名混乱等问题。在2018年9月发布了support库的最后一个版本28.0.0,之后support库将不再维护。AndroidX 1.0.0版本对应于support库28.0.0版本。为了确保迁移过程顺畅,迁移前请先将support库升级28.0.0版本。

开始迁移

使用Android Studio提供的功能Refactor > Migrate to AndroidX进行迁移。首先会弹出一个对话框,提示你备份工程代码,并告知可能需要手动处理一些错误。

点击Migrate后Android Studio会对工程中所有文件进行搜索需要处理的文件列表,点击Do Refactor开始迁移。

这会在gradle.properties文件中添加以下属性:

android.useAndroidX=true //表示启用androidx
android.enableJetifier=true //会对依赖库进行迁移

迁移后效果类似下面的修改:

尝试编译发现有很多错误,有导包错误,搜索发现还有好多文件未能成功迁移:

自动化迁移

显然Android Studio提供的工具还有很大的缺陷,由于58同城源码工程代码文件庞大且包含多个业务线,如果手动修改这些导包错误花费的时间成本比较大,并且官方提供了support库和androdx库类映射关系,所以这里我们使用python脚本获取映射关系,通过映射关系对工程中所有文件进行扫描迁移。备注:该脚本不支持多行替换和依赖替换。

传送门:https://github.com/yuweiguocn/MigrateToAndroidX

在gradle.properties文件中添加以下属性:

android.useAndroidX=true //表示启用androidx
android.enableJetifier=true //会对依赖库进行迁移

打开终端在工程根目录执行以下命令:

git clone git@github.com:yuweiguocn/MigrateToAndroidX.git
python MigrateToAndroidX/migrate.py

运行结果:

通过此脚本工具迁移后执行打包只出现了一两个小问题,其中一个是使用的butterknife需要升级。从实践结果来看使用脚本工具对源码迁移成本还是相对很低的,并且目前androidx的多个库已经发布了新的版本,所以还没有迁移到androidx的小伙伴是时候进行迁移了。

验证迁移结果

执行混淆打包后查看混淆后的mapping文件,全局搜索android.support发现有800多处,经过确认是androidx库版本带有的support包名的class文件,分别是:

这样可以确认迁移androidx已经完成,最后来看下support库和androidx库的版本对比,确保迁移后不会对现有功能产生影响。

除了黑色加粗的依赖有版本升级其余库均没有版本变动,从官方网站查看版本变更记录:

  • arch:

    没有变更说明

  • constraintlayout:

    没有变更说明,小版本升级影响不大

  • lifecycle:

    修复ProGuard规则

  • multidex:

    修复了与 Robolectric 测试的兼容性问题。

    提升了版本检查代码的性能。

  • room:

    问题修复及api变更

  • sqlite:

    没有变更说明

重点测试以上依赖库对应的相应功能即可。

Jetifier源码分析

当我们在gradle.properties文件中添加 android.enableJetifier=true 属性开启Jetifier后执行打包时会自动将依赖库改为新的androidx库,这个是如何做到的?接下来我们对Jetifier的相关源码进行一下分析。备注:基于android插件gradle_3.4.0版本。

Android Gradle插件源码

相关源码可以从这里获取:https://github.com/yuweiguocn/build-system

一个工程中的Module可能包括application和library,这里我们只分析application插件。

apply plugin: 'com.android.application'

从应用的插件名称找到插件配置文件com.android.application.properties,可以看到实现类是AppPlugin。

implementation-class=com.android.build.gradle.AppPlugin

AppPlugin的父类继承自BasePlugin,BasePlugin最终会在插件apply时调用到VariantManager的createAndroidTasks方法,该方法最终会调用到configureDependencies方法,接下来该主角登场了:

VariantManager.java

public void configureDependencies() {
        final DependencyHandler dependencies = project.getDependencies();
        // 如果开启了Jetifier没有开启androidX会抛出异常
        if (!globalScope.getProjectOptions().get(BooleanOption.USE_ANDROID_X)
                && globalScope.getProjectOptions().get(BooleanOption.ENABLE_JETIFIER)) {
            throw new IllegalStateException(
                    "AndroidX must be enabled when Jetifier is enabled. To resolve, set "
                            + BooleanOption.USE_ANDROID_X.getPropertyName()
                            + "=true in your gradle.properties file.");
        }

        // 如果开启了Jetifier会使用AndroidX替换support
        if (globalScope.getProjectOptions().get(BooleanOption.ENABLE_JETIFIER)) {
            AndroidXDepedencySubstitution.replaceOldSupportLibraries(project);
        }

        final String jetifierBlackList =
                Strings.nullToEmpty(
                        globalScope.getProjectOptions().get(StringOption.JETIFIER_BLACKLIST));
        dependencies.registerTransform(
                transform -> {
                    transform.getFrom().attribute(ARTIFACT_FORMAT, AAR.getType());
                    transform.getTo().attribute(ARTIFACT_FORMAT, TYPE_PROCESSED_AAR);
                    if (globalScope.getProjectOptions().get(BooleanOption.ENABLE_JETIFIER)) {
                        transform.artifactTransform(
                                JetifyTransform.class, config -> config.params(jetifierBlackList));
                    } else {
                        transform.artifactTransform(IdentityTransform.class);
                    }
                });
        dependencies.registerTransform(
                transform -> {
                    transform.getFrom().attribute(ARTIFACT_FORMAT, JAR.getType());
                    transform.getTo().attribute(ARTIFACT_FORMAT, PROCESSED_JAR.getType());
                    if (globalScope.getProjectOptions().get(BooleanOption.ENABLE_JETIFIER)) {
                        transform.artifactTransform(
                                JetifyTransform.class, config -> config.params(jetifierBlackList));
                    } else {
                        transform.artifactTransform(IdentityTransform.class);
                    }
                });
      ...
}

该方法中判断如果开启了Jetifier会对工程中依赖进行处理,然后注册了JetifyTransform用于处理aar和jar文件。接下来看下对工程中的依赖的处理:

AndroidXDepedencySubstitution.kt

object AndroidXDepedencySubstitution {

    /**
     * 老的依赖到AndroidX依赖的映射
     * map中的key是"old-group:old-module" (不包含版本) value是
     * "new-group:new-module:new-version" (包含版本).
     */
    @JvmStatic
    val androidXMappings: Map = Processor.createProcessor3(
            config = ConfigParser.loadDefaultConfig()!!,
            dataBindingVersion = Version.ANDROID_GRADLE_PLUGIN_VERSION
        ).getDependenciesMap(filterOutBaseLibrary = false)

    @JvmStatic
    fun replaceOldSupportLibraries(project: Project) {
        project.dependencies.components.all { component ->
            component.allVariants { variant ->
                variant.withDependencies { metadata ->
                    val oldDeps = mutableSetOf()
                    val newDeps = mutableListOf()
                    metadata.forEach { it ->
                        val newDep = if (bypassDependencySubstitution(it)) {
                            null
                        } else {
                            androidXMappings["${it.group}:${it.name}"]
                        }
                        if (newDep != null) {
                            oldDeps.add(it)
                            newDeps.add(newDep)
                        }
                    }
                    // 某些情况下 metadata.removeAll(oldDeps) 不起作用,所以使用循环处理
                    for (oldDep in oldDeps.map { it -> "${it.group}:${it.name}" }) {
                        metadata.removeIf { it -> "${it.group}:${it.name}" == oldDep }
                    }
                    for (newDep in newDeps) {
                        metadata.add(newDep)
                    }
                }
            }
        }

        project.configurations.all { config ->
            // 只处理可解决的配置
            if (config.isCanBeResolved) {
                config.resolutionStrategy.dependencySubstitution.all { it ->
                    maybeSubstituteDependency(it, config, androidXMappings)
                }
            }
        }
    }

    /**
     * 如果依赖是老的support库则使用新的androidx替换
     */
    private fun maybeSubstituteDependency(
        dependencySubstitution: DependencySubstitution,
        configuration: Configuration,
        androidXMappings: Map
    ) {
        // 只处理 Gradle module 依赖 (group:module:version这种形式的)
        if (dependencySubstitution.requested !is ModuleComponentSelector) {
            return
        }

        val requestedDependency = dependencySubstitution.requested as ModuleComponentSelector
        if (bypassDependencySubstitution(requestedDependency, configuration)) {
            return
        }

        androidXMappings[requestedDependency.group + ":" + requestedDependency.module]?.let {
            dependencySubstitution.useTarget(
                it,
                BooleanOption.ENABLE_JETIFIER.name + " is enabled"
            )
        }
    }
    ...
}

AndroidXDepedencySubstitution文件中有一个androidXMappings变量存储的是support依赖和AndroidX依赖之间的映射,key是”old-group:old-module” (不包含版本) ,value是”new-group:new-module:new-version” (包含版本),通过该映射关系对现有工程依赖进行替换。这里用到了Processor类,该类在jetifier工程中,稍后再分析该依赖配置是怎么获取的。

接下来看下刚才注册的JetifyTransform类:

JetifyTransform.kt

class JetifyTransform @Inject constructor(blackListOption: String) : ArtifactTransform() {

    companion object {

        private val jetifierProcessor: Processor by lazy {
            Processor.createProcessor3(
                config = ConfigParser.loadDefaultConfig()!!,
                dataBindingVersion = Version.ANDROID_GRADLE_PLUGIN_VERSION,
                allowAmbiguousPackages = false,
                stripSignatures = true
            )
        }
    }

    private val jetifierBlackList: List = getJetifierBlackList(blackListOption)

    private fun getJetifierBlackList(blackListOption: String): List {
        val blackList = mutableListOf()
        if (!blackListOption.isEmpty()) {
            blackList.addAll(Splitter.on(",").trimResults().splitToList(blackListOption))
        }

        // Jetifier should not jetify itself (http://issuetracker.google.com/119135578)
        blackList.add("jetifier-.*\\.jar")
        return blackList.map { Regex(it) }
    }

    override fun transform(aarOrJarFile: File): List {
        Preconditions.checkArgument(
            aarOrJarFile.name.toLowerCase().endsWith(".aar")
                    || aarOrJarFile.name.toLowerCase().endsWith(".jar")
        )

        /*
         * aars 或 jars 可以分为四类
         *  - AndroidX 库
         *  - 老的 support 库
         *  - 黑名单中的其他库
         *  - 非黑名单中的其他库
         * 下面会相应处理这些情况
         */
        // 情况 1: 是AndroidX library不需要处理
        if (jetifierProcessor.isNewDependencyFile(aarOrJarFile)) {
            return listOf(aarOrJarFile)
        }

        // 情况 2:如果是老的support库表示在之前的依赖替换阶段没有被替换,可能它还没有androidx版本
        // 也不需要对它处理
        if (jetifierProcessor.isOldDependencyFile(aarOrJarFile)) {
            return listOf(aarOrJarFile)
        }

        // 情况 3: 如果在黑名单也不需要处理
        if (jetifierBlackList.any { it.containsMatchIn(aarOrJarFile.absolutePath) }) {
            return listOf(aarOrJarFile)
        }

        // 情况 4: 对剩下的库进行处理
        val outputFile = File(outputDirectory, "jetified-" + aarOrJarFile.name)
        val maybeTransformedFile = try {
            jetifierProcessor.transform(
                setOf(FileMapping(aarOrJarFile, outputFile)), false
            )
                .single()
        } catch (exception: Exception) {
            throw RuntimeException(
                "Failed to transform '$aarOrJarFile' using Jetifier." +
                        " Reason: ${exception.message}. (Run with --stacktrace for more details.)",
                exception
            )
        }
        ...
        return listOf(maybeTransformedFile)
    }
}

这里主要看下transform方法,aar或jar可以分为四类:1.AndroidX库 2.老的support库,表示在之前的依赖替换阶段没有被替换 3.黑名单中的其他库 4.非黑名单中的其他库。只对情况4进行处理,最终还是调用了jetifier库中Processor类。备注:我们可以通过在gradle.properties文件中添加 android.jetifier.blacklist 属性指定不需要处理的黑名单。

Jetifier源码

jetifier库源码地址:https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-jetifier-release/jetifier/

创建Processor对象是config的参数值是ConfigParser.loadDefaultConfig()表示加载默认的配置,最终是通过读取default.generated.config文件中的json数据转换为Config对象。

ConfigParser.kt

object ConfigParser {

    private const val TAG: String = "Config"

    private val gson = GsonBuilder().setPrettyPrinting().create()
    ...
    fun parseFromString(inputText: String): Config? {
        return gson.fromJson(inputText, Config.JsonData::class.java).toConfig()
    }
    ...
    fun loadDefaultConfig(): Config? {
        Log.v(TAG, "Using the default config '%s'", Config.DEFAULT_CONFIG_RES_PATH)

        // Use getResource().openStream() instead of getResourceAsStream() as the latter can result
        // in concurrency issues (see http://issuetracker.google.com/137929327 for details).
        val inputStream = javaClass.getResource(Config.DEFAULT_CONFIG_RES_PATH).openStream()
        inputStream.reader().use {
            return parseFromString(it.readText())
        }
    }
    ...
}

default.generated.config文件中存放了support库和androidx库之间的映射关系,通过注释可以看出该文件是由default.config配置文件和preprocessor/scripts/processDefaultConfig.sh脚本生成的,这里不再对该shell脚本进行分析,有兴趣的同学可以自己看下。

回过头来继续分析Processor的处理,上面注册的JetifyTransform最终调用了Processor的transfrom方法,transform方法已经被废弃,内部调用了transform2方法,这里主要看下transform2方法:

Processor.kt

fun transform2(
        input: Set,
        copyUnmodifiedLibsAlso: Boolean = true,
        skipLibsWithAndroidXReferences: Boolean = false
    ): TransformationResult {
        val nonSingleFiles = HashSet(input)
        for (fileMapping in nonSingleFiles) {
            // 将所有文件视为单个文件并检查是否可转换
            val file = ArchiveFile(fileMapping.from.toPath(), fileMapping.from.readBytes())
            file.setIsSingleFile(true)
            val transformer = transformers.firstOrNull { it.canTransform(file) }
            if (transformer != null) {
                // 单个文件java和xml是可转换的,设置相对路径为输出路径
                file.updateRelativePath(fileMapping.to.toPath())
                transformer.runTransform(file)
                nonSingleFiles.remove(fileMapping)
            }
        }
        if (nonSingleFiles.isEmpty()) {
            // 所有文件都是单个文件,处理完成
            return TransformationResult(librariesMap = emptyMap(), numberOfLibsModified = 0)
        }

        val inputLibraries = nonSingleFiles.map { it.from }.toSet()
        if (inputLibraries.size != input.size) {
            throw IllegalArgumentException("Input files are duplicated!")
        }

        // 1) 解压并加载所有库文件
        val allLibraries = loadLibraries(input)

        // 2) 过滤出包含AndroidX 引用的库
        val librariesToProcess =
            if (skipLibsWithAndroidXReferences) {
                filterOutLibrariesWithAndroidX(allLibraries)
            } else {
                allLibraries
            }

        // 3) 搜索 POM 文件
        val pomFiles = scanPomFiles(librariesToProcess)

        // 4) 转换所有 libraries
        librariesToProcess.forEach { transformLibrary(it) }

        if (context.errorsTotal() > 0) {
            if (context.isInReversedMode && context.rewritingSupportLib) {
                throw IllegalArgumentException("There were ${context.errorsTotal()} errors found " +
                    "during the de-jetification. You have probably added new androidx types " +
                    "into support library and dejetifier doesn't know where to move them. " +
                    "Please update default.config and regenerate default.generated.config via " +
                    "jetifier/jetifier/preprocessor/scripts/processDefaultConfig.sh")
            }

            throw IllegalArgumentException("There were ${context.errorsTotal()}" +
                " errors found during the remapping. Check the logs for more details.")
        }

        // 5) 转换 POM 文件
        transformPomFiles(pomFiles)

        // 6) 找到签名文件如果需要则抛出异常
        runSignatureDetectionFor(librariesToProcess)

        val numberOfLibsModified = librariesToProcess.count { it.wasChanged }

        // 7) 重新打包到存档文件
        var result = allLibraries
            .map {
                if (it.wasChanged || copyUnmodifiedLibsAlso) {
                    it.relativePath.toFile() to it.writeSelf()
                } else {
                    it.relativePath.toFile() to null
                }
            }.toMap()

        return TransformationResult(
            librariesMap = result,
            numberOfLibsModified = numberOfLibsModified)
    }

先来分析下主要流程:

0)首先对传入的单个文件包含java和xml做了转换处理,如果列表为空则表示处理完成

1)解压并加载所有库文件

2)根据方法参数值过滤掉包含androidx引用的库,默认不过滤

3)扫描加载所有pom文件

4)转换所有库文件

4.1)判断转换如果发生错误则抛出异常

5)转换所有pom文件

6)找出签名文件如果需要则抛出异常

7)重新打包到存档文件

8)最后返回处理结果

然后来看一下具体的转换细节,其中transformers是一个list包含四个类,分别用于处理字节码class非单个文件、xml非pom文件、proguard非单个文件、单个java源码文件,上面流程首先将传入的所有文件视为单个文件使用JavaTransformer和XmlResourcesTransformer对java源码文件和xml文件进行了处理。第4步使用transformers对所有库文件进行了处理,第5步对所有pom文件进行了处理。

Processor.kt

private fun createTransformers(context: TransformationContext) = listOf(
            ByteCodeTransformer(context),// class && !single
            XmlResourcesTransformer(context),// xml && !pom
            ProGuardTransformer(context), // proguard && !single
            JavaTransformer(context) // java && single
        )

不同类型的转换处理相关代码原理一样,都是通过映射关系使用正则匹配等方法进行替换。这里只看下字节码的处理,字节码的处理类ByteCodeTransformer使用了ASM工具对class文件进行了处理并将结果保存在ArchiveFile对象中,在上述流程第7步重新打包到了存档文件中,完成了对第三方库文件的处理。

ByteCodeTransformer.kt

class ByteCodeTransformer internal constructor(
    private val context: TransformationContext
) : Transformer {
    ...
    override fun runTransform(file: ArchiveFile) {
        val reader = ClassReader(file.data)
        val writer = ClassWriter(0 /* flags */)

        val remapper = CoreRemapperImpl(context, writer)
        reader.accept(remapper.classRemapper, 0 /* flags */)

        if (!remapper.changesDone) {
            file.setNewDataSilently(writer.toByteArray())
        } else {
            file.setNewData(writer.toByteArray())
        }

        file.updateRelativePath(remapper.rewritePath(file.relativePath))
    }
}

这里使用了ClassRemapper,ClassRemapper是一个使用Remapper重新映射类型的ClassVisitor,这里对Remapper进行了自定义:

CustomRemapper.kt

class CustomRemapper(private val remapper: CoreRemapper) : Remapper() {

    override fun map(typeName: String): String {
        return remapper.rewriteType(JavaType(typeName)).fullName
    }

    override fun mapPackageName(name: String): String {
        return remapper.rewriteType(JavaType(name)).fullName
    }

    override fun mapValue(value: Any?): Any? {
        val stringVal = value as? String
        if (stringVal == null) {
            return super.mapValue(value)
        }

        fun mapPoolReferenceType(typeDeclaration: String): String {
            if (!typeDeclaration.contains(".")) {
                return remapper.rewriteType(JavaType(typeDeclaration)).fullName
            }

            if (typeDeclaration.contains("/")) {
                // Mixed "." and "/"  - not something we know how to handle
                return typeDeclaration
            }

            val toRewrite = typeDeclaration.replace(".", "/")
            return remapper.rewriteType(JavaType(toRewrite)).toDotNotation()
        }

        if (stringVal.startsWith("L") && stringVal.endsWith(";")) {
            // L denotes a type declaration. For some reason there are references in the constant
            // pool that ASM skips.
            val typeDeclaration = stringVal.substring(1, stringVal.length - 1)
            if (typeDeclaration.isEmpty()) {
                return value
            }

            if (typeDeclaration.contains(";L")) {
                // We have array of constants
                return "L" +
                            typeDeclaration
                                .split(";L")
                                .joinToString(";L") { mapPoolReferenceType(it) } +
                        ";"
            }

            return "L" + mapPoolReferenceType(typeDeclaration) + ";"
        }
        return remapper.rewriteString(stringVal)
    }
}

CustomRemapper重写了Remapper的3个方法,分别是map方法用于映射类的内部名称和新名称,mapPackageName方法用于映射包名和新名称,mapValue方法用于映射值。方法内部最终调用了CoreRemapper接口中的rewriteType和rewriteString方法,CoreRemapper的实现类是CoreRemapperImpl:

CoreRemapperImpl.kt

class CoreRemapperImpl(
    private val context: TransformationContext,
    visitor: ClassVisitor
) : CoreRemapper {
    ...

    private val typesMap = context.config.typesMap

    var changesDone = false
        private set

    val classRemapper = ClassRemapper(visitor, CustomRemapper(this))

    override fun rewriteType(type: JavaType): JavaType {
        val result = context.typeRewriter.rewriteType(type)
        if (result != null) {
            changesDone = changesDone || result != type
            return result
        }

        context.reportNoMappingFoundFailure(TAG, type)
        return type
    }

    override fun rewriteString(value: String): String {

        ...

        // Try rewrite rules
        if (context.useFallbackIfTypeIsMissing) {
            val rewrittenType = context.config.rulesMap.rewriteType(type)
            if (rewrittenType != null) {
                Log.i(TAG, "Map string: '%s' -> '%s' via fallback", value, rewrittenType)
                return if (hasDotSeparators) {
                    rewrittenType.toDotNotation()
                } else {
                    rewrittenType.fullName
                }
            }
        }

        // We do not treat string content mismatches as errors
        Log.i(TAG, "Found string '%s' but failed to rewrite", value)
        return value
    }

    fun rewritePath(path: Path): Path {
        val owner = path.toFile().path.replace('\\', '/').removeSuffix(".class")
        val type = JavaType(owner)

        val result = context.typeRewriter.rewriteType(type)
        if (result == null) {
            context.reportNoMappingFoundFailure("PathRewrite", type)
            return path
        }

        if (result != type) {
            changesDone = true
            return path.fileSystem.getPath(result.fullName + ".class")
        }

        return path
    }
}

方法内部根据条件判断最终调用了映射关系数据Config类获取对应的映射关系完成了转换。

总结

使用本文提供的脚本工具对工程源码迁移到anroidx成本相对较低,迁移到androidx后并非需要对所有依赖库进行升级到对应androidx版本,原因是当我们在gradle.properties文件中添加 android.enableJetifier=true 属性开启Jetifier后执行打包时会自动将依赖的support库修改为新的androidx库,对于第三方库会对aar中class文件、xml文件以及proguard文件和pom依赖进行处理。

参考

  • https://developer.android.com/jetpack/androidx/migrate#migrate

  • https://developer.android.com/jetpack/androidx/migrate/class-mappings

  • https://github.com/yuweiguocn/MigrateToAndroidX

  • https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-jetifier-release/jetifier/

  • https://github.com/yuweiguocn/build-system

听说点在看年终奖翻倍 :point_down: