app瘦身 - CSDN
精华内容
参与话题
  • App瘦身

    2019-08-29 19:34:13
    App瘦身这方面有几个点: 1.资源:咱们图片mipmap{ hdpi xhdpi ...
    App瘦身这方面有几个点:
    
             1.资源:咱们图片mipmap{
                                         
                                         hdpi
                                         xhdpi
                                         xxhdpi
                                         xxxhdpi
    
                                 }
                                 多套图在apk会造成apk包过大所以对于这一点我们需要删除无用选择合适的
                     
                                  图片我们尽量用WebP格式
                                  (它支持有损无损压缩、支持完整的透明通道、也支持多针动画,是一种比较理想的图片格式)
                                  用sharp进行绘制
    
                                  然后能用代码进行绘制的图片尽量用代码实现
      
                                  少用帧动画
    
             2.代码:
                        对于在代码方面移除吴用代码或者对无用代码进行注释这样我们在混淆打包的时候会对这些代码忽略 
                        对代码进行抽取和封装
    
             3.源码:
                        如果有能力的情况下我们可以对代码进行源码解析,对于我们有用的代码抽出来,将无用的代码删除掉来对我们的apk瘦身
             4.依赖:
                      使用V7包
    
                   对于依赖统一管理
    

    动态导入(比如我们酷狗的背景图片在我们下载完成的时候它不会下载只有在我们用到的时候才会进行下载)

    展开全文
  • 业务方和开发都希望app尽量的小,本文会给出多个实用性的技巧来帮助开发者进行app瘦身工作。瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。 一、背景 目前...

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


    一、背景

    目前app的大小越来越大,用户对于过大的app接受度不高,所以除了插件化和RN的方案外,我们只能老老实实的进行app的瘦身工作。

    二、需求

    1. 我要利用混淆来让我的代码尽可能少

    2. 最好能用最少的切图完成功能

    3. layout文件不要太多,太多了乱

    4. 能动态下载的就做动态

    5. 我希望能用大小最小的图片

    6. 如果能用svg,我就用svg

    7. 对于无用的资源,我要as能自动删除掉

    8. 中国文字博大精深,而我只要我需要的字的字体

    9. 最好能根据下载用户手机的cpu和分辨率来引入不同的资源

    三、实现

    分析app组成结构


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

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

    我们都知道apk是由:

    • asserts

    • lib

    • res

    • dex

    • META-INF

    • androidManifest

    这几个部分构成的。

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

    概览

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

    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的网站,方便大家进行测试:https://icomoon.io/app/

    • svg:549字节

    • png:375字节(单一分辨率的一张图)

    • ion-font:1.1kb


    动态下载资源


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


    压缩资源文件


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



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

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


    优化lib


    配置abiFilters


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



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


    官方例子


    按 ABI 拆分



    • enable: 启用ABI拆分机制

    • exclude: 默认情况下所有ABI都包括在内,允许移除一些ABI

    • include:指明要包含哪些ABI

    • reset():重置ABI列表为只包含一个空字符串(这也是允许的,在与include一起使用来可以表示要使用哪一个ABI)

    • universalApk:指示是否打包一个通用版本(包含所有的ABI)。默认值为 false。


    根据手机的cpu来引入so


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



    注意:

    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 中,可以通过如下的方式进行申明:



    如果想了解更多信息或者想知道这种配置的限制,可以浏览下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资源混淆打包工具 



    使用这个工具的时候需要注意一些东西:像友盟这种喜欢用反射获取资源的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方法:




    上述代码说明了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中的值。



    CERT.RSA


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

    这点和app瘦身就完全无关了,这块我平时也没有仔细研究过,就不误人子弟了。具体的签名过程可以参考:http://blog.csdn.net/asmcvc/article/details/9312123


    优化建议


    通过分析得出,除了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后,每次打包的时候就会自动排除无用的资源(不仅仅是图片)。有了它的帮忙,即使你忘记手动删除无用的资源文件也没事。




    删除无用的语言


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




    这样在打包的时候就会排除私有项目、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、mp3、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的便利性。


    展开全文
  • app瘦身

    千次阅读 2016-06-03 16:29:04
    经过多次版本迭代,app中难免会产生不少冗余代码和无用资源,整个app越发臃肿。 再加上目前我们的app整体架构变成了hybrid,很多native代码都不需要了,考虑到要为客户的流量负责,减小应用的大小已经迫在眉睫。 ...

    经过多次版本迭代,app中难免会产生不少冗余代码和无用资源,整个app越发臃肿。

    再加上目前我们的app整体架构变成了hybrid,很多native代码都不需要了,考虑到要为客户的流量负责,减小应用的大小已经迫在眉睫。

    整体改造方案分为三部分:













    分析应用程序大小

    找到.ipa包,解压缩后得到应用程序安装包:

    点击显示包内容后按大小给文件夹排序,减包之前:

    减包之后(可执行文件变大是因为加入了环信的sdk,2M多):

     

     

    应用中各个文件的大小一目了然,为了使瘦身效果明显,我们先从体积最大的文件开始。

    清除项目中无用的图片

    在我们的项目中主要包含的资源类型是图片,在之前的迭代中,开发和设计人员对图片资源的优化、管理一直都不够重视,所以这部分是瘦身的重头戏,无用图片可以分为四类:

    1.引入图片框架时错误导入了demo中的图片。

    2.开发人员的疏忽,导入了无用的屏幕截图图片。

    3.历史项目中残留的图片,未及时清理。

    4.本期项目中由于删除大量native功能导致好多图片都失效了。

    针对这些图片,我们编写了脚本,进行删除。使用脚本有一点要注意:脚本中的图片命名方式务必和项目中的一致,否则可能会导致忽略和误删图片。

    删除方法:

    1.进入终端,输入: brew install ack 安装ack库。

    2.cd到项目路径下,执行脚本文件,即可进行查找、移动、删除项目中没有引用到的图片,脚本内容如下:

    ------------------------------------------------

    for i in `find . -name "*.png"`do

    file=`basename -s .png "$i"| xargs basename -s @2x | xargs basename -s @3x`

    result=`ack -i "$file"`

    if [ -z "$result" ]; then

    #mv "$i" /Users/apple/Desktop   移动文件到指定路径

    #echo "$i"                      输出文件名

    #rm "$i"                        删除文件

    fi

    done

    ------------------------------------------------

    脚本注释:

    1、2——声明接下来的语句中使用的命令存放的文件夹,当执行命令时,则直接进入该文件夹路径内寻找。(:号是文件夹分隔符

    3——是forin结构相信诸位iOS开发者是非常属性的,遍历find命令找到的文件.(find后面的.的含义是当前目录,-name是指定文件名,*号表示0-n个字符的通配,-o 是or的含义

    4——是为变量file赋值,其中使用了basename命令、xargs命令以及至关重要的 | 管道。(如果对bash shell感兴趣,对管道和xargs命令的深入学习是必不可少的)

    5——使用了ack命令。

    6——是bash shell里面的if使用方式,-z 表示判定后面的文件是否为空。

    7——是打印

    8,9——加了#号表明是注释的语句,rm 是删除命令。

    10——是if fi 的搭配。(bash shell语法)
    11——是for in ; do done 的 for循环语句搭配(bash shell语法)

    脚本的原理是用关键字(通常是文件名,图片资源需要去掉@2x @3x),搜索代码,搜不到就是没有被引用。当然,有些资源在使用过程中是拼接而成的,需要手工过滤。

    优化图片

    图片的处理主要由设计部的同事来处理

    1.不需要透明的地方,使用jpg而不是png。比如:背景图片、引导图、bundle里面的png文件。但是启动画面只能是png格式否则审核时可能被拒。

    2.可拉伸素材。界面导航条背景、弹框背景、按钮等都可以考虑用拉伸素材。

    3.压缩素材。可以进一步减小20%~40%左右的图片大小。在不影响视觉效果的情况下尽可能的压缩。

    修改相关代码,让程序识别jpg格式的图片:

    巧用Asset Catalog

    对于一张pattern image或者是有圆角的图片,考虑到有更大的屏幕,你需要重新调整图片的大小,以免图片拉伸出现失真。Natasha发布了一篇很棒的文章来说明如何编程解决这个问题,但是我们也可以在Xcode 6的Asset Catalog中搞定它。顺便说一下,我强烈建议你在继续往下读之前,看一下Natasha的文章,这样你就能理解到底发生了什么。免责声明:下面的图片等是直接从Natasha的文章中拷贝过来的。Sorry!

    好了,我们继续。

    在之前,一般用类似下面的代码来获得可改变大小的图片:

     

    1
    2
    3
    4
    let edgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
    let backgroundButtonImage = UIImage(named:"purple_button")?.resizableImageWithCapInsets(edgeInsets)
      
    purpleButton.setBackgroundImage(backgroundButtonImage, forState: .Normal)

     

    这将会得到一张和下面类似的图片:

    blob.png

    在运行时,会拉伸距离UIImageView的container的边框8像素的中间部分,这样就能保留圆角,得到下面这样的:

    blob.png

    多亏了Xcode中Asset Catalog的slice和dice,我们不需要代码也能拉伸图片。首先在Xcode中选中图片,然后点击右下角的Show Slicing:

    blob.png

     

     

     

     

     

     

     

     

     

     

     

    你现在应该能看到slicing 面板和一个按钮"Start Slicing"。

    blob.png

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    在你点击按钮之后,会显示下面的三个选项:

    blob.png

     

     

     

     

     

     

    左边的按钮用于horizontal edge insets,右边的按钮用于vertical edge insets,中间的则是两个都有。在我们的例子中要保留圆角,所以我们按中间的按钮,告诉系统我们想要按钮的中间在水平和垂直方向拉伸。在按下按钮之后,就能看到一些可以拖动的细条,这可以设置从哪里开始拉伸图片。

    blob.png

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    系统会保留深紫色的区域,浅紫色的区域会被拉伸。

    更厉害的是,Xcode自动找到了圆角,所以我们不需要设置从哪里开始拉伸图片。最后别忘了在Attribtues pane中设置图片是可拉伸的。

    blob.png

     

     

     

     

     

     

     

     

     

     

     

     

     

    有了这个无价之宝,你就不用再在resizableImageWithCapInsets方法中填写那些神奇的数字了,也能帮助你分离view逻辑和app逻辑。

    可执行文件优化

    以上资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。在讲可执行文件瘦身之前先介绍Xcode的LinkMap文件。LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。只要设置Project->Build Settings->Write Link Map File为YES,并设置Path to Link Map File,build完后就可以在设置的路径看到LinkMap文件了。这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。

     

    每个LinkMap由3个部分组成,以58到家为例:

     1. Object files:

     ..........

    第一部分列举可执行文件里所有.obj文件,以及每个文件的编号。

    2. Sections:

    第二部分是可执行文件的段表,描述各个段在可执行文件中的偏移位置和大小。第一列是段的偏移量,第二列是段占用大小,Address(n)=Address(n-1)+Size(n-1);第三列是段类型,代码段和数据段数据段(__DATA,保存变量值); 第四列是段名字,如__text是可执行机器码,__cstring是字符串常量 。有关段的概念可参考苹果官方文档《OS X ABI Mach-O File Format Reference》。


    3. Symbols:

    .........

     

    第三部分详细描述每个obj文件在每个段的分布情况,按第二部分Sections顺序展示。例如序号1的JZScrollBanner.o文件,-[JZScrollBanner initWithFrame:]方法在__TEXT.__text地址是0x100001DD0,占用大小是256字节。根据序号累加每个obj文件在每个段的占用大小,从而计算出每个obj文件在可执行文件的占用大小,进而算出每个静态库、每个功能模块代码占用大小。这里要注意的地方是,由于__DATA.__bbs是代表未初始化的静态变量,Size表示应用运行时占用的堆大小,并不占用可执行文件,所以计算obj占用大小时,要排除这个段的Size。

     

    使用LinkMap给可执行文件瘦身(基本都是使用脚本):

    利用LinkMap统计第三方库

     

    项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。我们可以从linkmap中统计出这个信息,对此写了个node.js脚本(详见附件:linkmap.js),可以通过linkmap统计每个.o目标文件占用的体积和每个.a静态库占用的体积。

    操作步骤:

    1.打开终端,cd到存放linkmap.js的文件夹

    2.执行命令:node linkmap.js linkmap.txt的文件路径 -hl

    得到输出结果resultOfObjectFile.txt

    整理得到第三方库占用详情:

     

    利用LinkMap清理无用的类

     

    在项目里新建一个类,给它添加几个方法,但不要在任何地方import它,build完项目后观察linkmap,你会发现这个类还是被编译进可执行文件了。按C++的经验,没有被使用到的类和方法编译器都会优化掉,不会编进最终的可执行文件,但object-c不一样,因为object-c的动态特性,它可以通过类和方法名反射获得这个类和方法进行调用,所以就算在代码里某个类没被使用到,编译器也没法保证这个类不会在运行时通过反射去调用,所以只要是在项目里的文件,无论是否又被使用到都会被编译进可执行文件。对此我们可以通过脚本,遍历整个项目的文件,找出所有没有被引用的类文件和没有被调用的方法,在保证没有其他地方动态调用的情况下把它们去掉。由于58到家app整个项目历时很长,历时代码遗留较多,这个清理对可执行文件省出的空间还是挺可观的。根据resultOfObjectFile.txt使用shell脚本:searchUnusedClass.sh搜索出引用次数等于1的类,但在实际中发现有的类虽然也引用了头文件但代码中并未使用该类,或者有引用代码但是被注销了,所以配合实际情况,在脚本中我们把引用次数设置成小于6。58到家中我们删除了41个无用的类,大小512KB,

    删除无用的类后可执行代码体积减小400KB!

    删除前:

       

    删除后:

     

    利用LinkMap清理无用的方法

    结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。另外第三方库的无用selector也可以这样扫出来的。

    编译选项优化(实际上然并卵)

    Strip Link Product设成YES

    Make Strings Read-Only设为YES

    去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题,假如ABC三个文件,AC文件支持了异常,B不支持,如果C抛了异常,在模拟器下A还是能捕获异常不至于Crash,但真机下捕获不了。个人认为关键路径支持异常处理就好,像启动时NSCoder读取setting配置文件得要支持捕获异常,等等

    展开全文
  • 史上最全App瘦身实践

    2019-05-06 11:38:49
    业务方和开发都希望app尽量的小,本文会给出多个实用性的技巧来帮助开发者进行app瘦身工作。瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。 分析app组成结构 做...

    转载:https://mp.weixin.qq.com/s/_d5ztuOv9PxMrbK53FZiSQ

    目标

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

    分析app组成结构

    做瘦身之前一定要了解自己app的组成结构,然后要有针对性的进行优化,并且要逐步记录比对,这样才能更好的完成此项工作。目前as的2.2预览版中已经有了apk分析器,功能相当强大,此外你还可以利用nimbledroid来分析apk。nimbledroid是一个强大的工具,推荐一试。

    我们都知道apk是由:

    • asserts

    • lib

    • res

    • dex

    • META-INF

    • androidManifest

    这几个部分构成的。下面我会利用as的分析工具,以微信、微博、淘宝为例进行讲述。

    开始分析后几秒钟就能输出结果,你还可以看到具体类目占的百分比,清晰明了。旁边的“对比”按钮提供了diff的功能,让你可以方便的进行apk优化前后的对比,简直利器。

     

    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文件等等。因为微信做了资源混淆,图片名字都不可读了。

    微博就没有做资源混淆,所以可读性较好:

    dex

    dex文件是java代码打包后的字节码,一个dex文件最多只支持65536个方法,这也是为什么微信有了三个dex文件的原因。

    因为dex分包是不均匀的,你可以理解为装箱,一个箱子的大小是固定的,但你代码的量是不确定的,微信把前两个箱子装满了,最后还剩了2m多的代码,这些代码也占用了一个箱子,最终产生了上图不均匀的结果。现在,我们已经知道了apk中各个文件的大小和它们占的比例,下面就可以开始针对性的进行优化了。

    优化assets

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

    删除无用字体

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

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

    减少icon-font的使用

    icon-font和svg都能完成一些icon的展示,但因为icon-font在assets中难以管理,并且功能和svg有所重叠,所以我建议减少icon-font的使用,利用svg进行代替,毕竟一个很小的icon-font也比svg大呢。我给出一个提供各种格式icon的网站,方便大家进行测试:https://icomoon.io/app/

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

    动态下载资源

    字体、js代码这样的资源能动态下载的就做动态下载,虽然这样会有出错的可能性,复杂度也会提升,但这个对于app的瘦身和用户来说是有长远的好处的。如果你用了RN,你就可以在app运行时动态去拉取最新的代码,将图片和js代码一并下载后解压使用。

    压缩资源文件

    有些资源文件是必须要随着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 {
                try {
                    if (zipStream != null) {
                        zipStream.close();
                    }
                    if (bufferedStream != null) {
                        bufferedStream.close();
                    }
                    if (fileStream != null) {
                        fileStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

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

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

    优化lib

    配置abiFilters

    一个硬件设备对应一个架构(mips、arm或者x86),只保留与设备架构相关的库文件夹(主流的架构都是arm的,mips属于小众,默认也是支持arm的so的,但x86的不支持)可以大大降低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"
        }
    }

    之后生成的apk中就会排出多余的平台文件了。armeabi就不用说了,这个是必须包含的,v7是一个图形加强版本,x86是英特尔平台的支持库。
    官方例子:

    分析用户手机的cpu

    我们在舍弃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;
        }
    }

    注意:

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

    避免复制so

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

    在6.0+中,可以通过如下的方式进行申明:

    
     

    <application
       android:extractNativeLibs=”false”
       ...
    >

    如果想了解更多信息或者想知道这种配置的限制,可以浏览下SmallerAPK(8)。

    优化resources.arsc

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

    我们在程序运行的时候肯定要经常用到id,因此它在安装之后仍需要被频繁的读取。如果将这个文件进行压缩,在每次读取前系统都必须进行解压的操作,这就会有一些性能和内存的开销,综合考虑下这是得不偿失的。

    删除无用的资源映射

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

    进行资源名称混淆

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

    表2:

    我们一眼就可以知道表2肯定比表1存储的字符要小,所以整个文件的大小肯定也要小一些。

    关于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瘦身就完全无关了,就是android的apk签名机制。这块我平时也没有仔细研究过,就不误人子弟了。具体的签名过程可以参考:http://blog.csdn.net/asmcvc/article/details/9312123
    优化建议
    通过分析得出,除了CERT.RSA没有压缩机会外,其余的两个文件都可以通过混淆资源名称的方式进行压缩。

    优化res

    资源文件的优化一直是我们的重头戏。如果要和它进行对比,上文的META-INF文件的优化简直可以忽略不计。这里的优化会分为两块,一个是文本资源(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、mp3、ogg,我更加推荐淘宝的做法。当然,你仍旧不要忘记opus格式,opus也是一种有损压缩格式,如果感兴趣的话也可以尝试一下。

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

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

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

    上面的各个元素会产生大量的组合,shape和layer-list当然可以实现各种组合,但这样的话光按钮的背景文件就有多个,很不好维护。一般为了开发方便,都会把需要用到的各种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"
        />

     

    1. 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图也是需要切多份的,所以这里我还是先分析一下大厂的放图策略,最后咱们再讨论下较优的方案。

    1. 淘宝

    mdpi:

    mdpi中存留了一些android原始的icon,这个从命名和前缀就能看出来。通过图片大小分析,这个目录下面都是一些很小的icon,还有一些没有用到的icon(这个launcher图片也很好的说明了淘宝的历史)。
    hdp:

    hdp中分为两部分:表情和其他图片。f+数字的图片都是表情图片,淘宝仅仅有一套表情图片,并且都放在这个目录下。除了少量的图片和mdip的图片一致(比如用户头像的place_holder)外,其余的图片和mdpi的图片完全不同。顺便说一下,此目录下除了表情之外,其余的都是一些小icon,绝对属于尺寸很小的那类。
    xhdpi:

    xhdpi又和hdpi不同了,它里面有大量的国家icon。除此之外就是一些对清晰度要求较高的icon。
    xxhdpi:

    xxhdpi就没什么东西了,几张图而已。
    其他:

    有后缀的文件夹中除了5张左右的淘宝自己的icon外,其余都是系统的图片,均以abc开头。我不清楚淘宝到底有没有使用到这些图片,但我可以肯定地说其中有着冗余图片,或许有着进一步优化的方案。

    总结:

    淘宝的放置图片策略是大量的图片在hdpi,xhdpi中,比如表情图在hdpi中,国家图在xhdpi中,大多数图片都仅有一套,少数全局的icon是会有多张的情况(极少,估计只有十几张)。xxhdpi和mhdpi仅仅作为补充,没有太大的作用。
    淘宝最令人好奇的点在于它的资源文件很小,但是so文件相当大:

    2. 微博

    微博是一个典型的android风格的app,它的drawable全都是有后缀的,完全符合安卓标准的默认打包策略,它还有根据像素密度的图片,甚至有ldpi的目录。

    mdpi-v4

    mdpi中有大量的小icon,里面有个叫做share_wx的,从名字一下子就知道是微信分享的icon,但实际是微博的logo,比较有趣。其余的都是一些边边角角的图标,量不大,所以主力图肯定不在这里。
    hdpi-v4

    这是微博图片存放的主要目录,有很多大背景和表情,微博的表情图片和淘宝一样都是在hdpi中的,它以lxh,emoji等前缀开头,用来区分不同风格的表情。
    xhdpi-v4和xxhdpi-v4*

    这里放了一些背景大图,我也发现了大量和hdpi中一样的图片,所以可以大胆的假设微博是做了不同像素的图片的。这也证实了我的想法——微博是很标准的android应用。
    ldpi-v4

    这个目录的确没什么用,微博自身也不会维护这个目录,这全都是第三方库和应用商店给的图片,微博开发者只需要放进来就好。
    sw400dp和sw32dp-400dp

    这些目录放了一些为不同分辨率准备的长得相同的icon,当然还有微博自己的logo。

    3. 微信

    通过上面的分析,我们是不是可以得出一些经验了呢?

    • 大量的图片都在hdpi和xdpi中

    • 表情图片在hdpi中

    • anim目录中都是xml文件

    • drawable目录中有大量的xml和少量的png和.9图

    • layout文件中是全部的xml

    • raw中放置音频

    • svg图片在raw、drawable或assets中

    • 最大的文件夹是图片文件

    • layout文件较小

    • 相同的图片,高分屏的肯定比低分屏的大

    ok,现在咱们就可以来看微信的资源了,混淆怕啥,友盟混淆的abcd代码都能看懂,微信的adcd资源也应该不难。
    raw(a9)

    这个目录中放置了大量的svg图片和mp3文件,从专业的角度来想,drawable目录下肯定不会放mp3,所以这个肯定是raw文件了。这个目录下有大量的svg,所以可以看出微信已经实现了全svg化,并且已经在线上稳定运行了。这点在微信早期的公众号上也可以得到佐证,详细请看Android微信上的SVG。
    文中提到了:

    第一步,拿到.svg后缀的资源文件(UI很容导出这种图片),放在raw目录下而不是drawable目录。
    第二步,把 R.drawable.xxx 换成 R.raw.xxx;把 @drawable/xxx 换成 @raw/xxx。
    layout(f)

    现在有了两个线索,那么初步估计上面的三个目录肯定是我们常见的目录,否则不会那么大。

    打开f后发现这个就是layout文件,所以其余的肯定就是图片文件了。
    hdpi(y)

    y是2.9m,里面有大量的表情,所以我判断它是hdpi,为了更加证实这个猜测,我找到了两张相同的图片。
    a2中:

    y中:

    y中图片是11kb,a2中图片是76kb,这明显说明y是hdpi,a2是xhdpi。
    xhdpi(a2):

    xhdpi中的图片和hdpi中的图片相同的不多,微信在这里放的是一些大图。
    drawable(k)

    drawable本身没啥可以说的,但是微信的drawable中.9图份额很少,所以我在想是否svg可以在一定的程度上完成一些.9的工作呢?
    总结:

    微信的表情都在hdpi中,仅有一套图片,这点几乎已经成为了标准
    微信已经实现了svg化,svg图片在raw中
    微信倾向于把较大的图片放在xhdpi中,仅出一套图
    微信和淘宝一样都是尽量选择一套图来完成需求

    优化思路

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

    • 聊天表情就出一套图,放在hdpi中

    • 纯色小icon用svg做

    • 背景等大图,出一套放在xhdpi中

    • logo等权重较大的图片可针对hdpi,xhdpi做两套图

    • 如果某些图在真机中确实展示异常,那就用多套图

    • 如果遇到奇葩机型,可针对性的补图

    成年人不看对错,只看利弊,所以还请大家权衡一二。

    优化图片

    对于图片的优化应该是放在优化res一节中进行讲解的,但是因为图片这块比重太大了,所以我让其独立成为一节。本节主要会从图片格式、复用图片和压缩图片三个方面进行讲解。

    使用VectorDrawable

    想要做好图片的优化工作最重要的一点是知道应该选择什么样的图片格式,对于这点我推荐一个视频,方便大家进行深入的了解。

    这是谷歌给出的建议,简单来说就是:VD->WebP->Png->JPG

    如果是纯色的icon,那么用svg
    如果是两种以上颜色的icon,用webp
    如果webp无法达到效果,选择png
    如果图片没有alpha通道,可以考虑jpg

    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转VectorDrawable

    先去这里下载svg图片:https://icomoon.io/app/#/select

    然后利用这个在线工具转换成VectorDrawable。

    svg的兼容性

    support库的代码质量还是不错的,但是svg毕竟是一个图片格式,所以使用svg前还是需要格外慎重的。我写了一个demo,把用到的所有属性都做了示例,然后利用云测服务进行兼容性测试。

    测试svg也挺简单的,首先看会不会崩溃,然后看各个分辨率、各个api下是否会有显示不正常的情况,如果都ok,那么就可以准备引入到项目里面了。具体的测试代码在SelectorInjection,我测试下来100%通过。

    svg的使用技巧

    设置恰当的宽高

    svg图片是有默认宽高的,设计也会给出一个默认宽高,设置一个合适的默认宽高对以后的图片复用会有很大帮助。

     

    TextView中drawableLeft等属性是不能设置图片的宽高的,但ImageView可以。如果你的图片会被复用,建议将图片的宽高设置为TextView中的drawable宽高。

    利用padding和scaleType属性

    ImageView中的svg默认情况下是会随着控件的大小而改变的,它不会像png那样保持自己的原始大小。我们可以利用这一特性,再配合padding和scaleType属性来完成各种效果。

    图1:60x60,svg自动铺满控件
    图2:30x30,svg被压缩到原始大小以下
    图3:60x60,使用scaleType,让svg保持原始宽度
    图4:60x60,使用padding,对svg进行任意比例的压缩

    svg的问题和解决方案

    svg有很多好处,但也免不了一些问题,本小节中写代码的猴子提出了一些很实用的建议,感谢他的帮助。

    容易写错属性

    svg的支持是要通过app:srcCompat这个属性来做的,如果稍微一不注意写成了src,那么就会出现低版本手机上不兼容的问题。你可以尝试通过配置Lint规则或是利用脚本进行文件的遍历等方式来防止出现因开发写错属性而崩溃的问题。

    不兼容selector

    将svg放入selector中的时候可能会出现一些问题,stackoverflow上也给出了解决方案,就是下面这段代码放在Activity中。

    
     

    static {
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    }

    开启这个flag后,你就可以正常的使用Selector这样的DrawableContainers了。

    不支持自定义控件

    AppCompatActivity会自动将xml文件中的ImageView替换为AppCompatImageView,但是如果用了你的自定义控件,那么这种机制就无效了,所以自定义控件中尽量使用AppCompatImageView来代替ImageView,使用 setImageResource()来设置资源。如果你的自定义控件中需要获得drawable或者是有自定义需求,那么可以参考AppCompatImageView中的svg的helper类来编写。

    不兼容第三方库

    市面上有很多优秀的图片加载库,它们一般都会支持多种图片路径的加载,比如磁盘图片,网络图片,res图片等等,对于svg这样的图片格式,它们是否支持就要大家结合自己的图片框架进行调研了。像glide目前(2016.09.10)就不支持加载本地的svg图片,详见: Vector drawable can't be used as error drawable 。
    PS:因为第三方库更新十分频繁,建议在用到svg的时候再调研。

    性能问题

    关于动画和性能方面的问题,《Android Vector曲折的兼容之路》中给出了具体的示例和建议:

    Bitmap的绘制效率并不一定会比Vector高,它们有一定的平衡点,当Vector比较简单时,其效率是一定比Bitmap高的,所以为了保证Vector的高效率,Vector需要更加简单,PathData更加标准、精简,当Vector图像变得非常复杂时,就需要使用Bitmap来代替了。

    Vector适用于icon、Button、ImageView的图标等小的icon,或者是需要的动画效果,由于Bitmap在GPU中有缓存功能,而Vector并没有,所以Vector图像不能做频繁的重绘。Vector图像过于复杂时,不仅仅要注意绘制效率,初始化效率也是需要考虑的重要因素。这点可以参考微信中的svg。SVG加载速度会快于PNG,但渲染速度会慢于PNG,毕竟PNG有硬件加速,但平均下来,加载速度的提升弥补了绘制的速度缺陷。

    不便于管理

    svg是没办法在文件目录下进行预览的,其放置的目录也和其他图片不同,如果没有做好管理工作,未来的drawable目录就会变得越发的混乱。其实,对于目录或者包内文件的管理有个很简单的原则:同目录多类型文件,以前缀区分;不同目录,同类型文件,以意义区分。
    drawable目录下有多种类型的文件,我们利用英文排序的原则将这些文件简单分为svg、.9图、shape、layer-list这几类。

    通过规范特定的前缀,就可以形成一个便于查找和理解的目录树,以达到分类的目的。

    在特定前缀的规范下,次级分类的命名就可以按照功能或用途来做区分,比如button或share的icon就可以用不同的前缀来标识。强烈建议开发和设计定一个命名标准,这样开发就不用对设计出的图片进行重命名了,而且还可以保证两个部门有一致的认知。这个仅仅是一个简单的分类方法,在实际中需要灵活使用。如果一些文件都是用于某种特定类型的,那么可以自定义前缀。比如我对于按钮使用的形状就用了btn作为前缀,而忽略了它们本身的文件类型。

    不便于预览

    svg是一个特殊格式的文件,可预览性大大低于png等常用的图片格式,但幸好win下可以直接在文件目录下预览svg图像,效果十分不错。

    但是vd是一个xml文件,预览器很难识别出这是什么格式,所以对于vd的预览才是一个难点。我之后准备写一个as插件来解决这个问题。

    使用WebP

    webp作为一种新的图片格式,从Android4.0+开始原生支持,但是不支持包含透明度,直到4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。webp相比于png最明显的问题是加载稍慢,不过现在的智能设备硬件配置越来越高,这点差异越来越小。腾讯之前有一篇对于webp的分析文十分不错,如果你准备要用webp了,那么它绝对值得一看。
    注意:如果你的项目最低支持到4.2.1,那么你可以继续阅读了,如果项目还需要支持到4.0版本,我建议暂时不要上webp,成本太高。

    png转webp

    我们可以通过智图或者isparta将其它格式的图片转换成webP格式。

    webp的问题

    兼容性不好

    官方文档中说只有在4.2.1+以上的机型,才能解析无损或者有透明度调整的webp图片,4.0+才开始支持无透明度的webp图片。我通过云测发现,在4.0~4.2.1的系统中,带有透明度的webp图片虽然不会崩溃,但是完全无法显示。

    《APK瘦身记,如何实现高达53%的压缩效果》一文中也提到有alpha值的jpg图片,经过webp转换后,无法在4.0,4.1的Android系统上运行的问题,具体原因见官方文档:

    除了兼容性问题外,webp在某些机型和rom上可能会出现一些“神奇”的问题。在三星的部分机型上,部分有alpha通道的图中会有一条很明显的黑线(三星的rom对于shape的alpha的支持也有问题,是红线)。在小米2刷成4.xx的手机上,系统未能正确识别xml文件中描述的webp图片,也会导致加载webp失败。

    不便于预览

    因为webp的图片格式是很难预览的,as也没有办法直接预览webp格式,我一般是通过chrome浏览器打开webp,十分不方便。

    我们知道gradle在build时,有一个mergeXXXResource Task,它将项目的各个aar中所有的res资源统一整合到/build/intermediates/res/flavorName/{buildType}目录下。
    webpConvertPlugin这个gradle插件可以在mergeXXXResource Task和processXXXResource Task之间插入一个task,这个task会将上述目录下的drawable进行统一处理,将项目目录里的png、jpg图片(不包含.9图片,webp转换后显示效果不佳)批量处理成webp图片,这样可以让我们在日常开发时用png、jpg,正式发包时用webp。

    复用图片

    复用相同的icon

     

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

    上图中我通过组合的方式将长得一样的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,我的优化思路如下:

    • 列表由LinearLayout、RecyclerView组成

    • 分割线用统一的shape进行绘制,不用切图

    • 整个列表背景设置为白色

    • 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" />

    这种方法虽好,但是不要滥用。需要看代码的人知道这种思路,否则会出现不好维护的情况。当设计真的是认为两个图有如此的关系的时候才可这样实现,万不可耍小聪明。

    压缩图片

    图片的压缩策略是:

    • 优先压大图,后小图

    • 不压.9图(svg在侠义上不算图)

    • 对于开屏大图片的压缩需注意力度,要和设计确认后再做

    • 对于体积特别大(超过50k)的图片资源可以考虑有损压缩

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

    ImageOptim

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

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

    pngquant

    pngquant也是一款著名的压缩工具,对于png的疗效还不错,但它不一定就适合app中那种背景透明的小icon,所以对比起tinypng来说,优势不明显。

    tinypng

    tinypng是一款相当著名的商用压缩工具,tinypng提供了开放接口供开发者开发属于自己的压缩工具(付费服务)。tinypng对于免费用户也算友好,每月可以免费压缩几百张图片。
    我用gradle插件来使用tinypng,更加简单方便。我一般的做法是发版本前才做一次图片压缩,每次debug的时候是直接跳过这个task的,完全不影响日常的debug。

    
     

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

    有人说tinypng的缺点是在压缩某些带有过渡效果(带alpha值)的图片时,图片可能会失真,对于这种图片你可以将png图片转换为webP格式。

    注意事项

    aapt默认会在打包时进行图片的压缩工作(无论你知不知道,它一直在默默的工作),如果你已经做了图片压缩了,那么建议手动禁止这个功能,否则“可能会”出现图片二次压缩后反而变大的情况,原因请看:Smaller PNGs, and Android’s AAPT tool。

    
     

    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插件来评估项目中开发人员写的代码量的,它生成的报表也不错:

     

    现在我可以知道:

    • 哪些类空行数太多,是不是没有按照代码规范来;

    • 哪些类的代码量很少,是否有存在的必要;

    • 哪些类行数过多,是否没有遵守单一职责原则,是否可以进行进一步的拆分

    apk method

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

    利用Lint分析无用代码

    如果你想删掉没有用到的代码,可以借助as中的Inspect Code对工程做静态代码检查。

     

    Lint是一个相当强大的工具,它能做的事情自然不限于检查无用资源和代码,它还能检测丢失的属性、写错的单位(dp/sp)、放错像素目录的图片、会引起内存溢出的代码等等。从eclipse时代发展到现在,lint真的是越来越方便了,我们现在只需要点一点就行。Lint的强大也会带来相应的缺点,缺点就是生成的信息量过多,不适合快速定位无用的代码。我推荐的流程是到下图中的结果中直接看无用的代码和方法。

    注意:
    这种删除无用代码的工作需要反复多次进行(比如一月一次)。当你删除了无用代码后,这些代码中用到的资源也会被标记为无用,这时就可以通过上文提到的Remove Unused Resources来删除。

    通过proguard来删除无用代码

    手动删除无用代码的流程太繁琐了,如果是一两次倒还会带来删除代码的爽快感,但如果是专人机械性持续工作,那个人肯定要疯的。为了保证每次打包后的apk都包含尽可能少的无用代码,我们可以在build.gradle中进行如下配置:

    
     

    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全开,并且会有一些帮助调试的工具(比如:leakcanary,stetho),我们可以通过debugCompile和releaseCompile来做不同的依赖,有时候也会需要no-op(关于no-op的内容可以参考下开发第三方库最佳实践)。

    debug和release是android本身自带的两种生产环境,在实际中我们可能需要有多个环境,比如提测环境、预发环境等,我以rtm(Release to Manufacturing 或者 Release to Marketing的简称)环境做例子。
    首先在目录下创建rtm文件:

    复刻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库层面的做法:

    C++运行时库统一使用stlport_shared

    之前微信中的C++运行库大多使用静态编译方式,使用stlport_shared方式可减小APK包大小,相当于把大家公有的代码提取出来放一份,减少冗余。同时也会节省一点内存,加载so的时候动态库只会加载一次,静态库则随着so的加载被加载多份内存映像。

    把公用的C++模块抽成功能库

    其实与上面的思路是一致的,主要为了减少冗余模块。大家都用到的一些基础功能,应该抽成基础模块。

    总结

    app的瘦身是一个长期并且艰巨的工作,如果是小公司建议一个月做一次。大公司的话一般都会对app的大小进行持续的统计和追踪,有余力的小公司也可以多多借鉴一下。希望大家阅读完本文后可以着手对项目进行优化工作,带来真正的收益。

    展开全文
  • Android中5种app瘦身方式

    千次阅读 2017-03-06 11:13:19
    Android中5种app瘦身方式App瘦身的概念 App瘦身是指在不减少App功能的前提下,通过一些技巧将打包出来的apk体的体积尽可能减少。 这样做的好处有几个:加快用户下载速度,节省用户下载流量,提升用户下载体验。 ...
  • Android APP终极瘦身指南

    千次阅读 2018-07-23 14:09:54
    【原文地址 ...之前写了一篇《APK瘦身实践》侧重于实践和效果对比,后来受徐川兄点拨,建议改写成一篇更全面的瘦身终极杀招大全,深以为然,思考良久,新开一篇。 指南条例 第1条:使用一套资源 ...
  • APP瘦身优化

    2019-12-20 20:35:48
    1 为什么要瘦身优化 1.1 瘦身优化的好处 提高下载转化率 大型APP都有Lite版本 渠道合作商要求,降低推广成本 应用市场比如 Google Play 要求超过 100MB 的应用只能使用APK 扩展文件方式上传,由此可见应用包体积对...
  • Android App瘦身优化实践

    千次阅读 2017-04-10 14:11:24
    随着业务的快速迭代增长,美团App里不断引入新的业务逻辑代码、图片资源和第三方SDK,直接导致APK体积不断增长。包体积增长带来的问题越来越多,如CDN流量费用增加、用户安装成功率降低,甚至可能会影响用户的留存率...
  • iOS app瘦身优化之路

    千次阅读 2017-04-11 13:48:11
    缩减iOS安装包大小是很多中大型APP都要做的事,一般首先会对资源文件下手,压缩图片/音频,去除不必要的资源。这些资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的...对于app瘦身之前
  • 接手这个APP因为急着上线功能,所以我也没有对APP进行优化,先前是三个程序员开发的,上线以后APP包达到116M,每次运营出去推广下载是个痛苦的事,非常的慢,所以每次运营的小伙伴出去推广,都会带上地推盒子,我...
  • 今日,打开电脑,发现C盘可用容量竟然变得非常小,觉得很是可疑,如下图所示: 最初的反应是电脑中毒了,于是使用360卫士、360杀毒对C盘查杀,因为明明记得C盘有40多G的可用空间才对,出现这种情况,肯定是哪里出...
  • ios 如何减小app的大小

    千次阅读 2017-05-26 14:46:27
    本文译自:Guides and Sample Code 中对问题:How can I make my app smaller, so that it is faster to download and install? 的回答。本文介绍一些如何减小app首次下载安装时的大小的技巧。注意这里说的是“首次...
  • APP加固新方向——混淆和瘦身

    千次阅读 2017-03-15 20:39:41
    在阿里云云栖社区举办的在线培训中,来自阿里移动安全部的陵轩带来了题为《APP加固新方向——混淆和瘦身》的精彩分享。本次分享的主要内容包括APP加固的发展历程以及阿里内部对移动安全加固最新的研究——混淆和APK...
  • 像美团点评这种app体量,安装包都已经100多M,对于瘦身是由较强的意愿的。经过不断的积累,整理了一些App常见的减肥方法。 1、废弃的资源文件与代码 这个就不用多说了,随着“历史的沉淀”,会有大量的废弃业务的...
  • Mac OSX终极瘦身

    万次阅读 2018-09-18 09:58:14
    进入目录的两个方法 1. 打开Finder,使用快捷键command+shift+G打开“前往文件夹”功能,输入路径,确定即可 2. 打开命令行Terminal,输入命令: open 路径   === 独创秘方 === ...参考:https://app...
  • iOS 9 App 瘦身功能

    千次阅读 2016-04-26 10:59:26
    App 瘦身简介 当前市场上存在大量的 iOS 设备,因此有多种屏幕尺寸和分辨率,要想保证一个 App 在多种屏幕下的展示效果需要大量的资源(比如 png、jpeg 以及二进制的 PDF)。不幸的是,这导致用户需要下载一个巨大的...
  • 《React-Native系列》40、 ReactNative之bundle文件瘦身

    千次阅读 热门讨论 2017-03-28 09:58:26
    【背景】目前,我们的app中采用Native+RN的混合模式开发,每个由RN开发的页面,页面的加载都是加载的一个Bundle文件,而一个Bundle文件的大小为500-600Kb。在没有内置bundle文件的情况下,用户想要使用所有由RN开发...
  • 最近随着单位的线下单子变多,线上项目感觉打包慢的要死。所以得感觉解决一下了。 原因:如上图所示,发现app.js和...解决思路:对症下药--瘦身app.js和vendor.js. 解决方案:1、通过cdn来加载第三方库,具...
  • iOS App瘦身注意armv7 armv7s arm64

    千次阅读 2014-08-29 10:57:50
    iOS App 瘦成除了平常的ya'suo
  • 这是iOS 11 新增4个新功能的其中一个(自动卸载长期未使用app),当您开启之后,iPhone就会自动卸载掉未使用的应用,不过应用的文稿和数据将会保存。当您需要继续使用此应用时,就要重新下载安装。 ### 下面粘贴4个...
1 2 3 4 5 ... 20
收藏数 5,741
精华内容 2,296
热门标签
关键字:

app瘦身