引言
随着业务的高速发展,代码量也越来越多,良好的工程结构和依赖管理对构建速度有着积极的作用,文章介绍了最近一段时间得物Android工程gradle依赖优化的一些实践,以及未来这个方向需要做的事情。原文链接 https://mp.weixin.qq.com/s/2EwKJpr9PGlvErbjN4tkuA
优化点
扁平化依赖
在不断的开发迭代中,业务模块implementation的组件中可能存在一部分已经不再使用了,但是仍然留在gradle配置里。另外一些common模块中存在大量的api出去的组件,有很大一部分上层模块使用率较低,甚至没有用到。这两种情况会对编译造成以下影响
1、大量的时间花费在解析无用的依赖上增加gradle运行时间消耗
2、compileClasspath链路任何一个组件发生变化(增加/删除方法、增加/删除注解等),都会触发javac、kotlinc的重新编译,拖慢编译速度
3、增加编译过程中类寻找过程的时间消耗无法确定snapshot类型的组件是否为最新的
我们使用SNAPSHOT类型组件,来解决组件化开发中的实时性问题,为了编译的效率刷新的时间设置的比较长,只有主动调用idea-plugin的刷新组件缓存时才会去拉新的aar包,组建锁定功能上线后有业务同学经常会反馈组件无法刷新,业务同学判断没有刷新的依据是sources.jar中的内容,AndroidStudio显示sources.jar有时候会有问题(gradle编译时使用的aar是最新的,但是sources.jar显示的是老版本的),studio这个bug的存在对排查编译的问题造成麻烦,无法定位是不是因为idea-plugin刷新逻辑的问题导致的无法刷新源码依赖/aar依赖行为对齐
我们的组件化方案中使用了resolutionStrategy.dependencySubstitution来实现组件库源码切换,同一个组件存在源码/aar两种依赖方式,老版本发布插件从configuration中取组件打进pom文件时,没有区分implementation、api,scope设置的都是compile(可以理解为api的行为)。aar依赖时编译正常,如果开了多个源码模块时,由于基础库源码模块的implementation传递依赖被拦截,所以有可能会造成上层源码模块因缺失传递的依赖而导致编译报错,这个问题会对开发人员造成困扰,解决方案是生成的 POM 文件仅将api依赖项放入编译范围,其余implementation依赖项放入运行时范围,具体可以参考java_library_separation。duapp源码模块有200+,每处理一个基础组件都有可能造成很多上层模块因缺失传递的依赖而造成编译错误,手工补组件工作量巨大,另外处理时需要渐进的去做使影响范围做到可控,尽量少的block开发
结合以上的场景,我们需要一个分析依赖的工具为调整工程的依赖提供依据,由于很多用于分析的元数据在gradle运行期才能拿到,所以这个工具做成gradle plugin更为合适,这个工具需要满足以下的需求
1、拿到每个模块类级别的依赖图,进而拿到实际的组件依赖图(非./gradlew dependencies拿到的依赖图),实际的组件依赖图平铺后就是每个模块的最小compileClasspath
2、分析模块中那些implementation依赖用不到,剪除某个implementation依赖时,需要补上那些被翦除依赖的传递依赖
3、拿到gradle编译时snapshot组件的时间戳,这样就能和nexus上的maven-metadata.xml做对比确定是否为最新的版本
接下来让我们一起一步一步来实现poizon-jdeps插件吧
实现过程
拿到依赖的组件信息
AGP中有一个androidDependencies任务可以打印出组件信息,这里通过一个小技巧查看某个gradle task的实现类,调用./gradlew help --task androidDependencies
查阅DependencyReportTask类中的代码,找到了以下实现
Set<ResolvedArtifact> compileArtifacts =
com.android.build.gradle.internal.ide.dependencies.getAllArtifacts(
variant,
AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH,
null,
buildMapping)
ResolvedArtifact包含了以下信息,可以满足我们的需求
● artifactFile aar或者jar的路径
● extractedFolder aar的解压目录,jar包的路径为jars/classes.jar、jars/libs/*.jar
● dependencyType 扩展名aar/jar
● componentIdentifier 组件标识符接口,主要的子类如下
l_ ProjectComponentIdentifier 工程依赖描述符
l_ ModuleComponentIdentifier 远程依赖描述符
|_ group 例如com.shizhuang.duapp.modules
|_ module 例如du_common
|_ version 例如4.78.0-SNAPSHOT
|_ MavenUniqueSnapshotComponentIdentifier snapshot类型远程依赖描述符
|_ timestamp 例如20210915.071815-14
timestamp字段,就是snapshot组件的时间戳,有这个值就可以确认是否为最新的包
获取jar包中包含哪些类
下面的内容中称为selfClasses
获取jar包中的外部引用类
下面的内容中称为refClasses
使用Java字节码的类库javassist,解析jar包里class中分散在AttributeInfo、MethodInfo、AttributeInfo中所有涉及的类记为allClasses
jar包外部引用的类refClasses = allClasses - selfClasses
为了提高后面扫描类依赖图时的效率,输出的数据结构尽量的丰富,大致的格式如下
● jarFile: jar包路径
● classNames: jar包里包含的所有类
● classSuperclasses: 类的父类
● classInterfaces: 类实现的接口
● refClassNames: jar外部引用类
● reverseClassRefInfos:jar反向引用
● classRefInfos:每个类引用的类信息
● reverseClassRefInfos:类反向引用
{
"jarFile": "/Users/admin/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"cacheFile": "/Users/admin/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/47b280a467fc1cb913b13f5b6a278232a54fab11-jdeps-jarinfo-2.json",
"classNames": [
"androidlibrary1.Test2",
"androidlibrary1.Test1",
"com.example.androidlibrary1.BuildConfig",
"androidlibrary1.TestKt1"
],
"refClassNames": [
"java.lang.Object",
"java.io.Serializable",
"androidlibrary2.Test1",
"javalib1.Test1",
"java.lang.Boolean",
"java.lang.String",
"com.google.gson.Gson",
"kotlin.Metadata"
],
"classSuperclasses": {
"androidlibrary1.Test2": "java.lang.Object",
"androidlibrary1.Test1": "androidlibrary2.Test1",
"androidlibrary1.TestKt1": "java.lang.Object",
"com.example.androidlibrary1.BuildConfig": "java.lang.Object"
},
"classInterfaces": {
"androidlibrary1.Test2": [
"java.io.Serializable"
],
"androidlibrary1.Test1": [],
"androidlibrary1.TestKt1": [],
"com.example.androidlibrary1.BuildConfig": []
},
"classRefInfos": {
"androidlibrary1.Test2": [
"java.lang.Object",
"java.io.Serializable"
],
"androidlibrary1.Test1": [
"androidlibrary2.Test1",
"javalib1.Test1"
],
"com.example.androidlibrary1.BuildConfig": [
"java.lang.Boolean",
"java.lang.Object",
"java.lang.String"
],
"androidlibrary1.TestKt1": [
"com.google.gson.Gson",
"java.lang.Object",
"java.lang.String",
"kotlin.Metadata"
]
},
"reverseClassRefInfos": {
"java.lang.Object": [
"androidlibrary1.Test2",
"com.example.androidlibrary1.BuildConfig",
"androidlibrary1.TestKt1"
],
"java.io.Serializable": [
"androidlibrary1.Test2"
],
"androidlibrary2.Test1": [
"androidlibrary1.Test1"
],
"javalib1.Test1": [
"androidlibrary1.Test1"
],
"java.lang.Boolean": [
"com.example.androidlibrary1.BuildConfig"
],
"java.lang.String": [
"com.example.androidlibrary1.BuildConfig",
"androidlibrary1.TestKt1"
],
"com.google.gson.Gson": [
"androidlibrary1.TestKt1"
],
"kotlin.Metadata": [
"androidlibrary1.TestKt1"
]
}
}
聚合的信息下面的内容中称为JarInfo
类与组件信息建立映射
使用map维护className与JarInfo的映射关系,key为className,value为jar包信息。
扫描最小的compileClasspath链路
可以把依赖树看做成一个以project的full_jar为根结点允许存在相同叶子结点的多叉树,扫描最小的compileClasspath链路可以理解为寻找有效的叶子结点,从project的full_jar出发,广度优先搜索依赖树,按层级通过refClass寻找链路上的所有jar包。
具体的实现如下:
输出的格式如下:
- bootJarPath: 扫描的入口(当前工程打出来的jar包)
- classpathJarPaths:送进去扫描的所有jar包,一般是compileClassPath或者- runtimeClasspath
- linkedJarPaths:实际关联的jar包(这个就是我们要的结果)
- jarRefMap: 两个jar包之间关联的信息
- jarPathDependencyMap: jar路径和依赖的映射关系
{
"bootJarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"classpathJarPaths": [
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/9af88b1981097178b09bd573014bfda6/lifecycle-viewmodel-2.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/f183f4d072c7e7d4fa8026ec8c727cff/interpolator-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/502ef6f4cbcb90b320b6408c384a70fd/vectordrawable-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/8f9b4d8db9e4811a4566f2bdfead37eb/jetified-appcompat-resources-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.70/e5d97e25bb5b30dcfc022ec1c8f3959a875257fb/kotlin-stdlib-1.3.70.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/androidx.collection/collection/1.1.0/1f27220b47669781457de0d600849a5de0e89909/collection-1.1.0.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/0322df2ec19980a4718110fbe6330e13/jetified-savedstate-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/40c8e4c3a7c2cb95ed86268e19d54370/lifecycle-livedata-2.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/58739eda211c462d06229a67014dc847/lifecycle-livedata-core-2.0.0/jars/classes.jar",
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/49d437b0f3c29ae27e87cbafb4a6f260/transition-1.2.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.70/3fa8dd6c896d635e78201e5e811545f3846dec04/kotlin-stdlib-common-1.3.70.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/2cad5196d8d124071b7754ea606ee3b4/customview-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/androidx.annotation/annotation/1.1.0/e3a6fb2f40e3a3842e6b7472628ba4ce416ea4c8/annotation-1.1.0.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/e953bd98d3b04a8878566e2fea3195f3/vectordrawable-animated-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/androidx.arch.core/core-common/2.1.0/b3152fc64428c9354344bd89848ecddc09b6f07e/core-common-2.1.0.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/41fc22079476e3e282d2ed3641ff14fa/jetified-activity-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/a6a8b9240bb260704cdd96bff17d1157/drawerlayout-1.0.0/jars/classes.jar",
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/javalib1/build/libs/javalib1.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/2a0e305da24846f3e20625eb0c9d62a7/fragment-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.7/69d9503ea0a40ee16f0bcdac7e3eaf83d0fa914a/gson-2.8.7.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/e71699873477e0531c44b35d6fedee9e/viewpager-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/7781c1a15ccac1e25e1944a3f9bc8c67/core-runtime-2.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/8c4fb1441f20b8346a52b9617ff00b80/jetified-viewpager2-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/9715cdb6aa73e3311549b7c7d5697dc9/lifecycle-runtime-2.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/0148c504bf49d69fc0db21ace9bc27ec/recyclerview-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/d0a6d74b07aaab2a5b17c8f4b8a47e74/cursoradapter-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/61d8d915a7e283b3eed7aa5bb2bd0f75/material-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/5e3a688aef4d72c6a02a888ee3ce07ca/coordinatorlayout-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/df8638fb6ce6747faa4768159d06f4e6/cardview-1.0.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/fa18f164ebc4d3af2c50af480e90366c/core-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.1.0/c67e7807d9cd6c329b9d0218b2ec4e505dd340b7/lifecycle-common-2.1.0.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/8dfc6584720f127bd1a3c835e5587f9d/appcompat-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/1be1b54c35cd270cecfbe779ae5b51ed/versionedparcelable-1.1.0/jars/classes.jar",
"/Users/xxx/.gradle/caches/transforms-2/files-2.1/eeb01b137525576c25e6ec6ca5ca18c0/loader-1.0.0/jars/classes.jar"
],
"linkedJarPaths": [
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar",
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/javalib1/build/libs/javalib1.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.70/e5d97e25bb5b30dcfc022ec1c8f3959a875257fb/kotlin-stdlib-1.3.70.jar",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.7/69d9503ea0a40ee16f0bcdac7e3eaf83d0fa914a/gson-2.8.7.jar"
],
"jarRefMap": {
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar": [
{
"jarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"refJarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/javalib1/build/libs/javalib1.jar",
"level": 1,
"refClasses": [
"javalib1.Test1"
],
"classRefInfos": {
"androidlibrary1.Test1": [
"javalib1.Test1"
]
},
"reverseClassRefInfos": {
"javalib1.Test1": [
"androidlibrary1.Test1"
]
}
},
{
"jarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"refJarPath": "/Users/xxx/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.7/69d9503ea0a40ee16f0bcdac7e3eaf83d0fa914a/gson-2.8.7.jar",
"level": 1,
"refClasses": [
"com.google.gson.Gson"
],
"classRefInfos": {
"androidlibrary1.TestKt1": [
"com.google.gson.Gson"
]
},
"reverseClassRefInfos": {
"com.google.gson.Gson": [
"androidlibrary1.TestKt1"
]
}
},
{
"jarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"refJarPath": "/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.70/e5d97e25bb5b30dcfc022ec1c8f3959a875257fb/kotlin-stdlib-1.3.70.jar",
"level": 1,
"refClasses": [
"kotlin.Metadata"
],
"classRefInfos": {
"androidlibrary1.TestKt1": [
"kotlin.Metadata"
]
},
"reverseClassRefInfos": {
"kotlin.Metadata": [
"androidlibrary1.TestKt1"
]
}
},
{
"jarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib1/build/intermediates/full_jar/debug/full.jar",
"refJarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar",
"level": 1,
"refClasses": [
"androidlibrary2.Test1"
],
"classRefInfos": {
"androidlibrary1.Test1": [
"androidlibrary2.Test1"
]
},
"reverseClassRefInfos": {
"androidlibrary2.Test1": [
"androidlibrary1.Test1"
]
}
}
],
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar": [
{
"jarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar",
"refJarPath": "/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/javalib1/build/libs/javalib1.jar",
"level": 2,
"refClasses": [
"javalib1.Test1"
],
"classRefInfos": {
"androidlibrary2.Test1": [
"javalib1.Test1"
]
},
"reverseClassRefInfos": {
"javalib1.Test1": [
"androidlibrary2.Test1"
]
}
}
]
},
"jarPathDependencyMap": {
"/Users/xxx/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.7/69d9503ea0a40ee16f0bcdac7e3eaf83d0fa914a/gson-2.8.7.jar": "com.google.code.gson:gson:2.8.7",
"/Users/xxx/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.70/e5d97e25bb5b30dcfc022ec1c8f3959a875257fb/kotlin-stdlib-1.3.70.jar": "org.jetbrains.kotlin:kotlin-stdlib:1.3.70",
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/javalib1/build/libs/javalib1.jar": "sample:javalib1:null",
"/Users/xxx/Projects/plugin/poizon-jdeps-gradle-plugin/sample/androidlib2/build/intermediates/full_jar/debug/full.jar": "sample:androidlib2:null"
}
}
最小的compileClasspath链路下面的内容中称为realRefDeps。
扫描无用的implementation
有了扫描最小的compileClasspath链路的能力,后面的事情就迎刃而解了。
没有用到的implementation = implementationDeps - apiDeps - realRefDeps,具体的实现如下:
源码模块中有可能会用到将要被剪除的implementation传递的依赖,所以还需要扫描剪除后需要补的组件列表(移除的组件pom文件带过来的)。
具体的实现是首先探测剪除依赖后的平铺组件列表,然后遍历realRefDeps平铺列表中不存在的组件就是需要补上的组件。
应用于工程
源码依赖/aar依赖对齐
使用shell调用所有业务模块的依赖分析任务,把输出的结果保存下来,方便以后的对比。
#!/bin/bash
output_dir="$(pwd)/poizon-jdeps"
mkdir -p $output_dir > /dev/null 2>&1
err_log="$output_dir/generate-minimum-deps-error.txt"
rm $err_log > /dev/null 2>&1
for project_path in $(cat << 'EOF'
/Users/xxx/codes/duapp-dependency-analysis/module_biz1
......
/Users/xxx/codes/duapp-dependency-analysis/lib1
EOF) ;do
group_id=$(cat $project_path/build.gradle | grep "group " | sed "s/group \"//g" | sed "s/group \'//g" | sed "s/\"//g" | awk '$1=$1')
module_name=$(basename $project_path)
echo "${group_id}:${module_name} $project_path"
echo "{\"local\":[{\"path\":\"${project_path}\",\"target\":\"${group_id}:${module_name}\"}}" > ./.idea/poizon.pzn
./gradlew :${module_name}:debugClassRefGraphWithFullJar
if [[ $? == 0 ]];then
rm -rf $output_dir/$module_name/ > /dev/null 2>&1
cp -r $project_path/build/poizon-jdeps $output_dir/$module_name/
else
echo "${module_name}: 执行失败"
echo "${module_name}: 执行失败" >> $err_log
fi
done
打开所有业务组件的源码,和需要处理的某个基础组件的源码(相当于把所有的implementation在pom中的节点scope设置为runtime),获取当前的compileClasspath和上一步获取的内容做比较 ,就能得知缺失的组件。
#!/bin/bash
output_dir="poizon-jdeps/miss-deps"
mkdir -p $output_dir > /dev/null 2>&1
for project_path in $(cat << 'EOF'
/Users/xxx/codes/duapp-dependency-analysis/module_biz1
......
/Users/xxx/codes/duapp-dependency-analysis/lib1
EOF) ;do
group_id=$(cat $project_path/build.gradle | grep "group " | sed "s/group \"//g" | sed "s/group \'//g" | sed "s/\"//g" | awk '$1=$1')
module_name=$(basename $project_path)
rm "$output_dir/${module_name}.txt" > /dev/null 2>&1
rm "$output_dir/${module_name}-with-version.txt" > /dev/null 2>&1
./gradlew :${module_name}:debugCompileClasspathDeps -DlockVersion=false
for dep in $(cat poizon-jdeps/$module_name/debug/compileClasspath/minimum-deps-by-class-ref.txt) ;do
dep_group=$(echo $dep | awk -F ':' '{print $1}')
dep_name=$(echo $dep | awk -F ':' '{print $2}')
dep_version=$(echo $dep | awk -F ':' '{print $3}')
echo "check $dep_group:$dep_name"
cat $project_path/build/poizon-jdeps/debug/compileClasspath/foreach-artifacts.sh | grep "${dep_group}:${dep_name}" > /dev/null
if [[ $? != 0 ]]; then
echo "implementation \"${dep_group}:${dep_name}\"" >> "$output_dir/${module_name}.txt"
echo "implementation \"${dep_group}:${dep_name}:${dep_version}\"" >> "$output_dir/${module_name}-with-version.txt"
fi
done
done
如果把组件发布一刀切全部切到新的发布插件,会造成大面积的业务组件报错block开发,所以使用了白名单的方式决定是否把scope设置为runtime,重新发布后风险较低的组件(依赖比较少)直接放进白名单里面,风险高的组件(依赖较多)处理一个使用上述的方式扫描一下把缺失的组件补进业务组件后,在加到白名单里面。
优化common模块的api组件
首先拿到common都有哪些api的依赖。
ConfigurationContainer configurationContainer = project.getConfigurations() Configuration
apiConfiguration = configurationContainer.getByName("releaseApi")
apiConfiguration.getAllDependencies() //需要的结果
然后循环的调用每个业务模块的依赖分析任务
遍历common的api依赖,去业务模块的分析结果文件里寻找是否包含这个依赖,发现一个计数+1。
每个组件的使用率 = 命中的次数 / 总的业务模块的个数
按照70%的阈值
= 70%的留在common中
0% && < 70%的放进业务模块中单独implementation
== 0%的直接移除掉
移除无用的implementation
首先需要知道组件的仓库有哪些,把以下代码放进build.gradle中,执行gradle -q
import groovy.json.JsonSlurper
def groups = ['xxx']
groups.each { groupId->
String url = "https://gitlab.xxx.com/api/v4/groups/${groupId}/projects?private_token=xxx&per_page=100"
def projects = new JsonSlurper().parse(url.toURL())
projects.eachWithIndex{it, index->
println(it.ssh_url_to_repo)
}
}
使用gradle init.d机制把jdeps-plugin加到工程里,把下面这个脚本放到~/.gradle/init.d/apply_jdeps.gradle
import com.shizhuang.duapp.jdepsplugin.GradlePlugin
initscript {
dependencies {
classpath 'com.shizhuang.duapp.plugin:poizon-jdeps:0.0.18'
}
}
gradle.rootProject{
it.buildscript {
dependencies {
classpath 'com.shizhuang.duapp.plugin:poizon-jdeps:0.0.18'
}
}
apply plugin: GradlePlugin
}
使用shell调用所有组件的分析任务,扫描了112个gradle project,有52个组件存在无用的implementation组件,总计移除221个。
总结
poizon-jdeps应用于工程后,简化了处理源码依赖/aar依赖对齐的工作,拉掉无用implementation组件和处理完common模块的api组件后,平均提高了10%左右的业务模块compileClasspath组件有效使用率,对提高增量编译的占比有一定作用,sync时间从之前的25秒左右降到10秒左右。
未来规划
1、常量引用,编译时做值copy丢失关联关系;从java、kotlin源码出发解析出AST生成JarInfo
2、通过Class.forName("com.example.Test") 引用的扫描不出来;解析MethodInfo字节码
3、资源文件里的引用暂时不支持;解析layout的xml文件取出自定义View对应的类,hook aapt流程拿到资源的link信息
4、扫描的入口是工程的full_jar,所以需要依赖编译成功才能扫描;如果从java、kotlin源文件出发解析出工程的selfClasses、refClasses,就可以脱离编译直接扫描出丢失的类,进而从组件池里映射出来丢失的依赖
5、studio External libraries侧边栏只会展示所有工程中compileClasspath中的组件,很多基础组件拉掉以后在这里就展示不出来了,会造成开发人员的困扰,也算是一个副作用吧;hook studio sync,把runtime组件也加到External libraries里
6、在工程中使用时有些场景需要shell脚本配合;后续尽量把通用的需求收敛到poizon-jdeps中