# 02.Apple官方资源瘦身方案ODR（二）：践行｜换肤系统改造

## 前言

> 如果你还不太清楚 `ODR: On-Demand Resources` 是什么，可以看看[Apple官方资源瘦身方案ODR（一）：初见](https://ryukiedev.gitbook.io/wiki/ios/you-hua/01.apple-guan-fang-zi-yuan-shou-shen-fang-an-odr-yi-chu-jian)

既然知道了 `ODR` 能干什么了，那就拿自己的项目开个刀。

这里使用我的个人项目[梦见账本](https://apps.apple.com/cn/app/id1498426607)，由于项目中有多套皮肤可以更换，所以存了很多套图标，这些图标就很适合使用 `ODR` 来优化。

关于 `ODR` 除了官方文档，也没找到很多实践的资料，这里就结合文档手摸手一起来实践一波吧。

### 先来个卖家秀

使用 `ODR` 之前

![1](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F1760e3d94c8b172b92af962f91aa56c851fdf7e3.jpg?generation=1628009053681528\&alt=media)

使用 `ODR` 之后！

![2](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F2a3125fb8019e0be181da991ae28c7d283c78b63.jpg?generation=1628009037709805\&alt=media)

> `TestFlight` 上看的大小，写文这会儿 `2.7.1` 还没提审发布，不过和真实下载的不会有多少差别。

## 一、 启用ODR

![build settings](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F5acbae6596657e57e10b3f9c55cfbe445fc79cf8.png?generation=1628009039793611\&alt=media)

> 从 `iOS9` 开始就是默认开启的了

## 二、 创建标签

标签用于识别和管理一组 `ODR`。将一个或多个标签分配给项目中的资源可将其标识为 `ODR`。在运行时，所有 `ODR` 的访问都与标签（而非单个资源）配合工作。

在 Target 的 Resource tags 可以看到添加标签的入口，这里可以选择项目中的资源，以添加到标签下。

![Resource tags](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F598d2002aa57774ebd997dee12b044b0ff947003.png?generation=1628009040855674\&alt=media)

***项目之前的资源组织方式：***

![之前的资源目录](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F4b290258a56c5409d59fcd9a80c18e4fb5e0737e.png?generation=1628009047602423\&alt=media)

***这里为了方便标签管理，进行一下资源包的拆分：***

![03](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Faf34de5ed9a31e7fbccf52d875ef1efdf4494924.png?generation=1628009044882914\&alt=media)

### 2.1 为 Assets 添加标签

将 `Assets` 添加到 Tag 下：

![04](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fb25cc4f7cb0b8710701796f9b38def264a7cbddc.png?generation=1628009052900987\&alt=media)

![05](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F4f3b425d5a5c59baf6a2bcf9c9c57917cb0459ff.png?generation=1628009047582408\&alt=media)

### 2.2 确认资源标签设置成功

打开资源包查看资源信息，就可以看到这里匹配的资源标签了。（一个资源可以设置多个标签）

![06](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fed5eb7b8cbed3d504b83bc5b8999ecf351410225.png?generation=1628009051049155\&alt=media)

在构建项目时，`Xcode` 会检查项目中的所有标记项目，并生成供操作系统使用的资源包。

### 2.3 为单个资源快速添加标签

也可以通过直接输入联想，快速为单个资源添已有标签，或者快速创建一个标签。

![07](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Ff9bf82c2d6485241ad7cc1626e37f33c30ec9b99.png?generation=1628009043812541\&alt=media)

## 三、 标签分类

通过切换到 `Prefetched` 我们看到了三种分类：

![08](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Ffe08ce4a543b7b4da3dc055df61880ac2048cefe.png?generation=1628009050350754\&alt=media)

创建的标签的默认类别是`仅按需下载ODR`。标签可以在类别之间拖动。

* ***Initial install tags.***&#x20;
  * 资源与应用程序同时下载。资源的大小包含在应用商店中应用的总大小中。当没有任何 `NSBundleResourceRequest` 访问时，可能会被清除。
* ***Prefetch tag order.***&#x20;
  * 安装应用后，资源开始下载。按标签顺序下载。
* ***Dowloaded only on demand.***&#x20;
  * 按需加载。当应用请求时，将下载这些标签。

> 当操作系统下载标签时，它只会下载设备上没有的资源。

### 3.1 设置标签分类

设置标签分类很简单，从默认的 ODR 列表中拖到相应的分类下即可。

这里我把默认的两个皮肤的资源设置为：`Initial install`

![09](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fa76c5147e0afa2b8d47bb121725e24322247a386.png?generation=1628009042483738\&alt=media)

## 四、 大小限制

应用剪切后，标签中资源的总大小不得超过 `512 MB`。应用商店中存储的按需资源的总大小不得超过 `20 GB`。

标签的理想大小不大于 `64 MB`。此大小在下载速度和本地存储之间提供了良好的平衡，以便在设备的本地存储处于低位时可以清除。

![10](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F6b13016a41c1ad893dc39d1a7a05e77cd42b210b.png?generation=1628009037273177\&alt=media)

* Slicing.&#x20;
  * 复选标记（✓）表示大小反映的是应用剪切后的大小。
* App bundle.&#x20;
  * 下载到设备的剪切后的应用程序包的大小。
* Asset packs.
  * 资产包由 Xcode 生成。
* ***Initial install tags.***&#x20;
  * 标记为初始安装的标记的总剪切后的大小。
* ***Initial install and prefetched tags..***&#x20;
  * 标记为 `Initial install` 和 `Prefetch tag` 的资源剪切后的总大小
* ***In use on-demand resources.***&#x20;
  * 一次行最多可用 `ODR` 的剪切后的大小。
* ***Hosted on-demand resources.***&#x20;
  * 所有 `ODR` 大小，不剪切的大小。

## 五、 管理ODR

配置了标签后，直接运行项目。可以看出所有的 `ODR` 资源均没有下载。

![11](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F2086fda8655f13f62136f9a68944f749a62680c6.png?generation=1628009039435398\&alt=media)

### 5.1 ODR 更新时机确定

这里先描述下 `梦见账本` 的换肤逻辑，来帮助确定 ODR 的更新时机:

* 皮肤分为两种模式：普通模式、暗黑模式。
  * 即：用户可以为两种模式分别设置皮肤，切换系统暗黑模式开关即可无缝切换
* 设置皮肤后会重新设置根控制器
  * 这里是需要更新 ODR 的一个节点
* 另一个检查更新的节点是应用启动的时候
  * 检查当前的两种皮肤，准备相关标签的资源

### 5.1 NSBundleResourceRequest 请求资源

一个标签可以被多个实例管理，下面列举三个重要的 `API`。

#### 初始化

* tags: 标签集合
* bundle: 一般是 main

```swift
public init(tags: Set<String>, bundle: Bundle)
```

#### 检查状态

检查请求的标签的资源是否全部都已经在设备中保存了。

```swift
open func conditionallyBeginAccessingResources(completionHandler: @escaping (Bool) -> Void)
```

#### 开始请求资源

请求不在本地的标签资源。

```swift
open func beginAccessingResources(completionHandler: @escaping (Error?) -> Void)
```

## 六、改造「梦见账本」

### 6.1 封装 OdrTool

```swift
protocol OdrTagProtocol {
    var tagName: String { get }
}

struct OdrTool {
    static func requestODR(of tags: OdrTagProtocol..., in bundle: Bundle = Bundle.main) {
        var tagsSet: Set<String> = []
        tags.forEach { tagsSet.insert($0.tagName) }
        let request = NSBundleResourceRequest(tags: tagsSet, bundle: bundle)
        request.conditionallyBeginAccessingResources { hasCache in
            if hasCache == false {
                // 这些资源不全在本地
                request.beginAccessingResources { error in
                    if let _ = error {
                        // 下载失败
                    }
                    else {
                        // 下载成功
                    }
                }
            }
            else {
                // 这些资源已经在本地了
            }
        }
    }
}
```

### 6.2 为皮肤系统 AppearanceStyle 拓展 Odr

`AppearanceStyle` 是真个换肤系统的核心，主体是一个 `枚举`。

* 这里让原先的皮肤都匹配一下自己的标签名就好了。
* 然后添加一个 `reloadOdr` 方法来更新当前两个皮肤的 `ODR`

```swift
extension AppearanceStyle: OdrTagProtocol {
    static func reloadOdr() {
        OdrTool.requestODR(of: currentLightStyle, currentDarkStyle)
    }

    var tagName: String {
        switch self {
        case .Charge, .ChargeMain:
            return "Charge"
        case .ChargeLight:
            return "ChargeLight"
        case .Eternal, .Hachimitsu:
            return "Eternal"
        case .Spark:
            return "Sakura"
        case .SparkMain:
            return "Spark"
        case .NYXL:
            return "NYXL"
        case .Justice, .Dream, .Phoenix:
            return "Justice"
        case .Kiwi:
            return "Kiwi"
        case .Banshee:
            return "Banshee"
        case .Punk:
            return "Punk"
        }
    }
}
```

### 6.3 在合适节点更新Odr资源

***节点一：应用启动***

```swift
AppearanceStyle.reloadOdr()
```

***节点二：设置皮肤***

```swift
static func reloadApp() {
    guard let tabVC = UIStoryboard(name: "Home", bundle: nil).instantiateInitialViewController() else {
        return
    }
    UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController = tabVC
    reloadOdr()
}
```

### 6.4 运行项目，检查效果

`Xcode` 中显示我设置的两个皮肤的资源已经下载完成了。

![12](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fc6768b00440fd9a1f7e2ca3483d5217246af204e.png?generation=1628009042856635\&alt=media)

但是打开 App，发现还是空白？

![13](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2F17a180e8f948af3ca8b5827f5f28ed9c37b19c80.png?generation=1628009048955664\&alt=media)

### 6.5 解决问题

通过之前的了解，如果 ODR 资源没有被任何 `NSBundleResourceRequest` 使用的话就会被清理。而上面的封装并没有持有 `NSBundleResourceRequest`。对代码进行一下调整。

***OdrTool***

```swift
struct OdrTool {
    static func requestODR(of tags: OdrTagProtocol..., in bundle: Bundle = Bundle.main) -> NSBundleResourceRequest {
        ...
        let request = NSBundleResourceRequest(tags: tagsSet, bundle: bundle)
        ...
        // 把 NSBundleResourceRequest 抛出
        return request
    }
}
```

***AppearanceStyle***

```swift
extension AppearanceStyle: OdrTagProtocol {
    // 添加一个静态变量保存
    static var odrRequest: NSBundleResourceRequest?

    static func reloadOdr() {
        odrRequest = OdrTool.requestODR(of: currentLightStyle, currentDarkStyle)
    }

    var tagName: String {
        switch self {
            ...
        }
    }
}
```

### 6.6 检查修改后的效果

完美～ 又测了下切换皮肤的功能也是没问题的

![14](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fba32d284d4473cf9904899291432f92942bb5878.png?generation=1628009047889720\&alt=media)

再看 `Xcode` 发现，标签的状态不一样了，两个正在使用中的标签成了 `In use` 正在使用。

![15](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI8JgbGh3U6X_oedqkm%2Fsync%2Fe0745e7729ab0c6935e4e15ce1a6e3f55676ec6b.png?generation=1628009051802675\&alt=media)

### 6.7 使用资源的地方需要改动么？

不用

## 七、 问题

### 7.1 覆盖安装的问题

我遇到了这样的问题：

* 设备安装了线上发布的版本（ODR优化过的），运行展示都没问题
* 准备进行下个版本的开发，直接运行，覆盖正式包，无法展示资源
* 尝试一：猜测和版本号有关，修改版本号，依旧无效
* 尝试二：删除旧包再运行，有效

> 上面的问题均可以 100% 复现
>
> ## 总结

体验下来还是很顺滑的，感觉接入成本不高。个人项目自己比较熟，而且换肤体系封的比较好😜也为接入 ODR 带来了很大便利。而且我单个皮肤的资源自由 `2M` 左右下载也挺快。如果是一些较大的资源加载可能就不能像我处理的这么简单了。而且 `NSBundleResourceRequest` 还有很多设置我没用，想深入了解的可以看下官方文档，这里我就抛个砖🧱。

而且任何优化方案的选择还是要结合实际项目来，能一股脑照搬的方案是不存在的。苹果的这个 `ODR` 挺好的，却感觉一直没有进入到广大 `iOSer` 的视野中，感觉挺可惜，如果感觉合适你的项目真的可以用起来哦～

## 👋 Bye～

* Wechat: RyukieW
* 📦 [技术文章归档](https://ryukiedev.gitbook.io/wiki/)
* 🐙 [Github](https://github.com/RyukieSama)

|                                   我的个人项目                                   |                     扫雷Elic 无尽天梯                    |                         梦见账本                        |
| :------------------------------------------------------------------------: | :------------------------------------------------: | :-------------------------------------------------: |
|                                     类型                                     |                         游戏                         |                          财务                         |
| [AppStore](https://apps.apple.com/cn/developer/rongqing-wang/id1264542103) | [Elic](https://apps.apple.com/cn/app/id1488204246) | [Umemi](https://apps.apple.com/cn/app/id1498426607) |

## 参考

* [Apple官方资源瘦身方案ODR（一）：初见](https://ryukiedev.gitbook.io/wiki/ios/you-hua/01.apple-guan-fang-zi-yuan-shou-shen-fang-an-odr-yi-chu-jian)
* [What is app thinning? (iOS, tvOS, watchOS)](https://help.apple.com/xcode/mac/current/#/devbbdc5ce4f)
* [On-Demand Resources Guide](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/On_Demand_Resources_Guide/index.html#//apple_ref/doc/uid/TP40015083-CH2-SW1)
