上篇说过要做一次自定义gradle插件的实战,本篇文章就记录下两个场景下的实践,实践内容属于入门级别的,相对简单,第一:查找多模块中出现的相同Activity名称;第二:找出图片资源中重复的png图片。
场景一:找出相同的activity
因为多模块开发,所以难免出现这样的场景,不同人负责不同的模块,难免出现同样的命名,这给统计埋点也造成了影响,我们的目的就是找出这些相同的命名。这里应用到的是ASM字节码操控框架,直接操作字节码,继而改变java类,以下是ASM的概念
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
背后的原理大概是,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了。
所以我们需要在.class转成dex文件前,扫描class并修改它,这里gradle提供了Transform API让我们更方便地完成这个操作(这个场景我们这里只做扫描,不做操作)
1.继承Transform,实现以下几个方法
class ScanDuplicateTransform(var mProjectDir:File) : Transform() {
/**
* 设置我们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上
* @return String
*/
override fun getName(): String = "ScanDuplicateTransform"
/**
* 在项目中会有各种各样格式的文件,该方法可以设置 Transform 接收的文件类型
* 具体取值范围
* CONTENT_CLASS .class 文件
* CONTENT_JARS jar 包
* CONTENT_RESOURCES 资源 包含 java 文件
* CONTENT_NATIVE_LIBS native lib
* CONTENT_DEX dex 文件
* CONTENT_DEX_WITH_RESOURCES dex 文件
* @return
*/
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
/**
* 定义 Transform 检索的范围
* PROJECT 只检索项目内容
* SUB_PROJECTS 只检索子项目内容
* EXTERNAL_LIBRARIES 只有外部库
* TESTED_CODE 由当前变量测试的代码,包括依赖项
* PROVIDED_ONLY 仅提供的本地或远程依赖项
* @return
*/
//只检索项目内容
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.PROJECT_ONLY
/**
* 表示当前 Transform 是否支持增量编译 返回 true 标识支持 目前测试插件不需要
* @return Boolean
*/
override fun isIncremental(): Boolean = false
//对项目 class 检索操作
override fun transform(transformInvocation: TransformInvocation) {
println("transform 方法调用")
//获取所有 输入 文件集合
val transformInputs = transformInvocation.inputs
val transformOutputProvider = transformInvocation.outputProvider
transformOutputProvider?.deleteAll()
transformInputs.forEach { transformInput ->
// Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.appcompat.R$drawable" on path 问题
// gradle 3.6.0以上R类不会转为.class文件而会转成jar,因此在Transform实现中需要单独拷贝,TransformInvocation.inputs.jarInputs
// jar 文件处理
transformInput.jarInputs.forEach { jarInput ->
val file = jarInput.file
val dest = transformOutputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
println("find jar input:$dest")
FileUtils.copyFile(file, dest)
}
//源码文件处理
//directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
transformInput.directoryInputs.forEach { directoryInput ->
//遍历所有文件和文件夹 找到 class 结尾文件
directoryInput.file.walkTopDown()
.filter { it.isFile }
.filter { it.extension == "class" }
.forEach { file ->
// println("find class file:${file.name}")
val classReader = ClassReader(file.readBytes())
val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//字节码插桩处理
//2.class 读取传入 ASM visitor
val scanDuplicateClassVisitor = ScanDuplicateClassVisitor(mProjectDir,classWriter)
//3.通过ClassVisitor api 处理
classReader.accept(scanDuplicateClassVisitor,ClassReader.EXPAND_FRAMES)
//4.处理修改成功的字节码
val bytes = classWriter.toByteArray()
//写回文件中
val fos = FileOutputStream(file.path)
fos.write(bytes)
fos.close()
}
//复制到对应目录
val dest = transformOutputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes,directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file,dest)
}
}
}
}
代码中把字节码操作的流程都写了出来,其实我们的场景只是要扫描,不会动到class文件,这里只是借来看看,马上还回去,主要是通过ScanDuplicateClassVisitor
这个类来进行扫描
val scanDuplicateClassVisitor=ScanDuplicateClassVisitor(mProjectDir,classWriter)
classReader.accept(scanDuplicateClassVisitor,ClassReader.EXPAND_FRAMES)
2.通过ASM框架里的ClassVisitor来扫描class文件
class ScanDuplicateClassVisitor( file: File,classVisitor: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, classVisitor) {
private var className:String? = null
private var superName:String? = null
private var mFile:File? =null
init {
mFile = File(file,"activity_name_c.txt")
}
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String>?) {
super.visit(version, access, name, signature, superName, interfaces)
this.className = name
this.superName = superName
if (superName == "xxx"){
getLastName(name)
}
if (superName == "yyy"){
getLastName(name)
}
}
override fun visitModule(name: String?, access: Int, version: String?): ModuleVisitor {
println("------visitModule------ "+name)
return super.visitModule(name, access, version)
}
private fun getLastName(name: String?){
val pos = name?.lastIndexOf("/")!!+1
if (name.isEmpty().not()){
val lastName = name.substring(pos,name.length)
println(lastName)
writeFileName(lastName)
}
}
private fun writeFileName(name: String?){
val bytes: ByteArray = name!!.toByteArray()
val fos = FileOutputStream(mFile,true)
fos.apply {
write(bytes)
flush()
close()
}
}
override fun visitMethod(access: Int, name: String, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
val methodVisitor = cv.visitMethod(access,name,descriptor,signature,exceptions)
//找到 androidX 包下的 Activity 类
return methodVisitor
}
override fun visitEnd() {
super.visitEnd()
}
}
利用ASM框架的classVisit就可以扫描到所有class文件名甚至是方法名,进而为所欲为,非常的intersting,我先是把所有继承activity的页面名称打印出来,保存在activity_name_c文件里,这里我正好之前学了点python皮毛,寻找相同名称,我用的如下
import shutil
def openFile():
f = open("E:\\MyTestSpace\\kplugin\\app\\build\\activity_name_c.txt","r")
new_file = open("E:\\MyTestSpace\\kplugin\\app\\build\\duplicate_activity.txt","a")
new_file.write("")
files = f.readlines()
words_dic = {}
for line in files:
if line in words_dic:
words_dic[line]+=1
else:
words_dic[line] = 1#第一次出现的单词我们把其值赋值为1
for (key,value) in words_dic.items():
if(value > 1):
print(key)
print(words_dic)
f.close()
openFile()
有点费事,主要是自己练手,不是实战写法,比较野生。以后有机会深入这块在再做完善。.
3、自定义Plugin,将ScanDuplicateTransform进行注册
import com.android.build.gradle.AppExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.api.BaseVariantImpl
import com.kunsan.plugin.utils.Md5Util
import com.kunsan.plugin.utils.PathUtils
import com.sun.imageio.plugins.common.ImageUtil
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.util.*
import java.util.stream.Collectors
import kotlin.collections.ArrayList
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
//////////////////////////////// 场景一 ///////////////////////////////////////////////
val asmTransform = target.extensions.getByType(LibraryExtension::class.java)
val transform = ScanDuplicateTransform(target.buildDir)
asmTransform.registerTransform(transform)
}
}
场景二:找出重复的png资源
做这个的目的同样的也是多人开发,避免导入同个png图片,当然这涉及到公司开发规范问题,正常是不应该出现这种情况的,其实场景一与场景二比较像,都是扫描,但是应用的工具不一样,我们直接利用gradle提供的方法allRawAndroidResources.files
就可以获取所有的图片,先找出相同大小的图片,再计算他们的MD5值,就可以知道哪些图片是一样的,进而手动删除(比较稳妥)
import com.android.build.gradle.AppExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.api.BaseVariantImpl
import com.kunsan.plugin.utils.Md5Util
import com.kunsan.plugin.utils.PathUtils
import com.sun.imageio.plugins.common.ImageUtil
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import java.util.*
import java.util.stream.Collectors
import kotlin.collections.ArrayList
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
//////////////////////////////// 场景二 ///////////////////////////////////////////////
//check is library or application
val hasAppPlugin = target.plugins.hasPlugin("com.android.application")
val variants = if (hasAppPlugin) {
(target.property("android") as AppExtension).applicationVariants
} else {
(target.property("android") as LibraryExtension).libraryVariants
}
//获取资源
target.afterEvaluate {
variants.all{ variant ->
val mergeResourceTask = variant.mergeResourcesProvider.get()
val mcPicTask = target.task("KsImage${variant.name.capitalize()}")
mcPicTask.doLast{
val dir = variant.allRawAndroidResources.files
for (channelDir: File in dir) {
traverseResDir(channelDir)
}
sameLengthFileMap.forEach {
it.value.forEach{ name ->
if (sameMd5List.contains(Md5Util.getMD5Str(File(name)))){
println("======= 重复图片 ========== "+PathUtils.getLastName(name))
}
sameMd5List.add(Md5Util.getMD5Str(File(name)))
}
}
}
}
}
}
/**
* key -> 图片size
* value -> 具有相同size的图片集合
*/
var sameLengthFileMap = hashMapOf<Long,ArrayList<String>>()
/**
* 用于判断是否存在相同MD5值的图片
*/
var sameMd5List = arrayListOf<String?>()
/**
* 递归res文件夹
*/
private fun traverseResDir(file: File){
if (file.isDirectory){
file.listFiles().forEach { it ->
if (it.isDirectory){
if (it.absolutePath.contains(".gradle\\caches")){
return@forEach
}else{
traverseResDir(it)
}
}else{
if (ImageUtils.isImage(it) && !it.absolutePath.contains("ic_launcher")){
filterImage(it)
}
}
}
}
}
/**
* 将相同size的图片放到map中
* 做第一次过滤
*/
private fun filterImage(file: File){
var mList = sameLengthFileMap[file.length()]
if (mList == null){
mList = ArrayList()
sameLengthFileMap[file.length()] = mList
}
val imageName = file.absolutePath
if (!mList.contains(imageName)){
mList.add(imageName)
}
}
}
总结
以上就是立下的flag终于完成了,算是很入门,作为以后有机会进一步的基础,给自己做个笔记,不然容易忘,同时也希望小伙伴指点。