本篇文章是针对上述想法的分析调研,具体内容如下。
在iOS开发中,读取本地图片资源的方式有两种:UIImage(named: "")、UIImage(contentsOfFile: "")。
这种方式是通过文件的特定路径来加载图片:首先会通过图片文件的特定路径来找到图片文件资源,然后将图片文件资源转成NSData二进制,然后将NSData二进制转成UIImage对象。
以这种方式来读取图片,每一次读取操作都会重复进行上面的流程,读取一次数据就会产生一次NSData以及产生一个UIImage,当图片创建好之后销毁对应的NSData,当UIImage的引用计数变为0的时候自动销毁UIImage,这样的话就会保证图片不会长期存在于内存中。
以这种方式加载出来的图片,在使用完之后就会被销毁,图片的生命周期可以得到管理,因此可以减少内存的浪费。
当我们需要某图片的时候,就会去沙盒中读取这个图片文件,转换成UIImage对象来使用,现在假设这样的一个场景:
如果我们采用imageWithContentsOfFile:这种方式加载图片,那么在上面的场景下,就会占用5*7=35kb内存;而且会涉及到多次的IO操作,这是很耗性能的。可是如果是使用imageNamed:方式加载图片的话,由于全部取自字典缓存中的UIImage,这样无论有几处显示图片,都只会占用5*1=5kb内存;而且同一张图片资源最多只会涉及到一次IO操作。这个场景下,孰优孰劣一看便知。
这种图片加载方式一般是用在图片数据很大,并且该图片不需要多次使用的情况下。比如:引导页图片,一般只在软件第一次启动的时候会展示,以后就不会用到了。
这种方式是通过文件的名称来加载图片:它会在bundle中去根据名称查找对应的图片资源,找到之后将图片文件资源转成NSData二进制,然后将NSData二进制转成UIImage对象。可以看到,不管是UIImage(named: "")还是UIImage(contentsOfFile: ""),都是现将图片转成NSData,再将NSData转成UIImage,这是二者的共性。
二者的不同点在于,UIImage(named: "")这种方式,会建立一个图片缓存,我们可以理解成,缓存的图片是放入一个字典中,key是图片名,value是图片对象。调用UIImage(named: "")这个方法加载图片的时候,会首先在这个字典里面获取图片,如果取到就直接返回;如果没有取到,就再从文件中进行创建,然后保存到这个字典之后再返回。由于字典的key和value都是强引用,所以一旦创建后放入缓存字典中的图片,将永不销毁。
以这种方式加载出来的图片,即便是在多个地方同时使用,那么其对应的UIImage对象也只会被转换、创建一次,这就减少了沙盒的读取操作。
第一次读取的图片会保存到缓冲区,然后永不销毁,如果这个图片过大,占用几百kb,并且图片的使用频率不高甚至只会使用到一次,那么由于这一块的内存不会释放,将必然导致内存的浪费!而且这个浪费的周期是与app的生命周期同步的。
这种图片加载方式适用于小图,或者使用频率很高的图片。比如:界面中的各种小icon等。
大致思路就是,客户端将图片资源打包压缩,然后传到服务端,应用程序启动的时候将压缩包下载下来,解压后保存到本地沙盒。
在最大程度上减小了包体积。
苹果官方瘦身方案(App Thinning)有三种:App Slicing、BitCode、On-Demand Resources,其中App Slicing是App Thinning的主体流程,也是我们在平常开发中一直在使用的,详述如下。
App Slicing是为应用捆绑包创建、分发不同变体以适应不同目标设备的过程,一个变体只包含针对某个目标设备的可执行架构和资源。换句话说就是,App Slicing只会向设备传送与之相关的资源(这取决于设备的屏幕分辨率、CPU架构等)。
举个例子,现在你准备要提交一个版本的APP,于是你向iTunes Connect上传了ipa文件,然后AppStore会对该应用程序进行分割,针对不同的设备来创建不同的变体。
可以看到,我们虽然在向iTunes Connect上传的ipa文件中包含了1倍图、2倍图和3倍图,但是真正分发到用户设备上的,只会是其一,要么是1倍图,要么是2倍图,要么是3倍图,这取决于用户设备的屏幕分辨率。
以上就是官方瘦身方案中的App Slicing的过程。
如果我们是将所有图片打包压缩,在启动的时候再去下载图片压缩包资源,那么区分1倍图、2倍图和3倍图就没有什么意义了,因为这个时候已经不能使用官方的App Slicing瘦身方案了,这个时候为了确保大屏上的显示效果,只能是将3倍图放入压缩包中,这样对那些使用2倍图或者1倍图就可满足分辨率需求的手机用户而言,就非常不友好了。
该方案会在应用程序启动的时候将压缩包下载下来,解压后保存到本地沙盒,在加载图片的时候会在沙盒中去获取对应的图片资源。
而要获取沙盒中的图片资源,只能是使用UIImage(contentsOfFile: "")这种方式,以这种方式来加载图片的话,每一次加载都会重新产生一次NSData以及产生一个UIImage,当图片创建好之后销毁对应的NSData,当UIImage的引用计数变为0的时候自动销毁UIImage(这就是一次IO操作)。
而一个页面中各种大大小小的图标少则十几、二十个,多则上百个,也就是说,每打开一个页面就会涉及到几十上百次的图片IO操作,这会占用大量的CPU和内存资源,极有可能影响到用户界面流畅度,进而降低用户体验。
接着上面第(2)点,如果只是简单粗暴使用UIImage(contentsOfFile: "")这种方式来将图片加载出来,那么就会涉及到大量的IO操作,进而影响用户体验。
为了减少IO,那么就要自己去做缓存,我们可以参考UIImage(named: "")的缓存思路去自己实现。如果要把这套缓存方案做好,需要花费不小的精力。
由于要在第一次打开应用程序的时候下载压缩包资源,这会导致启动时间增长。
而且压缩包资源要么就整体下载成功,要么就整体下载失败,不能只下载一部分来使用,所以如果下载失败的话,整个聊天室模块就使用不了。
这些都是影响用户体验的。
如果只是为了减小包体积,业界很少有开发者会采取该方案。少数使用该方案的项目,其目的也不是说为了减小包体积,而是为了动态调整图片资源。
上述列明的几条只是目前能想到的弊端,由于采用该方案的项目很少,后面可能有很多坑点我们无法预知,不确定性比较大。
图片资源以压缩包的形式放到服务端,在应用程序启动的时候批量下载,这个方案(下面简称"该方案")的目的就是为了尽可能减小包体积。
但是该方案有很多的弊端,比如放弃了苹果官方瘦身方案中的App Slicing、IO操作过于频繁进而影响用户体验、增加了开发时间、不确定性大。
除此之外,关于压缩包资源的版本更新以及新老版本兼容问题也需要从长计议,不然版本问题处理不好很容易出问题,如果是最简单粗暴的将每一个版本的压缩包资源都在服务器单独存放一份,这也很浪费服务器资源。
该方案只是为了在用户下载IPA的时候节省流量和磁盘空间,但是实际上用户还是会将图片在启动的时候一次性下载下来,本质上并没有为用户省流和节省磁盘空间,甚至在一些低分辨率手机上还会增加用户的磁盘占用(本来用@2x图就可以的,但是却下载了@3x图)。
总之,该方案很多坑点,不建议采用。
对于大图片资源,我们可以采用已经存在的动效资源(大礼物MP4、头饰卡/头像框SVGA等)的配置下发方式;
对于能够动态加载的图片资源,直接在对应业务中通过完整URL加载即可;
对于图标类的小图资源(将大图资源剔除之后,剩下的也就5M左右),依旧放在.xcassets里面,打包进IPA,不采用压缩包动态下发的方式。