CocoaPods的资源管理和Asset Catalog优化

6,169 阅读7分钟

针对目前公司业务子库资源引用方式各异,包体大小超出正常值的问题,作出了一些调研.同时网上的部分说法可能随着时间的推移和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这种过程式的构建工具)。对于资源的管理,目前有两个方式进行声明并引入,即resourcesresource_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文件放入主目录下!

优点:

最简单暴力,而且由于固定了资源的路径在根路径上,如果先前在主工程目录中使用的代码,不需要更改一行即可继续使用。

缺点:

  1. 命名冲突问题,存在同名文件,会进行暴力合并!导致一个被替换掉.。因此,这要求所有资源文件命名本身,加入特定的前缀以避免冲突。类似的不止是图片,所有资源如bundle, js, css都可能存在这个问题,难以排查。而且由于这种拷贝到根路径的机制,这个问题不可从根源避免。

resource_bundles文件*.xcassets会放入 命名空间.bundle下的Assets.car,*.bundle放入主目录下的命名空间.bundle

优点:

解决了部分命名冲突问题,*.xcassets存在自己的命名空间.

缺点:

  1. bundle文件依然可能命名冲突
  2. 由于最终的资源文件增减了一级父文件夹.所以资源引用方式要做出改变

使用use_framework!

当使用了use_framework!之后,CocoaPods会对每个Pod单独建立一个动态链接库的Target,每个Pod最后会直接以Framework集成到App中。而资源方面,由于Framework本身就能承载资源,所有的资源都会被拷贝到Framework文件夹中而不再使用单独的脚本处理。

优点:

  1. 不会存在命名冲突。因为在使用use_framework!的情况下,由于资源本身被拷贝到Framework中,已经能最大程度减少冲突,因此这时候一般不需要考虑名称冲突问题.

  2. 都会使用Xcode的优化

缺点: 由于最终的资源文件增减了一级父文件夹.所以资源引用方式要做出改变

采用方案:

结合我们公司目前现状不能使用动态库.所以使用如下方案:

  1. 业务层使用resources导入资源

    • 资源文件使用 *.xcassets *.bundle *.text方式导入,但是各子库文件会存在命名冲突问题,好处是不用改变资源引用方式,

    • 同时约定,各子库的资源添加子库名字缩写的前缀,避免暴力拷贝问题

  2. 基础服务层,和工具层使用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];
    }