可能是最通用全面的Android studio打包jar方法

原文链接

现状

网上关于Android studio打包jar的教程很多,基本思路如下

  1. 项目build.gradle中增加一个Jar任务,
  2. 指定打包路径。如下:
task buildJar(dependsOn: ['assembleDebug'], type: Jar) {

    ....
      
    def srcClassDir = [project.buildDir.absolutePath + "/intermediates/classes/debug"];
    from srcClassDir
    include "**/*.class"
    ....
}

这样做个人觉得有几个问题:

  1. 只能给当前项目应用module打包/intermediates/classes/debug

    对于依赖的aar,如support v7,编译输出class是在/intermediates/exploded-aar/
    对于依赖的jar包,目测在intermediates中根本找不到

  2. 不能混淆,当然你也可以在build.gradle写一个ProGuardTask,具体可参见这篇文章,这里直接复制其最终生成build.gradle如下:

   import com.android.build.gradle.AppPlugin
   import com.android.build.gradle.LibraryPlugin
   import proguard.gradle.ProGuardTask
   apply plugin: 'com.android.application'
   android {
       compileSdkVersion 23
       buildToolsVersion "23.0.2"
       defaultConfig {
           applicationId "org.chaos.demo.jar"
           minSdkVersion 19
           targetSdkVersion 22
           versionCode 1
           versionName "1.0"
       }
       buildTypes {
           release {
               minifyEnabled true
               proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
           }
       }
   }
   dependencies {
       compile fileTree(dir: 'libs', include: ['*.jar'])
   }
   //dependsOn 可根据实际需要增加或更改
   task buildJar(dependsOn: ['compileReleaseJavaWithJavac'], type: Jar) {
       appendix = "demo"
       baseName = "androidJar"
       version = "1.0.0"
       classifier = "release"
       //后缀名
       extension = "jar"
       //最终的 Jar 包名,如果没设置,默认为 [baseName]-[appendix]-[version]-[classifier].[extension]
       archiveName = "AndroidJarDemo.jar"
       //需打包的资源所在的路径集
       def srcClassDir = [project.buildDir.absolutePath + "/intermediates/classes/release"];
       //初始化资源路径集
       from srcClassDir
       //去除路径集下部分的资源
   //    exclude "org/chaos/demo/jar/MainActivity.class"
   //    exclude "org/chaos/demo/jar/MainActivity\$*.class"
   //    exclude "org/chaos/demo/jar/BuildConfig.class"
   //    exclude "org/chaos/demo/jar/BuildConfig\$*.class"
   //    exclude "**/R.class"
   //    exclude "**/R\$*.class"
       //只导入资源路径集下的部分资源
       include "org/chaos/demo/jar/**/*.class"
       //注: exclude include 支持可变长参数
   }
   task proguardJar(dependsOn: ['buildJar'], type: ProGuardTask) {
       //Android 默认的 proguard 文件
       configuration android.getDefaultProguardFile('proguard-android.txt')
       //manifest 注册的组件对应的 proguard 文件
       configuration project.buildDir.absolutePath + "/intermediates/proguard-rules/release/aapt_rules.txt"
       configuration 'proguard-rules.pro'
       String inJar = buildJar.archivePath.getAbsolutePath()
       //输入 jar
       injars inJar
       //输出 jar
       outjars inJar.substring(0, inJar.lastIndexOf('/')) + "/proguard-${buildJar.archiveName}"
       //设置不删除未引用的资源(类,方法等)
       dontshrink
       Plugin plugin = getPlugins().hasPlugin(AppPlugin) ?
               getPlugins().findPlugin(AppPlugin) :
               getPlugins().findPlugin(LibraryPlugin)
       if (plugin != null) {
           List<String> runtimeJarList
           if (plugin.getMetaClass().getMetaMethod("getRuntimeJarList")) {
               runtimeJarList = plugin.getRuntimeJarList()
           } else if (android.getMetaClass().getMetaMethod("getBootClasspath")) {
               runtimeJarList = android.getBootClasspath()
           } else {
               runtimeJarList = plugin.getBootClasspath()
           }
           for (String runtimeJar : runtimeJarList) {
               //给 proguard 添加 runtime
               libraryjars(runtimeJar)
           }
       }
   }

看起来真不太舒服不是?(无意冒犯)

  1. 对于一个强迫症的程序员,除了代码要整洁之外,编译脚本文件build.gradle不整洁也不能忍
apply plugin: 'com.android.application'
apply plugin: 'jar-gradle-plugin'
android {
    compileSdkVersion 24
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.adison.testjarplugin"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.0.0'
    compile 'com.android.support:design:24.0.0'
}

BuildJar{
    //输出目录
    outputFileDir= project.buildDir.path+"/jar"
    //输出原始jar包名
    outputFileName="test.jar"
    //输出混淆jar包名
    outputProguardFileName="test_proguard.jar"
    //混淆配置
    proguardConfigFile="proguard-rules.pro"
    //是否需要默认的混淆配置proguard-android.txt
    needDefaultProguard=true
}

这样感觉是不是好些了哈

实践

关于第一个问题,我们可以利用Android Transform Task解决,其官方说明如下:

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files.(The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

可见Transform Task的输入文件肯定包含apk所有依赖class及其本身class,我们只要取得其输入文件就行了

关于第三个问题,我们写一个Gradle插件,把业务逻辑都交给插件处理就好了,关于Gradle及自定义Gradle插件可以参考Gradle深入与实战系列文章,在此不展开说明。废话不多说,直接上插件代码:

class BuildJarPlugin implements Plugin<Project> {
    public static final String EXTENSION_NAME = "BuildJar";

    @Override
    public void apply(Project project) {
        DefaultDomainObjectSet<ApplicationVariant> variants
        if (project.getPlugins().hasPlugin(AppPlugin)) {
            variants = project.android.applicationVariants;
            project.extensions.create(EXTENSION_NAME, BuildJarExtension);  
            applyTask(project, variants);
        }
    }

    private void applyTask(Project project, variants) {

        project.afterEvaluate {
            BuildJarExtension jarExtension = BuildJarExtension.getConfig(project);
            def includePackage = jarExtension.includePackage
            def excludeClass = jarExtension.excludeClass
            def excludePackage = jarExtension.excludePackage
            def excludeJar = jarExtension.excludeJar

            variants.all { variant ->
                if (variant.name.capitalize() == "Debug") {
                    def dexTask = project.tasks.findByName(BuildJarUtils.getDexTaskName(project, variant))
                    if (dexTask != null) {
                        def buildJarBeforeDex = "buildJarBeforeDex${variant.name.capitalize()}"

                        def buildJar = project.tasks.create("buildJar", Jar)
                        buildJar.setDescription("构建jar包")
                        Closure buildJarClosure = {
                            //过滤R文件和BuildConfig文件
                            buildJar.exclude("**/BuildConfig.class")
                            buildJar.exclude("**/BuildConfig\$*.class")
                            buildJar.exclude("**/R.class")
                            buildJar.exclude("**/R\$*.class")
                            buildJar.archiveName = jarExtension.outputFileName
                            buildJar.destinationDir = project.file(jarExtension.outputFileDir)
                            if (excludeClass != null && excludeClass.size() > 0) {
                                excludeClass.each {
                                    //排除指定class
                                    buildJar.exclude(it)
                                }

                            }
                            if (excludePackage != null && excludePackage.size() > 0) {
                                excludePackage.each {
                                    //过滤指定包名下class
                                    buildJar.exclude("${it}/**/*.class")
                                }

                            }
                            if (includePackage != null && includePackage.size() > 0) {
                                includePackage.each {
                                    //仅仅打包指定包名下class
                                    buildJar.include("${it}/**/*.class")
                                }

                            } else {
                                //默认全项目构建jar
                                buildJar.include("**/*.class")

                            }
                        }
                        project.task(buildJarBeforeDex) << {
                            Set<File> inputFiles = BuildJarUtils.getDexTaskInputFiles(project, variant, dexTask)

                            inputFiles.each { inputFile ->
                                def path = inputFile.absolutePath
                                if (path.endsWith(SdkConstants.DOT_JAR) && !BuildJarUtils.isExcludedJar(path, excludeJar)) {
                                    buildJar.from(project.zipTree(path))
                                } else if (inputFile.isDirectory()) {
                                    //intermediates/classes/debug
                                    buildJar.from(inputFile)
                                }
                            }
                        }

                        def buildProguardJar = project.tasks.create("buildProguardJar", ProGuardTask);
                        buildProguardJar.setDescription("混淆jar包")
                        buildProguardJar.dependsOn buildJar
                        //设置不删除未引用的资源(类,方法等)
                        buildProguardJar.dontshrink();
                        //忽略警告
                        buildProguardJar.ignorewarnings()
                        //需要被混淆的jar包
                        buildProguardJar.injars(jarExtension.outputFileDir + "/" + jarExtension.outputFileName)
                        //混淆后输出的jar包
                        buildProguardJar.outjars(jarExtension.outputFileDir + "/" + jarExtension.outputProguardFileName)

                        //libraryjars表示引用到的jar包不被混淆
                        // ANDROID PLATFORM
                        buildProguardJar.libraryjars(project.android.getSdkDirectory().toString() + "/platforms/" + "${project.android.compileSdkVersion}" + "/android.jar")
                        // JAVA HOME
                        def javaBase = System.properties["java.home"]
                        def javaRt = "/lib/rt.jar"
                        if (System.properties["os.name"].toString().toLowerCase().contains("mac")) {
                            if (!new File(javaBase + javaRt).exists()) {
                                javaRt = "/../Classes/classes.jar"
                            }
                        }
                        buildProguardJar.libraryjars(javaBase + "/" + javaRt)
                        //混淆配置文件
                        buildProguardJar.configuration(jarExtension.proguardConfigFile)
                        if (jarExtension.needDefaultProguard) {
                            buildProguardJar.configuration(project.android.getDefaultProguardFile('proguard-android.txt'))
                        }
                        //applymapping
                        def applyMappingFile=jarExtension.applyMappingFile
                        if(applyMappingFile!=null){
                            buildProguardJar.applymapping(applyMappingFile)
                        }
                        //输出mapping文件
                        buildProguardJar.printmapping(jarExtension.outputFileDir + "/" + "mapping.txt")
                        def buildJarBeforeDexTask = project.tasks[buildJarBeforeDex]
                        buildJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(dexTask)
                        buildJar.dependsOn buildJarBeforeDexTask
                        buildJar.doFirst(buildJarClosure)
                    }
                }

            }
        }
    }


}

插件使用

既然标题说了这是一个通用的打包jar插件,那么一些基本特性,如过滤包名指定包名等是必须要支持的,目前该插件支持特性如下:

  1. 按需打包jar:
    • 全项目打包jar
    • 指定输出Jar包的包名路径列表
    • 过滤指定包名路径列表
    • 过滤指定class
    • 过滤指定jar
  2. 支持混淆打包jar
  3. 支持applymapping

具体使用说明

  1. 引入依赖

    dependencies {
            classpath 'com.android.tools.build:gradle:2.1.3'
            classpath 'com.adison.gradleplugin:jar:1.0.1'
        }
    

  2. 应用插件

    apply plugin: 'jar-gradle-plugin'
    BuildJar{
        //输出目录
        outputFileDir= project.buildDir.path+"/jar"
        //输出原始jar包名
        outputFileName="test.jar"
        //输出混淆jar包名
        outputProguardFileName="test_proguard.jar"
        //混淆配置
        proguardConfigFile="proguard-rules.pro"
        //是否需要默认的混淆配置proguard-android.txt
        needDefaultProguard=true
        applyMappingFile="originMapping/mapping.txt"
        //需要输出jar的包名列表,当此参数为空时,则默认全项目输出,支持多包,如 includePackage=['com/adison/testjarplugin/include','com/adison/testjarplugin/include1'...]
        includePackage=['com/adison/testjarplugin/include']
        //不需要输出jar的jar包列表,如['baidu.jar','baidu1.jar'...]
        excludeJar=[]
        //不需要输出jar的类名列表,如['baidu.calss','baidu1.class'...]
        excludeClass=['com/adison/testjarplugin/TestExcude.class']
        //不需要输出jar的包名列表,如 excludePackage=['com/adison/testjarplugin/exclude','com/adison/testjarplugin/exclude1'...]
        excludePackage=['com/adison/testjarplugin/exclude']
    }
    
  3. 使用

  • 打包普通jar
./gradlew buildJar
  • 打包混淆jar
./gradlew buildProguardJar
  > 使用可参见[使用demo](https://github.com/adisonhyh/TestJarPlugin)

插件源码

https://github.com/adisonhyh/buildJar

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

推荐阅读更多精彩内容