APP瘦身优化

956 阅读12分钟

1 为什么要瘦身优化

1.1 瘦身优化的好处

  • 提高下载转化率
  • 大型APP都有Lite版本
  • 渠道合作商要求,降低推广成本
  • 应用市场比如 Google Play 要求超过 100MB 的应用只能使用APK 扩展文件方式上传,由此可见应用包体积对应用市场的服务器带宽成本还是会有一点压力。
  • Resource 资源、Library 以及 Dex 类加载这些都会占用不少的内存,包体积减少也能起到优化内存的效果
  • 一个100M的apk,安装解压之后可能会占用200M以上的空间,对低端手机不够友好。

1.2瘦身优化3点方法论

  1. 删:删除无用的代码和资源
  2. 压:压缩代码和资源
  3. 移:实在不行就抽离出,动态加载,动态下发。

1.3 APK的组成

  • lib 存放so文件,有armeabi armeabi-v7a arm64-v8a x86 mips x86-64。一般只需要支持armeabi和x86即可
  • res:存放编译后的资源文件 例如drawable layout等
  • assets:应用程序的资源,应用程序可以使用AssetManager来检索该资源
  • MEAT-INFO:存在于已经签名的apk中,包含了apk中左右的文件的签名摘要等信息
  • classes.dex:Java Class 被Dex编译后可提供给Dalvik/ART虚拟机所理解的文件格式
  • resources.arsc 编译后的二进制资源文件
  • AndroidManifest.xml Android中清单文件

1.4 APK分析工具

  1. ApkTool

https:ibotpeaches.github.io/Apktool/

apktool d xx.apk

  1. Analyze APK Android Studio 2.2之后

查看APK组成 大小 占比

查看Dex文件组成

APK对比

  1. nimbledroid.com App性能分析

显示文件大小及排行榜

dex 方法数等

还可以可以分析启动速度

  1. android-classyshark 二进制检查工具

github.com/google/andr…

支持多种格式 Apk Jar Class So

  1. 代码分析工具 PMD

pmd.github.io

  1. 下载后输入: ./run.sh cpdgui
  2. pmd -d /usr/src -R rulesets/java/quickstart.xml -f text
  3. 文档地址:pmd.github.io/pmd-6.17.0/…
  4. 资料:www.jianshu.com/p/5fe23ed8d…

2 图片压缩相关

一个工程中图片所占的资源是相当大的,所以第一步就从图片压缩开始

2.1 SVG

官方SVG使用介绍:developer.android.google.cn/studio/writ…

使用矢量可绘制对象代替位图可以减小 APK 的大小,因为可以针对不同的屏幕密度调整同一文件的大小,而不会降低图片质量。对于不支持矢量可绘制对象的较低版本的 Android 系统,Vector Asset Studio 可以在编译时针对每种屏幕密度将矢量可绘制对象转换为不同大小的位图。

SVG在Android中主要是在各种图标中使用,不过Android中不是直接使用SVG图片,而是将其转化为一个以vector为标签的xml文件来使用。

单个转换在可以在AndroidStudio中转换,具体步骤右键点击 res 文件夹,然后依次选择 New > Vector Asset,出现下图

在文件夹中选择自己的SVG图标后点击确定,就会在drawable文件夹下生成相应的xml文件比如上面的图片生成的结果:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="64dp"
    android:height="64dp"
    android:viewportWidth="1024"
    android:viewportHeight="1024">
  <path
      android:fillColor="#FF000000"
      android:pathData="M177.74,652.95A364.07,364.07 0,0 0,331.31 826.52v-310.83L192.49,645.15c-4.3,4.01 -9.4,6.61 -14.75,7.81zM157.75,590.03l209.71,-195.56L170.67,394.46c-0.62,0 -1.24,-0.01 -1.85,-0.05A362.19,362.19 0,0 0,149.33 512c0,26.78 2.9,52.91 8.42,78.03zM459.83,394.46a32.06,32.06 0,0 1,-5.15 6.19l-62.11,57.91c1.76,3.96 2.73,8.34 2.73,12.95v79.54c3.58,1.64 6.91,3.98 9.8,7.01l67.57,70.85h90.57l68.3,-72.94v-95.87l-68.18,-66.83c-2.77,0.78 -5.69,1.19 -8.69,1.19h-94.84zM395.23,855.47A362.22,362.22 0,0 0,512 874.67c32.77,0 64.52,-4.34 94.72,-12.49l-211.41,-221.65L395.31,853.33c0,0.71 -0.03,1.42 -0.07,2.13zM670.7,838.2a364.27,364.27 0,0 0,155.69 -145.28L533.71,692.92l131.95,138.35c2.03,2.13 3.71,4.47 5.05,6.93zM855.39,628.98A362.21,362.21 0,0 0,874.67 512c0,-36.66 -5.44,-72.05 -15.55,-105.41L650.91,628.91L853.33,628.91c0.69,0 1.38,0.02 2.06,0.06zM197.96,330.46h301.31l-139.73,-136.94a31.98,31.98 0,0 1,-5.9 -7.89A364.27,364.27 0,0 0,197.97 330.47zM418.34,161.55l213.21,208.94v-195.2c0,-1.92 0.17,-3.79 0.49,-5.62A362.11,362.11 0,0 0,512 149.33c-32.38,0 -63.79,4.25 -93.65,12.21zM695.55,446.61L695.55,487.64l134.43,-143.57c0.75,-0.8 1.55,-1.56 2.35,-2.26a364.37,364.37 0,0 0,-136.78 -142.67v247.49zM512,938.67C276.36,938.67 85.33,747.64 85.33,512S276.36,85.33 512,85.33s426.67,191.03 426.67,426.67 -191.03,426.67 -426.67,426.67z"/>
</vector>

批量转换可以使用一个开源库地址:

developer.android.google.cn/studio/writ…

svg的正确使用方法:在build.gradle中的defaultConfig闭包中使用入下配置

// 将svg图片生成 指定维度的png图片
vectorDrawables.generatedDensities('xhdpi','xxhdpi')
// 使用support-v7兼容5.0以上
vectorDrawables.useSupportLibrary = true
  • 5.0以下 将svg图片生成指定维度的png图片,下面写几个就会生成几个相应的图片
  • 5.0以上 以上使用support-v7进行兼容
 <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/ic_friend_circle"
        />

2.2 Tint着色器

上面的ImageView可以通过着色器直接上色,相同的图片,不同的效果。

   <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/ic_friend_circle"
        />
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:srcCompat="@drawable/ic_friend_circle"
        android:tint="@color/colorPrimary"
        />

效果如下:

那使用一张图片,能实现我们平时常用的selector效果吗,当然也是可以的

首先在drawable文件夹下创建一个selector

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_friend_circle" android:state_pressed="true" />
    <item android:drawable="@drawable/ic_friend_circle" />
</selector>

正常状态和按下状态使用同一张图片,然后在res目录下面创建一个color文件夹,里面创建一个color的选择器

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@android:color/holo_purple" android:state_pressed="true" />
    <item android:color="@android:color/transparent" />
</selector>

最后在layout中使用

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:focusable="true"
        app:srcCompat="@drawable/ic_friend_circle"
        android:tint="@color/tint_color_selector" />

效果:

2.3 使用webp格式

webp是谷歌提供的一种有损压缩格式,这种图片格式相比png和jpg格式的图片损失的质量几乎可以忽略不计,但是压缩之后的图片体积却比png或者jpg小很多。

AndridStudio就有接将png和jpg格式的图转换成webp格式的图片的功能。右键想要转化的图片,在弹出菜单中点击convert to webp条目就会进入转化的界面,默认是压缩为75%的质量,可以自己调节。有时候几M的图片可以压缩成几百kb而且清晰度肉眼几乎看不出来,瘦身效果非常棒。(如果想要一次转化多张图片就在文件夹上右键点击转换)

也可以下载WebP转换工具

developers.google.com/webp/doc/pr…

2.4 png压缩

如果非要使用png,png也有几个压缩工具

几个压缩png的工具

  1. tinypng.com
  2. github.com/waynell/Tin…
  3. imageoptim.com/mac
  4. www.apprcn.com/pngyu.html

jpg压缩 一般不压缩

如果要压缩可以使用下面工具

  1. packJPG: roov.org/2013/03/pac…
  2. guetzli:github.com/google/guet…

2.5 使用纯色图片

如果项目中用到了纯色图片,可以使用代码实现 比如shape或者自定义view

3 配置资源打包

在app下的build.gradle文件中的defaultConfig 闭包中配置下面

 defaultConfig {
        ...
        // 只保留指定和默认资源
        resConfigs('zh-rCN', 'ko')
        ...
    }

这样配置有什么好处呢?我们知道APK打包之后,资源文件会映射到resources.arsc文件中,我们使用AndroidStudio来查看这个apk中的resources.arsc文件中的string文件,可以看到下图中的样子。

平时开发的时候,我们都会把一些写死的文字放到string.xml文件中,上图中可以看到,Android中默认会把我们的string.xml文件中的数据翻译成几十种国家的语言,相当于平白多出了几十个string.xml文件,而我们的应用可能只需要一两种语言就可以了。

加上前面的配置,就能将语言只保留指定资源和默认资源。效果如下图:

4 配置so库

如果项目中使用了第三方SDK或者NDK,如果不配置cpu架构,可能就会导入全部的so库文件,这时候可以在gradle中配置,选择特定的so库打包到apk中,这块瘦身效果是很可观的。配置方式:app->build.gradle->android->defaultConfig中

defaultConfig {
        ...
        // 配置so库架构(真机:arm,模拟器:x86)
        ndk {
            abiFilters('armeabi','armeabi-v7a')
        }
        ...
    }

那该选择那些库呢?

x86_64兼容x86

armeabi几乎可以兼容所有机型

armeabi-v7a兼容armeabi

arm64-v8a兼容armeabi-v7a

x86一般是模拟器所以我们可以只留一份armeabi,arm64-v8a或者armeabi-v7a中的一个即可。现在是主流手机基本都是armeabi-v7a以上了,谷歌play已经要求所有apk必须适配arm64-v8a的cpu

如果下载微信和淘宝和支付宝的apk解压之后可以看到,微信中只使用了arm64-v8a,淘宝中只使用了armeabi-v7a,支付宝中只保留了armeabi

提供so库的时候,要么全给要么不给。不能arm64-v8a文件夹中给两个so库armeabi-v7a文件夹中给一个so库

对于cpu敏感的so库,我们可以在armeabi-v7a中放一套v8a的库,根据CPU的架构来动态的选择使用哪个

也可以构建的时候分包,利用应用市场来动态分发(google play)

如果项目中用的so库比较多,使用此过滤方法瘦身效果非常明显

5 Lint检查

Lint 是Android Studio 提供的 代码扫描分析工具,它可以帮助我们发现代码结构/质量 问题,同时提供一些解决方案,而且这个过程不需要我们手写测试用例。 随着代码迭代版本的增多,很容易会遗留一些无用的代码、资源文件,我们可以使用 Lint 进行检查并清除。

打开AndroidStudio在菜单栏中,依次选择 Analyze > Inspect Code。如下:

弹出窗口中选择检查的包的范围,之后点击OK,就会自动检查筛选出一些错误,警告,无用代码资源。

查看检查出来的文件,根据实际情况判断是修改还是删除。

需要注意的是,如果代码中通过反射等手段动态获取资源id,这时候也会被lint检查到认为是没有用过的资源所以lint检查出来的资源删除的时候需要谨慎

6 混淆和资源压缩

项目做的时间长了,代码中肯定会有一些没有用到的资源文件,删除这些无用的资源当然也可以起到瘦身的作用

前面知道了Lint检查出来,删除后是是物理删除,删了之后就找不到了,比较危险,比如我们代码中是通过反射找到某些资源,而这些资源会被上面的操作删除掉,最终导致应用崩溃。

比较安全的优化策略是使用资源压缩

developer.android.google.cn/studio/buil…

 buildTypes {
        release {
            // 源代码混淆开启
            minifyEnabled true
            // 启动资源压缩
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

6.1 代码混淆

  • 代码中各个元素改成无意义的名字
  • 以更难理解的形式重写部分逻辑
  • 打乱代码格式

Proguard 代码中元素名称改短 取出无用元素等。

在混淆的时候,不要乱keep,有的类可能不需要keep

当使用Android Gradle 插件 3.4.0 或更高版本编译您的项目时,Gradle插件不再使用 ProGuard 执行编译时代码优化,而是与 R8 编译器一起使用,共同处理以下编译时任务

  • 代码压缩:从应用及其库依赖项中检测并安全地移除未使用的类、字段、方法和属性
  • 资源压缩:从应用中移除未使用的资源,包括应用的库依赖项中未使用的资源。此功能可与代码压缩结合使用,这样一来,移除未使用的代码后,也可以安全地移除不再引用的任何资源。
  • 混淆:缩短类和成员的名称,从而减小 DEX 文件大小。
  • 优化:检查并重写代码,以进一步减小应用 DEX 文件的大小。例如,如果 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,R8 便会移除 else {} 分支的代码。

6.2 资源压缩

shrinkResources可以压缩资源,比如可以把一些无用的资源压缩成只有名字没有内容的文件

只有开启了混淆之后使用这个才能有效果,因为混淆之后会把一些无用的类删掉,那么无用的类中引用的资源也就可以放心的压缩了。

如果要保留或者压缩特定的资源,可以子在res/raw/文件夹下创建一个keep.xml文件内如可以如下:

    <?xml version="1.0" encoding="utf-8"?>
    <resources xmlns:tools="http://schemas.android.com/tools"
        tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
        tools:discard="@layout/unused2" />
  • tools:keep 属性中指定要保留的每个资源
  • tools:discard 属性中指定要舍弃的每个资源
  • 这两个属性都接受逗号分隔的资源名称列表。也可以将星号字符用作通配符。

只是开启shrinkResources,系统会使用一种比较安全的模式去压缩资源,对于一些不确定判断都会选择不去压缩比如,如果代码中或者某个库中使用了Resources.getIdentifier(),这就说明我们的代码是通过动态生成的字符串来查找资源id,这时候资源压缩器就会开启防御行为,把所有相关的资源都标记为使用,不会压缩比如下面的代码就会把所有的以img_为前缀的资源都标记为已用。

 String name = String.format("img_%1d", angle + 1);
 res = getResources().getIdentifier(name, "drawable", getPackageName());

资源压缩器还会浏览代码以及各种 res/raw/ 资源中的所有字符串常量,查找格式类似于 file:///android_res/drawable//ic_plus_anim_016.png 的资源网址。如果它找到这样的字符串,或发现一些其他字符串看似可用来构建这样的网址,就不会将它们移除。

如果想要完全压缩,停用这种安全模式,可以在res/raw/文件夹下创建一个keep.xml文件来开启严苛模式

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

开启严苛模式之后,如果代码中还是用了上面Resources.getIdentifier()方式来查找资源,就必须在keep.xml中添加keep属性,把所要找的资源keep住

7 资源混淆和7zip压缩

资源混淆和压缩不仅可以减少包体积,还能增大资源文件的反编译的难度

通过resource.arsc文件混淆步骤

  • 解析arsc文件(主要解析资源名字符串池)
  • 修改字符串池中的字符串(替换为a/b等无意义字符)
  • 修改apk中res目录资源文件名
  • 打包(7zip)、对齐、签名

资源混淆和压缩可以使用微信开源方案

AndResGuard

AndResGuard原理

8 其他

减少Enum枚举的使用 每减少一个枚举可以减少1k左右

优化引用的库,基础库统一 选择更小的库

AndroidStudio插件:Android Methods Count ,使用这个插件可以看到引入库的方法数量

使用第三方库的时候,最好只引入使用到的代码。

音频资源压缩

资料:

mp.weixin.qq.com/s/WGl2TBkz8…

mp.weixin.qq.com/s/QRIy_apwq…

tech.meituan.com/2017/04/07/…