当前位置: 首页 > 科技观察

说说iOS应用瘦身的想法

时间:2023-03-22 14:34:24 科技观察

1。前言前段时间发现我们APP的包大小超过了100MB,就随口问了卢大哥能不能用字体文件(.ttf)代替PNG图片。陆老板同意了。我对应用程序瘦身很感兴趣,所以让我做技术研究。本文主要是对我们各种技术方案的思路进行梳理和总结,希望对大家有所帮助。2.iOS内置资源集中化在介绍技术方案之前,我们先了解一下iOS内置图片资源的常见方式:2.1将图片打包存储是一种很常见的方式。项目中的各种文件分类放在每个bundle下,项目整齐,可以达到资源隔离的目的。我们项目中的大部分图片都是这样构建的,加载方式是[UIImageimageNamed:"xxx.bundle/xxx.png"](请记住这个字符串的规则,因为这个规则非常非常重要!!!"xxx.bundle/xxx.png").但是,这种方式有明显的缺点:首先,iOS系统没有使用bundle对图片进行压缩存储,增加了应用的体积。二是使用bundle存储图片,放弃APP瘦身。明显的表现是使用2倍屏手机的用户和使用3倍屏手机的用户下载的应用程序包大小相同。如果能实现APP瘦身,那么2倍屏手机的包体往往会比3倍屏手机的包体更小,达到差异化优化的目的。在研究过程中,我们还发现应用的大小与图像资源的数量密切相关(听起来像是废话)。也就是说,iPhonerom是4K对齐的,一张498B的图片在应用包里也占了4KB。因此,每个添加到项目中的图像至少增加4KB。为了确认这一点,创建了一个空应用程序进行测试。首先创建一个空的应用程序,在7P上大小为213KB。引入498B镜像前后对比如下:498B镜像占用4KB磁盘空间。添加图片资源后不添加资源的应用大小以上实验未在AppStore认证上线,仅通过本地打包测试,观点仅供参考。2.2使用.ttf字体文件替换图标使用字体文件替换图片也是一种比较常见的资源构建方式。很多应用都采用了这种方案,比如淘宝、爱奇艺等知名应用都采用了这种方式。使用字体文件的好处是显而易见的。如果APP中的图片比较大,UI可能会提供一个比较大的图标来保证清晰度。使用字体文件就可以避免这个问题,而且不需要导入@2x和@3x图片,一套字体文件就可以保证UI的清晰度。.ttf文件的生成方法这里就不赘述了(因为我不喜欢这种方案),我们只需要用到就行了。字体文件使用起来比较简单,但是使用方法和png图片有很大的不同,因为字体文件时显示的图标都是UTF8编码转换的字符串。所以当我们需要显示一个图标时,我们不再使用UIImageView,而是使用UILabel。字体文件中显示图片的代码示例由于我们使用的是字体而不是图片,所以我们可以通过设置字体的颜色来改变图标的??颜色。之前我们经常遇到一个场景,比如两个相同的图标但是由于颜色不同,UI同学需要提供2组图片,每组图片包含@2x和@3x图片。如果用字体代替简单的图标,那么UI只需要提供一套字体,拉伸后不会变形。使用字体文件的好处可以概括为两点:可以减少应用图片内置资源的体积。您可以随意缩放和修改颜色。但它的缺点也很明显:查找和替换图标比较麻烦,不像直接用图片那么简单。最重要的是,如果用在58同城APP中,就意味着不能更换已有的图片,只能起到减少增量的作用,不能减少全量。ps:任何一种需要大刀阔斧改革的优化都是不明智的行为。2.3图片存储在Assets.xcassets下(苹果推荐,我也推荐)使用Assets.xcassets是苹果推荐的一种方式。Assets.xcassets是iOS7推出的一款图片资源管理工具。Assets.xcassets内置图片时,系统会对图片资源进行压缩,支持APP瘦身。APPSlicing项目优化离不开场景,很多好的解决方案受限于场景无法优化。所以先简单介绍一下我们的项目场景:为了达到快速跨团队开发的目的,我们的项目很早就使用了cocoapods来实现组件化。项目中有多个业务pod,每个pod都有自己的团队维护。每个team的代码互不开放,每个pod最终都会编译成.a的形式。这里需要说明一下为什么强调.a。还有一个.framework对应.a。它们之间的一个重要区别是资源问题。资源可以存放在framework中,.a不可以,所以生成.a的pod下的资源会被转移到mainbundle中,存在资源冲突的隐患。为了避免这种冲突,我们在Resources之前使用了bundle管理,bundle名称很少重复,大大降低了资源冲突的可能性。优化的前提之一就是不破坏这种组件化的开发模式。也就是说,各个业务线不会产生资源耦合,业务线的RD之间不用担心资源冲突,业务Pod下的资源文件相互隔离。就算招聘组有a.png,房产组a.png也没有问题。所以我们首先要问两个问题:1、cocoapods是否支持使用Assets.xcassets。2、每个pod是否维护自己的Assets.xcassets会造成资源冲突。为了搞清楚以上两个问题,我们首先需要看一下podspec的几个重要参数:podspecs.public_header_files:表示该路径下的哪些文件可以在框架外引用。source_files:源文件路径。s.resources:资源文件路径和文件类型。s.resource_bundles:资源文件路径和类型,资源文件将被捆绑。(推荐使用)。实验发现每个pod都可以创建自己的xcassets,所以问题1不是问题而是问题。如果我们在每个业务pod下的.xcassets文件中创建内置镜像,那么cocoapods脚本在编译时会提取每个目录下xcassets文件的内容,合并成一个xcassets,生成一个.car文件。在这种情况下,如果资源文件同名,很可能其中一个文件会被覆盖替换。所以我们主要是想解决问题2,看podspec的写法,发现s.resource_bundles似乎是我们需要的法宝。为此,我们天真地以为问题很快就解决了:将指定路径下的资源打包成一个bundle的最终打包结果是很理想的。确实可以生成ImagesBundle.bundle,并且bundle下存在Assets.car。ImagesBundle存在于mainbundle下。Assets.car存在于ImageBundle.bundle下。可能已经看到了曙光,但是我们发现无法通过[UIImageimageNamed:@"ImagesBundle.bundle/1"];加载图片。您必须使用[UIImageimageNamed:@"1"inBundle:[WBIMViewControllericonBundle]compatibleWithTraitCollection:nil];加载它。如果图像加载失败并且指定了包,则加载成功。也就是说,只有Assets.car不在主bundle下,才需要指定bundle加载图片。既然加载图片需要指定一个bundle,那么如何获取这个bundle呢?也就是说,如何低成本地将当前项目中的图片放到特定bundle下的Assets.car文件中呢?我们为此提出一个解决方案:1.在pod下创建一个空文件夹。找到pod中存储图像的所有bundle,并在新文件夹中创建与bundle数量相等的Assets。2.修改podspec文件,设置resource_bundles指定Asset为资源,并指定bundle名称。比如A.bundle,它对应的Asset最终资源包是A_Asset.bundle。3、新方法imageWithName:从满足xxx.bundle/yyy.png特征的参数中获取bundle名称和图片名称xxx_Asset.bundle和yyy.png,获取图片并返回。4、将imageNamed:和imageWithContentOfFile:全部查找替换为imageWithName:只要能拿到原代码中imageNamed:的参数就可以知道当前图片存在于哪个bundle中,这样就可以通过imageNamed获取图片了:inBundle:,思路如下如图:imageWithName:方法内部处理打包后的bundle情况。看到这里,老司机应该能承受这个优化的代价。加载图片需要指定一个bundle,这意味着需要修改上千个API。刚开始在这里讨论的时候,我们首先想到的是脚本,但是这个方案很快就被否决了,因为项目中的XIB很多,我们不能用脚本来代替XIB中设置图片的API。为了解决XIB设置图片的问题,我们首先想到了AOP。hookXIb加载图片的方法偷偷换成了imageNamed:inBundle:的方法,但是很遗憾我们hook了UIImage中所有加载图片的方法,没有一个能拿到XIB上设置的图片名称,也就是说我们不能如果你知道优化图像在哪个包中,您不知道如何加载图像。虽然有波折,但是我们始终坚信XIB一定是通过一些方法来加载图片的,这个过程我们一定可以搞定!为了验证这个问题,首先定义一个UIImageView的子类,并指定XIB上的UIImageView为这个子类。大家都知道通过XIB加载的view肯定会执行initWithCoder:方法加载UIImageView的子类。我们发现通过lldb查看的slef.image在执行之前是nil[superinitWithCoder:aDecoder]。当这行代码被执行时,self.image就会有一个值。因此推断图片的信息(图片名称,路径等信息)在一个Decoder中!在网上查找了一些资料,发现aDecoder有一些固定的key,通过这些固定的key可以获取到部分信息。例如,解码器可以通过某些键获取信息。很明显,可以通过key“UIImage”获取到图片,可惜尝试了很多次都找不到图片的路径信息。因此,这个问题的关键是如何找到合适的密钥。为了解决这个问题,最好弄一个Decoder的解码过程。所以hookaDecoderdecodeObjectForKey:的解码方法是一个不错的选择。如果我们能拿到xib上设置的图片名,那么我们就可以根据图片名得到正确的图片路径。通过断点查看aDecoder是UINibDecoder类型(私有类)。aDecoderhookUINibDecoder的decode方法打印系统解码的所有key,发现有一个key是UIResourceName,value是图片的名字。也就是说,我们可以得到XIB上图片集的名称。但是如何将图片的名称传递给XIB对应的UIImageView对象呢?也就是说,我们如何将图片传递给XIB对应的view呢?为了将图片名称传递给UIImageView,我们需要添加一个block与aDecoder关联的引用。UIImageView在initWithCoder:中设置回调,并在hook的decodeObjectForKey:方法中将图片名称返回给initWithDecoder:方法:aDecoder钩住图片名称,然后回调到UIImageView类。这里需要注意一点,XIB默认设置的图片是在rentun值之后的,也就是说如果我们回调的太早,图片可能会被替换为nil。所以需要先dispatch_after,然后回调图片名,设置返回后的图片。受此启发,我们也可以hookUIImage的imageNamed:方法,根据参数的规则获取xxxCopy.bundle下的图片,返回图片。这意味着放弃通过脚本修改API,减少代码改动。这里看似没有问题,但是我们忽略了一个很严重的问题。aDecoder对象和UIImageView类型对象是一一对应的吗?imageView的aDecoder是唯一的吗?带着这个疑问,我们先来看一下打印信息:RepeatedgenerationofUIImageViewobjectandaDecodercomparisonrelationship重复生成对象打印,发现aDecoder的地址是一样的,也就是说有一个现象一个aDecoder对应多个UIImageView。所以异步方案不适用,需要同步设置图片,全局变量最合适。其实这很容易理解。aDecoder对应XIB,XIB不变,所以aDecoder不变。所以异步回调方案不适用,需要同步设置图片。这种情况下(主线程串行执行),最适合跨类传递全局变量:hookUINibDecoder的decodeObjectForKeyhookUIImageView的initWithCoder:以上两段代码只是介绍思路,加载图片的代码可能不是很严谨,请各位读者阅读自己识别。同理,hook项目中UIImage使用的加载图片的API可以加载图片。如果把所有的hook方法都放到一个类中,直接把这个类拖到项目中,把项目中bundle下的所有图片放到对应的Assets.xcassets文件中,那么一行代码都不需要修改将所有图片迁移到Assets.xcassets中,达到应用瘦身的目的。但是,我们组有经验的架构师指出,hooks作为项目中非常重要的API对,增加了项目维护的难度。这也引发了我对项目中AOP场景的思考。项目中hook了多少个API?可能在我这个领域混了很多年的老司机很难回答。为此,专门制作了一款以鱼钩为基础的钩印工具。检测并统计项目中的AOP情况。但缺点是必须调整编译顺序,保证先加载工具类。hookmethod_exchangeImplementationsmethod检测方法(写字典的时候别忘了加锁)