Android打包那些事

作者: 慕涵盛华 | 来源:发表于2018-11-01 15:32 被阅读316次

    目录

    一.混淆(Proguard)

    混淆是打包过程中最重要的流程之一,这里所说的的混淆是包括代码混淆和资源的“混淆”。

    1.代码混淆

    通过代码混淆不仅能够提高APK的反编译,还能较小APK大小;混淆可以删除注释和无用的代码;可以将Java文件,变量,方法名登改为短名,这样就可以缩减字符所占的空间,但并不是所有的文件都可以混淆;比如res下的文件,文件res下的资源文件在R.java 文件中都会有对应的ID,如果直接将文件名改了,资源和ID就不对应了。导致程序崩溃。

    混淆配置

    在app下的build.gradle中开启混淆

    android {
    
        .......
    
        signingConfigs {
            release {//release版的签名配置信息
                storeFile file(rootProject.ext.signConfigs.storeFile)
                storePassword rootProject.ext.signConfigs.storePassword
                keyAlias rootProject.ext.signConfigs.keyAlias
                keyPassword rootProject.ext.signConfigs.keyPassword
            }
        }
    
        buildTypes {
            release {
                minifyEnabled true //开启混淆编译
                shrinkResources true //移除无用资源
                zipAlignEnabled true //zipalign优化
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
                signingConfig signingConfigs.release
                buildConfigField("boolean", "LOG_DEBUG", "false")//正式版不打印日志
            }
            debug {//为了方便调试微信支付、登陆、分享等把deug签名配置成了Release版的签名
                signingConfig signingConfigs.release
                buildConfigField("boolean", "LOG_DEBUG", "true")
            }
        }
    
      ......
    
    }
    

    getDefaultProguardFile('proguard-android.txt'):表示默认的混淆规则,文件位于android sdk 目录 \sdk\tools\proguard\proguard-android.txt 中


    这里把proguard-android.txt替换成了proguard-android-optimize.txt,两者的区别是前置没有开启优化。使用proguard-android-optimize.txt开启优化,这样我们配置的优化选项才会生效。zipAlignEnabledshrinkResources

    proguard-android.txt的部分内容如下:

    .......
    # 表示不进行优化
    -dontoptimize
    表示不进行预校验
    -dontpreverify
    ......
    

    proguard-rules.pro是我们自定义的混淆规则,位于app根目录下,默认为空,需要我们自己添加规则。这里贴出一根模板:

    #############################################
    # 对于一些基本指令的添加
    #############################################
    # 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
    -optimizationpasses 5
    # 混合时不使用大小写混合,混合后的类名为小写
    -dontusemixedcaseclassnames
    # 指定不去忽略非公共库的类
    -dontskipnonpubliclibraryclasses
    # 这句话能够使我们的项目混淆后产生映射文件
    # 包含有类名->混淆后类名的映射关系
    -verbose
    # 指定不去忽略非公共库的类成员
    -dontskipnonpubliclibraryclassmembers
    # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
    -dontpreverify
    # 忽略警告
    -ignorewarnings
    # 保留Annotation不混淆
    -keepattributes *Annotation*,InnerClasses
    # 避免混淆泛型
    -keepattributes Signature
    # 抛出异常时保留代码行号
    -keepattributes SourceFile,LineNumberTable
    # 指定混淆是采用的算法,后面的参数是一个过滤器
    # 这个过滤器是谷歌推荐的算法,一般不做更改
    -optimizations !code/simplification/cast,!field/*,!class/merging/*
    
    #############################################
    # Android开发中一些需要保留的公共部分
    #############################################
    # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
    # 因为这些子类都有可能被外部调用
    -keep public class * extends android.app.Activity
    -keep public class * extends android.app.Appliction
    -keep public class * extends android.app.Service
    -keep public class * extends android.content.BroadcastReceiver
    -keep public class * extends android.content.ContentProvider
    -keep public class * extends android.app.backup.BackupAgentHelper
    -keep public class * extends android.preference.Preference
    -keep public class * extends android.view.View
    -keep public class com.android.vending.licensing.ILicensingService
    # 保留support下的所有类及其内部类
    -keep class android.support.** {*;}
    # 保留继承的
    -keep public class * extends android.support.v4.**
    -keep public class * extends android.support.v7.**
    -keep public class * extends android.support.annotation.**
    # 保留R下面的资源
    -keep class **.R$* {*;}
    # 保留本地native方法不被混淆
    -keepclasseswithmembernames class * {
        native <methods>;
    }
    # 保留在Activity中的方法参数是view的方法,
    # 这样以来我们在layout中写的onClick就不会被影响
    -keepclassmembers class * extends android.app.Activity{
        public void *(android.view.View);
    }
    # 保留枚举类不被混淆
    -keepclassmembers enum * {
        public static **[] values();
        public static ** valueOf(java.lang.String);
    }
    # 保留我们自定义控件(继承自View)不被混淆
    -keep public class * extends android.view.View{
        *** get*();
        void set*(***);
        public <init>(android.content.Context);
        public <init>(android.content.Context, android.util.AttributeSet);
        public <init>(android.content.Context, android.util.AttributeSet, int);
    }
    # 保留Parcelable序列化类不被混淆
    -keep class * implements android.os.Parcelable {
        public static final android.os.Parcelable$Creator *;
    }
    # 保留Serializable序列化的类不被混淆
    -keepnames class * implements java.io.Serializable
    -keepclassmembers class * implements java.io.Serializable {
        static final long serialVersionUID;
        private static final java.io.ObjectStreamField[] serialPersistentFields;
        !static !transient <fields>;
        !private <fields>;
        !private <methods>;
        private void writeObject(java.io.ObjectOutputStream);
        private void readObject(java.io.ObjectInputStream);
        java.lang.Object writeReplace();
        java.lang.Object readResolve();
    }
    # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
    -keepclassmembers class * {
        void *(**On*Event);
        void *(**On*Listener);
    }
    # webView处理,项目中没有使用到webView忽略即可
    -keepclassmembers class fqcn.of.javascript.interface.for.webview {
        public *;
    }
    -keepclassmembers class * extends android.webkit.webViewClient {
        public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
        public boolean *(android.webkit.WebView, java.lang.String);
    }
    -keepclassmembers class * extends android.webkit.webViewClient {
        public void *(android.webkit.webView, jav.lang.String);
    }
    
    # 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用
    # 记得proguard-android.txt中一定不要加-dontoptimize才起作用
    # 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制
    #-assumenosideeffects class android.util.Log {
    #    public static int v(...);
    #    public static int i(...);
    #    public static int w(...);
    #    public static int d(...);
    #    public static int e(...);
    #}
    
    #############################################
    # 项目中特殊处理部分
    #############################################
    
    #-----------处理反射类---------------
    
    #-----------处理js交互---------------
    -keepattributes *JavascriptInterface*
    -keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
    }
    
    -keepclassmembers class * extends android.webkit.WebViewClient {
     public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
     public boolean *(android.webkit.WebView, java.lang.String);
    }
    
    -keepclassmembers class * extends android.webkit.WebViewClient {
     public void *(android.webkit.WebView, java.lang.String);
    }
    
    #-----------处理实体类---------------
    -keep class com.xiyang51.platform.entity.**{ *; }
    
    #-----------处理第三方依赖库---------
    # gson
    -keep class com.google.**{*;}
    -keep class sun.misc.Unsafe { *; }
    -keep class com.google.gson.stream.** { *; }
    # FastJson
    -dontwarn com.alibaba.fastjson.**
    -keep class com.alibaba.fastjson.** { *; }
    # 百度地图(jar包换成自己的版本,记得签名要匹配)
    -keep class com.baidu.** {*;}
    -keep class vi.com.** {*;}
    -keep class com.sinovoice.** {*;}
    -keep class pvi.com.** {*;}
    -dontwarn com.baidu.**
    -dontwarn vi.com.**
    -dontwarn pvi.com.**
    # Bugly
    -dontwarn com.tencent.bugly.**
    -keep class com.tencent.bugly.** {*;}
    # ButterKnife
    -keep public class * implements butterknife.Unbinder {
        public <init>(**, android.view.View);
    }
    -keep class butterknife.*
    -keepclasseswithmembernames class * {
        @butterknife.* <methods>;
    }
    -keepclasseswithmembernames class * {
        @butterknife.* <fields>;
    }
    # EventBus
    -keepclassmembers class ** {
        @org.greenrobot.eventbus.Subscribe <methods>;
    }
    -keep enum org.greenrobot.eventbus.ThreadMode { *; }
    # Glide
    -keep public class * implements com.bumptech.glide.module.GlideModule
    -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
      **[] $VALUES;
      public *;
    }
    ### greenDAO 3
    -keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
    public static java.lang.String TABLENAME;
    }
    -keep class **$Properties
    -dontwarn org.greenrobot.greendao.database.**
    -dontwarn rx.**
    # 极光推送
    -dontoptimize
    -dontpreverify
    -dontwarn cn.jpush.**
    -keep class cn.jpush.** { *; }
    # OkHttp
    -dontwarn okio.**
    -dontwarn okhttp3.**
    -dontwarn javax.annotation.Nullable
    -dontwarn javax.annotation.ParametersAreNonnullByDefault
    # OrmLite
    -keepattributes *DatabaseField*
    -keepattributes *DatabaseTable*
    -keepattributes *SerializedName*
    -keep class com.j256.**
    -keepclassmembers class com.j256.** { *; }
    -keep enum com.j256.**
    -keepclassmembers enum com.j256.** { *; }
    -keep interface com.j256.**
    -keepclassmembers interface com.j256.** { *; }
    # Retrofit
    -keep class retrofit2.** { *; }
    -dontwarn retrofit2.**
    -keepattributes Exceptions
    -dontwarn okio.**
    -dontwarn javax.annotation.**
    # RxJava RxAndroid
    -dontwarn sun.misc.**
    -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
        long producerIndex;
        long consumerIndex;
    }
    -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
        rx.internal.util.atomic.LinkedQueueNode producerNode;
    }
    -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
        rx.internal.util.atomic.LinkedQueueNode consumerNode;
    }
    -dontnote rx.internal.util.PlatformDependent
    # 微信支付
    -dontwarn com.tencent.mm.**
    -dontwarn com.tencent.wxop.stat.**
    -keep class com.tencent.mm.** {*;}
    -keep class com.tencent.wxop.stat.**{*;}
    # 新浪微博
    -keep class com.sina.weibo.sdk.* { *; }
    -keep class android.support.v4.* { *; }
    -keep class com.tencent.* { *; }
    -keep class com.baidu.* { *; }
    -keep class lombok.ast.ecj.* { *; }
    -dontwarn android.support.v4.**
    -dontwarn com.tencent.**s
    -dontwarn com.baidu.**
    # 友盟统计分析
    -keepclassmembers class * { public <init>(org.json.JSONObject); }
    -keepclassmembers enum com.umeng.analytics.** {
        public static **[] values();
        public static ** valueOf(java.lang.String);
    }
    # 支付宝钱包
    -dontwarn com.alipay.**
    -dontwarn HttpUtils.HttpFetcher
    -dontwarn com.ta.utdid2.**
    -dontwarn com.ut.device.**
    -keep class com.alipay.android.app.IAlixPay{*;}
    -keep class com.alipay.android.app.IAlixPay$Stub{*;}
    -keep class com.alipay.android.app.IRemoteServiceCallback{*;}
    -keep class com.alipay.android.app.IRemoteServiceCallback$Stub{*;}
    -keep class com.alipay.sdk.app.PayTask{ public *;}
    -keep class com.alipay.sdk.app.AuthTask{ public *;}
    -keep class com.alipay.mobilesecuritysdk.*
    -keep class com.ut.*
    

    里面每一行都有对应的注释,如果你还使用到了别的三方库,需要使用的时候自行添加,切勿照搬。

    双击assembleRelease打正式包


    打正式包的时候开启了混淆,打包结束后会在\app\build\outputs\mapping\release\目录下生成一个mapping.txt文件,是代码混淆前后的名字对应关系。

    资源“混淆”

    上面提到,res下的资源文件不能混淆,如果我们可以将res下的资源文件名也改成短名,并且ID对应关系也改成对应的;这样就可以实现资源文件的“混淆了”,apk解压后会有一个resources.arsc文件:资源索引表,资源的描述文件,用来描述具有ID值的资源的对应关系。所以我们把res下的文件改成短名后,还需要更改resources.arsc中的对应关系。这里使用的是微信开源的资源混淆工具 AndResGuard

    配置

    1.在工程的build.gradle中配置插件

    buildscript {
        repositories {
            jcenter()
            google()
        }
        dependencies {
            ......
            classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.15'
        }
    }
    

    2.可以在app下的build.gradle中直接配置混淆,为了更直观一些,这里在工程的根目录中单独建立了一个gradle文件:and_res_guard.gradle内容如下:

    apply plugin: 'AndResGuard'//引入插件
    //资源"混淆"配置文件
    andResGuard {
        mappingFile = null
        use7zip = true
        useSign = true
        keepRoot = false // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
        compressFilePattern = [//对哪些类型的图片进行“混淆”
                "*.png",
                "*.jpg",
                "*.jpeg",
                "*.gif",
                "resources.arsc"
        ]
        whiteList = [ //白名单,忽略哪些资源文件
                      "R.drawable.icon",
                      "R.string.com.crashlytics.*",
                      // google-services.json
                      "R.string.google_app_id",
                      "R.string.gcm_defaultSenderId",
                      "R.string.default_web_client_id",
                      "R.string.ga_trackingId",
                      "R.string.firebase_database_url",
                      "R.string.google_api_key",
                      "R.string.google_crash_reporting_api_key",
                      //友盟
                      "R.string.tb_*",
                      "R.layout.tb_*",
                      "R.drawable.tb_*",
                      "R.drawable.u1*",
                      "R.drawable.u2*",
                      "R.color.tb_*",
                      "R.drawable.sina*",
                      "R.string.umeng*",
                      "R.string.UM*",
                      "R.layout.umeng*",
                      "R.drawable.umeng*",
                      "R.id.umeng*",
                      "R.anim.umeng*",
                      "R.color.umeng*",
                      "R.style.*UM*",
                      "R.style.umeng*",
                      //极光推送
                      "R.drawable.jpush_notification_icon"
        ]
    
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.2.15' //使用7Zip
        }
        digestalg = "SHA-256" //签名算法
    }
    

    3.在app下的build.gradle文件中引入and_res_guard.gradle文件

    apply plugin: 'com.android.application'
    apply plugin: 'org.greenrobot.greendao'
    apply from: rootProject.file('libbase.gradle')
    apply from: rootProject.file('and_res_guard.gradle')
    ......
    

    4.配置完成后在Tasks下多出了一个andresguard,我们再打正式包的时候点击它下的resguardRelease即可,默认的apk输出文件在app/build/outputs/

    二.自定义APK输出的名字

    我们默认打包输出的名字为:app-release.apk,我们希望加上时间,版本号等信息。
    在app下的build.gradle添加如下代码:
    gradle3.0以下

    android{
    ......
       applicationVariants.all {
            variant ->
                variant.outputs.each { output ->
                    if (buildType.name == 'release') {//打正式包的时候
                        output.outputFile = new File(
                                output.outputFile.parent, //输出目录
                               "app_v" + defaultConfig.versionName + "_" +
                                new Date().format("yyyy-MM-dd") + "_" +  //打包时间
                                buildType.name + ".apk")
                    }
                }
       }
    }
    ......
    

    gradle3.0及以上

     //自定义输出apk名字
     applicationVariants.all {
            variant ->
                variant.outputs.all { output ->
                    if (buildType.name == 'release') {
                        outputFileName = new File("app_v" + defaultConfig.versionName + "_" +
                                new Date().format("yyyy-MM-dd") + "_" +
                                buildType.name + ".apk")
                    }
                }
     }
    

    最终输出的格式为:app_v1.0_2018-11-01_release.apk,你可以按照自己的格式定义输出。

    三.自动维护版本发布文档

    我们APP每次迭代需要记录APP的版本信息和对应的变化信息,以某一特定的格式记录,方便以后对比管理。我们可以写一个gradle脚本来自动实现。
    在工程的根目录下新建一个releaseinfo.gradle文件,内容如下:

    import groovy.xml.MarkupBuilder
    
    /**
     * 描述:版本发布文档自动维护脚本
     * 流程描述:
     *           1、将版本相关信息解析出来
     *           2、将解析出的数据生成xml格式数据
     *           3、写入到已有的文档数据中
     **/
    ext {
        versionName = rootProject.ext.android.versionName
        versionCode = rootProject.ext.android.versionCode
        versionInfo = 'App的第1个版本,上线了一些最基础核心的功能,。。。。。。'
        destFile = file('releasesInfo.xml')//指定输出文件
        if (destFile != null && !destFile.exists()) {
            destFile.createNewFile()
        }
    }
    //挂在到应用构建的过程中
    this.project.afterEvaluate { project ->
        def buildTask = project.tasks.getByName('assembleRelease')
        if (buildTask != null) {
            buildTask.doLast {//assembleRelease 之后执行
                releaseInfoTask.execute()
            }
        }
    }
    //创建一个Task,并指定输入输出
    task releaseInfoTask {
        inputs.property('versionCode', this.versionCode)
        inputs.property('versionName', this.versionName)
        inputs.property('versionInfo', this.versionInfo)
        outputs.file this.destFile
        doLast {
            //将输入的内容写入到输出文件中去
            def data = inputs.getProperties()
            File file = outputs.getFiles().getSingleFile()
            def versionMsg = new VersionMsg(data)
            //将实体对象写入到xml文件中
            def sw = new StringWriter()
            def xmlBuilder = new MarkupBuilder(sw)
            if (file.text != null && file.text.size() <= 0) {
                //没有内容
                xmlBuilder.releases {
                    release {
                        versionCode(versionMsg.versionCode)
                        versionName(versionMsg.versionName)
                        versionInfo(versionMsg.versionInfo)
                    }
                }
                //直接写入
                file.withWriter { writer ->
                    writer.append("<?xml version=\"1.0\" encoding=\"GBK\"?>" + '\r\n')
                    writer.append(sw.toString())
                }
            } else {//有内容判断版本是否改变
                def releases = new XmlParser().parse(file)
                def codeName = releases.release[-1].versionName.text()
                if (codeName != versionMsg.versionName) {//新的版本信息
                    xmlBuilder.release {
                        versionCode(versionMsg.versionCode)
                        versionName(versionMsg.versionName)
                        versionInfo(versionMsg.versionInfo)
                    }
                    //插入到最后一行前面
                    def lines = file.readLines()
                    def lengths = lines.size() - 1
                    file.withWriter { writer ->
                        lines.eachWithIndex { line, index ->
                            if (index != lengths) {
                                writer.append(line + '\r\n')
                            } else if (index == lengths) {
                                writer.append('\r\n' + sw.toString() + '\r\n')
                                writer.append(lines.get(lengths))
                            }
                        }
                    }
                }
            }
        }
    }
    //信息实体类
    class VersionMsg {
        String versionCode
        String versionName
        String versionInfo
    }
    

    将自动维护版本信息文档的Task,releaseInfoTask加入到了构建过程中:

    //挂在到应用构建的过程中
    this.project.afterEvaluate { project ->
        def buildTask = project.tasks.getByName('assembleRelease')//只有正式包才会写入文档
        if (buildTask != null) {
            buildTask.doLast {//assembleRelease 之后执行
                releaseInfoTask.execute()
            }
        }
    }
    

    打正式包结束后会在app的根目录下生成一个releaseInfo.xml文件,内容格式如下:

    <?xml version="1.0" encoding="GBK"?>
    <releases>
      <release>
        <versionCode>1</versionCode>
        <versionName>1.0</versionName>
        <versionInfo>App的第1个版本,上线了一些最基础核心的功能,。。。。。。</versionInfo>
      </release>
    </releases>
    

    当版本改变的时候,我们只需要修改versionInfo 对应的内容即可。

    四.多渠道打包

    渠道包就是要在安装包中添加渠道信息,也就是channel,对应不同的渠道,例如:小米市场、360市场、应用宝市场等,我们要在安装包中添加不同的标识,应用在请求网络的时候携带渠道信息,方便后台做运营统计。

    以友盟统计为例:
    1.在AndroidManifest添加如下信息

     <meta-data
           android:name="UMENG_CHANNEL"
           android:value="${UMENG_CHANNEL_VALUE}" /> 
    

    2.在app下的build.gradle中配置渠道

    /*配置渠道*/
    productFlavors {
        wandoujia{
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
        }
        yingyongbao{
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "yingyongbao"]
        }
       huawei{
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "huawei"]
        }
      ........
    }
    

    3.然后在代码中调用友盟统计的相关代码。此处省略。

    这样打渠道包的缺点:
    • 每一个渠道包需要release一次
    • 我们上传应用市场的时候都需要加固,加固后渠道信息会丢失,所以必须加固后再打渠道包。

    使用360加固时,支持多渠道打包,这样使用比较方便。


    注意:友盟统计的渠道号不能全是数字。

    还有一种是使用三方开源的打包工具,比如美团开源的打包工具:walle

    项目地址:KotlinAndroid

    相关文章

      网友评论

      • 皮特天:用360加固,你的应用用kotlin的话,你把加固好的apk,解压可以看到代码,不知道是360加固原因,还是kotlin问题
        慕涵盛华:@心愿宝宝 我今天试了一下,没有问题,你解压后看到的具体是啥?
        皮特天:@心愿宝宝 可惜我的出问题了,明明没有keep kotlin的代码,而且是按照官网配置的kotlin
        慕涵盛华:@心愿宝宝 kotlin的我没有试过,我试一下,应该不会的。就是在外层加一层壳。

      本文标题:Android打包那些事

      本文链接:https://www.haomeiwen.com/subject/yteboftx.html