随着业务的高速发展,代码量也越来越多,良好的工程结构和依赖管理对构建速度有着积极的作用,文章介绍了最近一段时间得物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组件的时间戳,有这个值就可以确认是否为最新的包
下面的内容中称为selfClasses

下面的内容中称为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包信息。

可以把依赖树看做成一个以project的full_jar为根结点允许存在相同叶子结点的多叉树,扫描最小的compileClasspath链路可以理解为寻找有效的叶子结点,从project的full_jar出发,广度优先搜索依赖树,按层级通过refClass寻找链路上的所有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。
有了扫描最小的compileClasspath链路的能力,后面的事情就迎刃而解了。
没有用到的implementation = implementationDeps - apiDeps - realRefDeps,具体的实现如下:

源码模块中有可能会用到将要被剪除的implementation传递的依赖,所以还需要扫描剪除后需要补的组件列表(移除的组件pom文件带过来的)。
具体的实现是首先探测剪除依赖后的平铺组件列表,然后遍历realRefDeps平铺列表中不存在的组件就是需要补上的组件。

使用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的依赖。
ConfigurationContainer configurationContainer = project.getConfigurations() Configuration
apiConfiguration = configurationContainer.getByName("releaseApi")
apiConfiguration.getAllDependencies() //需要的结果
然后循环的调用每个业务模块的依赖分析任务

遍历common的api依赖,去业务模块的分析结果文件里寻找是否包含这个依赖,发现一个计数+1。
每个组件的使用率 = 命中的次数 / 总的业务模块的个数
按照70%的阈值
= 70%的留在common中
0% && < 70%的放进业务模块中单独implementation
== 0%的直接移除掉
首先需要知道组件的仓库有哪些,把以下代码放进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中
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在尝试修改当前依赖于定义为activeresource的gem:s.add_dependency"activeresource","~>3.0"为了让gem与Rails4一起工作,我需要扩展依赖关系以与activeresource的版本3或4一起工作。我不想简单地添加以下内容,因为它可能会在以后引起问题:s.add_dependency"activeresource",">=3.0"有没有办法指定可接受版本的列表?~>3.0还是~>4.0? 最佳答案 根据thedocumentation,如果你想要3到4之间的所有版本,你可以这
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来
我有一个要在我的Rails3项目中使用的数组扩展方法。它应该住在哪里?我有一个应用程序/类,我最初把它放在(array_extensions.rb)中,在我的config/application.rb中我加载路径:config.autoload_paths+=%W(#{Rails.root}/应用程序/类)。但是,当我转到railsconsole时,未加载扩展。是否有一个预定义的位置可以放置我的Rails3扩展方法?或者,一种预先定义的方式来添加它们?我知道Rails有自己的数组扩展方法。我应该将我的添加到active_support/core_ext/array/conversion
有什么方法可以告诉sidekiq一项工作依赖于另一项工作,并且在后者完成之前无法开始? 最佳答案 仅使用Sidekiq;答案是否定的。正如DickieBoy所建议的那样,您应该能够在依赖作业完成时将其启动。像这样。#app/workers/hard_worker.rbclassHardWorkerincludeSidekiq::Workerdefperform()puts'Doinghardwork'LazyWorker.perform_async()endend#app/workers/lazy_worker.rbclassLaz
参见下面的示例,我想最好使用第二种方法,但第一种也可以。哪种方法最好,使用另一种的后果是什么?classTestdefstartp"started"endtest=Test.newtest.startendclassTest2defstartp"started"endendtest2=Test2.newtest2.start 最佳答案 我肯定会说第二种变体更有意义。第一个不会导致错误,但对象实例化完全过时且毫无意义。外部变量在类的范围内不可见:var="string"classAvar=A.newendputsvar#=>strin
如果我构建了一个应用程序来访问来自Gmail、Twitter和Facebook的一些数据,并且我希望用户只需输入一次他们的身份验证信息,并且在几天或几周后重置,那会怎样是在Ruby中动态执行此操作的最佳方法吗?我看到很多人只是拥有他们客户/用户凭证的配置文件,如下所示:gmail_account:username:myClientpassword:myClientsPassword这看起来a)非常不安全,b)如果我想为成千上万的用户存储此类信息,它就无法工作。推荐的方法是什么?我希望能够在这些服务之上构建一个界面,因此每次用户进行交易时都必须输入凭据是不可行的。