Android App瘦身实战

随着业务的快速迭代增长,不断引入新的业务逻辑代码、图片资源和第三方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 streams
            zin.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 true
            proguardFiles getDefaultProguardFile(‘proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

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

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

每次执行完ProGuard之后,ProGuard都会在${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 = true
            if (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 true
            minifyEnabled true
            proguardFiles 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_GRAY
    public static final long TINY_PNG_CRC = 0x88b2a3b0L;

    // A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
    public static final long TINY_9PNG_CRC = 0x1148f987L;

    // The XML document <x/> as binary-packed with AAPT
    public static final long TINY_XML_CRC = 0xd7e65643L;

2 通过android-chunk-utils把resources.arsc中对应的定义移除;

  1. 删除资源包中对应的资源文件。

重复资源优化

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

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

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

时间: 2024-11-05 17:31:45

Android App瘦身实战的相关文章

Android APP瘦身(清除工程中没用到的资源)详解_Android

清除Android工程中没用到的资源 项目需求一改再改,UI一调再调,结果就是项目中一堆已经用不到但却没有清理的垃圾资源,不说工程大小问题,对新进入项目的人或看其他模块的代码的人来说,这些没清理的资源可能也可能会带来困扰,所以最好还是清理掉这些垃圾,对于一个稍微大一点的工程来说,手工清理明显是不现实的,这就需要一个方法做这些事情. 清理资源文件 要清理没用的资源,首要的工作当然是找到他们,我们知道Anroid SDK中有一个工具叫lint,可以帮助我们查看工程中存在的问题,其中有一项功能就是查找

Android APP瘦身(清除工程中没用到的资源)详解

清除Android工程中没用到的资源 项目需求一改再改,UI一调再调,结果就是项目中一堆已经用不到但却没有清理的垃圾资源,不说工程大小问题,对新进入项目的人或看其他模块的代码的人来说,这些没清理的资源可能也可能会带来困扰,所以最好还是清理掉这些垃圾,对于一个稍微大一点的工程来说,手工清理明显是不现实的,这就需要一个方法做这些事情. 清理资源文件 要清理没用的资源,首要的工作当然是找到他们,我们知道Anroid SDK中有一个工具叫lint,可以帮助我们查看工程中存在的问题,其中有一项功能就是查找

致Android开发者:APP 瘦身经验总结

随着移动端产品功能的逐渐增加,APP 的体积也不可避免地呈现上升趋势,如果不加以重视,几个版本迭代下来,可能你的 APP 体积会达到用户不能忍受的程度. 如果你是 SDK 开发者,你的 SDK 包大小是用户决定是否采用的关键因素:如果你的APP 想要预装到某款手机或者某款 Android 系统中,APP 的体积也会受到很严格的限制. 因此,APP 的瘦身是每个移动端产品都会遇到的一个普遍问题,本文选自<Android高级进阶>将从不同的角度切入,全面介绍APP 瘦身相关知识. APP 为什么变

iOS - Bitcode App 瘦身中间码

1.Bitcode 随着 Xcode7 的发布,Apple 提供了一项新的技术来支持 App 瘦身功能,那就是 Bitcode. 1.BitCode 是什么 Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the store. Including

Android APK瘦身实践

因为推广的需要,公司需要把APK的大小再"减小"一下,4M以内! 当达到4M以内之后,公司建议说,能否再压压?2M如何? 瘦身前 因为平时就考虑到大小的限制,所以很多工作已经做过了,如下列举现在的状态: 7.3M(Debug版本)和6.5M(Release版本) 开启minifyEnabled 开启shrinkResources 已经去除不相关的大型库 图片和代码已经经历过粗略的一轮清理 开始魔鬼瘦身 1. tinypng有损压缩 android打包本身会对png进行无损压缩,不信大家

Android应用瘦身,从18MB到12.5MB

开篇语 前阵子老大交给了我一个任务,主要是帮我们开发的直播应用做 Android 端的安装包瘦身,花了大概一周的时间把安装包从 18MB 减小到了 12.5MB.原本完全可以优化到 10MB 之下,但由于其他原因的限制,所以目前阶段只到 12.5MB 为止.在此记录一下优化的思路和用到的工具,方便自己以后 Review ,有需要的童鞋也可供参考. 瘦身的目的 从目的导向来看,我们是不会无缘无故去做一件事情的,那我们对应用瘦身的目的是为了什么?答案是:提高下载转化率.什么是下载转化率?举个栗子:你

再谈Android应用瘦身

Android应用apk安装包的大小,虽然对于现在WiFi普及情况而言消耗流量已经微乎其微,但是,对于一款好的应用,对于一款负责任的应用,当然是越小越好了.   引言: .应用越小,下载越快,也就意味着新用户能在最短时间内安装,体验应用,而不是看着通知栏里面的丑陋的下载进度条,盯着看几分钟(30-50M的应用很常见,网不好,下载几分钟很正常)就像这样...   . 随着应用的迭代,应用必须满足人们越来越高的体验需求,应用需要更多的代码,更多的第三方库,更多的资源文件,随着设备的分辨率越来越高,资

我的Android进阶之旅------&amp;gt;Android APP终极瘦身指南

首先声明,下面文字转载于: APK瘦身实践 http://www.jayfeng.com/2015/12/29/APK%E7%98%A6%E8%BA%AB%E5%AE%9E%E8%B7%B5/ APP终极瘦身指南 http://www.jayfeng.com/2016/03/01/Android-APP%E7%BB%88%E6%9E%81%E7%98%A6%E8%BA%AB%E6%8C%87%E5%8D%97/                                           

APP加固新方向--混淆和瘦身

近些年来移动APP数量呈现爆炸式的增长,黑产也从原来的PC端转移到了移动端,造成数据泄漏.源码被盗.APP被山寨.破解后注入病毒或广告现象让用户和生产高质量的程序员苦不堪言,APP加固意义愈发重大. 传统加固和脱壳技术的发展经过了三代的发展和升级,时至今日,传统加固面临到挑战,如容易被脱壳,脱壳类教程非常多,通用脱壳机可轻易脱大部分壳. 加固新方向,混淆和瘦身吸引了人们的眼球.代码混淆技术是对抗逆向攻击最有效的方式之一.此外越来越多的新特性正在啃蚀着大型APP的用户体验,APP瘦身减肥也成了亟待