APK瘦身

先看下APK打包流程:APK打包流程_贺兰猪的博客-CSDN博客

知道了APK打包流程后想要瘦身,其实无非就是把整个APK的一些文件进行一个瘦身。

看下apk的这个文件。包括class、资源,资源生成arsc(资源映射表),manifest清单,再就是meta。

对这些文件如果能缩小的话,就能达到缩小apk的目的

SVG导入

SVG(Scalable Vector Graphics),可缩放矢量图。SVG不会像位图一样因为缩放而让图片质量下降。优点在于可以减小APK的尺寸。常用于简单小图标(200dp*200dp以下,超过会损耗性能)。

svg是由xml定义的,标准svg根节点为<svg>。 Android中只支持<vector>,我们可以通过 vector 将svg的根节点<svg> 转换为<vector>。

不能直接把美工小姐姐的svg复制进drawable,要手动在Android Studio中打开工程,在res目录中点击右键创建vector,选择svg做一个这样的转换。

批量转换SVG

如果有多个svg需要转换为android的vector,则可以通过第三方工具 svg2vector 进行批量转换。

执行转换命令:

java -jar svg2vector-cli-1.0.1.jar -d . -o a -h 20 -w 20

-d 指定svg文件所在目录

-o输出android vector图像目录 -h 设置转换后svg的高 -w设置转换后svg的宽

可以优化倍图和不同颜色同样的图,用tint 属性设置你需要的颜色,也可以用颜色选择器,定义不同点击状态下的颜色。坑:编译(不是run)的时候会出现在每个倍图下面都生成了png,与期望不合。要进行适配

方式一:在gradle中设置生成的密度图

defaultConfig {
      //只生成xxhdpi
     generatedDensities = ['xxhdpi']
       
}

方式二:支持库

在 build.gradle 中配置如下,适用于 Gradle插件2.0及以上版本:

android{

    // Gradle Plugin 2.0+ 
    defaultConfig{

       //利用支持库中的 VectorDrawableCompat类,可实现2.1版本及更高版本中支持VectorDrawable     
        vectorDrawables.useSupportLibrary=true

}

dependencies{

    // 支持库版本需要是 23.2 或更高版本
    compile'com.android.support:appcompat-v7:23.2.0'
}

使用矢量图 必须使用app:srcCompat属性,而不是android:src。

国际化资源配置

比如只配置英语

defaultConfig {
        // 只适配英语
        resConfigs 'en'
       
}

动态库打包配置

so文件是由ndk编译出来的动态库,是c/c++写的,所以不是跨平台的。即每一个平台需要使用对应的so库。

ABI 是应用程序二进制接口简称(Application Binary Interface),定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。

在Android系统上,每一个CPU架构对应一个ABl:armeabi,armeabi-v7a,arm64-v8a,x86,x86_64,mips,mips64.

现在我们一般只需要配置armeabi-v7a即可。

正常手机上运行支持armeabi-v7a/armeabi就行了,像微信也就只支持了armeabi-v7a,定制机可能要支持多个

defaultConfig {
       
        ndk {
            abiFilters "armeabi-v7a"
        }
    }

移除无用的资源

AS 给我们提供了一键移除所有无用的资源,如图。但是这种方式不建议使用,因为如果某资源仅存在动态获取资源id 的方式,那么这个资源会被认为没有使用过,从而会直接被删除。

动态获取方式:

getResources().getldentifier("name","defType",getPackageName());

 或者lint检查也是

代码压缩

minifyEnabled设置成true就好了。当应用中包含许多库依赖项,但只使用它们的一小部分功能时,代码压缩起到的效果是很好的。

minifyEnabled不仅压缩了代码也混淆了代码,不需要混淆的类需要单独配置。未使用的Java代码会删除,开启的话编译会比较慢,因为要去做资源检查,平时运行一般关掉,不然打包调试很慢。

buildTypes{
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'
    }
}

在某些情况下, R8 编译器很难做出正确判断,因而可能会移除应用实际上需要的代码。下面列举了几个示例,说明它在什么情况下可能会错误地移除代码:

  • 当应用通过 Java 原生接口 (JNI) 调用方法时
  • 当您的应用在运行时查询代码时(如使用反射)

如需修复错误并强制 R8 保留某些代码,要在 ProGuard 规则文件中添加 -keep 代码行。例如:

-keep public class MyClass

或者,也可以为要保留的代码添加 @Keep 注解。在类上添加 @Keep 可按原样保留整个类。在方法或字段上添加该注释,将使该方法/字段(及其名称)以及类名称保持不变。请注意,只有在使用 AndroidX 注解库且添加 Android Gradle 插件随附的 ProGuard 规则文件时,此注解才可用。

资源压缩

shrinkResources true

buildTypes{
    release {
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'
    }
}

要和代码压缩结合使用。 why?

因为有些未使用的方法里面使用了某些资源,如果不做代码压缩,那么这个方法被保存,里面链接的资源也不会被清掉,资源压缩就删的不够干净。只有未使用的代码干掉,相应的资源才能清理更干净。

从编译结果来看,某个xml没被使用,使用资源压缩,文件还在,但是文件里面的内容会被清掉

资源文件有一个严格模式和非严格模式。默认情况下是非严格模式。非严格模式是怎样的?如果是动态加载xml文件,他会匹配文件名前缀,相当于匹配一个通配符,也就说只要是以设置的链接资源为前缀的xml,他都认为是在使用的,所以在APP里面其他有相同前缀的资源也不会被删除。那如何设置严格模式?

res-》raw-〉建立一个keep.xml,设置内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:shrinkMode="strict"></resources>

但是严格模式的缺点是跟上面移除无用资源的方式一样,动态引用的资源文件也会被删除 ,需要单独在keep.xml里面设置tools:discard=""

注意:资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具不允许 Gradle插件为资源指定预定义版本。

启动图片优化

可以用webp,它是安卓它特制的一种格式,主要是用于颜色复杂的图片,转成webp清晰度不会变低但整体会变小。

也可以用jpg替代,png是有透明度的,如果图片是不需要透明度的话,那你可以用jpg去替换。那么这是一种形式,因为一个像素点你用png的话占的的位数要比jpg高。像你是用argb8888,一个像素占4字节RGB_565,一个像素占2字节。相比ARGB_8888将节省一半的内存开销。一张大图片往往省下几K或者几十k。

还可以进行图片压缩。Android 图片优化_贺兰猪的博客-CSDN博客

apk拆分

根据屏幕不同或者abi进行拆分。国内上架不支持,了解一下

splits {
        density {
            enable true
            reset()
            include "mdpi", "hdpi"
        }
        abi {
            enable true
            reset()
            include "x86", "mips"
        }

    }
android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            // Filter is null for universal APKs.
            def filter = output.getFilter(com.android.build.OutputFile.ABI)

            if (filter == null) {
                tasks.findAll {
                    it.name.startsWith(
                            "crashlyticsUploadDistribution${variant.name.capitalize()}")
                }.each {
                    it.doFirst {
                        ext.betaDistributionApkFilePath = output.outputFile.absolutePath
                    }
                }

                tasks.findAll {
                    it.name.startsWith(
                            "crashlyticsUploadSymbols${variant.name.capitalize()}")
                }.each {
                    it.doFirst {
                        ext.betaDistributionApkFilePath = output.outputFile.absolutePath
                    }
                }
            }
        }
    }

效果如下图: 

 

splits里面配置属性的具体含义可以看 

Android官方技术文档翻译——Apk 拆分机制

资源混淆

启用资源文件res混淆及压缩的好处:

  • 1、减少apk文件大小
  • 2,增加反编译和二次打包的难度,混淆之后的apk不能用apktool之类的工具直接反编译。当然只是增加了难度,毕竟没有绝对安全;
  • 3减少运行时内存占用,在运行时各个res的key都是以String形式加载到内存中的,混淆之后key变短了很多内存占用肯定会变少。

当然上述的这些优势是有在一些大体量的应用上才会比较明显,比较小的apk可能不太明显。

资源加载过程分析
在开发过程中我们通过aapt生成的R.java中的常量来使用资源,而在编译之后使用常量的地方都会被替换为常量的值。见下图。

 也就是说我们通过Resource使用一个int数值来查找使用资源。那么Resource是怎么通过int数值找到具体的资源呢?我们解压apk可以看到里面有个resources.arsc文件,这个文件也是由aapt生成,文件中保存着资源id和资源key的映射关系。Resource就是按照这个映射关系找到资源的。

接下来我们具体看看这个arsc文件的具体内容。我们以单包名的应用来讲。结构见下图

其中res string pool 是res的字符串池,里面包含了字符串(就是大家开发过程中在String.xml文件增加的)和资源文件路径(包括图片,xml文件,raw文件。其中的值类似这样res/drawable/icon.png)
type string pool 是程序中使用的资源分类名称,包括 attr,anim,drawable。。。注意这里只是分类。

spec string pool 这个比较关键,这个字符串池中是所有资源的key。没有分类和config。具体的值就是R.java中各个常量的名称。

在这个字符串池后面就是各个资源分类列表,attr,anim。。。。每个分类中又包括若干config,config就是指hdpi,xhdpi,zh_rCN , land这些,大家知道android资源config有18个纬度。每个config中就是具体的res资源列表。

举例来说一下一个资源的查找过程,比如当我们调用getDrawable(R.drawable.icon)时候,先找到drawable分类,再根据当前的系统config找到匹配的config表。然后根据id找到对应的res数据。drawable在arsc文件中是当作string类型保存的。res数据中有这个资源在res string pool池中的索引。根据这个索引可以在字符串池中找到这个字符串。这个字符串其实是一个文件的路径,比如:res/drawable-hdpi/icon.png,然后读取apk中这个文件。

资源混淆原理和实现
看上面的例子,我们注意到一般情况下查找资源的过程中不会用到资源名,也就是key。当然res数据中也有当前res的key在key string pool中的索引,但一般情况下用不到。根据这个原理我们可以在打包完成之后对key进行混淆。而且刚刚说到res数据中保存的都是字符串在string pool中的索引。所以我们混淆只需要修改res string pool和spec string pool两个字符串池。其他的数据不用修改。

我们混淆的流程大概是这样的:

  • 1,解压apk文件;
  • 2,读取arsc文件;
  • 3,混淆spec string pool;
  • 4根据上一步混淆结果混淆res string pool;
  • 5,生成新的arsc文件;
  • 6,输出mapping文件;
  • 7,根据3,4步结果,修改res目录下所有文件名;
  • 8,打包压缩(7zip)(压缩程度更高);
  • 9,签名;
  • 10.zipalign。

keep
再说key的问题,刚刚说一般情况下用不到res的key。但是开发中我们有时候会通过key来查找资源。也就是说需要注意,需要通过key来使用的资源的key不能混淆。

可选方案

目前的实施方案有微信资源混淆打包工具AndResGuard美团方案
美团方案需要自己实现函数,下面看下微信方案AndResGuard

1.根目录build文件中,添加插件的依赖

dependencies {
       classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
    }

2.在主module(默认为APP)目录下,创建res_guard.gradle配置文件,名称可自定义。官方使用文档上是直接写在build.gradle文件中,这样会使build文件内容过多且繁杂,故建议放在单独的gradle中。

apply plugin: 'AndResGuard'
andResGuard {
    // mappingFile = file("./resource_mapping.txt")
    mappingFile = null
    use7zip = true
    useSign = true
    // It will keep the origin path of your resources when it's true
    keepRoot = false
    // If set, name column in arsc those need to proguard will be kept to this value
    fixedResName = "arg"
    // It will merge the duplicated resources, but don't rely on this feature too much.
    // it's always better to remove duplicated resource from repo
    mergeDuplicatedRes = true
//需要将所有通过getIdentifier访问的资源放入白名单。
//各种第三方的也要加进来,看github 的md
    whiteList = [
        // your icon
        "R.drawable.icon",
        // for fabric
        "R.string.com.crashlytics.*",
        // for google-services
        "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.project_id",
    ]
    compressFilePattern = [
        "*.png",
        "*.jpg",
        "*.jpeg",
        "*.gif",
    ]
    sevenzip {
        artifact = 'com.tencent.mm:SevenZip:1.2.21'
        //path = "/usr/local/bin/7za"
    }

    /**
    * Optional: if finalApkBackupPath is null, AndResGuard will overwrite final apk
    * to the path which assemble[Task] write to
    **/
    // finalApkBackupPath = "${project.rootDir}/final.apk"

    /**
    * Optional: Specifies the name of the message digest algorithm to user when digesting the entries of JAR file
    * Only works in V1signing, default value is "SHA-1"
    **/
    // digestalg = "SHA-256"
}

3.在主module(APP)目录下的build.gradle中,添加使用

apply from: 'res_guard.gradle'

资源混淆核心是修改资源映射表,资源混淆后查错比较麻烦,会有一个mapping 去看映射关系

可以看到在gradle插件中有替换压缩工具(7zip替换apkbuilder )进行压缩,这样压缩率更高

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值