针对目前公司业务子库资源引用方式各异,包体大小超出正常值的问题,作出了一些调研.同时网上的部分说法可能随着时间的推移和cocoapods的更新已经过时或不可用.本文展示了资源引用的各种方式,也防止大家走弯路.
这篇文章介绍了关于CocoaPods的资源管理行为,对于Pod库作者是必须了解的知识。同时介绍了CocoaPods使用Asset Catalog的注意事项。如果已经了解某方面知识,可以大致略过直接看结论。
Asset Catalog和App Thinning
Asset Catalog,是Xcode提供的一项图片资源管理方式。每个Asset表示一个图片资源,但是可以对应一个或者多个实际PNG图,比如可以提供@1x
, @2x
, @3x
多张尺寸的图以适配;,还可以通过指定日间和夜间不同Appearances的两套图片。
这种资源,在编译时会被压缩,然后在App运行时,可以通过API动态根据设备scale factor来选择对应的真实的图片渲染。
App Thinning,是苹果平台(iOS/tvOS/watchOS)上的一个用于优化App包下载资源大小的方案。在App包提交上传到App Store后,苹果后台服务器,会对不同的设备,根据设备的scale factor,重新把App包进行精简,这样不同设备从App Store下载需要的容量不同,3x设备不需要同时下载1x和2x的图。
但是,这套机制直接基于Asset Catalog,换言之,只有在Asset Catalog中引入的图片,才可以利用这套App Thinning。直接拷贝到App Bundle中的散落图片,所有设备还是都会全部下载。因此如何尽量提升Asset Catalog利用率,是一个很大的包大小优化点。
CocoaPods的资源管理
CocoaPods是一个构建工具,它完全基于Pods的spec文件规则,在Podfile引入后,生成对应构建Xcode Target。也就是它是一个声明式构建工具(区别于Makefile这种过程式的构建工具)。对于资源的管理,目前有两个方式进行声明并引入,即resources
和resource_bundles
,参考podspec syntax
虽然Podspec中包含所有待构建库的声明,但于CocoaPods也会根据Podfile的配置,动态调整最终的Xcode工程的配置,根据是否开启use_framework!
,以下的资源声明最终的行为有所不同,这里分开介绍。
为了测试使用场景,我们新建了三个工程 工程目录如下:
主工程为 CatalogTest ,
资源工程为: ResourcesTest,ResourcesBubdlesTest
ResourcesTest 里包含资源文件:
ResourcesBubdlesTest包含资源文件:
不使用use_framework!
第一个测试:使用通配符导入 /Assets/**/*'
使用 resources
- 导入方式
s.resources = 'ResourcesTest/Assets/**/*'
使用resource_bundles
-
导入方式
s.resource_bundles = { 'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/**/*'] }
app 目录结构
而通过resources 导入的文件直接会在根目录导入两份并且不会 经过Xcode的资源优化
bundle也会以bundle和里面的所有文件在根目录导入
通过resource_bundles 形式导入的文件会生成一个自命名的 budle ,bundle里包含文件
结论: 按照**/*
导入的资源文件无论哪种形式都存在被重复导入的问题,这种懒省事的方式应该被制止
第二个测试:使用通配符导入 /Assets/*.xcasset'
,/Assets/*.bundle
ResourcesTest:
xcasseets并入主工程 .car
Bundle 并入主目录
s.resources = ['ResourcesTest/Assets/*.xcassets',
'ResourcesTest/Assets/*.bundle'
]
ResourcesBubdlesTest:
注意 bundle必须有自己的命名空间,否则还是以文件形式导入
Bundle 文件会以命名空间为名字放在主目录下
s.resource_bundles = {
'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets'],
'BlankLoading1' => ['ResourcesBubdlesTest/Assets/BlankLoading.bundle']
}
使用use_framework!
为了测试使用场景,我们新建了三个工程 工程目录如下:
主工程为 CatalogTest ,
资源工程为: ResourcesTest,ResourcesBubdlesTest
ResourcesTest 里包含资源文件:
ResourcesBubdlesTest包含资源文件:
第一个测试:使用通配符导入 /Assets/**/*'
使用 resources
-
导入方式
s.resources = 'ResourcesTest/Assets/**/*'
xcassets 生成.car, bundle文件依然双份
使用resource_bundles
-
导入方式
s.resource_bundles = { 'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/**/*'] }
app 目录结构
第二个测试:使用通配符导入 /Assets/*.xcasset'
,/Assets/*.bundle
每个子工程有自己的 framework,也就是有自己的命名空间,不会出现命名冲突等问题
ResourcesTest:
xcasseets并入主工程 .car
Bundle 并入主目录
s.resources = ['ResourcesTest/Assets/*.xcassets',
'ResourcesTest/Assets/*.bundle'
]
ResourcesBubdlesTest:
s.resource_bundles = {
'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets'],
'BlankLoading1' => ['ResourcesBubdlesTest/Assets/BlankLoading.bundle']
}
结论
无论任何场景,禁止 使用 podspecName/assers/**/*
形式引入资源文件,存在严重的资源重复引入问题,会显著增加包体大小!无法享有任何 Xcode的优化.
使用 resources
s.resources = ['ResourcesTest/Assets/*.xcassets',
'ResourcesTest/Assets/*.bundle'
]
使用 resource_bundles
s.resource_bundles = {
'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets'],
'BlankLoading1' => ['ResourcesBubdlesTest/Assets/BlankLoading.bundle']
}
不使用use_framework!
当不使用use_framework!时,最终对Pod库,会创建单独的静态链接库.a
的Target,然后CocoaPods会对主工程App Target增加自己写的脚本来帮助我们拷贝Pod的资源。
resources
里*.xcassets
资源会拷贝进入主目录Assets.car
,*.bundle
文件放入主目录下!
优点:
最简单暴力,而且由于固定了资源的路径在根路径上,如果先前在主工程目录中使用的代码,不需要更改一行即可继续使用。
缺点:
- 命名冲突问题,存在同名文件,会进行暴力合并!导致一个被替换掉.。因此,这要求所有资源文件命名本身,加入特定的前缀以避免冲突。类似的不止是图片,所有资源如
bundle
,js
,css
都可能存在这个问题,难以排查。而且由于这种拷贝到根路径的机制,这个问题不可从根源避免。
resource_bundles
文件*.xcassets
会放入 命名空间.bundle
下的Assets.car
,*.bundle
放入主目录下的命名空间.bundle
优点:
解决了部分命名冲突问题,*.xcassets
存在自己的命名空间.
缺点:
- bundle文件依然可能命名冲突
- 由于最终的资源文件增减了一级父文件夹.所以资源引用方式要做出改变
使用use_framework!
当使用了use_framework!之后,CocoaPods会对每个Pod单独建立一个动态链接库的Target,每个Pod最后会直接以Framework集成到App中。而资源方面,由于Framework本身就能承载资源,所有的资源都会被拷贝到Framework文件夹中而不再使用单独的脚本处理。
优点:
-
不会存在命名冲突。因为在使用use_framework!的情况下,由于资源本身被拷贝到Framework中,已经能最大程度减少冲突,因此这时候一般不需要考虑名称冲突问题.
-
都会使用Xcode的优化
缺点: 由于最终的资源文件增减了一级父文件夹.所以资源引用方式要做出改变
采用方案:
结合我们公司目前现状不能使用动态库.所以使用如下方案:
-
业务层使用
resources
导入资源-
资源文件使用
*.xcassets
*.bundle
*.text
方式导入,但是各子库文件会存在命名冲突问题,好处是不用改变资源引用方式, -
同时约定,各子库的资源添加子库名字缩写的前缀,避免暴力拷贝问题
-
-
基础服务层,和工具层使用
resource_bundles
- 防止出现基础服务里的资源被暴力替换
- 注意
.plist
.json
.html
需要建一个bundleName.bunlde
,否则无法导入!
资源文件取用方法:
-
resources
情况下s.resources = ['ResourcesTest/Assets/*.xcassets', 'ResourcesTest/Assets/*.bundle' ]
ResourcesBubdlesTest
是该私有库中的一个类需要注意的是 在不使用
use_framework
, 使用resources
的情况下, 所以的类文件都是打包到ipa根目录的,于此同时我们子项目的*.xcassets
文件同样并入 根目录 car.assets;bundle或者resouce 也是根目录,
而使用
use_framework
时候,寻址就是当前类所在framework的地址所以,无论是不是使用
use_framework
,对于私有库的image
文件我们都是可以通过下面方法调用bundle
寻址也一样!UIImage *ModuleImage(NSString *imageName) { NSBundle *bundle = [NSBundle bundleForClass:[ResourcesBubdlesTest class]]; return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil]; } NSBundle *BundleWithName(NSString *bundleName) { NSBundle *bundle = [NSBundle bundleForClass:[XHXRecordTools class]]; NSURL *url = [bundle URLForResource:bundleName withExtension:@"bundle"]; return [NSBundle bundleWithURL:url]; }
-
resource_bundles
情况下s.resource_bundles = { 'ResourcesBubdlesTest' => ['ResourcesBubdlesTest/Assets/*.xcassets'] 'BlankLoading1' => ['ResourcesBubdlesTest/Assets/BlankLoading.bundle'] }
使用
use_framework
那么都在命名空间内,下面读取方式不使用
use_framework
*.xcassets
文件保存在ResourcesBubdlesTest.bundle
的根目录下 ,使用UIImage *BundleImage(NSString *imageName, NSString *bundleName)
读取BlankLoading1.bundle
在ipa根目录,亦可读取UIImage *BundleImage(NSString *imageName, NSString *bundleName) { NSBundle *targetBundle = BundleWithName(bundleName); return [UIImage imageNamed:imageName inBundle:targetBundle compatibleWithTraitCollection:nil]; } NSBundle *BundleWithName(NSString *bundleName) { NSBundle *bundle = [NSBundle bundleForClass:[XHXRecordTools class]]; NSURL *url = [bundle URLForResource:bundleName withExtension:@"bundle"]; return [NSBundle bundleWithURL:url]; }