Android自定义Plugin替代重复劳动

       上篇说过要做一次自定义gradle插件的实战,本篇文章就记录下两个场景下的实践,实践内容属于入门级别的,相对简单,第一:查找多模块中出现的相同Activity名称;第二:找出图片资源中重复的png图片。

场景一:找出相同的activity

       因为多模块开发,所以难免出现这样的场景,不同人负责不同的模块,难免出现同样的命名,这给统计埋点也造成了影响,我们的目的就是找出这些相同的命名。这里应用到的是ASM字节码操控框架,直接操作字节码,继而改变java类,以下是ASM的概念

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

       背后的原理大概是,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了。

       但是在Android上,.class文件还需要转成dex文件,这里顺便贴上apk的打包过程示意图
apk打包流程

       所以我们需要在.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终于完成了,算是很入门,作为以后有机会进一步的基础,给自己做个笔记,不然容易忘,同时也希望小伙伴指点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容