Bundles and Packages

什么嘛,原来我挺会搬运的嘛.JPG

In this season of giving, let’s stop to consider one of the greatest gifts given to us by modern computer systems: the gift of abstraction.

在这个给予的季节,让我们停下脚步,思考一个现代计算机系统赐予我们的最棒的礼物:抽象。

Consider those billions of people around the world who use computers and mobile devices on a daily basis. They do this without having to know anything about the millions of CPU transistors and SSD sectors and LCD pixels that come together to make that happen. All of this is thanks to abstractions like files and directories and apps and documents.

在数百万 CPU 晶体管、SSD 扇区和 LCD 像素共同协作下,全球数十亿人能够日常使用计算机和移动设备而对此全然不知。这一切都应归功于像文件,目录,应用和文档这样的抽象。

This week on NSHipster, we’ll be talking about two important abstractions on Apple platforms: bundles and packages. 🎁

这周的 NSHipster,我们将讨论苹果平台上两个重要的抽象:包与包裹。🎁


Despite being distinct concepts, the terms “bundle” and “package” are frequently used interchangeably. Part of this is undoubtedly due to their similar names, but perhaps the main source of confusion is that many bundles just so happen to be packages (and vice versa).

尽管是不同的概念,包与包裹这两个术语经常会被替换使用。毫无疑问,造成困惑的部分原因出自它们相似的名称,但或许主要原因是许多包恰好也是包裹(反之亦然)。

So before we go any further, let’s define our terminology:

在我们深入之前,先定义一下这两个术语:

  • A bundle is a directory with a known structure that contains executable code and the resources that code uses.
  • A package is a directory that looks like a file when viewed in Finder.

  • 包是指具有已知结构的,包含可执行代码,以及代码所需的资源的目录。

  • 包裹是指在访达中看起来像是文件的目录。

The following diagram illustrates the relationship between bundles and packages, as well as things like apps, frameworks, plugins, and documents that fall into either or both categories:

下图展示了包与包裹之间的关系,将应用、框架包、插件包和文档分别放入一个或多个分类之中:

QQ20190818-173041.png

If you’re still fuzzy on these distinctions, here’s an analogy that might help you keep things straight:

Think of a package as a box (📦) whose contents are sealed away and are considered to exist as a single entity. Contrast that with bundles, which are more like backpacks (🎒) — each with special pockets and compartments for carrying whatever you need, and coming in different configurations depending on whether it’s for taking to school, work, or the gym. If something’s both a bundle and a package, it’s like a piece of luggage (🧳): sealed like a box and organized into compartments like a backpack.

如果对两者的区别你依然感到困惑,这个类比或许能帮助你理解:
把包裹想象成是一个内容被隐藏的盒子(📦),作为一个独立的实体而存在。这点与包不同,包更像是一个背包(🎒) —— 每一款都有特殊的口袋和隔层用来携带你需要的东西,不同的配置用以决定是带去学校,去工作,还是去健身房。如果某样东西既是包也是包裹,恰似行李(🧳)一般:像盒子一样浑然一体,像背包一样分隔自如。

Bundles

Bundles are primarily for improving developer experience by providing structure for organizing code and resources. This structure not only allows for predictable loading of code and resources but allows for system-wide features like localization.

包为代码和资源的组织提供了特定结构,意在提升开发者的体验。这个结构不仅允许预测性的加载代码和资源,同时也支持类似于本地化这样的系统性特性。

Bundles fall into one of the following three categories, each with their own particular structure and requirements:

包分属于以下三个类别,每一种都有它自己特殊的结构和要求:

  • App Bundles, which contain an executable that can be launched, an Info.plist file describing the executable, app icons, launch images, and other assets and resources used by the executable, including interface files, strings files, and data files.

  • Framework Bundles, which contain code and resources used by the dynamic shared library.

  • Loadable Bundles like plug-ins, which contain executable code and resources that extend the functionality of an app.

  • 应用包(App Bundles):包含一个能被启动的可执行文件,一个描述可执行文件的 Info.plist 文件,应用图标,启动图片,能被可执行文件调用的接口文件,字符串文件,以及数据文件。

  • 框架包(Framework Bundles):包含动态分享库所需要的代码和资源。
  • 可加载包(Loadable Bundles):类似于插件,包含扩展应用功能的可执行代码和资源。

Accessing Bundle Contents

In apps, playgrounds, and most other contexts the bundle you’re interested in is accessible through the type property Bundle.main. And most of the time, you’ll use url(forResource:withExtension:) (or one of its variants) to get the location of a particular resource.

对于应用,playgrounds,以及其它你感兴趣的包来说,都能通过 Bundle.main 进行访问。大多数情况,可以使用 url(forResource:withExtension:)(或它的一种变体)来获取特定资源的路径。

For example, if your app bundle includes a file named Photo.jpg, you can get a URL to access it like so:

举例来说,如果应用中包含了一个名叫 Photo.jpg 的文件,用下面的方法能获得访问它的 URL:

1
`Bundle.main.url(forResource: "Photo", withExtension: "jpg")`

Or if you’re using the Asset Catalog, you can simply drag & drop from the Media Library (⇧⌘m) to your editor to create an image literal.

如果使用 Asset Catalog,你可以从媒体库(⇧⌘M)拖拽到编辑器来创建图像。

For everything else, Bundle provides several instance methods and properties that give the location of standard bundle items, with variants returning either a URL or a Stringpaths:

除此之外,Bundle 提供了一些实例方法和变量来获取标准包内容的位置,返回 URL 或 String 类型的路径:

URL Path Description 描述
executableURL executablePath The executable 可执行文件
url(forAuxiliaryExecutable:) path(forAuxiliaryExecutable:) The auxiliary executables 辅助的可执行文件
resourceURL resourcePath The subdirectory containing resources 包含资源的子目录
sharedFrameworksURL sharedFrameworksPath The subdirectory containing shared frameworks 包含共享框架的子目录
privateFrameworksURL privateFrameworksPath The subdirectory containing private frameworks 包含私有框架的子目录
builtInPlugInsURL builtInPlugInsPath The subdirectory containing plug-ins 包含插件的子目录
sharedSupportURL sharedSupportPath The subdirectory containing shared support files 包含共享支援文件的子目录
appStoreReceiptURL The App Store receipt App Store 的收据

Getting App Information

All app bundles are required to have an Info.plist file that contains information about the app.

所有的应用包都必须有一个包含应用信息的 Info.plist 文件。

Some metadata is accessible directly through instance properties on bundles, including bundleURL and bundleIdentifier.

bundleURLbundleIdentifier 这样的原数据能够通过 bundle 实例被直接访问。

1
2
3
4
5
6
import Foundation
let bundle = Bundle.main
bundle.bundleURL // "/path/to/Example.app"
bundle.bundleIdentifier // "com.nshipster.example"

You can get any other information by subscript access to the infoDictionary property. (Or if that information is presented to the user, use the localizedInfoDictionary property instead).

通过下标能从 infoDictionary 变量获得其他信息(如果信息要展示给用户,请使用 localizedInfoDictionary)。

1
2
bundle.infoDictionary["CFBundleName"] // "Example"
bundle.localizedInfoDictionary["CFBundleName"] // "Esempio" (`it_IT` locale)

Getting Localized Strings

One of the most important features that bundles facilitate is localization. By enforcing a convention for where localized assets are located, the system can abstract the logic for determining which version of a file to load away from the developer.

包的存在让本地化变得容易。强制本地化资源的存放位置后,系统便能将加载哪个版本的文件的逻辑从开发者层面抽象出来。

For example, bundles are responsible for loading the localized strings used by your app. You can access them using the localizedString(forKey:value:table:) method.

举个例子,包负责加载应用的本地化字符串。使用 localizedString(forKey:value:table:) 方法就可以获取到这些值。

1
2
3
4
5
6
import Foundation
let bundle = Bundle.main
bundle.localizedString(forKey: "Hello, %@",
value: "Hello, ${username}",
table: nil)

However, it’s almost always a better idea to use NSLocalizedString so that utilities like genstrings can automatically extract keys and comments to .strings files for translation.

然而,通常来说用 NSLocalizedString 会更好,像 genstrings 这样的工具能够自动取出键和注释到 .strings 文件中便于翻译。

1
NSLocalizedString("Hello, %@", comment: "Hello, ${username}")

Packages

Packages are primarily for improving user experience by encapsulating and consolidating related resources into a single unit.

包裹把相关资源封装和加固成一个独立单元,意在提升用户体验

A directory is considered to be a package by the Finder if any of the following criteria are met:

满足以下任意一个条件,目录就会被访达认为是包裹:

  • The directory has a special extension like .app, .playground, or .plugin
  • The directory has an extension that an app has registered as a document type
  • The directory has an extended attribute designating it as a package *

  • 目录有类似于 .app.playground.plugin 等特殊扩展。

  • 目录有一个被一个应用注册作为文档类型的扩展。
  • 目录具有有扩展属性,将其指定为包裹。

Accessing the Contents of a Package

In Finder, you can control-click to show a contextual menu with actions to perform on a selected item. If an item is a package, “Show Package Contents” will appear at the top, under “Open”.

在访达中,右键展示选中项目的可操作目录。如果选中项目是包裹,“打开” 操作下会出现 “显示包内容” 选项。

show-package-contents-c7cc72f58a573cb2fbe349e6f76a4ef29d14fbada3cd9b8376fc37979da16bf3.png

Selecting this menu item will open a new Finder window from the package directory.

点击这个选项会从包裹目录打开一个新的访达窗口。

You can, of course, access the contents of a package programmatically, too. The best option depends on the kind of package:

当然,也可以通过代码访问包裹中的内容。包裹的类型决定了获取内容的最佳方式:

  • If a package has bundle structure, it’s usually easiest to use Bundle as described in the previous section.
  • If a package is a document, you can use NSDocument on macOS and UIDocumenton iOS.
  • Otherwise, you can use FileWrapper to navigate directories, files, and symbolic links, and FileHandler to read and write to file descriptors.

  • 如果包裹有包的结构,前文所说的 Bundle 就能轻松胜任。

  • 如果包裹是一个文档,在 macOS 上使用 NSDocument 或在 iOS 上使用 UIDocument 来访问。
  • 其他情况下,用 FileWrapper 导航目录,文件和符号链接,用 FileHandler 来读写文件描述。

Determining if a Directory is a Package

Although it’s up to the Finder how it wants to represent files and directories, most of that is delegated to the operating system and the services responsible for managing Uniform Type Identifiers (UTIs).

虽说是由访达决定如何展示文件和目录,大多数的判断会被代理给操作系统以及管理统一类型标识(UTI)的服务。

To determine whether a file extension is one of the built-in system package types or used by an installed app as a registered document type, access the URL resource isPackageKey:

如果想要确定一个文件扩展是一个内置系统包裹类型,还是一个被已安装的应用使用的文档类型,调用 Core Services 方法 UTTypeCreatePreferredIdentifierForTag(_:_:_:)UTTypeConformsTo(_:_:) 能满足你的需求:

1
2
let url: URL = ...
let directoryIsPackage = (try? url.resourceValues(forKeys: [.isPackageKey]).isPackage) ?? false

Or, if you don’t have a URL handy and wanted to check a particular filename extension, you could instead use the Core Services framework to make this determination:

或者,如果没有方便的 URL 想要检查特定的文件拓展名,你可以用 Core Services 框架来做出此决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Foundation
import CoreServices
func filenameExtensionIsPackage(_ filenameExtension: String) -> Bool {
guard let uti = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
filenameExtension as NSString, nil
)?.takeRetainedValue()
else {
return false
}
return UTTypeConformsTo(uti, kUTTypePackage)
}
let xcode = URL(fileURLWithPath: "/Applications/Xcode.app")
directoryIsPackage(xcode) // true

If you want to set the so-called “package bit” for a file the hard way (rather than calling URL.setResourceValues(_:)), spelunking through CarbonCore/Finder.h indicates that you can do this by setting the kHasBundle (0x2000) flag in the com.apple.FinderInfo extended attribute:

我们找不到任何描述如何设置所谓的包裹比特(package bit)的文档,但根据 CarbonCore/Finder.h,在 com.apple.FindlerInfo 扩展参数中设置 kHasBundle(0x2000) 标示能够实现:

1
2
3
$ xattr -wx com.apple.FinderInfo /path/to/package \
00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 \
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

As we’ve seen, it’s not just end-users that benefit from abstractions — whether it’s the safety and expressiveness of a high-level programming language like Swift or the convenience of APIs like Foundation, we as developers leverage abstraction to make great software.

正如我们看到的那样,并非只有终端用户从抽象中获益 —— 无论是像 Swift 这样的高级编程语言的安全性和表现力,还是像 Foundation 这样的 API 的便利性,作为开发者也可以利用抽象开发出优秀的软件。

For all that we may (rightfully) complain about abstractions that are leaky or inverted, it’s important to take a step back and realize how many useful abstractions we deal with every day, and how much they allow us to do.

或许我们会抱怨 抽象泄漏抽象反转 带来的问题,但重要的是退一步,了解我们每天处理多少有用的抽象,以及它们带给了我们多少可能性。

Reference

https://swift.gg/2019/07/19/nshipster-bundles-and-packages/

https://nshipster.com/bundles-and-packages/

Title: Bundles and Packages

Author: Tuski

Published: 08/18/2019 - 17:21:39

Updated: 08/18/2019 - 17:51:22

Link: http://www.perphet.com/2019/08/Bundles-and-Packages/

Protocol: Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) Reprinted please keep the original link and author

Thx F Sup