• 注册
  • Android博客 Android博客 关注:0 内容:1411

    Android高手笔记-包体积优化

  • 查看作者
  • 打赏作者
  • 当前位置: 职业司 > Android开发 > Android博客 > 正文
    • Android博客
    • 为什么要优化包体积

      • 下载转化率:安装包越小,转化率越高;
      • 推广成本:渠道推广成本和厂商预装的单价
      • 应用市场:App Store和Google Play对安装包大小都有限制;
      • 应用性能:
        • 安装时间:文件拷贝、Library 解压、编译 ODEX、签名校验
        • 运行内存:Resource 资源、Library 以及 Dex 类加载这些都会占用不少的内存
        • ROM空间:闪存空间不足,非常容易出现写入放大的情况

      包体积优化

      APK分析

      1. 使用 ApkTool 反编译工具分析 APK;
      2. 使用AS 2.2之后提供的Analyze APK;
      3. 使用 nimbledroid 进行 APK 性能分析

      Proguard

      • 混淆之后,默认会在工程目录 app/build/outputs/mapping/release 下生成一个 mapping.txt 文件,这就是 混淆规则;
      1. 作用:
        • 瘦身:它可以检测并移除未使用到的类、方法、字段以及指令、冗余代码,并能够对字节码进行深度优化。

        最后,它还会将类中的字段、方法、类的名称改成简短无意义的名字。

        • 安全:增加代码被反编译的难度,一定程度上保证代码的安全。
      2. 功能
        1. 压缩(Shrinking): 默认开启,以减小应用体积,移除未被使用的类和成员
        -dontshrink 关闭压缩
        复制代码
        1. 优化(Optimization): 默认开启,在 字节码级别执行优化,让应用 运行的更快
        -dontoptimize 关闭优化
        -optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5
        复制代码
        1. 混淆(Obfuscation): 默认开启,增大反编译难度,类和类成员会被随机命名
        -dontobfuscate 关闭混淆
        复制代码
      3. 优化细节
      1)、优化了 Gson 库的使用。
      2)、把类都标记为 final。
      3)、把枚举类型简化为常量。
      4)、把一些类都垂直合并进当前类的结构中。
      5)、把一些类都水平合并进当前类的结构中。
      6)、移除 write-only 字段。
      7)、把类标记为私有的。
      8)、把字段的值跨方法地进行传递。
      9)、把一些方法标记为私有、静态或 final。
      10)、解除方法的 synchronized 标记。
      11)、移除没有使用的方法参数。
      复制代码
      1. 配置
      buildTypes {
      release {
      // 1、是否进行混淆
      minifyEnabled true
      // 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
      zipAlignEnabled true
      // 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
      // 这些代码所引用的资源也会被标记为无用资源,然后
      // 系统通过资源压缩功能将它们移除。
      // 需要注意的是目前资源压缩器目前不会移除values/文件夹中
      // 定义的资源(例如字符串、尺寸、样式和颜色)
      // 开启后,Android构建工具会通过ResourceUsageAnalyzer来检查
      // 哪些资源是无用的,当检查到无用的资源时会把该资源替换
      // 成预定义的版本。主要是针对.png、.9.png、.xml提供了
      // TINY_PNG、TINY_9PNG、TINY_XML这3个byte数组的预定义版本。
      // 资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
      shrinkResources true
      // 4、混淆文件的位置,其中 proguard-android.txt 为sdk默认的混淆配置,
      // 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
      // 此外,proguard-android-optimize.txt 也为sdk默认的混淆配置,
      // 但是它默认打开了优化开关。并且,我们可在配置混淆文件将android.util.Log置为无效代码,
      // 以去除apk中打印日志的代码。而 proguard-rules.pro 是该模块下的混淆配置。
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
      signingConfig signingConfigs.release
      }
      }
      复制代码
      1. 混淆的基本规则
      每个module创建时都会创建一个混淆文件proguard-rules.pro
      # Add project specific ProGuard rules here.
      # By default, the flags in this file are appended to flags specified
      # in C:\sdk\android-sdk-windows/tools/proguard/proguard-android.txt
      # You can edit the include path and order by changing the proguardFiles
      # directive in build.gradle.
      #
      # For more details, see
      #    链接
      # Add any project specific keep options here:
      # If your project uses WebView with JS, uncomment the following
      # and specify the fully qualified class name to the JavaScript interface
      # class:
      #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
      #   public *;
      #}
      # 代码混淆压缩比,在0~7之间
      -optimizationpasses 5
      # 去除编译时警告
      -ignorewarnings
      #不压缩输入的类文件
      -dontshrink
      #不优化输入的类文件
      -dontoptimize
      # 不混淆输入的类文件
      #-dontobfuscate
      # 混合时不使用大小写混合,混合后的类名为小写
      -dontusemixedcaseclassnames
      # 指定不去忽略非公共库的类
      -dontskipnonpubliclibraryclasses
      # 指定不去忽略非公共库的类成员
      -dontskipnonpubliclibraryclassmembers
      #把混淆类中的方法名也混淆了
      -useuniqueclassmembernames
      #优化时允许访问并修改有修饰符的类和类的成员
      -allowaccessmodification
      #以下是打印出关键的流程日志
      # 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
      -dontpreverify
      #混淆时是否记录日志
      -verbose
      #apk包内所有class的内部结构
      -dump class_files.txt
      #未混淆的类和成员
      -printseeds seeds.txt
      #列出从apk中删除的代码
      -printusage unsed.txt
      #混淆前后的映射
      -printmapping mapping.txt
      # 避免混淆泛型
      -keepattributes Signature
      #google推荐算法
      -optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
      # 避免混淆Annotation注解、内部类、泛型、匿名类
      -keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
      #js调用java方法
      -keepattributes *JavascriptInterface*
      #将文件来源重命名为“SourceFile”字符串
      -renamesourcefileattribute SourceFile
      # 保留行号
      -keepattributes SourceFile,LineNumberTable
      # 处理support包
      -dontnote android.support.**
      -dontwarn 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$* {*;}
      #反射中使用的元素,需要保证类名,方法名,属性名不变,否则混淆后会反射不了
      # 保留四大组件,自定义的Application等这些类不被混淆
      #四大组件必须在AndroidManifest中注册,混淆后类名发生更改
      -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
      # 保持测试相关的代码
      -dontnote junit.framework.**
      -dontnote junit.runner.**
      -dontwarn android.test.**
      -dontwarn android.support.test.**
      -dontwarn org.junit.**
      # 保留在Activity中的方法参数是view的方法,
      # 这样以来我们在layout中写的onClick就不会被影响
      -keepclassmembers class * extends android.app.Activity{
      public void *(android.view.View);
      }
      # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
      -keepclassmembers class * {
      void *(**On*Event);
      void *(**On*Listener);
      }
      # 保留本地native方法不被混淆
      -keepclasseswithmembernames class * {
      native <methods>;
      }
      # 保留枚举类不被混淆
      -keepclassmembers enum * {
      public static **[] values();
      public static ** valueOf(java.lang.String);
      }
      # 保留Parcelable序列化类不被混淆
      -keep class * implements android.os.Parcelable {
      public static final android.os.Parcelable$Creator *;
      }
      #保持所有实现 Serializable 接口的类成员
      -keepclassmembers class * implements java.io.Serializable {
      static final long serialVersionUID;
      private static final java.io.ObjectStreamField[]   serialPersistentFields;
      private void writeObject(java.io.ObjectOutputStream);
      private void readObject(java.io.ObjectInputStream);
      java.lang.Object writeReplace();
      java.lang.Object readResolve();
      }
      #assume no side effects:删除android.util.Log输出的日志
      -assumenosideeffects class android.util.Log {
      public static *** v(...);
      public static *** d(...);
      public static *** i(...);
      public static *** w(...);
      public static *** e(...);
      }
      #保留Keep注解的类名和方法
      -keep,allowobfuscation @interface android.support.annotation.Keep
      -keep @android.support.annotation.Keep class *
      -keepclassmembers class * {
      @android.support.annotation.Keep *;
      }
      #Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
      -keep public class * extends android.support.v4.app.Fragment
      -keep public class * extends android.app.Fragment
      # 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);
      }
      # 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
      -keepclassmembers class * {
      void *(**On*Event);
      void *(**On*Listener);
      }
      复制代码
      • 在 AndroidMainfest 中的类默认不会被混淆,所以四大组件和 Application 的子类和 Framework 层下所有的类默认不会进行混淆,

      并且自定义的 View 默认也不会被混淆。因此,我们不需要手动在 proguard-rules.pro 中去添加;

      优化方式

      业务梳理:

      • 删除无用或者低价值的业务,永远都是最有效的性能优化方式;

      开发模式升级:

      • 如果所有的功能都不能移除,那可能需要倒逼开发模式的转变,更多地采用小程序、H5 这样开发模式;

      代码

      • 对于大部分应用来说,Dex 都是包体积中的大头。
      • 而且 Dex 的数量对用户安装时间也是一个非常大的挑战
      • 在不砍功能的前提下,我们看看有哪些方法可以减少这部分空间。
      1. ProGuard
      1. 要仔细检查最终合并的 ProGuard 配置文件,是不是存在过度 keep 的现象。
        • 可以通过下面的方法输出 ProGuard 的最终配置: -printconfiguration configuration.txt
      2. 一般来说,应用都会 keep 住四大组件以及 View 的部分方法,这样是为了在代码以及 XML 布局中可以引用到它们
        -keep public class * extends android.app.Activity
        -keep public class * extends android.app.Application
        -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.view.View
        复制代码
        • 事实上,我们完全可以把非 exported 的四大组件以及 View 混淆,但是需要完成下面几个工作:
          • XML替换: 在代码混淆之后,需要同时修改 AndroidManifest 以及资源 XML 中引用的名称。
          • 代码替换: 需要遍历其他已经混淆好的代码,将变量或者方法体中定义的字符串也同时修改。
            • 推荐使用 ASM
            • 饿了么曾经开源过一个可以实现四大组件和 View 混淆的组件Mess,可供参考;
            • 要注意的是,代码中不能出现经过运算得到的类名,这种情况会导致替换失败。
            // 情况一:变量
            public String activityName = "com.sample.TestActivity";
            // 情况二:方法体
            startActivity(new Intent(this, "com.sample.TestActivity"));
            // 情况三:通过运算得到,不支持
            startActivity(new Intent(this, "com.sample" + ".TestActivity"));
            复制代码
        • Android Studio 3.0 推出了新 Dex 编译器 D8 与新混淆工具 R8
          • D8 的 优化效果:
          1)、Dex的编译时间更短。
          2)、.dex文件更小。
          3)、D8 编译的 .dex 文件拥有更好的运行时性能。
          4)、包含 Java 8 语言支持的处理。
          复制代码
          • 开启D8: gradle.properties 文件中 android.enableD8 = true
          • Android Studio 3.1 或之后的版本 D8 将会被作为默认的 Dex 编译器。
          • R8 是 Proguard 压缩与优化部分的替代品,
          • Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本,R8 会作为默认编译器。
          • 否则gradle.properties 中配置支持R8:
          android.enableR8=true
          android.enableR8.libraries=true
          复制代码
      3. 去掉 Debug 信息或者去掉行号
        • 通过相同的 ProGuard 规则生成一个 Debug 包和 Release 包,大小却又差异,就是差在DebugItem;
        • DebugItem包含:
          • 调试的信息。函数的参数变量和所有的局部变量。
          • 排查问题的信息。所有的指令集行号和源文件行号的对应关系。
          • 占dex的5.5%左右
        • ProGuard 配置中一般我们也会通过下面的方式保留行号信息: -keepattributes SourceFile, LineNumberTable
        • debugItem 能直接去掉吗,显然不能,如果去掉了,那所有上报的 crash 信息就会没有行号,所有的行号都会变成 -1,会被喷的找不到北。
        • 下面是支付宝的两个方案:
          • 方案1:行号查找离线化
          让本来存放在 App 中的行号对应关系提前抽离出来存放在服务端,crash 上报的时候通过提前
          抽离的行号表进行行号反解,解决 crash 信息上报无行号,无法定位的问题,主要步骤如下:
          1. 修改 proguard:利用 proguard 来删除 debugItem (去掉 -keep lineNumberTable),在删除行号表之前 dump 出一个临时的 dex。
          2. 修改 dexdump:把临时的 dex 中的行号表关系 dump 成一个 dexpcmapping 文件(指令集行号和源文件行号映射关系),并存至服务端。
          3. hook app runtime 的 crash handler,把 crash 时的指令集行号上报到反解平台。
          4. 反解平台通过上报指令集行号和提前准备好 dexpcmapping 文件反解出正确的行号。
          复制代码
          • 方案2:尝试直接修改 dex 文件
          保留一小块 debugItem,让系统查找行号的时候指令集行号和源文件行号保持一致,
          这样就什么都不用做,任何监控上报的行号都直接变成了指令集行号,只需修改 dex 文件
          复制代码
        • 使用RxDex:(支付宝参考的是 Facebook 的一个开源编译工具ReDex)
          {
          "redex" : {
          "passes" : [
          "StripDebugInfoPass",
          "RegAllocPass"
          ]
          },
          "StripDebugInfoPass" : {
          "drop_all_dbg_info" : false,
          "drop_local_variables" : true,
          "drop_line_numbers" : false,
          "drop_src_files" : false,
          "use_whitelist" : false,
          "cls_whitelist" : [],
          "method_whitelist" : [],
          "drop_prologue_end" : true,
          "drop_epilogue_begin" : true,
          "drop_all_dbg_info_if_empty" : true
          },
          "RegAllocPass" : {
          "live_range_splitting": false
          }
          }
          复制代码
      2. Dex 分包
      • 跨 Dex 调用造成冗余信息:
        • 例如:Class A 与 Class B 分别编译到不同的 Dex 中,由于 method a 调用了 method b,所以在 classesA.dex 中也需要加上 method b 的 id
        1. 造成method id 爆表:
          • 每个 Dex 的 method id 需要小于 65536,因为 method id 的大量冗余导致每个 Dex 真正可以放的 Class 变少,这是造成最终编译的Dex 数量增多。
        2. 造成信息冗余
      • 为了进一步减少 Dex 的数量,我们希望每个 Dex 的方法数都是满的,即分配了 65536 个方法。
      • 如何实现 Dex 信息有效率提升呢?
        • 需要将有调用关系的类和方法分配到同一个 Dex 中,即减少跨 Dex 的调用的情况;
        • ReDex 在分析类调用关系后,使用的是贪心算法计算局部最优值,具体算法可查看CrossDexDefMinimizer。
        • 通过Redex实现,配置如下
        {
        "redex" : {
        "passes" : [
        "StripDebugInfoPass",
        "InterDexPass",
        "RegAllocPass"
        ]
        },
        "StripDebugInfoPass" : {
        "drop_all_dbg_info" : false,
        "drop_local_variables" : true,
        "drop_line_numbers" : false,
        "drop_src_files" : false,
        "use_whitelist" : false,
        "cls_whitelist" : [],
        "method_whitelist" : [],
        "drop_prologue_end" : true,
        "drop_epilogue_begin" : true,
        "drop_all_dbg_info_if_empty" : true
        },
        "InterDexPass" : {
        "minimize_cross_dex_refs": true,
        "minimize_cross_dex_refs_method_ref_weight": 100,
        "minimize_cross_dex_refs_field_ref_weight": 90,
        "minimize_cross_dex_refs_type_ref_weight": 100,
        "minimize_cross_dex_refs_string_ref_weight": 90
        },
        "RegAllocPass" : {
        "live_range_splitting": false
        },
        "string_sort_mode" : "class_order",
        "bytecode_sort_mode" : "class_order"
        }
        复制代码
      3. Dex 压缩
      - Facebook App 的 classes.dex 只是一个壳,真正的代码都放到 assets 下面。它们把所有的 Dex 都合并成同一个 secondary.dex.jar.xzs 文件,并通过 XZ 压缩。
      - XZ 压缩算法和 7-Zip 一样,内部使用的都是 LZMA 算法。对于 Dex 格式来说,XZ 的压缩率可以比 Zip 高 30% 左右。
      - 存在的问题:
      - 首次启动解压:应用首次启动的时候,需要将 secondary.dex.jar.xzs 解压缩,根据上图的配置信息,应该一共有 11 个 Dex。
      - ODEX 文件生成:Facebook 为了解决这个问题,使用了 ReDex 另外一个超级硬核的方法,那就是oatmeal
      复制代码

      Native Library

      • 各种三方库导致APK 中 Native Library 的体积越来越大
      1. 去除 Debug 信息
      2. 使用 c++_shared
      3. Library 压缩
        • 跟 Dex 压缩一样,Library 优化最有效果的方法也是使用 XZ 或者 7-Zip 压缩。
        • 只需要加载少数启动过程相关的 Library,其他的 Library 我们都在首次启动时解压。
        • Facebook 有一个 So 加载的开源库SoLoader,它可以跟这套方案配合使用
      4. Library 合并与裁剪
        • Facebook 中的编译构建工具Buck也有两个比较硬核的高科技
        1. Library 合并。在 Android 4.3 之前,进程加载的 Library 数量是有限制的。在编译过程,我们可以自动将部分 Library 合并成一个。
          • 具体思路你可以参考文章《Android native library merging》以及Demo。
        2. Library 裁剪。Buck 里面有一个relinker的功能,原理就是分析代码中 JNI 方法以及不同 Library 的方法调用,找到没有无用的导出 symbol,将它们删掉。

        这样 linker 在编译的时候也会把对应的无用代码同时删掉,这个方法相当于实现了 Library 的 ProGuard Shrinking 功能。

      5. So 移除方案
        • 目前,Android 一共 支持7种不同类型的 CPU 架构,其中x86架构目前没有真机,只是模拟器会用的这个架构的so库,那么生产包就可以去掉x86,x86_64的so文件;
        • 在 build.gradle 中配置这个 abiFiliters 去设置 App 支持的 So 架构
        //生产环境
        release {
        resValue "string", "app_name", "@string/app_name_release"
        ndk {
        abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
        //有些观点是只留下armeabi即可,armeabi 目录下的 So 可以兼容别的平台上的 So,
        //但是,这样 别的平台使用时性能上就会有所损耗,失去了对特定平台的优化,而且近期国内的应用市场也开始要求适配64位的应用了
        }
        }
        //开发环境
        debug {
        resValue "string", "app_name", "@string/app_name_debug"
        ndk {
        rootProject.ext.ndkAbis.each { abi ->
        abiFilter(abi)
        }
        }
        }
        复制代码

      包体积监控

      1. 大小监控
        • 如果某个版本体积增长过大,需要分析具体原因,是否有优化空间。
      2. 依赖监控
        • 添加开源库时需要注意其大小,可以对其进行功能剥离,只引入部分需要的代码
      3. 规则监控: 例如无用资源、大文件、重复文件、R 文件等,参考微信Matrix的ApkChecker

      资源优化

      使用 AndResGuard 工具
      • AndResGuard的两个主要功能
      1. 资源混淆
        • ProGuard 的核心优化主要有三个:Shrink、Optimize 和 Obfuscate,也就是裁剪、优化和混淆。
        1. resources.arsc: 因为资源索引文件 resources.arsc 需要记录资源文件的名称与路径,使用混淆后的短路径 res/s/a,可以减少整个文件的大小
        2. metadata 签名文件:签名文件 MF 与 SF都需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小
        3. ZIP 文件索引:ZIP 文件格式里面也需要记录每个文件 Entry 的路径、压缩算法、CRC、文件大小等信息。使用短路径,本身就可以减少记录文件路径的字符串大小;
      2. 极限压缩
        • 更高的压缩率。虽然我们使用的还是 Zip 算法,但是利用了 7-Zip 的大字典优化,APK 的整体压缩率可以提升 3% 左右。
        • 压缩更多的文件。Android 编译过程中,下面这些格式的文件会指定不压缩;在 AndResGuard 中,支持针对 resources.arsc、PNG、JPG 以及 GIF 等文件的强制压缩。
        /* these formats are already compressed, or don't compress well */
        static const char* kNoCompressExt[] = {
        ".jpg", ".jpeg", ".png", ".gif",
        ".wav", ".mp2", ".mp3", ".ogg", ".aac",
        ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
        ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
        ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
        ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
        };
        复制代码
        • 为什么 Android 系统会专门选择不去压缩这些文件呢?
          • 压缩效果并不明显。这些格式的文件大部分本身已经压缩过
          • 读取时间与内存的考虑。如果文件是没有压缩的,系统可以利用 mmap 的方式直接读取,而不需要一次性解压并放在内存中
        • Android 6.0 之后 AndroidManifest 支持不压缩 Library 文件,这样安装 APK 的时候也不需要把 Library 文件解压出来,系统可以直接 mmap 安装包中的 Library 文件。
        android:extractNativeLibs=“true”
        复制代码
      • AndResGuard的使用
      apply plugin: 'AndResGuard'
      buildscript {
      repositories {
      jcenter()
      google()
      }
      dependencies {
      classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.18'
      }
      }
      andResGuard {
      // mappingFile = file("./resource_mapping.txt")
      mappingFile = null
      use7zip = true
      useSign = true
      // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
      keepRoot = false
      // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
      fixedResName = "arg"
      // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
      mergeDuplicatedRes = true
      whiteList = [
      // for 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"
      ]
      compressFilePattern = [
      "*.png",
      "*.jpg",
      "*.jpeg",
      "*.gif",
      ]
      sevenzip {
      artifact = 'com.tencent.mm:SevenZip:1.2.18'
      //path = "/usr/local/bin/7za"
      }
      /**
      * 可选: 如果不设置则会默认覆盖assemble输出的apk
      **/
      // finalApkBackupPath = "${project.rootDir}/final.apk"
      /**
      * 可选: 指定v1签名时生成jar文件的摘要算法
      * 默认值为“SHA-1”
      **/
      // digestalg = "SHA-256"
      }
      复制代码
      进阶的优化方法
      1. 资源合并: 所有的资源文件都合并成同一个大文件

        • 事实上,大部分的换肤方案也是采用这个思路,这个大资源文件就相当于一套皮肤。因此我们完全可以把这套方案推广开来,但是实现起来还是需要解决不少问题的。
        1. 资源的解析。我们需要模拟系统实现资源文件的解析,例如把 PNG、JPG 以及 XML 文件转换为 Bitmap 或者 Drawable,这样获取资源的方法需要改成我们自定义的方法。
        // 系统默认的方式
        Drawable drawable = getResouces().getDrawable(R.drawable.loading);
        // 新的获取方式
        Drawable drawable = CustomResManager.getDrawable(R.drawable.loading);
        复制代码
        1. 资源的管理。考虑到内存和启动时间,所有的资源也是用时加载,我们只需要使用 mmap 来加载“Big resource File”。

        同时我们还要实现自己的资源缓存池 ResourceCache,释放不再使用的资源文件,这部分内容你可以参考类似 Glide 图片库的实现。

      2. 无用资源

        1. Lint: 使用Lint这个静态代码扫描工具,它里面就支持 Unused Resources 扫描。
        2. shrinkResources:资源压缩, 需要配合 ProGurad 的“minifyEnabled”功能同时使用
        //如果 ProGuard 把部分无用代码移除,这些代码所引用的资源也会被标记为无用资源,然后通过资源压缩功能将它们移除
        //没有真正删除,而是替换成空文件,防止resources.arsc 和 R.java 文件的资源 ID 会改变
        android {
        ...
        buildTypes {
        release {
        shrinkResources true
        minifyEnabled true
        }
        }
        }
        复制代码
      3. 对于 Assets 资源,代码中会有各种各样的引用方式,如果想准确地识别出无用的 Assets 并不是那么容易。在 Matrix 中尝试提供了一套简单的实现,可以参考UnusedAssetsTask;

      4. 避免产生 Java access 方法

        1. 在开发过程中需要注意在可能产生 access 方法的情况下适当调整,比如去掉 private,改为 package 可见性。
        2. 使用 ASM 在编译时删除生成的 access 方法。
        • 建议直接使用 ByteX 的 access_inline 插件
        • 除了 access_inlie 之外,在 ByteX 中还有 四个 很实用的代码优化 Gradle 插件可以帮助我们有效减小 Dex 文件的大小
        1、编译期间 内联常量字段:const_inline。
        2、编译期间 移除多余赋值代码:field_assign_opt。
        3、编译期间 移除 Log 代码:method_call_opt。
        4、编译期间 内联 Get / Set 方法:getter-setter-inline-plugin。
        复制代码
      5. 重复资源优化

        • 在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现重复资源的去除
        1. 首先,通过资源包中的每个ZipEntry的CRC-32 checksum来筛选出重复的资源。
        2. 然后,通过android-chunk-utils修改resources.arsc,把这些重复的资源都重定向到同一个文件上。
        3. 最后,把其它重复的资源文件从资源包中删除,仅保留第一份资源。
        实现代码如下:
        variantData.outputs.each {
        def apFile = it.packageAndroidArtifactTask.getResourceFile();
        it.packageAndroidArtifactTask.doFirst {
        def arscFile = new File(apFile.parentFile, "resources.arsc");
        JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);
        def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile);
        removeZipEntry(apFile, "resources.arsc");
        if (arscFile.exists()) {
        FileInputStream arscStream = null;
        ResourceFile resourceFile = null;
        try {
        arscStream = new FileInputStream(arscFile);
        resourceFile = ResourceFile.fromInputStream(arscStream);
        List<Chunk> chunks = resourceFile.getChunks();
        HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024);
        // 处理arsc并删除重复资源
        Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();
        while (iterator.hasNext()) {
        Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();
        // 保留第一个资源,其他资源删除掉
        for (def index = 1; index < duplicatedEntry.value.size(); ++index) {
        removeZipEntry(apFile, duplicatedEntry.value.get(index).name);
        toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);
        }
        }
        for (def index = 0; index < chunks.size(); ++index) {
        Chunk chunk = chunks.get(index);
        if (chunk instanceof ResourceTableChunk) {
        ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;
        StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();
        for (def i = 0; i < stringPoolChunk.stringCount; ++i) {
        def key = stringPoolChunk.getString(i);
        if (toBeReplacedResourceMap.containsKey(key)) {
        stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));
        }
        }
        }
        }
        } catch (IOException ignore) {
        } catch (FileNotFoundException ignore) {
        } finally {
        if (arscStream != null) {
        IOUtils.closeQuietly(arscStream);
        }
        arscFile.delete();
        arscFile << resourceFile.toByteArray();
        addZipEntry(apFile, arscFile);
        }
        }
        }
        }
        复制代码
      6. 图片压缩

        1. 通过tingpng.com/网站
        2. McImage
        3. TinyPngPlugin
        4. TinyPIC_Gradle_Plugin
        • 需要注意的是,在 Android 的构建流程中,AAPT 会使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,

        但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle 中 设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片

        aaptOptions {
        cruncherEnabled = false
        }
        复制代码
        • 图片格式的选择:VD(纯色icon)->WebP(非纯色icon)->Png(更好效果) ->jpg(若无alpha通道)
      7. R Field 的内联优化

        • 通过内联 R Field 来进一步对代码进行瘦身,此外,它也解决了 R Field 过多导致 MultiDex 65536 的问题。

        要想实现内联 R Field,我们需要 通过 Javassist 或者 ASM 字节码工具在构建流程中内联 R Field;

        • 可以使用或参考蘑菇街的ThinR gradle plugin
      8. 资源文件最少化配置

        • 根据 App 目前所支持的语言版本去选用合适的语言资源,例如使用了 AppCompat,如果不做任何配置的话,

        最终 APK 包中会包含 AppCompat 中所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。
        对此,我们可以 通过 resConfig 来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。
        同理,也可以使用 resConfigs 去配置你应用需要的图片资源文件类,如 "xhdpi"、"xxhdpi" 等等

        android {
        ...
        defaultConfig {
        ...
        resConfigs "zh", "zh-rCN"
        resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
        }
        ...
        }
        //还可以利用 Density Splits 来选择应用应兼容的屏幕尺寸大小
        android {
        ...
        splits {
        density {
        enable true
        exclude "ldpi", "tvdpi", "xxxhdpi"
        compatibleScreens 'small', 'normal', 'large', 'xlarge'
        }
        }
        ...
        }
        复制代码
      9. 资源在线化

        • 将一些图片资源放在服务器,然后 结合图片预加载 的技术手段,既可以满足产品的需要,同时可以减小包大小。
      10. 统一应用风格

        • 设定统一的 字体、尺寸、颜色和按钮按压效果、分割线 shape、selector 背景 等
      11. 插件化

        • 使用插件化的手段 对代码结构进行调整,如果我们 App 当中的每一个功能都是一个插件,并且都是可以从服务器下发下来的,那 App 的包体积肯定会小很多。

      参考文章

      • Android开发高手课-包体积优化(上):如何减少安装包大小?
      • Android安装包相关知识汇总
      • AndResGuard--微信Android资源混淆打包工具
      • AndResGuard
      • 支付宝 App 构建优化解析:Android 包大小极致压缩
      • ReDex: An Android Bytecode Optimizer
      • Matrix-ApkChecker — Apk 分析减包利器
      • Android开发高手课-包体积优化(下):资源优化的进阶实践
      • Android App包瘦身优化实践
      • Android APK 签名原理
      • 深入探索 Android 包体积优化
      • Android 性能优化:使用 Lint 优化代码、去除多余资源
      • 使用Simian工具扫描重复代码
      • 西瓜视频apk瘦身之 Java access 方法删除
      • access-inline-plugin

      我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

      请登录之后再进行评论

      登录

      手机阅读天地(APP)

      • 微信公众号
      • 微信小程序
      • 安卓APP
      手机浏览,惊喜多多
      匿名树洞,说我想说!
      问答悬赏,VIP可见!
      密码可见,回复可见!
      即时聊天、群聊互动!
      宠物孵化,赠送礼物!
      动态像框,专属头衔!
      挑战/抽奖,金币送不停!
      赶紧体会下,不会让你失望!
    • 实时动态
    • 签到
    • 做任务
    • 发表内容
    • 偏好设置
    • 到底部
    • 帖子间隔 侧栏位置:
    • 还没有账号?点这里立即注册