SimpleNews 项目的重构之旅(5) - Android Gradle 打包&混淆应用

应用场景

之前一直没有做 Android APK 发包管理,所以这次重构把这打包这部分考虑进去,之后可能会发布到一些应用市场。

要实现的功能

混淆代码
实现签名
过滤无用资源
生成 release 版本 APK
自定义 APK 名称
生成多渠道包

带着这几个问题,一步一步在 Simplenews 项目中实现

混淆代码

说到混淆代码,还真看过一些上线的 app 没有做混淆,这种可以用反编译工具给反编译出来,安全性太差,虽说这个小项目没有商业价值可言,但是还是要掌握app的安全性的,了解如何混淆代码,很简单看实例 app proguard-rules.pro

本项目中一些不能混淆的类或者方法:

-keep class com.library.event.AppExitEvent
-keepattributes *Annotation*
-keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
-keep class com.kong.app.news.beans.** { *; }
-keep class com.actionbarsherlock.** { *; }
-keep class com.google.gson.** {*;}

-dontobfuscate
-dontwarn okio.**
-dontwarn okhttp3.**
-dontwarn rx.**
-dontnote sun.misc.Unsafe
-dontnote android.net.http.*
-dontnote org.apache.commons.codec.**
-dontnote org.apache.http.**
-dontnote com.android.org.conscrypt.**
-dontnote org.apache.harmony.xnet.provider.jsse.**
-dontnote com.android.internal.**
-dontwarn android.support.v4.**
-dontwarn com.dsi.ant.**
-dontwarn com.samsung.**
-dontwarn com.vividsolutions.jts.awt.**
-dontwarn android.support.**
-dontwarn com.squareup.okhttp.**
-dontnote android.net.http.*
-dontnote org.apache.http.**
-dontwarn javax.annotation.**
-dontwarn java.lang.invoke.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

主要是集中在一些注解,第三方库以及 Android 源码部分类和方法。

例如:

-keep class com.kong.app.news.beans.** { *; }
bean 目录

实现签名

给 App 签名,就相当于给 APP 上了一个牌照,保证了 APP 在线上市场的唯一性,这样才能安装到用户手机上话不多说,看操作

选择 Build -> Generate Signed APK

1

Next

2

Create New

3
4

生成签名文件还是挺简单的,不过真正有上线项目的时候,签名文件要注意保密,同时也不要上传到 github 上。

过滤无用资源

release {
           minifyEnabled true //是否启动混淆
           shrinkResources true //是否移除无用资源文件,shrinkResources依赖于minifyEnabled
          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}

通过过滤能够减少apk包大小,增加安装率

生成 release 版本包

我们发到线上的 apk 都是 release 版本,是经过混淆,签名,打包的 apk,是可以在各大市场进行下载安装合法 app。

根据时间更改打包 APK 名称

android.applicationVariants.all { variant ->
                   variant.outputs.each { output ->
                       def outputFile = output.outputFile
                       if (outputFile != null && outputFile.name.endsWith('.apk')) {
                           def fileName = "SimpleNews_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
                           output.outputFile = new File(outputFile.parent, fileName)
                       }
                   }
           }

最终生成的:SimpleNews_v1.0_2017-06-12_product1.apk

生成多渠道包

顺带着提提多渠道打包,我这个项目目前没涉及到多渠道打包,但真正项目中有很多这样的需求,比如,产品要统计每一个市场的安装下载和用户使用情况,针对不同的市场就要有不同版本的apk,也很好配置:

1、在AndroidManifest.xml里配置PlaceHolder

<meta-data
            android:name="PRODUCT_CHANNEL"
            android:value="${PRODUCT_CHANNEL_VALUE}" />

2、在 build.gradle 设置productFlavors

productFlavors {
      product1 {
          manifestPlaceholders.put("PRODUCT_CHANNEL_VALUE", 'product1')
      }
      product2 {
          manifestPlaceholders.put("PRODUCT_CHANNEL_VALUE", 'product2')
      }
  }

批量处理

release {
        //多渠道
            productFlavors.all { flavor ->
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE", name)
        }
 }
最终结果

我这里有两个例子,product1 product2 两个渠道,同步代码,输入打包命令打包即可,就会在build目录下看到生成的 apk

这里正好使用上一条更改 apk 名称功能,根据配置生成不同渠道对应的名称。

完整的 Gradle 文件

apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion //编译的 SDK API 版本
    buildToolsVersion rootProject.ext.android.buildToolsVersion //构建工具版本

    defaultConfig {
        applicationId rootProject.ext.android.applicationId//配置包名的
        minSdkVersion rootProject.ext.android.minSdkVersion //支持最小的 SDK 版本
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode//版本号
        versionName rootProject.ext.android.versionName//版本名称

//        manifestPlaceholders = [PRODUCT_CHANNEL_VALUE: "github"]// 默认
    }

    signingConfigs {
        releaseConfig {
            keyAlias rootProject.ext.releaseConfig.keyAlias
            keyPassword rootProject.ext.releaseConfig.keyPassword
            storeFile file(rootProject.ext.releaseConfig.storeFile)
            storePassword rootProject.ext.releaseConfig.storePassword
        }
    }

    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
        }

        release {
            zipAlignEnabled true
            minifyEnabled true //是否启动混淆
            shrinkResources true //是否移除无用资源文件,shrinkResources依赖于minifyEnabled
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.releaseConfig

            android.applicationVariants.all { variant ->
                    variant.outputs.each { output ->
                        def outputFile = output.outputFile
                        if (outputFile != null && outputFile.name.endsWith('.apk')) {
                            def fileName = "SimpleNews_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
                            output.outputFile = new File(outputFile.parent, fileName)
                        }
                    }
            }

            //多渠道
            productFlavors.all { flavor ->
                manifestPlaceholders.put("UMENG_CHANNEL_VALUE", name)
            }
        }
    }

    productFlavors {
        product1 {
            manifestPlaceholders.put("PRODUCT_CHANNEL_VALUE", 'product1')
        }
        product2 {
            manifestPlaceholders.put("PRODUCT_CHANNEL_VALUE", 'product2')
        }
    }

}

repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}

dependencies {
    //将libs文件夹中所有的jar文件视为依赖包。
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'

    //依赖远程仓库
    compile rootProject.ext.dependencies.design
    compile rootProject.ext.dependencies.cardview
    compile rootProject.ext.dependencies.circleimageview
    compile rootProject.ext.dependencies.sufficientlysecure

    compile project(':lib.utils')
    compile project(':lib.style')
}

def releaseTime() {
    return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}

完整 App build.gradle

总结

了解了以上知识,已经能满足简单的打包功能了,小项目中可能也就够了,先掌握最基本的内容,之后在再这之上做扩展,后续可能还需要在优化一些内容,比如 使用微信的混淆工具来大幅度压缩apk大小。


SimpleNews 项目的重构之旅其他文章

SimpleNews 项目的重构之旅(1) -项目架构定位 & Gradle 全局配置
SimpleNews 项目的重构之旅(2) - 整理项目 .gitignore 文件
SimpleNews 项目的重构之旅(3) -EventBus 接入
SimpleNews 项目的重构之旅(4) -Gradle for Android 基础知识汇总
SimpleNews 项目的重构之旅(5) - Android Gradle 打包&混淆应用
SimpleNews 项目的重构之旅(6) - 命名规范 & Android Toolbar
SimpleNews 项目的重构之旅(7) - 改头换面&深度清理

推荐阅读更多精彩内容