Bundle时macOS和iOS的基础架构技术,用于封装代码和资源。bundle通过提供需要资源的已知位置的方式, 简化了开发者需要创建组合二进制文件的工作。bundles使用目录和文件提供更自然的组织类型——一种可以被在开发时和发布时都能修改的类型。
为了支持bundle,Cocoa和Core Foundation 提供了编程接口来访问bundles的内容。因为bundle使用一个有组织的结构,所有developer 理解bundle的基本组织规划是重点。

Bundle 和 Packages
尽管bundle和package 差不多,但是他们有非常不同的理念:
  • A package 是一个 Finder 呈现给 用户的任何目录,有如 单个文件一样。
  • A bundle 是一个有标准目录结构,持有可执行code和code使用资源的目录。
Packages提供了一个基本的抽象,让macOS方便使用。如果你查看你电脑的的app或者插件,实际上看的是一个目录。package目录内部是能让app运行的代码和资源文件。当你和package文件交互式,Finder把它视为一个文件。这种行为可以防止普通用户做出可能对包的内容产生负面影响的更改。比如,防止用户重新打包或者删除资源或者代码模块,组织app正确运行。
注意: 尽管默认情况下  package被视为不透明文件,但用户仍然可以查看和修改其内容。 在包目录的上下文菜单上是Show Package Contents命令。 选择此命令将显示一个新的Finder窗口,该窗口设置为  package目录的顶层。 用户可以使用这个窗口来导航  package的目录结构并进行更改,就好像它是常规的目录层次结构一样。
虽然  package 可以改善用户体验,但  package更适合帮助开发人员打包代码并帮助操作系统访问该代码。   package定义了组织与软件相关的代码和资源的基本结构。 这种结构的存在也有助于促进诸如本地化等重要功能。 bundle 的确切结构取决于您是在创建app、framework还是plug-in。 它还取决于其他因素,例如目标平台和插件类型。
bundle and package 的使用原因有时被认为是可以互换的,许多类型的bundle也是package。 例如,可以装入App的一个bundle 可以视为一个packlage,因为它们通常被系统视为不透明的目录。 然而,并非所有的bundle都是package,反之亦然。
系统如何识别bundlepackage
以下情况下,Finder会把目录看作一个 package:
  • 目录有以下扩展名: .app, .bundle, .framework, .plugin, .kext 等
  • 目录有某些其他app要求描述 package 类型的扩展名;请参考 Document Packages
  • 目录的package bit已经设置
推荐指定一个package的方式是赋予一个给定的package 扩展名。大部分情况,Xcode 用 模板 来提供合适的扩展名。大部分bundle 也是 package。比如, app和插件是典型被finder 表示为单个文件。但是,这不是所有的 bundle 类型。特别是,framework是一种类型的bundle,它被当作一个单独的单元来处理,目的是为了链接和运行时使用,但是framework 目录 是透明的,这样开发者可以查阅 header 文件和其他包含的资源。
关于 bundle 的显示名
显示名使用户可以控制Finder在Finder中的显示方式,而不会破坏依赖它们的客户端。尽管用户可以自由重命名文件,但重命名应用程序或框架可能会导致相关代码模块按名称引用应用程序或框架。因此,当用户更改一个包的名称时,这种改变只是表面的。 Finder不是在文件系统中更改包名称,而是将一个单独的字符串(称为显示名)与该包相关联,然后显示该字符串。
显示名仅用于向用户呈现。您从不使用显示名打开或访问代码中的目录,但在向用户显示目录名称时使用它们。默认情况下,bundle的显示名与软件包名称本身相同。但是,在以下情况下,系统可能会更改默认的显示名:
  • 如果该包是一个应用程序,Finder在大多数情况下隐藏.app扩展名。
  • 如果该软件包支持本地化显示名称(并且用户未明确更改软件包名称),则Finder会显示与用户当前语言设置相匹配的名称。
尽管Finder大多数时候隐藏应用程序的.app扩展名,但它可能会显示它以防止混淆。例如,如果用户更改应用程序的名称,并且新名称包含另一个文件扩展名,Finder将显示“.app”扩展名以明确该bundle是一个应用程序。例如,如果要将.mov扩展名添加到Chess应用程序,则Finder将显示Chess.mov.app,以防止用户认为Chess.mov是QuickTime文件。
Bundle的好处
Bundle为开发人员提供以下优势:
  • 因为bundle是文件系统中的目录层次结构,所以bundle只包含文件。因此,您可以像打开其他类型的文件一样,使用所有基于文件的相同界面来打开Bundle资源。
  • Bundle目录结构可以轻松支持多个本地化。您可以轻松添加新的本地化资源或删除不需要的资源。Bundle可以驻留在多种不同格式的文件系统上,包括HFS,HFS +和AFP等多种分叉格式,以及UFS,SMB和NFS等单叉格式。
  • 用户只需在Finder中拖动它们即可安装,重新定位和删除Bundle。
  • Bundle也是package,因此被视为不透明文件,因此不易受到意外用户修改的影响,例如删除,修改或重命名关键资源。
  • 一个bundle可以支持多种芯片架构(PowerPC,Intel)和不同的地址空间要求(32位/ 64位)。它也可以支持包含专门的可执行文件(例如,针对特定矢量指令集优化的库)。
  • 大多数(但不是全部)可执行代码可以Bundle在一起。应用程序,框架(共享库)和插件都支持bundle模型。静态库,动态库,shell脚本和UNIX命令行工具不使用bundle结构。
  • Bundle的应用程序可以直接从服务器运行。本地系统上不需要安装特殊的共享库,扩展和资源。
Bundle的类型
尽管所有bundle都支持相同的基本功能,但您定义和创建定义其用途的bundle的方式有所不同:
  • 应用程序 – 应用程序bundle管理与可启动进程相关的代码和资源。此bundle的确切结构取决于您所定位的平台(iOS或MacOS)。有关应用程序bundle结构的信息,请参阅Application Bundles
  • 框架 – framework bundle管理动态共享库及其相关资源,例如头文件。应用程序可以链接到一个或多个framework,以利用它们包含的代码。有关framework bundle结构的信息,请参阅Anatomy of a Framework Bundle
  • 插件 – macOS支持许多系统功能的插件。插件是应用程序动态加载自定义代码模块的一种方式。以下列表列出了您可能想要开发的一些主要插件类型:
    • 自定义插件是您为自己的目的定义的插件;参见 Anatomy of a Loadable Bundle. 。
    • Image Unit插件为Core Image技术添加自定义图像处理行为;请参阅Image Unit Tutorial.。
    • Interface Builder插件包含要集成到Interface Builder库窗口中的自定义对象。
    • Preference Pane plug-ins 定义您想要集成到系统偏好设置应用程序中的自定义偏好;请参阅Preference Pane Programming Guide
    • Quartz Composer插件为Quartz Composer应用程序定义自定义补丁;Quartz Composer Custom Patch Programming Guide.。
    • Quick Look插件支持使用Quick Look显示自定义文档类型;请参阅Quick Look Programming Guide. 。
    • Spotlight插件支持自定义文档类型的索引,以便用户可以搜索这些文档;请参阅Spotlight导入器编程指南。
    • WebKit插件扩展了常用Web浏览器支持的内容类型。
    • 小组件将新的基于HTML的应用程序添加到仪表板。
  • 尽管文档格式可以利用bundle结构来组织其内容,但文档通常不被视为最纯粹意义上的bundle。无论其内部格式如何,作为目录被实现并被视为不透明类型的文档都被视为文档包。有关文档包的更多信息,请参阅Document Packages
创建一个 Bundle
通过Xcode直接创建。某些 Xcode的tagets(比如 shell tools和静态库)不会引发bundle 或者 package的创建。因为同城不需要为这些targets创建bundle。
编程支持来访问 bundles
引用bundle的程序或捆绑在一起的程序可以利用Cocoa和Core Foundation中的接口来访问bundle的内容。 使用这些接口,您可以找到bundle软件资源,获取有关buncle软件配置的信息,并加载可执行代码。 在Objective-C应用程序中,您使用NSBundle类来获取和管理bundle信息。 对于基于C的应用程序,您可以使用与CFBundleRef opaque类型关联的函数来管理一个bundle。
注意:与许多其他Core FoundationCocoa类型不同,NSBundleCFBundleRef不是toll-free桥接数据类型,不能互换使用。 但是,您可以从任一对象中提取bundle路径信息,并使用它创建另一个对象。
使用 bundles 指南
在MacOS和iOS中,bundle是首选的软件组织机制。bundle结构允许您将可执行代码和资源分组在一个地方并以有组织的方式支持该代码。以下准则提供了关于如何使用bundle软件的其他建议:
  • 始终在您的bundle中包含信息属性列表(Info.plist)文件。确保包含推荐用于bundle类型的密钥。有关可包含在此文件中的所有密钥的列表,请参阅Runtime Configuration Guidelines. 。
  • 如果应用程序无法在没有特定资源文件的情况下运行,请将该文件包含在应用程序bundle中。应用程序应始终包含所有需要操作的image,strings file,本地化资源和plug in。非关键资源应尽可能地存储在应用程序bundle内,但可能会在需要时放置在包外。有关应用程序的bundle包结构的更多信息,请参阅Application Bundles.。
  • 如果您打算从一个bundle中加载C ++代码,则可能需要将您打算加载的符号标记为extern “C”。 NSBundle和Core Foundation CFBundleRef函数都不知道C ++名称修改约定,所以用这种方式标记符号可以使它更容易在以后识别它们。
  • 您不能使用NSBundle类来加载Code Fragment Manager (CFM) code。如果您需要加载基于CFM的代码,则必须使用CFBundleRef或CFPlugInRef不透明类型的函数。您可以使用这种技术从Mach-O可执行文件加载基于CFM的插件。
  • 您应该始终使用NSBundle类(而不是与CFBundleRef opaque类型关联的函数)来加载任何包含Java代码的bundle。
  • 在加载包含Objective-C代码的bundle时,可以使用NSBundle类或macOS v10.5及更高版本中与CFBundleRef opaque类型关联的函数,但每种行为都有不同。如果使用Core Foundation函数加载插件或其他可加载的bundle(与框架或动态库相对),则这些函数会私下加载该包并立即绑定其符号;如果你使用NSBundle,这个bundle会被全局加载,并且它的符号会被延迟地绑定。另外,使用NSBundle类加载的bundle会导致生成NSBundleDidLoadNotification通知,而使用Core Foundation函数加载的包不会。
Bundle的结构
bundle的结构非常依赖于平台。注意: 尽管bundles是打包可执行代码的一种方式,但不是唯一支持的方式。UNIX shell scripts command line tools 不使用 bundle 结构,静态库和动态库也不用。
应用程序 Bundle
应用程序bundle是开发人员创建的最常见的bundle之一。 应用程序bundle存储应用程序成功操作所需的所有内容。 尽管应用程序bundle的特定结构取决于您正在开发的平台,但在两种平台上使用该bundle的方式都是相同的。
应用程序Bundle中的文件类型如下表:
Info.plist file 必需的,配置app的配置文件。系统在文件存在的app相关信息和文件上应用操作。
Executable 必需,每个 app必需有一个可执行文件,这个文件包含app的主 entry point和任何静态链接到app target的 代码。
Resource files
Resource是存在于应用程序可执行文件之外的数据文件。 Resource通常由images, icons, sounds, nib files, strings files, configuration files, and data files (among others)等等组成。 大多数资源文件可以针对特定语言或地区进行本地化或由所有本地化版本共享。
资源文件在软件bundle目录结构中的位置取决于您是在开发iOS还是Mac应用程序。
Other support files Mac应用程序可以嵌入其他高级资源,例如私有框架,插件,文档模板和其他自定义数据资源,这些资源是应用程序不可或缺的组成部分。 虽然您可以在iOS应用程序包中包含自定义数据资源,但不能包含自定义框架或插件。(现在可以了
尽管应用程序包中的大部分资源都是可选的,但情况可能并非总是如此。例如,iOS应用程序通常需要为应用程序的图标和默认屏幕提供额外的图像资源。虽然没有明确的要求,但大多数Mac应用都有一个自定义图标,而不是系统提供的默认图标。
下面介绍了App bundle的基本结构和iOS,MacOS bundle的基本结构,还有他们的plist文件的部分配置说明,略过。
访问 Bundle 的内容
定位和打开Bundles

拿到 Main Bundle

main bundle是 包含 正运行的app 代码和资源的bundle。如果是一个开发者,main bundle是最常使用的,也是最简单的。
下面代码使用 Cocoa的 main bundle
NSBundle* mainBundle;
// Get the main bundle for the app.
mainBundle = [NSBundle mainBundle];
下面使用 CGBundleGetMainBundle 方法,编写 C-based app。
CFBundleRef mainBundle;
// Get the main bundle for the app
mainBundle = CFBundleGetMainBundle();

通过 Path 拿到 Bundles

如果想访问不是main bundle的bundle,你可以创建一个特定的 bundle object,如果你知道 bundle的目录的话。通过一个 path 创建 bundle 在 你定义了 framework或其他可加载的bundle时是非常有用的。
下图用 path 定位一个 Cocoa Bundle:
NSBundle* myBundle;
// Obtain a reference to a loadable bundle.
myBundle = [NSBundle bundleWithPath:@”/Library/MyBundle.bundle”];
下面用 C方式实现:
CFURLRef bundleURL;
CFBundleRef myBundle;
// Make a CFURLRef from the CFString representation of the
// bundle’s path.
bundleURL = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
CFSTR(“/Library/MyBundle.bundle”),
kCFURLPOSIXPathStyle,
true );
// Make a bundle instance using the URLRef.
myBundle = CFBundleCreate( kCFAllocatorDefault, bundleURL );
// You can release the URL now.
CFRelease( bundleURL );
// Use the bundle…
// Release the bundle when done.
CFRelease( myBundle );

通过已经目录获取 Bundle

即使您不知道bundle的确切路径,您仍然可以在某个已知位置搜索它。 例如,具有PlugIns目录的应用程序可能想要获取该目录中所有bundle的列表。 一旦你有了目录的路径,你可以使用适当的规则迭代该目录并返回任何包。
在特定目录中查找所有软件包的最简单方法是使用CFBundleCreateBundlesFromDirectory函数。 该函数为给定目录中的所有bundle返回新的CFBundleRef类型。 下图:显示了如何使用此函数来检索应用程序的PlugIns目录中的所有插件,注意,返回的是bundle array
CFBundleRef mainBundle = CFBundleGetMainBundle();
CFURLRef plugInsURL;
CFArrayRef bundleArray;
// Get the URL to the application’s PlugIns directory.
plugInsURL = CFBundleCopyBuiltInPlugInsURL(mainBundle);
// Get the bundle objects for the application’s plug-ins.
bundleArray = CFBundleCreateBundlesFromDirectory( kCFAllocatorDefault,
plugInsURL, NULL );
// Release the CF objects when done with them.
CFRelease( plugInsURL );
CFRelease( bundleArray );

通过 identifier 得到 bundle

使用 bundle identifier 或取加载进内存的bundle是一个 有效定位 bundle 的方式。一个 bundle identitier 是 在 bundle的Info.plist 关联 CFBundleIdentifier key的字符串。这个字符串使用 reverse-DNS 符号处理 不同公司 name space问题。
使用 NSBundle的 bundleWithIdentifier:方法获取bundle
NSBundle* myBundle = [NSBundle bundleWithIdentifier:@”com.apple.myPlugin”];
Core Foundation:
CFBundleRef requestedBundle;
// Look for a bundle using its identifier
requestedBundle = CFBundleGetBundleWithIdentifier(
CFSTR(“com.apple.Finder.MyGetInfoPlugIn”) );
注意,您只能使用bundle identifier来定位已经打开的bundle。例如,您可以使用这种技术为所有静态链接的框架打开 main bundlebundle。您不能使用这种技术来获得对尚未加载的插件的引用

搜索相关bundle

如果您正在编写Cocoa应用程序,则可以通过调用NSBundle的allBundles和allFrameworks类方法来获取与应用程序相关的包列表。 这些方法创建一个NSBundle对象数组,对应于您的应用程序当前使用的bundle或框架。 您可以将这些方法用作便利功能,而不是自己维护一组加载的捆绑包。
bundleForClass:类方法是另一种在Cocoa应用程序中获取相关包信息的方法。 此方法返回定义了特定类的包。 再说一次,这个方法主要是为了方便,所以你不必保留一个指向你只能偶尔使用的NSBundle对象的指针
获得bundle 资源的引用
为了在bundle中查找资源文件,您需要知道文件的名称,文件类型或两者的组合。文件名扩展名用于标识文件的类型。因此,资源文件包含适当的扩展名是很重要的。如果您使用bundle中的自定义子目录来组织资源文件,则可以通过提供包含所需文件的子目录的名称来加速搜索。
即使你没有bundle对象,你仍然可以在你已知的路径的目录中搜索资源。 Core Foundation和Cocoa都提供了API,仅使用基于路径的信息来搜索文件。 (例如,在Cocoa中,您可以使用NSFileManager对象枚举目录的内容并测试文件的存在。)但是,如果计划检索多个资源文件,则使用bundle对象总是更快。bundle对象缓存搜索信息,因此后续搜索通常更快。

使用 cocoa 搜索资源

如果你有了一个 NSbundle 对象,你可以使用 以下方法找到资源:
假设你已经放置了一个 名为 Seagull.jpg 的图片在 main bundle。下面 显示如果处理 image 文件的path:
NSBundle* myBundle = [NSBundle mainBundle];
NSString* myImage = [myBundle pathForResource:@”Seagull” ofType:@”jpg”]
你可以在top-level 资源目录中找出所有统一类型的图片path 数组:
NSBundle* myBundle = [NSBundle mainBundle];
NSArray* myImages = [myBundle pathsForResourcesOfType:@”jpg”
inDirectory:nil];
使用 Core Foundation完成上述功能:
CFURLRef    seagullURL;
// Look for a resource in the main bundle by name and type.
seagullURL = CFBundleCopyResourceURL( mainBundle,
CFSTR(“Seagull”),
CFSTR(“jpg”),
NULL );
CFArrayRef  birdURLs;
// Find all of the JPEG images in a given directory.
birdURLs = CFBundleCopyResourceURLsOfType( mainBundle,
CFSTR(“jpg”),
CFSTR(“BirdImages”) );
注意: 你可以搜索没有扩展名的资源。指定一个完整资源名和一个空的
nullresource type来得到没有扩展名资源的path
找到bundle里的其他文件
有了有效的bundle对象,您就可以检索到顶级bundle目录的路径以及它的许多子目录的路径。使用可用的接口来检索目录路径吧你的代码从必需知道确切bundle结构或者位置里分离出来。也让你在不同平台使用同样的代码。比如,你可以使用相同代码检索iOS app或者mac app的资源。他们有不同的bundle 结构。
使用 bundlePath 方法得到 top-level的bundle 目录。也可以使用builtInPlugInsPath, resourcePath, sharedFrameworksPath和sharedSupportPath方法来获得关键子目录的path。这些方法用字符串返回path 信息,你可以直接使用到大部分的NSbundle方法中(转换为NSUrl)。
Core Foundation也定义了处理以上功能的方法,如CFBundleCopyBundleURL.,CFBundleCopyBuiltInPlugInsURL, CFBundleCopyResourcesDirectoryURL, CFBundleCopySharedFrameworksURL, and CFBundleCopySupportFilesDirectoryURL 他们返回CFURLRef。
得到BundleInfo.plist 数据
每个bundle都应该有一个info.plist文件,这个文件是一个xml。
NSBundle类提供 objectForInfoDictionaryKey和 infoDictionary 方法来检索infoplist信息。objectForInfoDictionaryKey 方法返回 本地化的值,推荐使用。infoDictionary 返回NSDictionary类型的所有建值对,不返回这些key的本地化信息。Core Foundation提供CFBundleGetValueForInfoDictionaryKey CFBundleGetInfoDictionary方法。
得到 bundle的版本号
// This is the ‘vers’ resource style value for 1.0.0
#define kMyBundleVersion1 0x01008000
UInt32  bundleVersion;
// Look for the bundle’s version number.
bundleVersion = CFBundleGetVersionNumber( mainBundle );
// Check the bundle version for compatibility with the app.
if (bundleVersion < kMyBundleVersion1)
return (kErrorFatalBundleTooOld);
从plist里检索 信息
CFDictionaryRef bundleInfoDict;
CFStringRef     myPropertyString;
// Get an instance of the non-localized keys.
bundleInfoDict = CFBundleGetInfoDictionary( myBundle );
// If we succeeded, look for our property.
if ( bundleInfoDict != NULL ) {
myPropertyString = CFDictionaryGetValue( bundleInfoDict,
CFSTR(“MyPropertyKey”) );
}
加载和卸载可执行代码
从 外部bundle加载代码的关键是找到一个可执行文件的合适的 entry point。与其他插件方案一样,这需要应用程序开发人员和插件开发人员之间的一些协调。你可以发布一个自定义api给bundle来实现或者定义一个正式的插件接口。
更多信息见 Code Loading Programming Topics

加载 Functions

如果你处理 C或者C++,挥着OC,你可以用 C-base symbols发布你的接口,比如 function pointers和 global variables。使用 Core Foundation方法,你可以加载这些符号的引用。
你可以使用任意几种函数来检索符号。CFBundleGetFunctionPointerForName or CFBundleGetFunctionPointersForNames.用来检索函数指针。检索一个全局变量指针,使用
CFBundleGetDataPointerForName or CFBundleGetDataPointersForNames.。比如,假设一个 可加载的bundle 定义了以下函数:
// Add one to the incoming value and return it.
long addOne(short number)
{
      return ( (long)number + 1 );
}
给定一个CFBundleRef不透明类型,你需要先搜索所需的函数,然后才能在代码中使用它。下面显示了一个代码段,mybundle 变量是一个CGBundleRef不透明类型。
// Function pointer.
AddOneFunctionPtr addOne = NULL;
// Value returned from the loaded function.
long result = 0;
// Get a pointer to the function.
addOne = (void*)CFBundleGetFunctionPointerForName(
myBundle, CFSTR(“addOne”) );
// If the function was found, call it with a test value.
if (addOne)
{
// This should add 1 to whatever was passed in
result = addOne ( 23 );
}

加载Objective-C 类

如果你真在写一个Cocoa App,你可以使用 NSBundle加载整个class的代码。加载一个class的NSBundle方法只用于 OC Class,不能加载 c++或者其他 面向对象语言写的class。
如果一个可加载 bundle 定义了一个 principal class,你使用 principalClass 方法来加载他。这个方法使用info.plist的 NSPrincipalClass 键来加载需要的class。使用principal class可以减少对外部类的特定命名约定达成一致的需求,而让您专注于这些接口的行为。比如,你可能需要使用一个 principal class的实例作为工厂来创建其他相关对象。
如果你要加载一个任意类,使用 classNamed方法。这个方法搜索bundle,找出名称对应的class。如果class存在,返回 Class object。
加载 principal class
– (void)loadBundle:(NSString*)bundlePath
{
Class exampleClass;
id newInstance;
NSBundle *bundleToLoad = [NSBundle bundleWithPath:bundlePath];
if (exampleClass = [bundleToLoad principalClass])
{
newInstance = [[exampleClass alloc] init];
// [newInstance doSomething];
}
}
卸载 Bundles
在macOS v10.5及更高版本中,您可以使用unload方法卸载与NSBundle对象关联的代码。您可以使用这种技术通过删除不再需要的插件或其他可装入的包来释放应用程序中的内存。
如果您使用Core Foundation加载您的包,则可以使用CFBundleUnloadExecutable函数来卸载它。如果你的bundle可能被卸载,你需要确保通过设置一个合适的编译器标志来正确处理字符串常量。
当您使用macOS v10.2(或更高版本)的最低部署目标编译bundle时,编译器会自动切换到生成真正为响应CFSTR(“…”)而不变的字符串。如果使用标志-fconstant-cfstrings编译,编译器也会生成这些常量字符串。常量字符串有许多好处,应尽可能使用,但如果在包含它们的可执行文件被卸载后引用常量字符串,则引用将无效并导致崩溃。即使字符串已被保留,例如,由于被放入数据结构,直接保留,甚至在某些情况下被复制,也可能发生这种情况。与其试图确保所有这些引用在卸载时被清除(并且可能会在库中创建一些引用,使其难以跟踪),最好使用-fno-constant-cfstrings标志编译不可装入的捆绑包。
end

留下评论

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.