随着业务的快速迭代增长,不断引入新的业务逻辑代码、图片资源和第三方SDK等,很多app都面临一个一个结果,app越来越大,甚至很多无用的代码,包体积的增大带来了很多问题,诸如app启动更慢,代码维护越来越困难。公司业务发展到一定程度之后,重构,代码优化,app瘦身成为不得不做的一个任务。这里以xx外卖app为例给大家讲讲app瘦身过程中常用的几种方法(也都是网上老生常谈的)。

apk文件构成

我们可以用Zip工具打开APK,一个常见的APK结构如下:

可以看到APK由以下主要部分组成:

文件/目录 描述
lib/ 存放库文件,存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可
res/ 存放资源文件,例如:drawable、layout等等
assets/ 应用程序的资源,应用程序可以使用AssetManager来检索该资源
classes(n).dex classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc 编译后的二进制资源文件
AndroidManifest.xml Android的清单文件,用于描述应用程序的名称、版本、所需权限、注册的四大组件

在充分了解了APK各个组成部分以及它们的作用后,我们针对自身特点进行了分析和优化。下面将从Zip文件格式、classes.dex、资源文件、resources.arsc等方面来介绍下优化技巧。

Zip格式优化

通过命令来查看APK文件时会得到以下信息。命令如下:

aapt l -v xxx.apk或unzip -l xxx.apk

通过上图可以看到APK中很多资源是以Stored来存储的,根据Zip的文件格式中对压缩方式的描述Compression_methods可以看出这些文件是没有压缩的,那为什么它们没有被压缩呢?从AAPT的源码中找到以下描述:

/* 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"
};

上面的解释说的很明白,aapt在资源处理时对上述文件后缀类型的资源是不做压缩的,那是不是可以修改它们的压缩方式从而达到瘦身的效果呢?
答案是可以的,例如采用7Zip压缩等等。
为了大家更好的理解Android对资源的打包过程,我们下面来简单的分析一下。

aapt资源打包过程

首先来看一张Android打包过程图。

通过上图可以看到Manifest、Resources、Assets的资源经过AAPT处理后生成R.java、Proguard Configuration、Compiled Resources。其中,Proguard Configuration是AAPT工具为Manifest中声明的四大组件以及布局文件中(XML layouts)使用的各种Views所生成的ProGuard配置。Compiled Resources是一个Zip格式的文件,这个文件包含了res、AndroidManifest.xml和resources.arsc的文件或文件夹,其实就是APK的“资源包”(res、AndroidManifest.xml和resources.arsc等资源)。
我们可以通过这个文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的。

在自己的项目中是通过在package${flavorName} Task对resources.arsc进行优化。下面是部分代码:

appPlugin.variantManager.variantDataList.each { variantData ->variantData.outputs.each {def sourceApFile = it.packageAndroidArtifactTask.getResourceFile();def destApFile = new File("${sourceApFile.name}.temp", sourceApFile.parentFile);it.packageAndroidArtifactTask.doFirst {byte[] buf = new byte[1024 * 8];ZipInputStream zin = new ZipInputStream(new FileInputStream(sourceApFile));ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destApFile));ZipEntry entry = zin.getNextEntry();while (entry != null) {String name = entry.getName();// Add ZIP entry to output stream.ZipEntry zipEntry = new ZipEntry(name);if (ZipEntry.STORED == entry.getMethod() && !okayToCompress(entry.getName())) {zipEntry.setMethod(ZipEntry.STORED)zipEntry.setSize(entry.getSize())zipEntry.setCompressedSize(entry.getCompressedSize())zipEntry.setCrc(entry.getCrc())} else {zipEntry.setMethod(ZipEntry.DEFLATED)...}...out.putNextEntry(zipEntry);out.closeEntry();entry = zin.getNextEntry();}// Close the streamszin.close();out.close();sourceApFile.delete();destApFile.renameTo(sourceApFile);}}
}

classes.dex的优化

如何优化classes.dex的大小呢?大约有以下几种套路:

  1. 保持良好的编程习惯和对包体积敏锐的嗅觉,去除重复或者不用的代码,慎用第三方库,选用体积小的第三方SDK。
  2. 开启ProGuard,通过使用ProGuard来对代码进行混淆、优化、压缩等工作

第一个方案对程序猿的素质要求比较高,项目经验也很重要,所以因人而异。

压缩代码

可以通过开启ProGuard来实现代码压缩,可以在build.gradle文件相应的构建类型中添加:

minifyEnabled true

例如,常见的一段build.gradle脚本。

android {buildTypes {release {minifyEnabled trueproguardFiles getDefaultProguardFile(‘proguard-android.txt'),'proguard-rules.pro'}}...
}

要想做进一步的代码压缩,可尝试使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的ProGuard规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小APK大小和帮助提高其运行速度。

在Gradle Plugin 2.2.0及以上版本ProGuard的配置文件会自动解压缩到rootProject.buildDir/{rootProject.buildDir}/{AndroidProject.FD_INTERMEDIATES}/proguard-files/目录下,proguardFiles会从这个目录来获取ProGuard配置。

每次执行完ProGuard之后,ProGuard都会在project.buildDir/outputs/mapping/{project.buildDir}/outputs/mapping/{flavorDir}/生成以下文件:

文件名 描述
dump.txt APK中所有类文件的内部结构
mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换,可以通过proguard.obfuscate.MappingReader来解析
seeds.txt 列出未进行混淆的类和成员
usage.txt 列出从APK移除的代码

资源的优化

对于资源的优化也是最行之有效,最为直观的优化方案。通过对资源文件的优化,可以大大的减小apk体积大小。

图片优化

为了支持Android设备DPI的多样化([l|m|tv|h|x|xx|xxx]dpi)以及用户对高质量UI的期待,往往在App中使用了大量的图片以及不同的格式,例如:PNG、JPG 、WebP,那我们该怎么选择不同类型的图片格式呢?
Google I/O 2016大会上推荐使用WebP格式图片,可以大大减少体积,而显示又不失真。

通过上图我们可以看出图片格式选择的方法:如果能用VectorDrawable来表示的话优先使用VectorDrawable,如果支持WebP则优先用WebP,而PNG主要用在展示透明或者简单的图片,而其它场景可以使用JPG格式。这样就达到了什么场景选什么图片更好。

矢量图片

使用矢量图片能够有效的减少App中图片所占用的大小,矢量图形在Android中表示为VectorDrawable对象。 使用VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像,但系统渲染每个VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。 因此只有在显示小图像时才考虑使用矢量图形。

WebP

如果App的minSdkVersion>=14(Android 4.0+)的话,可以选用WebP格式,因为WebP在同画质下体积更小。但是Android从4.0才开始WebP的原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的WebP。所以为了更好的使用webP格式,我们需要读系统进行判断,这里我写了一个工具类:

boolean isPNGWebpConvertSupported() {if (!isWebpConvertEnable()) {return false} // Android 4.0+return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 14 // 4.0
}boolean isTransparencyPNGWebpConvertSupported() {if (!isWebpConvertEnable()) {return false} // Lossless, Transparency, Android 4.2.1+return GradleUtils.getAndroidExtension(project).defaultConfig.minSdkVersion.apiLevel >= 18 // 4.3
}def convert() {String resPath = "${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/merged/${variant.dirName}"def resDir = new File("${resPath}")resDir.eachDirMatch(~/drawable[a-z0-9-]*/) { dir ->FileTree tree = project.fileTree(dir: dir)tree.filter { File file ->return (isJPGWebpConvertSupported() && (file.name.endsWith(SdkConstants.DOT_JPG) || file.name.endsWith(SdkConstants.DOT_JPEG))) || (isPNGWebpConvertSupported() && file.name.endsWith(SdkConstants.DOT_PNG) && !file.name.endsWith(SdkConstants.DOT_9PNG))}.each { File file ->def shouldConvert = trueif (file.name.endsWith(SdkConstants.DOT_PNG)) {if (!isTransparencyPNGWebpConvertSupported()) {shouldConvert = !Imaging.getImageInfo(file).isTransparent()}}if (shouldConvert) {WebpUtils.encode(project, webpFactorQuality, file.absolutePath, webp)}}}
}

选择更优的压缩工具

可以使用pngcrush、pngquant或zopflipng等压缩工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。

开启资源压缩

Android的编译工具链中提供了一款资源压缩的工具,可以通过该工具来压缩资源,如果要启用资源压缩,可以在build.gradle文件中启用,例如:

android {...buildTypes {release {shrinkResources trueminifyEnabled trueproguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}
}

Android构建工具是通过ResourceUsageAnalyzer来检查哪些资源是无用的,当检查到无用的资源时会把该资源替换成预定义的版本。关于资源工具压缩的详细介绍请查看Shrink Your Code and Resources

如果想知道哪些资源是无用的,可以通过资源压缩工具的输出日志文件${project.buildDir}/outputs/mapping/release/resources.txt来查看。例如:

资源压缩工具只是把无用资源替换成预定义较小的版本,那我们如何删除这些无用资源呢?通常的做法是结合资源压缩工具的输出日志,找到这些资源并把它们进行删除。

resources.arsc的优化

关于resources.arsc的优化,主要从以下一个方面来优化:

  1. 开启资源混淆;
  2. 对重复的资源进行优化;
  3. 对被shrinkResources优化掉的资源进行处理。

资源混淆

这里推荐使用微信开源的资源混淆库AndResGuard,具体使用方法请查看安装包立减1M–微信Android资源混淆打包工具

无用资源优化

在上面的介绍中,可以通过shrinkResources true来开启资源压缩,资源压缩工具会把无用的资源替换成预定义的版本而不是移除,如果采用人工移除的方式会带来后期的维护成本,这里笔者采用了一种比较取巧的方式,在Android构建工具执行package${flavorName}Task之前通过修改Compiled Resources来实现自动去除无用资源。

使用流程如下:

  1. 收集资源包(Compiled
    Resources的简称)中被替换的预定义版本的资源名称,通过查看资源包(Zip格式)中每个ZipEntry的CRC-32
    checksum来寻找被替换的预定义资源,预定义资源的CRC-32定义在ResourceUsageAnalyzer,下面是它们的定义。例如:
  // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAYpublic static final long TINY_PNG_CRC = 0x88b2a3b0L;// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markerspublic static final long TINY_9PNG_CRC = 0x1148f987L;// The XML document <x/> as binary-packed with AAPTpublic static final long TINY_XML_CRC = 0xd7e65643L;

2 通过android-chunk-utils把resources.arsc中对应的定义移除;
3. 删除资源包中对应的资源文件。

重复资源优化

产生重复资源的原因是不同的人,在开发的时候没有注意资源的可重用,对于人数比较少,规范到位是可以避免的,但是对于业务比较多,就会造成资源的重复。那么,针对这种问题,我们该怎么优化呢?
具体步骤如下:

  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);}}}
}

通过这种方式可以有效减少重复资源对包体大小的影响,同时这种操作方式对各业务团队透明。

参考:http://tech.meituan.com/android-shrink-overall-solution.html
附:Android优化的若干方案

Android App瘦身实战相关推荐

  1. Android App 瘦身总结 第三章 代码混淆及优化

    目录 一.代码混淆proguard 二.调整第三方库 三.环境差异依赖 四.代码习惯 五.插件化 六.总结 在前两章我们分别从图片资源和jni动态库这两个方面来分析apk瘦身的优化点 Android ...

  2. Android App 瘦身总结 第二章 jni动态库及cpu兼容

    目录 一.利弊分析,按需引用 二.平台兼容 三.动态加载 四.总结 在前一章主要分析了图片资源部分的优化(Android App 瘦身总结 第一章 图片资源的优化处理_There is a Bug!! ...

  3. Android App 瘦身总结 第一章 图片资源的优化处理

    目录 一.去除无用的资源 二.忽略占比极少的分辨率 三.优化图片 四.使用更先进的图片格式 (1)使用矢量图 (2)使用webp图片格式 五.总结 当一款App经历了大量的迭代后,apk包会越来越臃肿 ...

  4. Android App 瘦身总结

    随着移动端产品功能的逐渐增加,APP 的体积也不可避免地呈现上升趋势,如果不加以重视,几个版本迭代下来,可能你的 APP 体积会达到用户不能忍受的程度. 如果你是 SDK 开发者,你的 SDK 包大小 ...

  5. iOS APP 瘦身实战

    前言 app为什么要瘦身,无非是下面几个情况, 第一,产品或者运营觉得包体积过大了! 第二,对技术的追求,也给自己涨点绩效! 第三,面试被问到了..... 哈哈 不管哪种情况吧,要瘦身就好好玩下吧. ...

  6. Android - App瘦身

    为什么要瘦身 安装包变大,导致很多用户不愿意安装更新 安装包变大,导致很多用户不愿意下载 安装包变大,流量使用增多,增加其他边际成本 优化方式 图片资源的优化 图片选择顺序 首先选选择SVG 否则选择 ...

  7. Android app瘦身计划

    上次在面试中被问到这个问题,只答了个使用webp格式图片,感觉有点尴尬,特地总结下有哪些减小app大小的办法 使用一套资源 这是最基本的一条规则,但非常重要. 对于绝大对数APP来说,只需要取一套设计 ...

  8. Android中app瘦身方式

    App瘦身的概念 App瘦身是指在不减少App功能的前提下,通过一些技巧将打包出来的apk体的体积尽可能减少.  这样做的好处有几个:加快用户下载速度,节省用户下载流量,提升用户下载体验.  如果不进 ...

  9. android APK瘦身全面总结——如何从32.6M到13.6M

    前言 之前我简单介绍了关于svg图片瘦身的问题,在公司,瘦身这个问题是我提出来的,所以这锅我背了.公司项目是32.6M,我给自己的要求就是低于20M.上周花了一个星期瘦身,至于为什么花了一周,主要是s ...

  10. App瘦身最佳实践(分析了微信、淘宝、微博图片文件的放法)

    本文会不定期更新,推荐watch下项目.如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request. 本文的示例代码主要是基于作者的经验来编写的,若你有其他 ...

最新文章

  1. Oracle 冷备份
  2. php 给图片增加背景平铺水印代码
  3. 【Trie】[CQOI2016]路由表
  4. css 相对单位rem详解
  5. c++ 编译时函数匹配和运行时类型识别
  6. 五、OpenStack安装Nova
  7. vue-cli 没有build如何配置_如何从零开发一个 gradle 插件(一)
  8. postman websocket_postman的“替代者”postwoman的使用体验—从入门到放弃
  9. C#对STK11.4二次开发的Hello World
  10. 用c语言设计一个菜单界面_最新,最全的NX二次开发Ribbon界面菜单的设计图文教程...
  11. NAS存储文件权限的设置方法
  12. Codeforces 985A. Chess Placing(1ni)(水题)(div.2)
  13. 关于自己ip地址的文章
  14. 笔记本电脑桌面上计算机打不开怎么办,笔记本电脑开了机一直进不去桌面怎么办...
  15. 微信公众平台开发(93) 关闭微信浏览器
  16. 聊一聊前端中常说的接口
  17. 修改SAPGUI的默认文件保存/下载路径 - SAP S/4 Basis Tips
  18. 入行半导体之ATE芯片都在做些什么
  19. Android Studio自带模拟器打不开,一直停留在带有Android的页面
  20. ArcGIS Maritime——初识海图模块(1)服务器端环境配置并加载海图服务

热门文章

  1. 10.2 校内集训 解题报告
  2. 六级考研单词之路-二十二
  3. ggplot2柱状图进阶画法
  4. utorrent不能下载的解决方法
  5. 跳马周游c++_c++广搜法跳马问题(队列)
  6. 北京内推 | 秘塔科技招聘NLP算法工程师(社招校招可转正实习)
  7. 关于VISIO2013显示首要事项闪退问题
  8. 线性代数中自由变量为什么取0和1?
  9. ivr cti_简而言之,网络威胁情报(CTI)— 1
  10. 性能指标TP99介绍