1 从几件小事说起
春节在家帮姐姐的iPhone手机安装市面上形形色色的App,忘记她是使用4G流量包月了,于是在下载了10个App后,不但耗尽了流量,还按照0.3元/兆的价格扣了七八十元话费。后来我检查了这几个App的体积,发现每个App体积都是40-50M的样子,这让我很吃惊,因为我记得两年前这些App也就在10-20M的样子。
另一件记忆犹新的事情,是去公园景点游玩,当时公园门口有个活动“扫二维码下载App下单立减10元”,但是我发现下载这个40M的App要花费12元的流量,这样其实是要额外多花2元钱,所以“扫码立减”这件事情对于我这种小市民而言是很不划算的。
由此而得到一个结论,App安装包的体积一定要小,至少要比竞争对手的App体积小。
对于Android而言,国内的各大市场商店已经发现这个问题了,所以对于用户升级App,会为每个App提供增量下载的功能,所以App版本升级不再是几十M的流量,而只是下载1-2M的增量包就能升级到最新版本,这样就极大节省了流量。[1]
对于iOS而言,AppStore从iOS6开始提供增量更新功能。对于iOS6.x和iOS7.0,只要有文件改动过,这个文件就会进入到增量更新包中,比如说1个10M的文件,只改动了1K的内容,这个10M文件就会进入到增量更新包中,包还是很大。到了iOS7.1及更高版本,这个机制进行了改良,它会把这1K的改动内容放到增量更新包中,从而极大的减少了增量更新包的大小,但是安装的时候会变慢,因为要把这1K的改动内容合并到10M文件中,这是个很繁琐很费时的工作。[2]
尽管如此,以上种种措施只能解决升级用户的流量困扰,对新用户并无帮助。我们必须减小安装包的大小,才能吸引更多的新用户。
2 安装包为什么那么大
是什么让App安装包的体积变得如此之大?
我们在前面的章节看到了iOS和Android安装包的内部结构,对于可执行文件,我们无能为力;对于xml文件,这些文件在App打包压缩后会极大减小体积,所以也不用管它们;那么就只能在图片和音频文件上做文章了。
各位读者看到这里,都请停下手中的工作,检查一下自家App包中图片和音频文件的大小。图片但凡是大于1M的,都是需要瘦身的。对于500k-1M这个区间内的,也有瘦身的可能。我研究过很多知名的App,其中有很多图片都在2-3M的样子,其实真没有必要,之所以这么大,是因为UI设计人员提供的设计稿就是这么大,开发人员拿过来也不看文件体积大小直接就往项目里放,久而久之,App包的体积就大了。
在众多App之中,我印象最深的是一款旅游类软件,它的所有图片都不超过100k,甚至说50k以上的都屈指可数。这是把极致做到家的表现。
接下来说音频文件,对于应用类App而言,我见到的大都是App推送时发出的声音,这个声音很简单,不应该超过10k。但我在很多App中看到的音频,都在100k左右。这是我们优化的一个方向。网上有很多这样的软件,可以对音频进行大幅压缩。
3 png和jpg的区别和使用场景
设计师曾经问过我,App为什么不使用jpg图片,同样的尺寸,png格式的图片要比jpg图片大很多。
众所周知,png有透明通道,而jpg没有,此外png是无损的而jpg是有损压缩的,所以png中存储的信息会很多,体积自然就大了。
但是手机却偏偏对png情有独钟,会对其进行硬件加速,所以我们会发现,同样一张背景图,png虽然体积比jpg大但是加载速度却要快一些。
终上所述,对于App包中的图片,我们都使用png格式的,而对于要从网上加载的图片,考虑到流量以及下载速度,则使用jpg格式的,因为它有较高的压缩率,体积很小。
但是对于背景图、引导页,这种大尺寸的图片,我们还是倾向于使用jpg格式,虽然加载慢一些,但是体积小,减少了包的体积。我看过的App基本都是这么做的。
对于Splash广告图,就是那个每次开启App一闪而过的广告,由于我们隔三岔五就要从线上下载新的广告图并展示在Spalsh页面上,所以这里使用jpg格式的图片。
对于iOS,苹果规定启动页(Launchimage)必须是png图片,否则审核时就会被拒。
Google后来发布了一种新的图片格式,WebP,它的压缩率比jpg更好,已经慢慢普及。Android自然是支持的,iOS想要使用这种格式的图片,需要在程序中引入WebP解码器。
4 Splash、引导图和背景图
通过对50多款App中的图片逐个分析,我发现有3种比较典型的场景,大多数公司的解决方案是雷同的:
- Splash默认广告是体积最大的,而且对应不同机型,要做多套,根据我的经验,每张图控制在300-500k左右就可以了。分辨率再高,对于手机而言,看不出效果。
- 引导图,设计师每次都会给几张高分辨率的图片,然后程序员不加思索直接放到App中,体积自然就变大了。其实,仔细观察,你会发现,为了保持风格统一,这些图片的背景都是一样的。所以我们完全可以这样做,比如说背景上有一只小兔子:
- l 把背景与小兔子拆分成2张图片。如果另一个引导图的背景上有一只小鸭子,那么就只需要这张小鸭子的图片了,背景图可以复用。
- l 根据分辨率,动态放置小兔子的位置,动态拉伸背景图,使之铺满整个屏幕。
- 对于背景图,为了达到一种视觉效果,这张图片经常被添加虚化等效果,既然如此,没有必要做得太清晰,应该控制在50k左右,看到很多App中类似的背景图都在1M左右,实在没有必要。背景图一般使用jpg文件。
5 iOS的1倍图、2倍图和3倍图
iOS不使用像素作为单位,而是使用点这个单位,对于iPhone4及之后,1点等于2个像素;而对于iPhone3GS及之前,1点等于1个像素。这样就保证了之前在iPhone3GS上运行的App,不用修改也能在iPhone4上运行。[3]
但是原先适用于iPhone3GS的图片,比如说a.png的尺寸是30 x 40像素,在iPhone4中看起来就模糊了。于是我们必须为a.png再准备一张60 x 80像素的图片,命名为[email protected],也放到App项目中,这样App在运行时会根据屏幕是否为iPhone3GS来选择相应的图片。iPhone3GS会选择a.png,iPhone4会选择[email protected],如果没有这张2倍图,则选择a.png,所以就模糊了。
iPhone4S、iPhone5、iPhone5c、iPhone5s、iPhone6,它们都使用[email protected]这张2倍图。
直到iPhone6 Plus,才需要提供[email protected]的图片。如果没有这张3倍图呢,它会选择1倍图或2倍图,我尝试过只有2倍图的情况,在iPhone6Plus上确实是模糊的效果。
那么问题就来了,我们需要为每张图都提供1倍图、2倍图和3倍图这3张图片吗?
我看到一款国际版的App是这么处理图片的。它在提供了多国语言文字的同时,还为每张图片生成了1倍图、2倍图和3倍图。这就导致了这款App的体积非常大。看上去有点“宁可错杀一千,不可放走一个”的感觉,但只要反过来想,图片一张也不缺,永远不会模糊。
我查看过很多App的图片,发现1倍图铺天盖地,但并不是每张1倍图都有相应的2倍图和3倍图,或者是只有相应的2倍图而没有3倍图,当然,也有只存在2倍图和3倍图而找不到1倍图的情况。图片管理五花八门,乱七八糟。
但是在中国,可不是这样哦。我看过友盟给出的数据报告,中国iPhone3GS用户不足0.1%。于是,我有一个大胆的设想,就是把iOS App的包中所有的1倍图都干掉,为每张图生成2倍图和3倍图。
很多公司都有根据1倍图批量生成2倍图和3倍图的工具,我也曾用C#写过一个。但是我发现有问题,并不是每张图片转换后都清晰,矢量图可以拉伸,但是拉伸位图就会失真。当我反过来根据3倍图批量生成1倍图和2倍图时,却发现位图可以压缩,而矢量图压缩后会失真。于是一种好的解决方案是,先把所有图片按照位图和矢量图进行分类,属于矢量图的,要提供1倍图,然后批量转换为2倍图和3倍图;而属于位图的,则是提供3倍图,然后批量转换为1倍图和2倍图。
本文给出的解决方案,并不能有效减小iOS包的体积,说不定反而会增大包的大小,但是却能系统的对图片进行管理,从而确保每张图片都是清晰的。
6 在iOS中进行图片拉伸和旋转
在Android技术领域,流行.9图这个概念,从而极大的节省了图片的体积。iOS其实也可以这么干,使用iOS的图片拉伸的语法,可以把一张.9图铺满一个区域,比如说按钮,如下所示:
(UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets
其中capInsets这个参数是一个UIEdgeInsets类型的结构体,被capInsets覆盖到的区域将会保持不变,而未覆盖到的部分将会被用来平铺。
以上这个方法只适用于iOS5.0及以上版本,5.0以下版本有另外的解决方案,但是目前国内的App都只支持5.0以上版本了,所以这里我就不提及了。
对于箭头,更没必要准备上下左右4张图片,准备一张图片就够了,使用的时候在方向上进行旋转即可。
7 使用xml配置动画
动画主要体现在引导图中的动画,以及加载进度条上的动画。
对于做应用类App的开发人员,他们做动画不是很在行,所以他们会要求设计师提供gif格式的动画,或者二十多张图片进行轮播,以达到gif动画的效果,殊不知,在编程上简单了,但是App的体积却相应变大了。
比较简单的解决方案是,减少动画中的关键帧,来降低动画的大小。
稍微正规一点,还是要使用原生的Android或iOS原生代码来实现。任何复杂的动画,都是由四种简单的动画组成的,分别是:移动、旋转、缩放、渐变。在Android中,是使用xml来配置的,上述这四种简单动画都有对应的xml语法,可以很快拼凑出一个复杂的动画;而对于iOS,只好使用编码方式了。
我们为什么不仿照Android的xml动画实现技术,为iOS也量身定制一套xml的动画标签呢?从而不用写任何object-C代码,配置几行xml就展现一个动画。
我见过一家App就是使用这个思想在plist中配置属性来做iOS动画的,如图9-4所示,就是一个平移的动画:
图9-4 配置文件中的平移动画
基于这个配置,还需要有一个动画引擎,来解析这个配置文件,将其翻译成object-C原生语言。在设计模式中,我们称之为解释器模式。
不单如此,我还需要有个测试页面,通过在这个页面中修改动画的属性,然后点击按钮能立刻看到改动后的效果,而不需要重新运行App程序。点个按钮就能执行输入框中的xml脚本。
使用上述的若干方法,我们可以把1个500k左右的gif文件,减小到50k的几张图片,并且极大地节省了而开发成本。
8 iOS使用storyboard还是xib?
抱歉,我始终不喜欢storyboard,但是存在即合理。我曾经认为storyboard比xib大,是导致iOS安装包体积变大的一个原因,于是我做了一件探索性的工作,就是把storyboard中的页面拆分为若干个xib文件,然后重新打包,但是结果却是前后大小一致。
结论是,是否使用storyboard,对ipa包大小没有影响。
9 字体文件的学问
我在某个ipa包中发现了ttf格式的字体文件。起初还以为是他们的App使用了某种特定字体,但打开这个ttf文件后才发现,这里面存放的居然是图片,如图9-5所示:
图9-5 字体文件中的icon图片
每个icon对应一个十六进制的数字,比如说第一个是\Ue600,这个值是唯一的。
观察这个字体文件,我们看到所有的icon具有以下共性:
- 这些icon都是单色的,可以在App中的页面里设置这些icon为其他颜色,但也必须是单色。
- 这些icon可大可小,因为它们是一种“字体”,字体是矢量图,所以拉伸不会失真。
- 这个ttf文件体积很小,比做成单独的png图片要小。
有人立刻就会联想到iOS的1倍图、2倍图和3倍图,每次都要准备3张图片,分别适用于不同的手机型号。如果做成一个字体,就可以减少体积,再也不用设计@2x和@3x两套图了——这种方案仅限于单色图片。
我们一般到下述网站来把单色icon转换成字体文件:https://icomoon.io。或者使用FontLab这样的工具自己来制作。
接下来我们来看如何在App中推广这门技术。
1)Android
首先把这个字体文件放到assets目录下,如图9-6所示:
图9-6 assets目录下的字体文件
接下来,我们将icon和十六进制编码的映射关系保存在drawable资源文件中:
这样就可以使用R.id.font_icon_1_normal这样的语法来取出这个图片了。
也可以将其设计为一个Drawable对象,然后设置给ImageView这样的控件。
2)对于iOS,实现思路差不多。
首先我们要把icmoon.ttf文件添加到项目中,如图9-7所示:
图9-7 UseIconInTTF中的icomoon.ttf文件
在Supporting Files目录下的UserIconInTTF-Info.plist文件中,增加一个配置,类型指定为Fonts provided byapplication,在其中添加对icomoon.ttf字体文件的声明,如图9-8所示:
图9-8 在UseIconInTTF-Info.plist配置icomoon.ttf
与Android类似,为了不直接使用\ue605这样的十六机制编码数字,我们将icon和十六进制编码的映射关系定义为一个宏TTFConstants:
接下来只要两行代码就能显示这个字体文件中的图片:
10 表情图片打包下载
对于表情图片。很多App中集成了聊天功能,有了聊天,自然就要提供各种表情图片,有静态图png,也有动画gif,虽然每个都不大,但是数量多啊,都打到包里面一起发布,会直接导致包变大。
考虑到实际的场景,用户不会一打开App就使用聊天功能,所以我们可以把这些表情图片打包成一个Zip包,在启动App的时候,在一个新的线程中异步下载这个Zip包然后解压到本地。这样以后聊天的时候就可以使用本地的图片了。对此,我们要做好版本增量升级功能,以确保有新表情图片的时候也能下载到本地后使用。
11 清除未使用图片
对于Android而言,Eclipse可以自动检查出哪些图片没有用到。
对于iOS而言,则需要写个小程序,逐一检查哪些图片没有使用到,注意,对于[email protected]和[email protected]的处理,要先将@2x和@3x过滤掉。
无论是Android还是iOS,即使发现到冗余图片,也不能直接删除,因为我们的程序经常会在代码中动态决定要显示哪些图片,我们只能检查这些图片在版本库的修改历史,来决定这些图片是否真的不需要了。
12 Proguard不只是用来混淆的
一提起Android中的Proguard,我们首先想到的是代码混淆,那是因为我们要经常去修改proguard.cfg文件,去keep那些不需要被混淆的类和方法。
其实,Proguard还能瘦身apk,在打包时它会帮忙检查那些不使用的类和方法,将其移除。最有效果的就是那些第三方SDK了,比如说aSmack这个用于xmpp的SDK有几万个方法,但其实我们只使用其中很小的一部分。Proguard会帮助我们把不使用的部分移除,从而极大的减小了apk的体积。
13 在iOS中使用pdf格式的图片
在研究各家App的过程中,我发现某款App的ipa文件中有几张pdf格式的图片。
在研究中还发现,有几款App的ipa包中的每张图片都做成imageset的形式,每个imageset目录中都同时存在这张图片的1倍图、2倍图和3倍图,如图9-9所示:
图9-9 cancelSelectedListBtn.imageset目录下的3张图片
上述这两件看似无关的事情,咋一看毫无关联,其实是使用了iOS的一个新技术。让我们从这些蛛丝马迹中,探赜索隐。
就是这几张pdf图片,我研究了半天,请设计师同学做成同样的png图片,体积相差不大,所以和png相比毫无优势。由于这个App的ipa包中有几百张图片,其中只有这3张图片是pdf格式的,所以我怀疑,这只是他们的新技术尝试。
再观察imageset目录下的那3张图片,我发现每张图片都是这样的。这不由使我意识到,一定是用了什么工具,一次性生成的这些图片。
搜索关键字ios+pdf,直到找到《Using Vector Images in Xcode 6》这篇文章[4],才发现这是iOS8才出现的一种新技术,只能在XCode6上使用。
在这里简单介绍一下这门技术,先绘制一张pdf矢量图,然后XCode6在编译的时候,会生成3张pdf格式的图片,分别是1倍图、2倍图、3倍图。这样就避免了图片不全导致的模糊,也避免了每次都要设计师准备3套图的麻烦。
但是,我们为什么要在用户的iPhone上装一些永远用不到的图片呢?苹果煞费苦心的搞出来这样一门技术,仍然没有解决App体积日益膨胀的现实,而且这门技术只会让App的体积变得更庞大——而这才是用户的痛点所在。如果是我来设计下一版iOS,我会让App中只包括pdf矢量图,只有在用户下载完App开始安装的时候,才会根据用户的机型,把pdf转换为相应的图片,比如说iPhone 6+上App生成的图片就是3倍图。
听说在15年6月9号召开的WWDC15大会上,苹果推出了AppThinning功能,效果如何有待进一步观察研究。
14 iOS的包永远比Android包体积大吗?
我比较了100多款App后发现,同一款App的iOS和Android版本,iOS的ipa包的一定比Android的apk包在体积上大很多。
但总是有特立独行的App,比如说某款著名视频播放软件,Android版本23.2M,而iOS版本才20.5M。我起初以为这个App的Android版本做的有问题,于是仔细研究了这个Android包里的内容,我就发现这家公司Android技术做的很精致,之所以比iOS版本体积大,是因为Android版本为几十种分辨率都适配了不同的图片和布局,以确保用户体验在任何分辨率下都是一致的。
如图9-10所示,居然有12种layout布局:
图9-10 Android项目中的Layout文件夹
drawable文件夹就更多了,高达28个,限于篇幅,这里就不贴图了。
以上只是特例,而大多数App并没有做的那么细致,比如说:
1)首先是不支持那么多分辨率。这是由当前App的开发现状导致的。一方面是产品经理和设计师在人力的不足,另一方面则因为设计师偏爱iPhone,一般只会给出iPhone版本的设计稿,然后让Android开发人员根据iPhone的设计稿去适配。于是Android开发人员只好去做UI自适应,使用.9图拉伸技术,实在搞不定了,才去找设计师重新给画一张。所以我们会看到iPhone的App大都很精致,相应的Android版本都很粗糙,这是因为Android App的UI很多都是开发人员凭着自己的审美观去二次加工的。
2)其次,你会发现,不同drawable目录下放置着不同内容的图片。用开发人员的画讲,好找。比如说drawable目录下放各种Selector文件,drawable–hdpi目录下放美食类图片,drawable-large目录下放门票图片。所以说,目录虽多,但其实只有一套图。殊不知,这样反而降低了App运行的速度,因为它在相应分辨率的drawable目录下找不到某张图片时,就会逐个遍历每个drawable目录下的图片,直至找到该图片的位置。