框架架构:指定 NSBundle 在单例初始化期间加载 .plist 文件
Framework Architecture: Specify NSBundle to load .plist file from during singleton init
tl;dr
如何创建一个能够在初始化期间从特定位置(不是从框架的包)读取.plist
的单例(它是框架的一部分) ?
解决方案发布在下方并基于已接受的答案。
设置说明
我的 iOS 应用程序使用专有的 UsefulKit.framework
所有通用代码都在其中。
框架有一个ConfigurationManager
(Singleton),负责从.plist
加载一些设置(例如Base URL、API Keys等)在初始化期间 (RAII) 并向有兴趣读取应用程序范围设置的其他组件提供 + (id)valueForKey:(NSString *)key;
API。
ConfigurationManager
存储一个默认名称 .plist
,它希望在初始化期间加载(请参阅下面的问题 #3),即 EnvironmentConfiguration-Default.plist
。
经理从 [NSBundle bundleForClass:[self class]]
加载 .plist
,在 经理成为 [=17= 的一部分之前它曾经工作正常 ].当它是主应用程序的一部分时,它在同一个包中有各自的 .plist
并且能够通过名称找到它。请参阅下面 ConfigurationManager.m
中的代码。
NSString * const kDefaultEnvironmentConfigurationFileName = @"EnvironmentConfiguration-Default";
@interface ConfigurationManager ()
@property (nonatomic, strong) NSMutableDictionary *environmentInfo;
@end
@implementation ConfigurationManager
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
- (instancetype)init {
self = [super init];
if (self) {
self.environmentInfo = [NSMutableDictionary new];
[self loadEnvironment];
}
return self;
}
- (void)loadEnvironment {
[self.environmentInfo removeAllObjects];
[self loadDefaultEnvironmentConfiguration];
}
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSString *defaultPlistPath = [bundle pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
assert(defaultPlistPath != nil); // <=== code crashes here
//
// processing the plist file here...
//
}
// ...
// some code omitted
// ...
@end
问题
现在,当它是一部分时 UsefulKit.framework
该方法不起作用。如果我将 EnvironmentConfiguration-Default.plist
与框架捆绑在一起,它就会工作,但我不想这样做,因为可能使用该框架的应用程序之间的配置不同。应用程序必须具有相应的 .plist
并使用框架的 ConfigurationManager
来访问设置。
此代码也不适用于框架 Xcode 项目中的单元测试目标。我将 EnvironmentConfiguration-Default.plist
文件放入测试目标包中并编写了这个单元测试:
- (void)testConfigurationManagerInstantiation {
[ConfigurationManager sharedInstance];
}
...代码在 -loadDefaultEnvironmentConfiguration
处崩溃(见上文)。
调试上述方法我看到这个:
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
// Printing description of bundle:
// NSBundle </Users/admin/Library/Developer/Xcode/DerivedData/MyWorkspace-asazpgalibrpubbrimxpbrebqdww/Build/Products/Debug-iphonesimulator/UsefulKit.framework> (loaded)
NSString *defaultPlistPath = [[NSBundle bundleForClass:[self class]] pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
// Printing description of defaultPlistPath:
// <nil>
这个包绝对不是我的 .plist
所在的包。所以,我开始怀疑我在架构上做错了什么。
问题
由于 ConfigurationManager
使用单例模式构建,我无法通过 Constructor Injection 注入包。事实上,我想不出任何类型的依赖注入会起到 "nicely" 的作用。我想念什么吗?可能是 static
客户端应用分配路径的 var?
框架可以在内部搜索一些其他包吗?
EnvironmentConfiguration-Default.plist
的名称被硬编码到 ConfigurationManager
的内部,这对我来说很奇怪,b/c 其他开发人员必须知道它并进行设置,但是,我在许多第 3 方框架(GoogleAnalytics、UrbanAirhip、Fabric)中看到了这种情况,框架希望在特定位置找到 .plist
(框架版本之间通常不同)。因此,开发人员应该阅读文档并准备环境作为框架集成的一部分。
欢迎提出任何更改架构的建议。
解决方案
以下内容高度基于 @NSGod 发布的建议,非常感谢!我会将这种方法称为某种(静态的?)依赖注入。
ConfigurationManager.m:
static NSBundle * defaultConfigurationBundle = nil;
@implementation ConfigurationManager
+ (void)initialize {
if (self == [ConfigurationManager class]) {
/// Defaults to main bundle
[[ConfigurationManager class] setDefaultConfigurationBundle:[NSBundle mainBundle]];
}
}
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle {
@synchronized(self) {
defaultConfigurationBundle = bundle;
}
}
// ...
@end
ConfigurationManager.h:
@interface ConfigurationManager : NSObject
// ...
/**
@brief Specify default NSBundle, other than [NSBundle mainBundle] (which is used, otherwise) where .plist configuration file is expected to be found during initialization.
@discussion For some purpose (e.g. unit-testing) there might be cases, where forcing other NSBundle usage is required. The value, assigned in this method might be [NSBundle bundleForClass:[self class]], to get the bundle for caller.
@attention This method must be called before any other method in this class for assignment to take effect, because default bundle setup happens during class instantiation.
@param An NSBundle to read Default .plist from.
*/
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle;
// ...
@end
在调用站点上:
@implementation ConfigurationManagerTests
- (void)setUp {
[super setUp];
/// Prepare test case with correct bundle
[ConfigurationManager setDefaultConfigurationBundle:[NSBundle bundleForClass:[self class]]];
}
- (void)testConfigurationManagerInstantiation {
// call sequence:
// 1. +initialize
// 2. +setDefaultConfigurationBundle
// 3. +sharedInstance
XCTAssertNoThrow([ConfigurationManager sharedInstance]);
}
// ...
@end
该方法允许简化应用程序目标的框架使用(mainBundle
是 .plist
所在的位置),因此到目前为止,仅单元测试需要 +setDefaultConfigurationBundle
。
其实仔细想想,如果想让框架和应用程序通信,只需要改一行代码:
- (void)loadDefaultEnvironmentConfiguration {
// NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSBundle* bundle = [NSBundle mainBundle];
当 ConfigurationManager
class 是您的主应用程序的一部分时,[NSBundle bundleForClass:[self class]]
return 编辑了主应用程序包(即与 returned by [NSBundle mainBundle]
。当你将 ConfigurationManager
class 移动到一个框架(也可以被认为是一个包)时,[NSBundle bundleForClass:[self class]]
开始 returning NSBundle
用于您的框架而不是主应用程序包。
当您从您的框架内调用 [NSBundle mainBundle]
时,它将 return 任何正在使用该框架的应用程序。
或者,您可以使用 class 方法来设置将在初始化期间使用的默认值。
比如在你的ConfigurationManager
classpublic界面中:
@interface ConfigurationManager : NSObject
+ (void)setDefaultConfigurationPath:(NSString *)aPath;
@end
在ConfigurationManager.m
中:
static NSString *defaultConfigurationPath = nil;
@implementation ConfigurationManager
+ (void)setDefaultConfigurationPath:(NSString *)aPath {
@synchronized(self) {
defaultConfigurationPath = aPath;
}
}
// additional methods
- (void)loadDefaultEnvironmentConfiguration {
NSDictionary *dic = [NSDictionary
dictionaryWithContentsOfFile:defaultConfigurationPath];
//
// processing the plist file here...
//
}
@end
通过声明 defaultConfigurationPath
static,您可以使它成为一个 "class" 变量而不是实例变量。因此,您甚至在创建 class 的实例之前就使用 class 方法更改其值。我相信代码应该和 ARC 一样工作,尽管我不是很肯定(我自己仍然习惯手动引用计数)。
您的主应用程序应确保在任何人调用 [ConfigurationManager sharedInstance]
之前使用正确的路径调用 [ConfigurationManager setDefaultConfigurationPath:]
。执行此操作的最佳位置是应用程序委托的 +initialize
方法,这是最先调用的方法之一:
+ (void)initialize {
NSString *path; // get path for plist
[ConfigurationManager setDefaultConfigurationPath:path];
}
tl;dr
如何创建一个能够在初始化期间从特定位置(不是从框架的包)读取.plist
的单例(它是框架的一部分) ?
解决方案发布在下方并基于已接受的答案。
设置说明
我的 iOS 应用程序使用专有的 UsefulKit.framework
所有通用代码都在其中。
框架有一个ConfigurationManager
(Singleton),负责从.plist
加载一些设置(例如Base URL、API Keys等)在初始化期间 (RAII) 并向有兴趣读取应用程序范围设置的其他组件提供 + (id)valueForKey:(NSString *)key;
API。
ConfigurationManager
存储一个默认名称 .plist
,它希望在初始化期间加载(请参阅下面的问题 #3),即 EnvironmentConfiguration-Default.plist
。
经理从 [NSBundle bundleForClass:[self class]]
加载 .plist
,在 经理成为 [=17= 的一部分之前它曾经工作正常 ].当它是主应用程序的一部分时,它在同一个包中有各自的 .plist
并且能够通过名称找到它。请参阅下面 ConfigurationManager.m
中的代码。
NSString * const kDefaultEnvironmentConfigurationFileName = @"EnvironmentConfiguration-Default";
@interface ConfigurationManager ()
@property (nonatomic, strong) NSMutableDictionary *environmentInfo;
@end
@implementation ConfigurationManager
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
- (instancetype)init {
self = [super init];
if (self) {
self.environmentInfo = [NSMutableDictionary new];
[self loadEnvironment];
}
return self;
}
- (void)loadEnvironment {
[self.environmentInfo removeAllObjects];
[self loadDefaultEnvironmentConfiguration];
}
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSString *defaultPlistPath = [bundle pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
assert(defaultPlistPath != nil); // <=== code crashes here
//
// processing the plist file here...
//
}
// ...
// some code omitted
// ...
@end
问题
现在,当它是一部分时 UsefulKit.framework
该方法不起作用。如果我将 EnvironmentConfiguration-Default.plist
与框架捆绑在一起,它就会工作,但我不想这样做,因为可能使用该框架的应用程序之间的配置不同。应用程序必须具有相应的 .plist
并使用框架的 ConfigurationManager
来访问设置。
此代码也不适用于框架 Xcode 项目中的单元测试目标。我将 EnvironmentConfiguration-Default.plist
文件放入测试目标包中并编写了这个单元测试:
- (void)testConfigurationManagerInstantiation {
[ConfigurationManager sharedInstance];
}
...代码在 -loadDefaultEnvironmentConfiguration
处崩溃(见上文)。
调试上述方法我看到这个:
- (void)loadDefaultEnvironmentConfiguration {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
// Printing description of bundle:
// NSBundle </Users/admin/Library/Developer/Xcode/DerivedData/MyWorkspace-asazpgalibrpubbrimxpbrebqdww/Build/Products/Debug-iphonesimulator/UsefulKit.framework> (loaded)
NSString *defaultPlistPath = [[NSBundle bundleForClass:[self class]] pathForResource:kDefaultEnvironmentConfigurationFileName ofType:@"plist"];
// Printing description of defaultPlistPath:
// <nil>
这个包绝对不是我的 .plist
所在的包。所以,我开始怀疑我在架构上做错了什么。
问题
由于
ConfigurationManager
使用单例模式构建,我无法通过 Constructor Injection 注入包。事实上,我想不出任何类型的依赖注入会起到 "nicely" 的作用。我想念什么吗?可能是static
客户端应用分配路径的 var?框架可以在内部搜索一些其他包吗?
EnvironmentConfiguration-Default.plist
的名称被硬编码到ConfigurationManager
的内部,这对我来说很奇怪,b/c 其他开发人员必须知道它并进行设置,但是,我在许多第 3 方框架(GoogleAnalytics、UrbanAirhip、Fabric)中看到了这种情况,框架希望在特定位置找到.plist
(框架版本之间通常不同)。因此,开发人员应该阅读文档并准备环境作为框架集成的一部分。
欢迎提出任何更改架构的建议。
解决方案
以下内容高度基于 @NSGod 发布的建议,非常感谢!我会将这种方法称为某种(静态的?)依赖注入。
ConfigurationManager.m:
static NSBundle * defaultConfigurationBundle = nil;
@implementation ConfigurationManager
+ (void)initialize {
if (self == [ConfigurationManager class]) {
/// Defaults to main bundle
[[ConfigurationManager class] setDefaultConfigurationBundle:[NSBundle mainBundle]];
}
}
+ (instancetype)sharedInstance {
static ConfigurationManager *sharedEnvironment;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!sharedEnvironment) {
sharedEnvironment = [self new];
}
});
return sharedEnvironment;
}
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle {
@synchronized(self) {
defaultConfigurationBundle = bundle;
}
}
// ...
@end
ConfigurationManager.h:
@interface ConfigurationManager : NSObject
// ...
/**
@brief Specify default NSBundle, other than [NSBundle mainBundle] (which is used, otherwise) where .plist configuration file is expected to be found during initialization.
@discussion For some purpose (e.g. unit-testing) there might be cases, where forcing other NSBundle usage is required. The value, assigned in this method might be [NSBundle bundleForClass:[self class]], to get the bundle for caller.
@attention This method must be called before any other method in this class for assignment to take effect, because default bundle setup happens during class instantiation.
@param An NSBundle to read Default .plist from.
*/
+ (void)setDefaultConfigurationBundle:(NSBundle *)bundle;
// ...
@end
在调用站点上:
@implementation ConfigurationManagerTests
- (void)setUp {
[super setUp];
/// Prepare test case with correct bundle
[ConfigurationManager setDefaultConfigurationBundle:[NSBundle bundleForClass:[self class]]];
}
- (void)testConfigurationManagerInstantiation {
// call sequence:
// 1. +initialize
// 2. +setDefaultConfigurationBundle
// 3. +sharedInstance
XCTAssertNoThrow([ConfigurationManager sharedInstance]);
}
// ...
@end
该方法允许简化应用程序目标的框架使用(mainBundle
是 .plist
所在的位置),因此到目前为止,仅单元测试需要 +setDefaultConfigurationBundle
。
其实仔细想想,如果想让框架和应用程序通信,只需要改一行代码:
- (void)loadDefaultEnvironmentConfiguration {
// NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSBundle* bundle = [NSBundle mainBundle];
当 ConfigurationManager
class 是您的主应用程序的一部分时,[NSBundle bundleForClass:[self class]]
return 编辑了主应用程序包(即与 returned by [NSBundle mainBundle]
。当你将 ConfigurationManager
class 移动到一个框架(也可以被认为是一个包)时,[NSBundle bundleForClass:[self class]]
开始 returning NSBundle
用于您的框架而不是主应用程序包。
当您从您的框架内调用 [NSBundle mainBundle]
时,它将 return 任何正在使用该框架的应用程序。
或者,您可以使用 class 方法来设置将在初始化期间使用的默认值。
比如在你的ConfigurationManager
classpublic界面中:
@interface ConfigurationManager : NSObject
+ (void)setDefaultConfigurationPath:(NSString *)aPath;
@end
在ConfigurationManager.m
中:
static NSString *defaultConfigurationPath = nil;
@implementation ConfigurationManager
+ (void)setDefaultConfigurationPath:(NSString *)aPath {
@synchronized(self) {
defaultConfigurationPath = aPath;
}
}
// additional methods
- (void)loadDefaultEnvironmentConfiguration {
NSDictionary *dic = [NSDictionary
dictionaryWithContentsOfFile:defaultConfigurationPath];
//
// processing the plist file here...
//
}
@end
通过声明 defaultConfigurationPath
static,您可以使它成为一个 "class" 变量而不是实例变量。因此,您甚至在创建 class 的实例之前就使用 class 方法更改其值。我相信代码应该和 ARC 一样工作,尽管我不是很肯定(我自己仍然习惯手动引用计数)。
您的主应用程序应确保在任何人调用 [ConfigurationManager sharedInstance]
之前使用正确的路径调用 [ConfigurationManager setDefaultConfigurationPath:]
。执行此操作的最佳位置是应用程序委托的 +initialize
方法,这是最先调用的方法之一:
+ (void)initialize {
NSString *path; // get path for plist
[ConfigurationManager setDefaultConfigurationPath:path];
}