http://blog.csdn.net/JspAndAsp/article/details/49339403
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开始提供增量更新功能。对于iOS 6.x和iOS7.0,只要有文件改动过,这个文件就会进入到增量更新包中,比如说1个10M的文件,只改动了1KB的内容,这个10M文件就会进入到增量更新包中,包还是很大。到了iOS7.1及更高版本,这个机制进行了改良,它会把这1KB的改动内容放到增量更新包中,从而极大的减少了增量更新包的大小,但是安装的时候会变慢,因为要把这1KB的改动内容合并到10M文件中,这是个很繁琐很费时的工作。[2]
尽管如此,以上种种措施只能解决升级用户的流量困扰,对新用户并无帮助。我们必须减小安装包的大小,才能吸引更多的新用户。
2 安装包为什么那么大
是什么让App安装包的体积变得如此之大?
我们在前面的章节看到了iOS和Android安装包的内部结构,对于可执行文件,我们无能为力;对于XML文件,这些文件在App打包压缩后会极大减小体积,所以也不用管它们;那么就只能在图片和音频文件上做文章了。
各位读者看到这里,都请停下手中的工作,检查一下自家App包中图片和音频文件的大小。图片但凡是大于1M的,都是需要瘦身的。对于500KB-1M这个区间内的,也有瘦身的可能。我研究过很多知名的App,其中有很多图片都在2-3M的样子,其实真没有必要,之所以这么大,是因为UI设计人员提供的设计稿就是这么大,开发人员拿过来也不看文件体积大小直接就往项目里放,久而久之,App包的体积就大了。
在众多App之中,我印象最深的是一款旅游类软件,它的所有图片都不超过100KB,甚至说50KB以上的都屈指可数。这是把极致做到家的表现。
接下来说音频文件,对于应用类App而言,我见到的大都是App推送时发出的声音,这个声音很简单,不应该超过10KB。但我在很多App中看到的音频,都在100KB左右。这是我们优化的一个方向。网上有很多这样的软件,可以对音频进行大幅压缩。
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,苹果规定启动页(Launch image)必须是png图片,否则审核时就会被拒。
Google后来发布了一种新的图片格式,WebP,它的压缩率比jpg更好,已经慢慢普及。Android自然是支持的,iOS想要使用这种格式的图片,需要在程序中引入WebP解码器。
4 Splash、引导图和背景图
通过对50多款App中的图片逐个分析,我发现有3种比较典型的场景,大多数公司的解决方案是雷同的:
1. Splash默认广告是体积最大的,而且对应不同机型,要做多套,根据我的经验,每张图控制在300-500k左右就可以了。分辨率再高,对于手机而言,看不出效果。
2. 引导图,设计师每次都会给几张高分辨率的图片,然后程序员不加思索直接放到App中,体积自然就变大了。其实,仔细观察,你会发现,为了保持风格统一,这些图片的背景都是一样的。所以我们完全可以这样做,比如说背景上有一只小兔子:
- 把背景与小兔子拆分成2张图片。如果另一个引导图的背景上有一只小鸭子,那么就只需要这张小鸭子的图片了,背景图可以复用。
- 根据分辨率,动态放置小兔子的位置,动态拉伸背景图,使之铺满整个屏幕。
3. 对于背景图,为了达到一种视觉效果,这张图片经常被添加虚化等效果,既然如此,没有必要做得太清晰,应该控制在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像素的图片,命名为a@2x.png,也放到App项目中,这样App在运行时会根据屏幕是否为iPhone3GS来选择相应的图片。iPhone3GS会选择a.png,iPhone4会选择a@2x.png。对于iPhone4而言,如果没有这张2倍图,则选择a.png,所以就模糊了。
iPhone4S、iPhone5、iPhone5c、iPhone5s、iPhone6,它们都使用a@2x.png这张2倍图。
直到iPhone6 Plus,才需要提供a@3x.png的图片。如果没有这张3倍图呢,它会选择1倍图或2倍图,我尝试过只有2倍图的情况,在iPhone6 Plus上确实是模糊的效果。
那么问题就来了,我们需要为每张图都提供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覆盖到的区域将会保持不变,而未覆盖到的部分将会被用来平铺。
以上这个方法只适用于iOS0及以上版本,0以下版本有另外的解决方案,但是目前国内的App都只支持0以上版本了,所以这里我就不提及了。
对于箭头,更没必要准备上下左右4张图片,准备一张图片就够了,使用的时候在方向上进行旋转即可。
7 使用XML配置动画
动画主要体现在引导图中以及加载进度条上。
对于做应用类App的开发人员,他们做动画不是很在行,所以他们会要求设计师提供gif格式的动画,或者二十多张图片进行轮播,以达到gif动画的效果,殊不知,在编程上简单了,但是App的体积却相应变大了。
比较简单的解决方案是,减少动画中的关键帧,来降低动画的大小。
稍微正规一点,还是要使用原生的Android或iOS原生代码来实现。任何复杂的动画,都是由四种简单的动画组成的,分别是:移动、旋转、缩放、渐变。在Android中,是使用XML来配置的,上述这四种简单动画都有对应的XML语法,可以很快拼凑出一个复杂的动画;而对于iOS,只好使用编码方式了。
我们为什么不仿照Android的XML动画实现技术,为iOS也量身定制一套XML的动画标签呢?从而不用写任何objective-C代码,配置几行XML就展现一个动画。
我见过一家App就是使用这个思想在plist中配置属性来做iOS动画的,如图9-4所示,就是一个平移的动画:
图9-4 配置文件中的平移动画
基于这个配置,还需要有一个动画引擎,来解析这个配置文件,将其翻译成objective-C原生语言。在设计模式中,我们称之为解释器模式。
不单如此,我还需要有个测试页面,通过在这个页面中修改动画的属性,然后点击按钮能立刻看到改动后的效果,而不需要重新运行App程序。点个按钮就能执行输入框中的XML脚本。
使用上述的若干方法,我们可以把1个500KB左右的gif文件,减小到50KB的几张图片,并且极大地节省了而开发成本。
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资源文件中:
<string name="font_icon_1_normal"></string> <string name="font_icon_1_pressed"></string>
这样就可以使用R.id.font_icon_1_normal这样的语法来取出这个图片了。
TextView textView1 = (TextView) findViewById(R.id.textView1); Typeface font = Typeface.createFromAsset(getAssets(), "icomoon.ttf"); textView1.setTypeface(font); textView1.setTextSize(12); textView1.setText(getResources().getString(R.string.font_icon_1_normal));
也可以将其设计为一个Drawable对象,然后设置给ImageView这样的控件。
2)对于iOS,实现思路差不多。
首先我们要把icmoon.ttf文件添加到项目中,如图9-7所示:
图9-7 UseIconInTTF中的icomoon.ttf文件
在Supporting Files目录下的UserIconInTTF-Info.plist文件中,增加一个配置,类型指定为Fonts provided by application,在其中添加对icomoon.ttf字体文件的声明,如图9-8所示:
图9-8 在UseIconInTTF-Info.plist配置icomoon.ttf
与Android类似,为了不直接使用\ue605这样的十六机制编码数字,我们将icon和十六进制编码的映射关系定义为一个宏TTFConstants:
#define font_icon_1_normal "\ue605" #define font_icon_1_pressed "\ue606"
接下来只要两行代码就能显示这个字体文件中的图片:
[self.label1 setFont:[UIFont fontWithName:@"icomoon" size:12]]; [self.label1 setText: [NSString stringWithUTF8String: font_icon_1_normal]];
10 表情图片打包下载
对于表情图片。很多App中集成了聊天功能,有了聊天,自然就要提供各种表情图片,有静态图png,也有动画gif,虽然每个都不大,但是数量多啊,都打到包里面一起发布,会直接导致包变大。
考虑到实际的场景,用户不会一打开App就使用聊天功能,所以我们可以把这些表情图片打包成一个Zip包,在启动App的时候,在一个新的线程中异步下载这个Zip包然后解压到本地。这样以后聊天的时候就可以使用本地的图片了。对此,我们要做好版本增量升级功能,以确保有新表情图片的时候也能下载到本地后使用。
11 清除未使用图片
对于Android而言,Eclipse可以自动检查出哪些图片没有用到。
对于iOS而言,则需要写个小程序,逐一检查哪些图片没有使用到,注意,对于a@2x.png和a@3x.png的处理,要先将@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倍图。
苹果公司在iOS9中推出了App瘦身功能,据说能大幅减少要下载的App包的体积。具体效果如何,我们拭目以待。
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目录下的图片,直至找到该图片的位置。
15 从代码层面减少iOS包的体积
对于iOS而言,在ipa包中会有一个.a格式的二进制文件,这是代码编译后生成的文件,往往占据了整个ipa包的50%到80%的体积。苹果曾要求所有的App都支持64位,于是在此基础上,ipa包的体积又扩大了将近一倍,主要是那个.a文件编译后变大了。
我们要想办法减少这个.a文件的大小,其实就是要减少项目中的冗余代码。经过不断地摸索和尝试,我发现这些冗余代码分为3部分:
1)已经不使用的类。为此,我们需要写一个Python脚本,逐个检查哪些类不再使用了。检查的过程中我发现,某个类即使不使用了,但是在其他类中仍然保持对它的引用,所以我们要排除掉这种特殊情况,不让它对我们的检查工作造成影响。
还存在这么一种情况,在A类中使用了B。A类不再使用了,第一遍执行Python脚本找出来A类,将其删除了。这时B类就孤零零的放在那里,也不再使用了,所以我们有必要再次执行Python脚本,将B找出来。以此类推,不停地执行这个Python脚本,直到再也找不到不再使用的类为止。
2)已经不再使用的方法。这个找起来有些费劲,因为Objective-C独特的方法签名形式(方法签名由三部分组成,包括方法名称、参数和返回类型)。
仍然是写一个Python脚本,逐个遍历每个类中的方法,然后到项目中查找是否使用到了。
在执行过程中,遇到这么一种情况,A类和B类都有这个loveBaobao这个方法,方法签名也完全相同。这时Python是区分不出来到底是使用了哪个类的loveBaobao方法的。我们也只能将其汇总起来,然后用手动检查。
此外,有很多方法是系统自带的,比如说UITableView的那6个方法,只要是使用了UITableView的页面,都有这6个方法。我们在执行Python脚本的时候,不应该统计这样的方法。所以需要做一个白名单,事先把这些方法填进去。
3)代码相似度问题。初级程序员在写代码时,喜欢把一段代码从A类粘贴到B类中,然后修改其中的几个变量名称,这个功能就算做完了。于是两段相似度极高的代码就产生了。
稍微懂得些面向对象思想的人,都知道这时候需要把这样的代码抽象出来,比如说在Utils类中新建一个方法,然后要用到这段逻辑的人调用Utils类的这个方法即可。
但并不是所有的程序员都有这样的境界,即使是有几年经验的人,也会因为急着下班二人世界或者给孩子换尿布而采用复制粘贴大法敷衍了事。久而久之,冗余代码就多了,包的体积自然就大了。为此,我们需要有一个检查代码相似度的工具。在iOS领域,我推荐Simian这个工具。有兴趣的读者可以尝试一下,对你们的项目使用一下这个工具,看能找出来多少相似的代码来。
[1] 关于Android增量更新技术,请参见http://blog.csdn.net/hmg25/article/details/8100896
[2] 关于iOS增量更新机制,请参见https://developer.apple.com/library/ios/qa/qa1779/_index.html?utm_source=iOS+Dev+Weekly&utm_campaign=iOS_Dev_Weekly_Issue_114&utm_medium=email
[3] 详细内容请参见知乎上的这篇文章:http://www.zhihu.com/question/25421514/answer/31623909
[4] 文章参见http://martiancraft.com/blog/2014/09/vector-images-xcode6/