必威App 瘦身最佳实践 – 收藏集 – 掘金。Android App包瘦身优化履(转自美团)

专访DroidPlugin作者张勇:安卓黑科技是何等炼成的 – Android – 掘金

前段时间,奇虎360在Github上宣告了一个Android开源项目DroidPlugin,这是一个实现动态加载的Android插件框架,可以无安装、免修改的运作第三方APK。一时间,它被叫作安卓黑科技,引起行业外之关心。
据其官文档介绍,DroidPlu…

近年来直接在研究有关apk瘦身方面的学识,看了累累稿子受益匪浅。
初稿地址
http://tech.meituan.com/android-shrink-overall-solution.html

微信 Android 资源混淆打包工具-安装包立减1M – Android – 掘金

及等同首文章我们讲述了Android减少设置包体积的片段tips,本文主要针对前文提到的资源混淆做一个略的解析。微信中之资源混淆工具要为混淆资源ID长度(例如将res/drawable/welcome.png混淆为r/s/a.png),同时利用7z深度压缩,…

乘胜工作的飞迭代增长,美团App里持续引入新的事务逻辑代码、图片资源和老三正SDK,直接促成APK体积不断增高。包体积增长拉动的题目进一步多,如CDN流量费用增加、用户安装成功率降低,甚至可能会见影响用户之留存率。APK的瘦身都是只能考虑的工作。在尝瘦身之进程被,我们借鉴了森业界其他公司提供的方案,同时也针对自己特点,发现了一些初的技能。本文将对里面的一对做详细介绍。

Android Proguard 指南 – Android – 掘金

android 混淆配置指南英文版 自家光是个幕后翻译的搬运工
,好吧,翻译是为了提高英文水准。 Android Proguard 指南 Android
Proguard 混淆配置指南 ProGuard 这个ProGuard工具得以经删…

以起说瘦身技巧之前,先来说话一下APK的结缘。

「个人总结」APK 瘦身实践 – Android – 掘金

因推广的内需,公司用拿APK的大大小小再“减多少”一下,4M中!当及4M里之后,公司建议说,能否再度杀压?2M哪些?
瘦身前因为平时就考虑到大大小小的限制,所以众多做事一度做了了,如下列举现在之状态:
7.3M(Debug版本)和6.5M(Release版本…

APK的构成


可以用Zip工具打开APK查看。比如,美团App 7.8.6的线达本的格式是这般的:

必威 1

得视APK由以下重点有构成:

水果 价格
lib/ 存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/ 存放编译后的资源文件,例如:drawable、layout等等
assets/ 应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/ 该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息
classes(n).dex classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc 编译后的二进制资源文件
AndroidManifest.xml Android的清单文件,格式为AXML,用于描述应用程序的名称、版本、所需权限、注册的四大组件

理所当然还见面起部分另的文书,例如上图备受的org/,src/,push_version等公事要文件夹。这些资源是Java
Resources,感兴趣之得组合编译工作流中的流程图以及MergeJavaResourcesTransform的源码看被打入APK包中之资源且有哪些,这里不举行了多介绍。

在充分了解了APK各个组成部分和它的打算后,我们对自身特点进行了解析与优化。下面用于Zip文件格式、classes.dex、资源文件、resources.arsc等地方来介绍下我们发现的一些优化技术。

初一代表 Android 渠道打包工具:1000 只沟保险就需要 5 秒 – Android – 掘金

源码:https://github.com/mcxiaoke/packer-ng-plugin
最新版本 v1.0.4 – 2016.01.19 – 完善得APK路径的方式,增加MarketInfo
v1.0.3 – 2016.01.14 – …

Zip格式优化


前方介绍了APK的文件格式以及要有,通过aapt l -v xxx.apk或unzip -l
xxx.apk来查APK文件时见面收获以下信息,见下面截图:

必威 2

由此上图可以看APK中有的是资源是为Stored来储存的,根据Zip的文件格式中针对减少方式的描述Compression_methods#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在资源处理常对这些文件后缀类型的资源是不举行缩减的,那是匪是可改其的滑坡方式因此达成瘦身之功能啊?

每当介绍怎么开事先,先来大概介绍一下App的资源是怎么为打上APK包里的。Android构建工具链使用AAPT工具来针对资源开展拍卖,来拘禁下图(图片来自Build
Workflow):

必威 3

通过上图可以看到Manifest、Resources、Assets的资源通过AAPT处理后生成R.java、Proguard
Configuration、Compiled
Resources。其中R.java大家都比较熟悉,这里虽可大多介绍了。我们来要看Proguard
Configuration、Compiled Resources都是召开呀的也罢?

  • Proguard
    Configuration是AAPT工具也Manifest中宣示的季大组件和布局文件中(XML
    layouts)使用的各种Views所生成的ProGuard配置,该公文一般存放于${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/proguard-rules/${flavorName}/${buildType}/aapt_rules.txt,下面是项目遭到该文件的截图,红框标记出来的就是对AndroidManifest.xml、XML
    Layouts中并行关Class的ProGuard配置。
![](https://upload-images.jianshu.io/upload_images/1815594-0be9ccd713fa7acd.png)
  • Compiled Resources
    凡是一个Zip格式的公文,这个文件的路通常也${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/res/resources-${flavorName}-${buildType}-stripped.ap_。
    通过下通过Zip解压后底截图,可以看来这文件包含了res,AndroidManifest.xml和resources.arsc的公文要文件夹。结合Build
    Workflow丁之讲述,可以望此文件(resources-${flavorName}-${buildType}-stripped.ap_
    )会给apkbuilder打包到APK包中,它实际上就算是APK的“资源包”(res,AndroidManifest.xml和resources.arsc)。

    必威 4

我们就是通过这个文件来修改不同后缀文件资源的压缩方式来达到瘦身效果的,而在后面“resources.arsc的优化”一节中也是操作的这个文件。

作者在和谐之类别被凡经以package${flavorName}
Task(感兴趣的同桌可以查源码)之前开展这个操作的。

下面是有些代码有:

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

当也堪在外构建步骤中应用重新强压缩率的艺术来上瘦身效果,例如采用7Zip压缩等等。

以技巧的应用需要留意以下问题:

  • 如若音视频资源被压缩存放在APK中的话,在采用一些旋律、视频API时更要留心,需要盘活充分的测试。
  • resources.arsc文件最好永不抽存储,如果缩减会影响肯定之属性(尤其是镇启动日)。
  • 万一想以Android 6.0
    上开启android:extractNativeLibs=”false”
    的言语,.so 文件为无可知吃裁减,android:extractNativeLibs
    的行使姿势看这里:App Manifest —
    application。

App 瘦身最佳实践 – Android – 掘金

正文会不期更新,推荐watch下档。如果喜欢求star,如果觉得出纰漏请提交issue,如果你生出更好之节骨眼可以交pull
request。本文的示范代码主要是基于作者的更来修的,若您发另外的技术及方法可以参与进去一起到这首稿子。
本文固定连接:ht…

classes.dex的优化


怎样优化classes.dex的尺寸也?大体有如下套路:

  • 无时无刻保持良好的编程习惯以及对包体积敏锐的嗅觉,去除重复或不用的代码,慎用第三方库,选用体积小之老三方SDK等等。
  • 敞开ProGuard来进展代码压缩,通过使用ProGuard来对代码进行模糊、优化、压缩等工作。

本着第一栽套路,因各个公司之类型之出入,共性的东西比少,需要case by
case的辨析,这里不开过多之牵线。

减少代码

足由此被ProGuard来实现代码压缩,可以以build.gradle文件相应的构建类型中添加minifyEnabled
true。

  • 央小心,代码压缩会拖慢构建速度,因此该尽可能避免以调试构建中使。不过早晚要也用于测试的最后APK启用代码压缩,如果非克尽地由定义要保存的代码,可能会见引入错误。

如,下面这段出自build.gradle文件的代码用于为公布构建启用代码压缩:

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

除minifyEnabled属性外,还有用于定义ProGuard规则之proguardFiles属性:

  • getDefaultProguardFile(‘proguard-android.txt’)是于Android
    SDKtools/proguard/文件夹到手默认ProGuard设置。
  • proguard-rules.pro文件用于添加自定义ProGuard规则。默认情况下,该文件在模块根目录(build.gradle文件旁)。

提示:要惦记做进一步的代码压缩,可尝下在同位置的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移除的代码

好透过在usage.txt文件被视怎样代码被删除了,如下图被所示android.support.multidex.MultiDex已经让去除了:

必威 5

R Field的优化

除了对品种代码优化和展代码压缩之外,笔者在《美团Android
DEX自动拆包及动态加载简介》当时首文章被关系了通过外联R
Field来化解R Field过多招MultiDex
65536底题材,而立即同手续对代码瘦身能够打至明确的功用。下面是笔者通过字节码工具在构建流程中内联R
Field的代码有(字节码的改动得采取Javassist或者ASM,该手续笔者利用的凡Javassist)。

ctBehaviors.each { CtBehavior ctBehavior ->
    if (!ctBehavior.isEmpty()) {
        try {
            ctBehavior.instrument(new ExprEditor() {
                @Override
                public void edit(FieldAccess f) {
                    try {
                        def fieldClassName = JavassistUtils.getClassNameFromCtClass(f.getCtClass())
                        if (shouldInlineRField(className, fieldClassName) && f.isReader()) {
                            def temp = fieldClassName.substring(fieldClassName.indexOf(ANDROID_RESOURCE_R_FLAG) + ANDROID_RESOURCE_R_FLAG.length())
                            def fieldName = f.fieldName
                            def key = "${temp}.${fieldName}"

                            if (resourceSymbols.containsKey(key)) {
                                Object obj = resourceSymbols.get(key)
                                try {
                                    if (obj instanceof Integer) {
                                        int value = ((Integer) obj).intValue()
                                        f.replace("\$_=${value};")
                                    } else if (obj instanceof Integer[]) {
                                        def obj2 = ((Integer[]) obj)
                                        StringBuilder stringBuilder = new StringBuilder()
                                        for (int index = 0; index < obj2.length; ++index) {
                                            stringBuilder.append(obj2[index].intValue())
                                            if (index != obj2.length - 1) {
                                                stringBuilder.append(",")
                                            }
                                        }
                                        f.replace("\$_ = new int[]{${stringBuilder.toString()}};")
                                    } else {
                                        throw new GradleException("Unknown ResourceSymbols Type!")
                                    }
                                } catch (NotFoundException e) {
                                    throw new GradleException(e.message)
                                } catch (CannotCompileException e) {
                                    throw new GradleException(e.message)
                                }
                            } else {
                                throw new GradleException("******** InlineRFieldTask unprocessed ${className}, ${fieldClassName}, ${f.fieldName}, ${key}")
                            }
                        }
                    } catch (NotFoundException e) {
                    }
                }
            })
        } catch (CannotCompileException e) {
        }
    }
}

另外优化手段

针对代码的瘦身还有不少优化的技术,例如:

  • 缩减ENUM的应用(详情可参见:Remove
    Enumerations),每抽一个ENUM可以减掉约1.0届1.4
    KB的高低;
  • 通过pmd
    cpd来检查再的代码用进行代码优化;
  • 移除掉所有无用或者功能更的依赖库。

这些优化技术就未开展介绍了。

资源的优化

图片优化

为支持Android设备DPI的多样化([l|m|tv|h|x|xx|xxx]dpi)以及用户对愈质量UI的希,美团App中使用了汪洋底图形,在Android下支持多格式的图片,例如:PNG、JPG
、WebP,那我们欠怎么取舍不同类型的图片格式呢?
在Google I/O 2016遭到提到了对图片格式的选项,来拘禁下图(图片来自Image
compression for Android
developers):

必威 6

通过上图可以看到一个横图片格式选择的不二法门。如果能因此VectorDrawable来代表的言语优先使用VectorDrawable,如果支持WebP则先行用WebP,而PNG主要用当显示透明或略的图纸,而另场景可以采取JPG格式。针对每种图片格式也生各的优化手段与优化工具。

使矢量图片

足采用矢量图形来创造独立为分辨率的图标与其余可伸缩图片。使用矢量图片能够使得的压缩App中图纸所占用的高低,矢量图形在Android中意味着也VectorDrawable对象。
使用VectorDrawable目标,100字节的文书可以转变屏幕尺寸的清晰图像,但系统渲染每个VectorDrawable对象急需大量的时,较生的图像需要更丰富之时空才能够出现在屏幕及。
因此只有以亮小图像时才考虑使用矢量图形。有关以VectorDrawable的双重多信息,请参阅
Working with
Drawables。

使用WebP

如果App的minSdkVersion
过量14(Android
4.0+)的话,可以选用WebP格式,因为WebP在跟画质下体积还有些(WebP支持透明度,压缩比比JPEG更高但显示力量倒是不输于JPEG,官方评测quality参数等于75动态平衡最佳),
可以经PNG到WebP转换工具来开展换。当然Android从4.0才开WebP的原生支持,但是不支持包含透明度,直到Android
4.2.1+才支撑显得含透明度的WebP,在作者使用受到凡判时App的minSdkVersion以及图片文件的品种(是否也透明)来选用是否适用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文件大小,同时保持图像质量。

pngcrush工具特别实用:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来减少图像。然后选取来无限小压缩输出的布局。

对JPEG文件,你可利用packJPG或guetzli当工具将JPEG文件减少的复有些,这些家伙能够当维系图片质量不换的景况下,把图片文件减少的双重有些。guetzli工具越来越会以图纸质量未换的图景下,将文件大小降低35%。

当Android构建流程中AAPT会下内置的压缩算法来优化res/drawable/
目录下之PNG图片,但为恐怕会见招本就优化了的图纸体积变死,可以通过在build.gradle中安cruncherEnabled来禁止AAPT来优化PNG图片。

aaptOptions {
    cruncherEnabled = false
}

拉开资源减少

Android的编译工具链中提供了同样舒缓资源减少的家伙,可以通过该工具来减资源,如果只要启用资源减少,可以以build.gradle文件中将shrinkResources
true。例如:

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

亟待小心的凡眼下资源压缩器目前未见面更换除了values/文件夹着定义的资源(例如字符串、尺寸、样式与颜料),有关详情,请参考问题
70869。

Android构建工具是由此ResourceUsageAnalyzer来检查哪些资源是无济于事的,当检查到不行的资源时会拿该资源替换成预定义的本子。详看下代码有(摘自com.android.build.gradle.tasks.ResourceUsageAnalyzer
):

public class ResourceUsageAnalyzer {
    ...

    /**
     * Whether we should create small/empty dummy files instead of actually
     * removing file resources. This is to work around crashes on some devices
     * where the device is traversing resources. See http://b.android.com/79325 for more.
     */
    public static final boolean REPLACE_DELETED_WITH_EMPTY = true;

      // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
    public static final byte[] TINY_PNG = new byte[] {
            (byte)-119, (byte)  80, (byte)  78, (byte)  71, (byte)  13, (byte)  10,
            (byte)  26, (byte)  10, (byte)   0, (byte)   0, (byte)   0, (byte)  13,
            (byte)  73, (byte)  72, (byte)  68, (byte)  82, (byte)   0, (byte)   0,
            (byte)   0, (byte)   1, (byte)   0, (byte)   0, (byte)   0, (byte)   1,
            (byte)   8, (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)  58,
            (byte) 126, (byte)-101, (byte)  85, (byte)   0, (byte)   0, (byte)   0,
            (byte)  10, (byte)  73, (byte)  68, (byte)  65, (byte)  84, (byte) 120,
            (byte) -38, (byte)  99, (byte)  96, (byte)   0, (byte)   0, (byte)   0,
            (byte)   2, (byte)   0, (byte)   1, (byte) -27, (byte)  39, (byte) -34,
            (byte)  -4, (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)  73,
            (byte)  69, (byte)  78, (byte)  68, (byte) -82, (byte)  66, (byte)  96,
            (byte)-126
    };

    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 byte[] TINY_9PNG = new byte[] {
            (byte)-119, (byte)  80, (byte)  78, (byte)  71, (byte)  13, (byte)  10,
            (byte)  26, (byte)  10, (byte)   0, (byte)   0, (byte)   0, (byte)  13,
            (byte)  73, (byte)  72, (byte)  68, (byte)  82, (byte)   0, (byte)   0,
            (byte)   0, (byte)   3, (byte)   0, (byte)   0, (byte)   0, (byte)   3,
            (byte)   8, (byte)   6, (byte)   0, (byte)   0, (byte)   0, (byte)  86,
            (byte)  40, (byte) -75, (byte) -65, (byte)   0, (byte)   0, (byte)   0,
            (byte)  20, (byte)  73, (byte)  68, (byte)  65, (byte)  84, (byte) 120,
            (byte) -38, (byte)  99, (byte)  96, (byte)-128, (byte)-128, (byte)  -1,
            (byte)  12, (byte)  48, (byte)   6, (byte)   8, (byte) -96, (byte)   8,
            (byte)-128, (byte)   8, (byte)   0, (byte)-107, (byte)-111, (byte)   7,
            (byte)  -7, (byte) -64, (byte) -82, (byte)   8, (byte)   0, (byte)   0,
            (byte)   0, (byte)   0, (byte)   0, (byte)  73, (byte)  69, (byte)  78,
            (byte)  68, (byte) -82, (byte)  66, (byte)  96, (byte)-126
    };

    public static final long TINY_9PNG_CRC = 0x1148f987L;

    // The XML document <x/> as binary-packed with AAPT
    public static final byte[] TINY_XML = new byte[] {
            (byte)   3, (byte)   0, (byte)   8, (byte)   0, (byte) 104, (byte)   0,
            (byte)   0, (byte)   0, (byte)   1, (byte)   0, (byte)  28, (byte)   0,
            (byte)  36, (byte)   0, (byte)   0, (byte)   0, (byte)   1, (byte)   0,
            (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0,
            (byte)   0, (byte)   1, (byte)   0, (byte)   0, (byte)  32, (byte)   0,
            (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0,
            (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   1, (byte)   1,
            (byte) 120, (byte)   0, (byte)   2, (byte)   1, (byte)  16, (byte)   0,
            (byte)  36, (byte)   0, (byte)   0, (byte)   0, (byte)   1, (byte)   0,
            (byte)   0, (byte)   0, (byte)  -1, (byte)  -1, (byte)  -1, (byte)  -1,
            (byte)  -1, (byte)  -1, (byte)  -1, (byte)  -1, (byte)   0, (byte)   0,
            (byte)   0, (byte)   0, (byte)  20, (byte)   0, (byte)  20, (byte)   0,
            (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0, (byte)   0,
            (byte)   0, (byte)   0, (byte)   3, (byte)   1, (byte)  16, (byte)   0,
            (byte)  24, (byte)   0, (byte)   0, (byte)   0, (byte)   1, (byte)   0,
            (byte)   0, (byte)   0, (byte)  -1, (byte)  -1, (byte)  -1, (byte)  -1,
            (byte)  -1, (byte)  -1, (byte)  -1, (byte)  -1, (byte)   0, (byte)   0,
            (byte)   0, (byte)   0
    };

    public static final long TINY_XML_CRC = 0xd7e65643L;

    ...
}

点截图中3独byte数组的概念就是是资源压缩工具为无用资源提供的预定义版本,可以见到对.png提供了TINY_PNG,
对.9.png提供了TINY_9PNG以及对.xml提供了TINY_XML的预定义版本。

资源压缩工具的详尽使用得参见Shrink Your Code and
Resources。资源压缩工具默认是采用安全压缩模式来运转,可以经过打开严格压缩模式来达到更好的瘦身效果。

若是想明白什么资源是行不通的,可以由此资源压缩工具的出口日志文件${project.buildDir}/outputs/mapping/release/resources.txt来查看。如下图所示res/layout/abc_activity_chooser_viewer.xml就是不行的,然后于预定义的本TINY_XML
所替换:

必威 7

资源压缩工具只是把管用资源替换成预定义较小的本,那咱们什么去这些不算资源为?通常的做法是成资源压缩工具的输出日志,找到这些资源并把它进行去。但每当笔者的品种中广大空头资源是为其他组件或第三着SDK所引入的,如果以这种优化措施会带这些SDK后期维护资产的增多,针对这种情形笔者是透过以以resources.arsc中做优化来缓解的,详情看下“resources.arsc的优化”一节的介绍。

言语资源优化

基于App自身支持的言语版选用合适的语言资源,例如利用了AppCompat,如果不举行其他配置来说,最终APK包被见面蕴藏AppCompat中信息之所有曾经翻语言字符串,无论使用之其余部分是否翻译为同一语言,可以透过resConfig来安排利用什么语言,从而被构建工具移除指定语言之外的持有资源。下图是有血有肉的配备示范:

android {
    ...
    defaultConfig {
        ...
        resConfigs "zh", "zh-rCN"
    }
    ...
}

对为不同DPI所提供的图样也堪运用同样的政策,需要对我之对象用户和对象设备做得的选项,可以参照Support
Only Specific
Densities来操作。有关屏幕密度的详细信息,请参考Screen
Sizes and
Densities。对.so
文本也可以动用类似之政策,比如笔者之品种面临特保留了armeabi
版本的.so
文件。

resources.arsc的优化


对resources.arsc,笔者尝试了之优化手段如下:

  • 启资源混淆;
  • 对还的资源进行优化;
  • 本着深受shrinkResources优化掉的资源拓展拍卖。

下面用分头对这些优化手段开展拓展介绍。

资源混淆

当笔者另一样首《美团Android资源混淆保护实施》章被介绍了采用对资源混淆的办法来保障资源的安康,同时为涉及了这种办法发出举世瞩目的瘦身成效。笔者就凡采取修改AAPT的有关源码的艺术,这种艺术的痛点是历次升级Build
Tools都设修改一潮AAPT源码,维护性较差。目前笔者使用了微信开源之资源混淆库AndResGuard,具体的法则同采取帮助可以参见装包立减1M–微信Android资源混淆打包工具。

不管用资源优化

于齐亦然省吃介绍了足由此shrinkResources
true来开启资源减少,资源压缩工具会把无用的资源替换成预定义的版要非是移除,如果采用人造移除的不二法门会带来后期的保安资金,这里笔者使用了同一种植比较取巧的法门,在Android构建工具实施package${flavorName}Task之前经过修改Compiled
Resources来实现活动删除无用资源。

具体流程如下:

  • 采集资源包(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;
  • 通过android-chunk-utils将resources.arsc中对应之概念移除;
  • 剔除资源包中对应的资源文件。

重新资源优化

即美团App是由于逐一业务集团共同开发完成,为了便利各业务集团的独立开发,美团App进行了平台化改造。改造时有许多资源文件(如:drawable、layout等)被不同的事情集团还拷贝到自己之Library下,同时为了避免引发资源埋的题材,每个业务团队还见面否自己的资源文件名添加前缀。这样虽导致了这些资源文件则情相同,但因名称的例外而无能够被蒙,最终都见面于购并到APK包中,针对这种问题笔者利用了和前“无用资源优化”一节约被讲述类似的方针。

具体步骤如下:

  • 经资源包中的每个ZipEntry的CRC-32 checksum来罗出还的资源;
  • 通过android-chunk-utils修改resources.arsc,把这些又的资源还重定向到跟一个文书及;
  • 把其余重复的资源文件由资源包中删除。

代码有:

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

由此这种方法可中削减重复资源对包体大小的熏陶,同时这种操作方式对各国业务团队透明,也无会见增加和谐一致资源如何让不同工作集团复用的财力。

总结

上述就是咱脚下在APK瘦身方面的召开的有些尝和积累,可以依据自身状况选择使用。当然我们尚可使部分据需要加载的国策来减设置包之体积。最后领取一点,砍掉不必要之功用才是安包瘦身之超级大招。一个吓的App的正式有成千上万点,但提供尽可能小的装包是里面一个重要的方,这吗是本着咱Android开发者人员自我之提出的骨干要求,要时刻保持良好的编程习惯和对包体积敏锐的嗅觉。

相关文章

Leave a Comment.