App瘦身最佳实践

本文会不按期更新,推荐watch下项目。
若是喜欢请star,若是以为有纰漏请提交issue,若是你有更好的点子能够提交pull request。
本文的示例代码主要是基于做者的经验来编写的,若你有其余的技巧和方法能够参与进来一块儿完善这篇文章。javascript

业务方和开发都但愿app尽可能的小,本文会给出多个实用性的技巧来帮助开发者进行app的瘦身工做。瘦身和减负虽好,但须要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。html

本文固定链接:github.com/tianzhijiex…java


1、背景

目前app的大小愈来愈大,用户对于过大的app接受度不高,因此除了插件化和RN的方案外,咱们只能老老实实的进行app的瘦身工做。react

2、需求

  1. 我要利用混淆来让个人代码尽量少
  2. 最好能用最少的切图完成功能
  3. layout文件不要太多,太多了乱
  4. 能动态下载的就作动态
  5. 我但愿能用大小最小的图片
  6. 若是能用svg,我就用svg
  7. 对于无用的资源,我要as能自动删除掉
  8. 中国文字博大精深,而我只要我须要的字的字体
  9. 最好能根据下载用户手机的cpu和分辨率来引入不一样的资源

3、实现

分析app组成结构

作瘦身以前必定要了解本身app的组成结构,要有针对性的进行优化,而且要逐步记录比对,这样才能更好的完成此项工做。关于apk的大小,我推荐google的这个视频。目前as的2.2预览版中已经有了apk分析器,功能至关强大,此外你还能够利用nimbledroid来分析apk。android

nimbledroid是一个强大的工具,推荐一试

咱们都知道apk是由: ios

  • asserts
  • lib
  • res
  • dex
  • META-INF
  • androidManifest

这几个部分构成的。git

下面我会利用as的分析工具,以微信、微博、淘宝为例进行讲述。github

概览

分析完成后你还能够看到具体类目占的百分比,清晰明了。旁边的“对比”按钮提供了diff的功能,让你能够方便的进行apk优化先后的对比,简直利器。web

diff

assets

assets目录能够存放一些配置文件或资源文件,好比webview的本地html,react native的jsbundle等,微信的整个assets占用了13.4M。若是你的应用对本地资源要求不多的话,这个文件应该不会太大。算法

lib

lib目录下会有各类so文件,分析器会检查出项目本身的so和各类库的so。微博和微信同样只支持了arm一个平台,淘宝支持了arm和x86两个平台。

resources.arsc

这个文件是编译后的二进制资源文件,里面是id-name-value的一个map。由于微信作了资源的混淆,因此这里能够看到资源名称都是不可读的。

索性放个微博的图,易于你们理解:

META-INF

META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全性,帮助用户避免安装来历不明的盗版apk。

res

res目录存放的是资源文件,包括图片、字符串。raw文件夹下面是音频文件,各类xml文件等等。由于微信作了资源混淆,图片名字都不可读了。

res

微博就没有作资源混淆,因此可读性较好:

dex

dex文件是java代码打包后的字节码,一个dex文件最多只支持65535个方法,这也是为何微信有了三个dex文件的缘由。

由于dex分包是不均匀的,你能够理解为装箱,一个箱子的大小是固定的,但你代码的量是不肯定的,微信把前两个箱子装满了,最后还剩了2m多的代码,这些代码也占用了一个箱子,最终产生了上图不均匀的状况。

如今,咱们已经知道了apk中各个文件的大小和它们占的比例,下面就能够开始针对性的进行优化了。

优化assets

assets中会存放资源文件,这个目录中不一样厂的app存放的内容各有不一样,因此优化也比较难。自从引入RN以来,这个目录下还会有jsbundle的信息(可参考全民k歌)。若是你有地址选择的功能,这里还会存放地址的映射文件。
对于这块的资源,as是不会进行主动的删减的,因此一切都是须要靠开发者进行手动管理的。

全民k歌中的bundle文件

删除无用字体

中文字体是至关大的,我一直不建议将字体文件随意丢弃到assets中。有时候一个小功能急着上,开发者为了追求速度,能够先放在这里图省事。但必定要知道这个隐患,而且必定要多和产品核对功能的必要性。对于有些只会用在logo中的字体,我推荐将字体文件进行删减处理。

FontZip是一个字体提取工具,readme中写到:

通过测试,已经把项目5MB的艺术字体,按需求提取后,占用只有20KB,而且可正常使用。

gif2 (1).gif-204.5kB

减小icon-font的使用

icon-font和svg都能完成一些icon的展现,但由于icon-font在assets中难以管理,而且功能和svg有所重叠,因此我建议减小icon-font的使用,利用svg进行代替,毕竟一个很小的icon-font也比svg大。这里给出一个提供各类格式icon的网站,方便你们进行测试:icomoon.io/app/

  • svg:549字节
  • png:375字节(单一分辨率的一张图)
  • ion-font:1.1kb

动态下载资源

字体、js代码这样的资源能动态下载的就作动态下载,虽然这样会增长出错的可能性,复杂度也会提高,但对于app的瘦身和用户来讲是有长远的好处的。
若是你用了rn,你能够在app运行时动态去拉取最新的代码,将图片和js代码一并下载后解压使用。也能够把rn模块化,主线的rn代码随着app发布,入口较深的次要界面能够在app启动后经过断点下载。

压缩资源文件

有些资源文件是必需要随着app一并发布的。对于这样的文件,能够采用压缩存储的方式,在须要资源的时候将其解压使用,下面就是解压zip文件的代码示例:

public static void unzipFile(File zipFile, String destination) throws IOException {
        FileInputStream fileStream = null;
        BufferedInputStream bufferedStream = null;
        ZipInputStream zipStream = null;
        try {
            fileStream = new FileInputStream(zipFile);
            bufferedStream = new BufferedInputStream(fileStream);
            zipStream = new ZipInputStream(bufferedStream);
            ZipEntry entry;

            File destinationFolder = new File(destination);
            if (destinationFolder.exists()) {
                deleteDirectory(destinationFolder);
            }

            destinationFolder.mkdirs();

            byte[] buffer = new byte[WRITE_BUFFER_SIZE];
            while ((entry = zipStream.getNextEntry()) != null) {
                String fileName = entry.getName();
                File file = new File(destinationFolder, fileName);
                if (entry.isDirectory()) {
                    file.mkdirs();
                } else {
                    File parent = file.getParentFile();
                    if (!parent.exists()) {
                        parent.mkdirs();
                    }

                    FileOutputStream fout = new FileOutputStream(file);
                    try {
                        int numBytesRead;
                        while ((numBytesRead = zipStream.read(buffer)) != -1) {
                            fout.write(buffer, 0, numBytesRead);
                        }
                    } finally {
                        fout.close();
                    }
                }
                long time = entry.getTime();
                if (time > 0) {
                    file.setLastModified(time);
                }
            }
        } finally {
            // ...
        }
    }复制代码

全民k歌中的assets目录下我就发现了大量的zip文件:

android上也有一个7z库帮助咱们方便的使用7z,这个库我目前没用到,有需求的同窗能够尝试一下。

优化lib

配置abiFilters

一个硬件设备对应一个架构(mips、arm或者x86),只保留与设备架构相关的库文件夹(主流的架构都是arm的,mips属于小众)能够大大下降lib文件夹的大小。配置方式也十分简单,直接配置abiFilters便可:

defaultConfig {
    versionCode 1
    versionName '1.0.0'

    renderscriptTargetApi 23
    renderscriptSupportModeEnabled true

    // http://stackoverflow.com/questions/30794584/exclude-jnilibs-folder-from-production-apk
    ndk {
        abiFilters "armeabi", "armeabi-v7a" ,"x86"
    }
}复制代码

armeabi就不用说了,这个是必须包含的,v7是一个图形增强版本(若是用到模糊算法,则不要删除),x86是英特尔平台的支持库。

官方例子

按 ABI 拆分

android {
  ...
  splits {
    abi {
      enable true
      reset()
      include 'x86', 'armeabi-v7a', 'mips'
      universalApk true
    }
  }
}复制代码
  • enable: 启用ABI拆分机制
  • exclude: 默认状况下全部ABI都包括在内,容许移除一些ABI
  • include:指明要包含哪些ABI
  • reset():重置ABI列表为只包含一个空字符串(这也是容许的,在与include一块儿使用来能够表示要使用哪个ABI)
  • universalApk:指示是否打包一个通用版本(包含全部的ABI)。默认值为 false。

根据手机的cpu来引入so

咱们在舍弃so以前必定要进行用户cpu型号的统计,这样你才能放心大胆地进行操做。
我先是花了几个版本的时间统计了用户的cpu型号,而后排除了没有或少许用户才会用到的so,以达到瘦身的目的。

@NonNull
public static String getCpuName() {
    String name = getCpuName1();
    if (TextUtils.isEmpty(name)) {
        name = getCpuName2();
        if (TextUtils.isEmpty(name)) {
            name = "unknown";
        }
    }
    return name;
}

private static String getCpuName1() {
    String[] abiArr;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        abiArr = Build.SUPPORTED_ABIS;
    } else {
        abiArr = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
    }

    StringBuilder abiStr = new StringBuilder();
    for (String abi : abiArr) {
        abiStr.append(abi);
        abiStr.append(',');
    }
    return abiStr.toString();
}

private static String getCpuName2() {
    try {
        FileReader e = new FileReader("/proc/cpuinfo");
        BufferedReader br = new BufferedReader(e);
        String text = br.readLine();
        String[] array = text.split(":\\s+", 2);
        e.close();
        br.close();
        return array[1];
    } catch (IOException var4) {
        var4.printStackTrace();
        return null;
    }
}复制代码

注意:

  1. 若是你和我同样用到了renderscript,那么你必须包含v7,不然会出现模糊异常的问题。
  2. 若是你用了RN,那么对于x86须要谨慎的保留,不然可能会出现用户找不到so而崩溃的状况。毕竟rn是一个全局的东西,稍有不慎就可能会出现开机崩的状况。
  3. so这个东西仍是比较危险的,咱们虽然能够经过统计cpu型号来下降风险,但我仍是推荐发布app前走一遍大量机型的云测,经过云测平台把风险进一步下降。
  4. 小厂的项目可能会舍弃一些so,但随着公司规模的增大,你将来仍旧要重复考虑这个问题。因此我推荐在崩溃系统中上传用户cpu型号的信息,这样咱们就能够在第一时间知道因找不到so引发的崩溃量,至因而否须要增长so就看问题的严重程度了。

避免复制so

so有个常年大坑:在Android 6.0以前,so文件会压缩到apk中,系统在安装应用的时候,会把so文件解压到data分区。这样同一个so文件会有两份存在,一个在apk里,一个在data中。这也致使多占用了一倍的空间,并且会出现各类诡异的错误。这个策略虽然和apk的瘦身无关,但它和app安装在用户手机中的大小有关,所以咱们也是须要多多留意的。

Starting from Android Studio 2.2 Preview 2 and newest build tools, the build process will automatically store native libraries uncompressed and page aligned in the APK

在6.0+中,能够经过以下的方式进行申明:

<application
   android:extractNativeLibs=”false”
   ...
>复制代码

若是想了解更多信息或者想知道这种配置的限制,能够浏览下SmallerAPK(8)

优化resources.arsc

resources.arsc中存放了一个对应关系:

id name default v11
0x7f090002 PopupAnimation @ref/0x7f040042, @ref/0x7f040041

咱们在程序运行的时候确定要常常用到id,所以它在安装以后仍须要被频繁的读取。若是将这个文件进行了压缩,在每次读取前系统都必须进行解压的工做。这就会有一些性能和内存的开销,综合考虑下来,压缩这个文件是得不偿失的。

删除无用的资源映射

resources.arsc的正确瘦身方式是删除没必要要的string entry,你能够借助 android-arscblamer 来检查出能够优化的部分,好比一些空的引用。

ArscBlamer

进行资源名称混淆

微信团队开源了一个资源混淆工具,AndResGuard。它将资源的名称进行了混淆,因此能够用它对resources.arsc进行优化,只是具体优化效果与编码方式、id数量、平均减小命名长度有关。

表1:

id name default v11
0x7f090001 Android @ref/0x7f040042, @ref/0x7f040041
0x7f090002 ios @ref/0x7f040042, @ref/0x7f040041
0x7f090003 Windows Phone @ref/0x7f040042, @ref/0x7f040041

表2:

id name default v11
0x7f090001 a @ref/0x7f040042, @ref/0x7f040041
0x7f090002 b @ref/0x7f040042, @ref/0x7f040041
0x7f090003 c @ref/0x7f040042, @ref/0x7f040041

咱们一眼就能够知道表2确定比表1存储的字符要小,因此整个文件的大小确定也要小一些。

详细信息请参考:smallerapk-part-3-removing-unused-resources

关于AndResGuard

这个压缩工具其实就是一个task,使用也十分简单,具体的用法请参考中文文档

原理介绍:安装包立减1M--微信Android资源混淆打包工具

andResGuard {
    mappingFile = null
    use7zip = true
    useSign = true
    keepRoot = false
    whiteList = [
        //for your icon
        "R.drawable.icon",
        //for fabric
        "R.string.com.crashlytics.*",
        //for umeng update
        "R.string.umeng*",
        "R.string.UM*",
        "R.layout.umeng*",
        "R.drawable.umeng*",
        //umeng share for sina
        "R.drawable.sina*"
    ]
    compressFilePattern = [
        "*.png",
        "*.jpg",
        "*.jpeg",
        "*.gif",
        "resources.arsc"
    ]
     sevenzip {
         artifact = 'com.tencent.mm:SevenZip:1.1.9'
         //path = "/usr/local/bin/7za"
    }
}复制代码

使用这个工具的时候须要注意一些东西:像友盟这种喜欢用反射获取资源的SDK就是一个坑(友盟的SDK就是坑王!)对于app启动图标这样的icon能够不作混淆,推荐将其放入白名单中。

优化META-INF

META-INF文件夹中有三个文件,分别是MANIFEST.MF、CERT.SF、CERT.RSA。下面我将会列出简要的分析,若是你但愿更详尽的了解原理,能够查看《Android APK 签名文件MANIFEST.MF、CERT.SF、CERT.RSA分析》

MANIFEST.MF

每个资源文件(res开头)下面都有一个SHA1-Digest的值。这个值为该文件SHA-1值进行base64编码后的结果。
若是要探究原理,能够看下SignApk.java。这个类中的main方法:

public static void main(String[] args) {
    //...

    // MANIFEST.MF
    Manifest manifest = addDigestsToManifest(inputJar);
    je = new JarEntry(JarFile.MANIFEST_NAME);
    je.setTime(timestamp);
    outputJar.putNextEntry(je);
    manifest.write(outputJar);

    //...
}复制代码
private static void writeSignatureFile(Manifest manifest, OutputStream out)
        throws IOException, GeneralSecurityException {
    Manifest sf = new Manifest();
    Attributes main = sf.getMainAttributes();
    main.putValue("Signature-Version", "1.0");
    main.putValue("Created-By", "1.0 (Android SignApk)");
    BASE64Encoder base64 = new BASE64Encoder();
    MessageDigest md = MessageDigest.getInstance("SHA1");
    PrintStream print = new PrintStream(
            new DigestOutputStream(new ByteArrayOutputStream(), md),
            true, "UTF-8");
    // Digest of the entire manifest
    manifest.write(print);
    print.flush();
    main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
    Map<String, Attributes> entries = manifest.getEntries();
    for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
        // Digest of the manifest stanza for this entry.
        print.print("Name: " + entry.getKey() + "\r\n");
        for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
            print.print(att.getKey() + ": " + att.getValue() + "\r\n");
        }
        print.print("\r\n");
        print.flush();
        Attributes sfAttr = new Attributes();
        sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
        sf.getEntries().put(entry.getKey(), sfAttr);
    }
    sf.write(out);
}复制代码

上述代码说明了SHA1-Digest-Manifest是MANIFEST.MF文件的SHA1并base64编码的结果。

CERT.SF

这里有一项SHA1-Digest-Manifest的值,这个值就是MANIFEST.MF文件的SHA-1并base64编码后的值。后面几项的值是对MANIFEST.MF文件中的每项再次SHA1并base64编码后的值。因此你会看到在manifest.mf中的资源名称在这里也出现了,好比abc_btn_check_material这个系统资源文件就出现了两次。

MANIFEST.MF:

CERT.SF

  • 前者:4XHnecusACTIgtImUjC7bQ9HNM8=
  • 后者:YFDDnTUd6St4932sE/Xk6H0HMoc=

若是你把前一个文件打开在后面加上\n\r,而后进行编码,你就会获得CERT.SF中的值。

Map<String, Attributes> entries = manifest.getEntries();
 for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
     // Digest of the manifest stanza for this entry.
     print.print("Name: " + entry.getKey() + "\r\n");
     for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
         print.print(att.getKey() + ": " + att.getValue() + "\r\n");
     }
     print.print("\r\n");
     print.flush();

     Attributes sfAttr = new Attributes();
     sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
     sf.getEntries().put(entry.getKey(), sfAttr);
 }

 sf.write(out);复制代码

CERT.RSA

CERT.RSA包含了公钥、所采用的加密算法等信息。它对前一步生成的MANIFEST.MF使用了SHA1-RSA算法,用开发者的私钥进行签名,在安装时使用公钥解密它。解密以后,将它与未加密的摘要信息(即,MANIFEST.MF文件)进行对比,若是相符,则代表内容没有被修改。

这点和app瘦身就彻底无关了,这块我平时也没有仔细研究过,就不误人子弟了。具体的签名过程能够参考:blog.csdn.net/asmcvc/arti…

优化建议

经过分析得出,除了CERT.RSA没有压缩机会外,其他的两个文件均可以经过混淆资源名称的方式进行压缩。

优化res

资源文件的优化一直是咱们的重头戏。若是要和它进行对比,上文的META-INF文件的优化简直能够忽略不计。res的优化分为两块:一个是文本资源(shape、layout等)优化和图片资源优化。本节仅探讨除图片资源优化外的内容,关于图片的内容下面会另起一节。

说明:
上图中有-v4,-v21这样的文件有些是app开发者本身写的,但大多都是系统在打包的时候自动生成的,因此你只须要考虑本身项目中的drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi便可。

经过as删除无用资源

在as的任何文件中右击,选择清除无用资源便可删除没有用到的资源文件。

不要勾选清除id!若是清除了id,会影响databinding的使用(id绝对占不了多少空间)

Tips:
作此操做以前,请务必产生一次commit,操做完成后必定要经过git看下diff。这样既方便查看被删除的文件,又能够利用git进行误删恢复。

打包时剔除无用资源

shrinkResources顾名思义————收缩资源。将它设置为true后,每次打包的时候就会自动排除无用的资源(不只仅是图片)。有了它的帮忙,即便你忘记手动删除无用的资源文件也没事。

buildTypes {
    release {
        zipAlignEnabled true
        minifyEnabled true

        shrinkResources true // 是否去除无效的资源文件

        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        signingConfig signingConfigs.release
    }

    rtm.initWith(buildTypes.release)
    rtm {}

    debug {
        multiDexEnabled true
    }
}复制代码

删除无用的语言

大部分应用其实并不须要支持几十种语言的,微信也作了根据地区选择性下载语言包的功能。做为国内应用,咱们能够只支持中文。推荐在项目的build.gradle中进行以下配置:

android {

    //...

    defaultConfig {
        resConfigs "zh"
    }
}复制代码

这样在打包的时候就会排除私有项目、android系统库和第三方库中非中文的资源文件了,效果仍是比较显著的。

控制raw中资源的大小

  • assets目录容许下面有多级子目录,而raw下不容许存在目录结构
  • assets中的文件不会产生R文件映射,但raw会
  • 若是你app最低支持的版本不是2.3的话,assets和raw应该都不会对资源文件的大小进行限制
  • raw文件会生成R文件映射,能够被as的lint分析,而assets则不能
  • raw缺乏子目录的缺点让其没法成为存放大量文件的目录

通常raw文件下会放音频文件。若是raw文件夹下有音频文件,尽可能不要使用无损(如:wav)的音频格式,能够考虑同等质量但文件更小的音频格式。

ogg是一种较适合作音效的音频格式。当年我初中作游戏的时候,我全都是用的mp3和png,最终游戏达到了2G。在换为ogg和jpg后,游戏缩小到了1G之内(由于游戏中音频和大图较多,因此效果比较夸张)。

移动端的音频主要是音效和短小的音频,因此淘宝大量选择了ogg格式,微博的选择格式比较多,有wav、mp三、ogg,我更加推荐淘宝的作法。固然,你仍旧不要忘记opus格式,opus也是一种有损压缩格式,若是感兴趣的话也能够尝试一下。

统一应用风格,减小shape文件

一个应用的界面风格是必需要统一的,这个越早作越好,最基本的就是统一颜色和按钮的按压效果。无UI设计和扁平化风格流行后,却是给应用瘦身带来了极大的的福利。界面变得越朴实,咱们能够用shape画的东西就越多。

当你的app统一过每种颜色对应的按下颜色后,接下来就须要统一按钮的形状、按钮的圆角角度、有无阴影的样子、阴影投射角度,阴影范围等等,最后还要考虑是否支持水波纹效果。

我简单将按钮分为下列元素:

元素 属性01 属性02 属性03 属性04
形状 正方形 三角形 圆角矩形 圆形
颜色 绿
有无阴影
阴影大小 3dp 5dp
阴影角度 90° 120° 180°
水波纹效果

各个元素组合后会产生大量的样式,shape和layer-list固然能够实现各类组合,但这样的话光按钮的背景文件就有n个,很很差维护。

通常为了开发方便,都会把须要用到的各类selector图片事先定义好,作业务的时候只须要去调用就行。但这大量的selector文件对于业务开发者来讲也是有记忆难度的,因此我推荐使用SelectorInjection这个库,它能够将上面的每一个元素进行各类组合,用最少的资源文件来实现大量的按压效果。

用库虽然好,但库也会带来学习成本,因此引入者能够将上述的组合定义为按钮的一个个的style。由于style自己是支持继承的,对于这样的组合形态来讲,继承真是是一大利器。当你的style有良好的命名后,调用者只须要知道引入什么style就行,至于你用了什么属性别人才不但愿管呢。

若是业务开发中有一些特别特殊的按压状态,没有任何复用的价值,那你就能够利用库提供的丰富属性在layout文件中进行实现,不再用手忙脚乱的处处定义selector文件了。

我将不能继承和不灵活的shape变成了一个个单一的属性,经过库将多个属性进行组合,接着利用支持继承的style来将多个属性固定成一个配置文件,最后对外造成强制的规范性约束,至此便完成了减小selector文件的工做。

使用toolbar,减小menu文件

menu文件是actionBar时代的产物,as虽然对于menu的支持作的还不错,但我也很难爱上它。

menu的设计初衷是解耦和抽象,但由于过分的解耦和定制,让开发变得很不方便,不少项目已经再也不使用menu.xml做为actionbar的菜单了。

就目前的形势来看,toolbar是android将来的方向。我虽然做为一个对actionbar和actionbar的兼容处理至关了解的人,但我仍是不得不认可actionbar的时代过去了。若是你不信,我能够告诉你淘宝的menu文件就3个,微博的menu文件就9个,若是你仍是苦苦依恋着actionbar的配置模式,我推荐一个库AppBar,它可让你在用灵活的toolbar的同时也享受到配置menu的便利性。

限制灵活性,减小layout文件

减小layout文件有两个方法:复用和融合(include)。

复用layout文件

把一些页面共用的布局抽出来,这不管是对layout文件的管理仍是瘦身都是极为有用的。

就好比说任何一个app的list页面是至关多的,从布局层面来讲就是一个ListView或者RecyclerView,其背后还可能会有loading的view,空状态的view等等,因此个人建议是创建一个list_layout.xml,其他的list页面能够复用或者include它,这样会从很大程度上减小layout文件的数目。

融合layout代码

对于能够被复用的layout咱们能够作统一管理,可是对于不会被复用的layout怎么办呢?
假设一个页面是由两个区域组合而成的,fragment的作法是一个页面中放两个container,而后再写两个layout,但实际上这两个layout常常是没有任何复用价值的。我但愿找到一种方式,在view区块尚未复用需求的时候用一个layout搞定,须要被复用的时候也能够快速、无痛的拆分出来。

1. UiBlock

UiBlock是一个相似于fragment的解耦库,它能够为同一个layout中不一样区域的view进行逻辑解耦(由于layout可预览的特性,ui定位方面不是难题),它能帮咱们尽量少地创建layout文件。

若是将来需求发生了变更,layout文件中的一块view须要抽出成独立的layout文件的时候,UiBlock的逻辑代码几乎不用改动,你只须要把抽出的layout文件include进来,而后在include标签上定义一个id便可。而这个工做能够经过as的重构功能自动完成,毫不拖泥带水。

<!-- 使用include -->
<include android:id="@+id/bottom_ub" layout="@layout/demo_uiblock" android:layout_width="match_parent" android:layout_height="100dp" />复制代码

2. ListHeader

public void addHeaderToListView(ListView listView, View header) {
    if (header == null) {
        throw new IllegalArgumentException("Can't add a null header view to ListView");
    }
    ViewGroup viewParent = (ViewGroup) header.getParent();
    viewParent.removeView(header);

    AbsListView.LayoutParams params = new AbsListView.LayoutParams(
            header.getLayoutParams().width,
            header.getLayoutParams().height);
    header.setLayoutParams(params);

    listView.addHeaderView(header); // add
}复制代码

我将listView和它的没有复用价值的header放到了同一个layout中,而后在activity中利用上述代码进行了操做,最终完成了用一个layout文件给listView加头的工做。这段代码我好久没动过了,有利有弊,放在这里我也仅仅是举个例子,但愿能够帮助你们扩展下思路。

动态下载图片

作过滤镜和贴纸的同窗应该会注意到贴纸、表情这类的东西是至关大的,对于这类的图片资源我强烈建议经过在线商店进行获取。这样既可让你踏踏实实的卖贴纸,又能够减少应用的大小。这么作虽然有必定的复杂度和出错几率,但投入产出比仍是很不错的。

准确放置不一样分辨率的图片

这个虽然不算是app大小的优化,可是若是你放错了图片,对于app启动时的内存大小会有必定的影响

思考一下,若是把一个原本应该放在drawable-xxhdpi里面的图片放在了drawable文件夹中会出现什么问题呢?
在xxhdpi设备上,图片会被放大3倍,图片内存占用就会变为原来的9倍!

国内也有不少人说能够用一套图片来作,不用出多套图,借此来达到app瘦身和给设计减负的目的。谷歌官方是建议为不一样分辨率出不一样的图片,为此国内也有很多文章讨论过这件事情,这篇总结的不错推荐一读。

每次说到这个话题的时候总有不少人有不一样的见解,何况不少人还不知道.9图也是须要切多份的,因此这里我仍是先分析一下大厂的放图策略,最后我们再讨论下较优的方案。

分析过程见:《淘宝、微博、微信的 Android 图片放置策略》

厂商 mdpi hdpi xhdpi xxhdpi
淘宝 小icon 表情 国家icon 弃用
微博 小icon 背景图&表情 背景图 背景图
微信 弃用 表情 大图 弃用

经过分析得出,传统的出多个分辨率图片的作法在大厂中已经发生了改变,阿里系、腾讯系的产品都采用了一套图走天下的路子。这样的作法仍是有利有弊的,权衡之下我给出以下建议:

  • 聊天表情就出一套图,放在hdpi中
  • 纯色小icon用svg作
  • 背景等大图,出一套放在xhdpi中
  • logo等权重较大的图片可针对hdpi,xhdpi作两套图
  • 若是某些图在真机中确实展现异常,那就用多套图
  • 若是遇到奇葩机型,可针对性的补图

成年人不看对错,只看利弊,因此还请你们权衡一二。

丢弃特定资源

若是咱们但愿保留或丢弃特定的资源,须要在项目中建立一个res/raw/keep.xml文件,这里可使用tools:keeptools:discard两个属性来保留或丢弃资源。

两个属性均可以使用逗号,分隔符声明资源名称列表。也可使用*做为匹配符。

android {
    ...
    buildTypes {
        release {
            shrinkResources true // 开启
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}复制代码

keep:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:discard="@layout/unused2" />复制代码

discard:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="safe" tools:discard="@layout/unused2" />复制代码

开启严格模式

开启严格模式后,可能对于编译会产生一些问题,警告也会增多,因此需谨慎开启此功能。

res/raw/keep.xml

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

shrinkMode默认的值为safe,你将它指定为strict便开启了严格模式。严格模式下,apk会保留肯定引用到的资源。

Gradle Console中的日志中也会有移除APK资源的信息::

:android:shrinkDebugResources
Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33%
:android:validateDebugSigning复制代码

apk构建完成后会Gradle会在/build/outputs/mapping/release/下生成resource.txt,这个文件包括详细信息,如资源参考其余资源和使用或删除资源的详细信息等。

例如:找出为何@drawable/ic_plus_anim_016,仍然包含在你的APK中,在resource.txt 搜索该文件名,你可能会发现它是被另外一个资源引用,以下:

16:25:48.005 [QUIET] [system.out] @drawable/add_schedule_fab_icon_anim : reachable=true
16:25:48.009 [QUIET] [system.out] @drawable/ic_plus_anim_016复制代码

要 为何add_schedule_fab_icon_anim 仍然在使用,搜索咱们能够知道应该有代码引用着add_schedule_fab_icon_anim

此部分的内容大量参考自《Shrink Your Code and Resources》一文,请移步官网去详细了解。

移除第三方库中的配置文件

有时候引用的三方库会带有一些配置文件xxxx.properties,或者license信息,打包的时候想去掉这些信息,就能够这样作

android {
    packagingOptions {
        exclude 'proguard-project.txt'
        exclude 'project.properties'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/DEPENDENCIES.txt'
        exclude 'META-INF/DEPENDENCIES'
    }
}复制代码

优化图片

对于图片的优化应该是放在优化res一节中进行讲解的,可是由于图片这块比重太大了,因此我让其独立成为一节。

选择合适的图片格式

想要作好图片的优化工做首先要知道应该选择什么样的图片格式,对于这点我推荐一个视频,方便你们进行深刻的了解。也推荐参考下《移动端图片格式调研 | Garan no dou》里面的相关内容。

这是谷歌给出的建议是:VD->WebP->Png->JPG

  1. 若是是纯色的icon,那么用svg
  2. 若是是两种以上颜色的icon,用webp
  3. 若是webp没法达到效果,选择png
  4. 若是图片没有alpha通道,能够考虑jpg

使用VectorDrawable

VD即VectorDrawable,是android上的svg实现类。在经历了长达半年的缓慢兼容之路后,如今终于被support库兼容了,官方文档中给出了这样一个例子:

// Gradle Plugin 2.0+ 
android {
    defaultConfig {  
     vectorDrawables.useSupportLibrary = true  
    }  
}复制代码
<ImageView  
  android:layout_width="wrap_content"  
  android:layout_height="wrap_content"  
  app:srcCompat="@drawable/ic_add" 
/>复制代码

配置好后,咱们就能够利用强大的svg来替换纯色icon了。

由于svg矢量图的优点,终于能够经过一套图适配多个机型了。svg的好处有不少,缺点也很多,关于svg的优缺点和实践方案,建议移步:http://todo(未写完)

使用WebP

webp是一种新的图片格式。从Android4.0+开始原生支持,可是不支持包含透明度,直到4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。

Lossy WebP support (suitable for replacing most JPEGs and some PNGs) is guaranteed on Android 4.0+ devices. Newer WebP features (transparency, lossless, suitable for PNGs) are supported since Android 4.2.1+

咱们能够经过智图或者isparta将其它格式的图片转换成webP格式。

关于webP的优缺点和实践方案,建议移步到《WebP的问题和解决方案》继续阅读。

复用图片

利用现有的图片进行复用是一个至关有用的方案,关于复用的原则建议和设计进行讨论,当设计师认为两者均为同一图形的时候才可被认为可复用。

复用相同的icon

咱们经过svg可让一张图片适用于不一样大小的容器中,以达到复用的目的。最多见的例子就是“叉”,除非你的x是有多种颜色的,那么这种表示关闭的icon能够复用到不少地方。

能够复用的x

利用svg和tint

上图中我经过组合的方式将长得同样的icon(facebook、renren等)复用到了不一样的界面中,不只实现了效果,可维护性也不错。

使用Tint

着色器(tint)是一个强大的工具,我将其和shape、svg等结合后产生了化学反应。

TintMode共有6种,分别是:add,multiply,screen,srcatop,srcin(默认),src_over。

通常用默认的模式就能够搞定大多数需求了,使用到的控件主要是TextView和ImageButton。ImageButton官方已经给出了支持方案,TextView由于有四个Drawable,官方的tint属性在低版本又不可用,因此我让SelectorTextView支持了一下。若是你想要了解具体的兼容方法,能够参考代码《Drawable 着色的后向兼容方案》

ImageButton

android:tint="@color/blue"复制代码

SelectorTextView

app:drawableLeftTint="@color/orange"
app:drawableRightTint="@color/green"
app:drawableTopTint="@color/green"
app:drawableBottomTint="@color/green"复制代码

由于我用了SelectorTextView和SelectorImageButton,因此我对于背景的tint没有什么需求,也就没作兼容性测试,有兴趣的同窗能够尝试一下。

若是你决定要采用tint,必定要经过云测等手段作下兼容性测试,下图是我对于上述属性的测试结果:

完美兼容

复用按压效果

一个应用中的list页面都应该作必定程度的统一,对于有限长度的list,咱们可能偏向于用ScrollView作,对于无限长的list用RecyclerView作,但对于它们的按压效果我强烈建议采用同一个样式。

以微信为例,它的全部列表都是白色的item,个人优化思路以下:

  1. 列表由LinearLayout、RecyclerView组成
  2. 分割线用统一的shape进行绘制,不用切图
  3. 整个列表背景设置为白色
  4. item的背景是一个selector文件,正常时颜色是透明,按下后出现灰色

经过旋转来复用

若是一个icon能够经过另外一个icon的旋转变换来获得,那么咱们就能够经过以下方法来实现:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/blue_btn_icon" // 原始icon
    android:fromDegrees="180" // 旋转角度
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="180" />复制代码

旋转图片

这种方法虽好,可是不要滥用。须要读代码的人具有这种知识,不然会出现很差维护的状况。并且在设计师真的是认为两个图有如此的关系的时候才可这样实现,万不可耍小聪明。

压缩图片

图片的压缩策略是:

  1. 优先压大图,后小图
  2. 不压.9图(svg在侠义上不算图)
  3. 对于开屏大图片的压缩需注意力度,要和设计确认后再作
  4. 对于体积特别大(超过50k)的图片资源能够考虑有损压缩

关于如何量化两张图片在视觉上的差异,Google 提供了一个叫butteraugli的工具,有兴趣的同窗能够尝试一下。

关于更加详细的内容能够参考:《smallerapk-part-6-image-optimization》《QQ音乐团队的PNG图片压缩对比分析》

ImageOptim

mac上超好用的图片压缩工具是ImageOptim,它集成了不少好用图片压缩库,不少blog中的图片也是用它来压缩的。

值得一提的是,借助Zopfli,它能够在不改变png图像质量的状况下使图片大小明显变小。

pngquant

pngquant也是一款著名的压缩工具,对于png的疗效还不错。它不必定适合app中那种背景透明的小icon,因此对比起tinypng来讲,优点不明显。

数据来自:www.jianshu.com/p/a721fbaa6…

tinypng

tinypng是一款至关著名的商用压缩工具,tinypng提供了开放接口供开发者开发属于本身的压缩工具(付费服务)。tinypng对于免费用户也算友好,每个月能够免费压缩几百张图片。

我经过TinyPic来使用tinypng,更加简单方便。我通常是发版本前才作一次图片压缩,每次debug的时候是直接跳过这个task的,彻底不影响平常的debug。

tinyinfo {
    apiKey = 'xxxxxxxxx'
    //编译时是否跳过此task
    skip = true
    //是否打印日志
    isShowLog = true
}复制代码

有人说tinypng的缺点是在压缩某些带有过渡效果(带alpha值)的图片时,图片可能会失真,这时你能够将png图片转换为webP格式的图片来解决此问题。

注意事项

aapt能够在构建过程期间优化放置在res/drawable/中的图像资源,以及无损压缩。 aapt可将不须要多于256种颜色的真彩色png转换为带有调色板的8位png,借此来获得质量相同但占用内存较小的图像。

请记住,aapt有如下限制:

  • aapt工具不会压缩资源/文件夹中包含的png文件
  • 图像文件须要使用256个或更少的颜色的aapt工具来优化它们
  • aapt工具可能会使已压缩的png文件膨胀。缘由请看:Smaller PNGs, and Android’s AAPT tool

若是你本身作了图片压缩,那么请使用cruncherEnabled来禁用aapt的压缩功能:

android {  
    defaultConfig {  
        //...
    }  

    aaptOptions {  
        cruncherEnabled = false 
    }  
}复制代码

优化dex

dex自己的体积仍是很可观的,虽然说代码这东西不占用多少存储空间,可是微信这样的大厂的dex已经达到了20多M。我大概估计了一下,若是你没有达到方法数上限,那么你的dex的大小大约是10M,可没有用multiDex的又有几家呢?

记录方法数和代码行数

dexcout

要优化这个部分,你首先要对公司的、android库的、第三方库的代码进行深刻的了解。我用了dexcount来记录项目的方法数:

dexcount {
    format = "list"
    includeClasses = false
    includeFieldCount = true
    includeTotalMethodCount = false
    orderByMethodCount = false
    verbose = false
    maxTreeDepth = Integer.MAX_VALUE
    teamCityIntegration = false
}复制代码

经过分析后你能够得出不少有用的结论,好比某个第三方库是否已经不用了、本身项目的哪一个包的方法数最多、目前代码状况是否合理等等。

statistic

我是经过Statistic这个as插件来评估项目中开发人员写的代码量的,它生成的报表也不错:

预览

java代码

如今我能够知道:

  • 哪些类空行数太多,是否是没有按照代码规范来
  • 哪些类的代码量不多,是否有存在的必要
  • 哪些类行数过多,是否没有遵照单一职责原则,是否能够进行进一步的拆分

apk method

你还能够用apk-method-count这个工具来查看项目中各个包中的方法数,它会生成树形结构的文档,十分直观。

利用Lint分析无用代码

若是你想删掉没有用到的代码,能够借助as中的Inspect Code对工程作静态代码检查。

search action

Lint是一个至关强大的工具,它能作的事情天然不限于检查无用资源和代码,它还能检测丢失的属性、写错的单位(dp/sp)、放错目录的图片、会引发内存溢出的代码等等。从eclipse时代发展到如今,lint真的是愈来愈方便了!

Lint的强大也会带来相应的缺点,缺点就是生成的信息量过多,不适合快速定位无用的代码。

我推荐的流程是到下图中的类目中直接看无用的代码和方法。

注意:
这种删除无用代码的工做须要反复屡次的进行(好比一月一次)。当你删除了无用代码后,这些代码中用到的资源也会被标记为无用,这时就能够经过上文提到的Remove Unused Resources来删除了。

经过proguard来删除无用代码

手动删除无用代码的流程太繁琐了,若是是一两次倒还会带来删除代码的爽快感,但若是是专人机械性的持续工做,那我的确定是要疯的。为了保证每次打包后的apk都包含尽量少的无用代码,咱们利用一下proguard这个强大的工具。

android {
    buildTypes {
        release {
            minifyEnabled true // 是否混淆
        }
    }
}复制代码

虽然这种方式成果显著,但也须要配合正确的proguard配置才能起做用,推荐看下《读懂Android中的代码混淆》一文。

这种利用混淆来删除代码的方式是一种保险措施,真正治本的方法仍是在开发过程当中随手删除无用的代码,毕竟开发者才是最清楚一段代码该不应被删的。我以前就是随手清理了下没用的代码,而后就莫名其妙的不用使用mulitdex了。

剔除测试代码

咱们在测试的时候可能会随便写点测试方法,好比main方法之类的,而且还会引入一些测试库。对于测试环境的代码gradle提供了很方便的androidTest和test目录来隔离生产环境。

对于测试时用到的大量库,能够进行test依赖,这样就能够保证测试代码不会污染线上代码,也能够防止把测试工具、代码等发布到线上的错误(微博就出过这样的错误)。

// Dependencies for local unit tests
testCompile 'junit:junit:4.12'
testCompile  'org.hamcrest:hamcrest-junit:2.0.0.0'

// Android Testing Support Library's runner and rules
androidTestCompile 'com.android.support:support-annotations:24.1.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'

// Espresso UI Testing
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
})复制代码

PS:在layout中利用tools也是为了达到上述目的。

区分debug/rtm/release模式

debug模式是开发者的调试模式,这个模式下log全开,而且会有一些帮助调试的工具(好比:leakcanarystetho),咱们能够经过debugCompilereleaseCompile来作不一样的依赖,有时候也会须要no-op(关于no-op的内容能够参考下开发第三方库最佳实践)。

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
 }复制代码

debug和release是android自己自带的两种生产环境,在实际中咱们可能须要有多个环境,好比提测环境、预发环境等,我以rtm(Release to Manufacturing 或者 Release to Marketing的简称)环境作例子。

首先在目录下建立rtm文件:

image_1arpi1tfq1dd5pga35j10ntuhjjc.png-12.3kB

复刻release的配置:

buildTypes {
    release {
        zipAlignEnabled true
        minifyEnabled true
        shrinkResources true // 是否去除无效的资源文件
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        signingConfig signingConfigs.release
    }
    rtm.initWith(buildTypes.release)
    rtm {}
    debug {
        multiDexEnabled true
    }
}复制代码

配置rtm依赖:

ext {
    leakcanaryVersion = '1.3.1'
}

dependencies {
    debugCompile "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion"
    rtmCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"
    releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion"复制代码

rtm环境天然也有动态替换application文件的能力,我为了方便非开发者区分app类别,我作了启动icon的替换。

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kale.example" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" > <application android:name=".RtmApplication" android:allowBackup="true" android:icon="@drawable/rtm_icon" tools:replace="android:name,android:icon" /> </manifest>复制代码

如今我能够将环境真正须要的代码打包,不须要的代码所有剔除,以达到瘦身的目的。

使用拆分后的support库

谷歌最近有意将support-v4库进行拆分,可无奈v4被引用的地方太多了,但这不失为一个好的开始。目前来看使用拆分后的support库是没有什么优势的,因此我也不建议如今就开始动手,当谷歌和第三方库做者都开始真的往这方面想的时候,你再开始吧。

减小方法数,不用mulitdex

mulitdex会进行分包,分包的结果天然比原始的包要大一些些,能不用mulitdex则不用。但若是方法数超了,除了插件化和RN动态发包等奇淫巧技外我也没什么好办法了。

使用更小库或合并现有库

同一功能就用一个库,禁止一个app中有多个网络库、多个图片库的状况出现。若是一个库很大,而且申请了各类权限,那么就去考虑换掉他。

话人人都会说,但若是一个项目是由多个项目成员合做完成的,是很难避免重复引用库的问题的。同一个功能用不一样的库,或者一个库用不一样版本的现象比比皆是,这也是很难去解决的。个人解决方案是部门之间多沟通,尽可能作base层,base层由少数人进行维护,正如微信在so库方面的作法:

  1. C++运行时库统一使用stlport_shared
    以前微信中的C++运行库大多使用静态编译方式,使用stlport_shared方式可减少APK包大小,至关于把你们公有的代码提取出来放一份,减小冗余。同时也会节省一点内存,加载so的时候动态库只会加载一次,静态库则随着so的加载被加载多分内存映像。
  2. 把公用的C++模块抽成功能库
    其实与上面的思路是一致的,主要为了减小冗余模块。你们都用到的一些基础功能,应该抽成基础模块。

4、总结

app的瘦身是一个长期而且艰巨的工做,若是是小公司建议一两个月作一次。大公司的话通常都会对app的大小进行持续的统计和追踪,瘦身工做会有专人负责。总之,但愿你们在阅读完本文后能够着手对项目进行优化工做,带来真正的收益。

developer-kale@foxmail.com

weibo:@天之界线2010

参考资料:
smaller apk系列文章
减小APK的大小,Android官方这样说
那些你不知道的APK 瘦身,让你的APK更小
Android技术专题]APK瘦身看这一篇文章就够了
如何将apk大小减小6M的
Android Vector曲折的兼容之路
淘宝、微博、微信的 Android 图片放置策略
Putting Your APKs on Diet
Shrink Your Code and Resources
Reduce APK Size

相关文章
相关标签/搜索