关键词:1. XIB在framework中加载失败;2. imageNamed在framework中加载失败;3. 第三方库冲突;4. 然后手工添加Pods库;5. 一些意想不到的事情(感觉这里是本文最干的干货,前面没意思的,可以直接拉到最后,(__) 嘻嘻……)
如何创建一个iOS动态framework的事情就不在本文赘述了,网上有很多的相关文章介绍。需要注意一点的是,网上有些文章会比较旧(iOS8以前的时代),会讲一些过时了的方法,注意选择正确的文章即可。
为了更加方便读者了解framework,放一个我认为写得用心的连接iOS 开发中的『库』
本人主要讲述实际做一个动态framework作为SDK提供给第三方使用时候遇到的实际问题以及我的一些解决办法。
情况是这样的:项目原先已经开发了一个APP了,现在需要把其中的一些功能部件包装成SDK给第三方使用。
为了方便同步开发SDK和APP,我选择在原来的APP工程中添加一个target的方式来产生SDK。
XIB在framework中加载失败
UIViewController的init方法默认使用mainBundle加载与类同名的XIB文件。而动态库在APP里面是一个独立的bundle。这样子的话,在动态库中用到这种方式加载的视图都会失败了。我解决的办法是使用runtime修改了一下init的实现:
@implementation UIViewController(InitFromFramewok)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(init));
Method swizzleMethod = class_getInstanceMethod(self, @selector(dwsdk_init));
method_exchangeImplementations(originalMethod, swizzleMethod);
});
}
- (instancetype)dwsdk_init{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
return [self initWithNibName:nil bundle:bundle];
}
@end
imageNamed方法失败了
失败的的原因和1的问题差不多,因为imageNamed默认也是从mainBundle加载,因此动态库里的图片都加载不到了。
一开始,我使用了和1类似的方法,想通过runtime修改imageNamed的默认实现,改成从[self class]的bundle中加载,结果却是不成功。
不成功的原因是,imageNamed是类方法,它的[self class]是UIImage,而UIImage类所在的bundle是UIKit这个系统动态库中。
于是,更换另外一个实现方式如下:
+ (nullable UIImage *)dwsdk_imageNamed:(NSString *)name {
NSBundle *bundle = [NSBundle bundleForClass:[FrameworkNibSwizzle class]];
UIImage *image = [UIImage imageNamed:name
inBundle:bundle
compatibleWithTraitCollection:nil];
NSLog(@"<DEBUG>load image:%@ from bundle:%@, result:%@", name, bundle, (image?@"YES":@"NO"));
return image;
}
其中的FrameworkNibSwizzle是一个确定在动态库中的类。
这下子虽然解决了SDK里加载图片的问题了,然而,这个方案会使得SDK外面的imageNamed也被一起替换了,结果就是变成SDK外面的imageNamed找不到图片了。
于是,引入了一套更加复杂一些的配套操作:
@implementation FrameworkNibSwizzle
static IMP orgImageNamedImp = nil;
+ (void)initialize {
orgImageNamedImp = [self currentImageNamedIMP];
}
+ (IMP)orgImageNamedIMP {
return orgImageNamedImp;
}
+ (IMP)currentImageNamedIMP {
Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
return method_getImplementation(currentImageNamed);
}
+ (IMP)sdkImageNamedIMP {
Method sdkImageNamed = class_getClassMethod(self, @selector(jcsdk_imageNamed:));
return method_getImplementation(sdkImageNamed);
}
+ (void)changeImageNamed{
IMP currIMP = [self currentImageNamedIMP];
IMP sdkIMP = [self sdkImageNamedIMP];
if (currIMP != sdkIMP) {
Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
method_setImplementation(currentImageNamed, sdkIMP);
}
}
+ (void)restoreImageNamed{
Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
method_setImplementation(currentImageNamed, orgImageNamedImp);
}
+ (nullable UIImage *)dwsdk_imageNamed:(NSString *)name {
NSBundle *bundle = [NSBundle bundleForClass:[FrameworkNibSwizzle class]];
UIImage *image = [UIImage imageNamed:name
inBundle:bundle
compatibleWithTraitCollection:nil];
NSLog(@"<DEBUG>load image:%@ from bundle:%@, result:%@", name, bundle, (image?@"YES":@"NO"));
return image;
}
@end
这套处理方法的基本过程是:SDK加载的时候先记录一下原始UIImage的imageNamed方法的实现函数,然后在需要使用SDK的时候,替换实现方法,使用完SDK之后,还原实现方法。
这个方案对于使用SDK的过程有明确界限的情况还勉强可以,如果是使用SDK的同时还要使用其他代码的情况,还是不能解决问题。
这里请教读者,基于runtime有没有可能完美解决此问题?
最后,为了完全解决imageNamed的问题,把项目的全部UIImage imageNamed方法统一改了一遍,改成调用[DWImageLoader imageNamed:]:
@implementation DWImageLoader
+ (UIImage *)imageNamed:(NSString *)name {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
return [UIImage imageNamed:name
inBundle:bundle compatibleWithTraitCollection:nil];
}
@end
哈哈,也就是说在工程里面明确的定义一个类和imageNamed方法,使用这个类的bundle来加载,完全解决了。
第三方库冲突
这个问题比较普遍,比如,我们自己的SDK使用的AFN这个开源库。使用我们SDK的人同时也使用AFN这个开源库,这样,在运行的时候会有告警,大致的意思是:现在runtime找到两个AFN的类,我警告你,只有其中一个类会被加载,至于是加载SDK里的AFN还是别的地方的AFN,我也保证不了:(
这个问题没有解决,目前我的情况是,确保AFN这些第三方库是相同的版本的话,就可以忽略这个告警
补充说明一下,动态库方式的framework和静态库方式framework的库冲突不同:静态库如果有库冲突,编译的时候就会编译错误,错误是说符号重复了,必须要重新调整framework把其中重复的代码去掉才行。动态库在编译的时候不会报错,执行的时候告警。
手工添加Pods
因为framework是自己手动添加的target,其中使用到一些原APP已经引入的Pods库。需要手工添加需要使用的Pods库
有读者知道这种情况下还可以用Pod install给我新加的target关联使用Pods吗?
具体需要做如下一些事情(中间过程磕磕碰碰,不一定能把完整的步骤复原出来,如果有遗漏,还请提问)
a. 在Setting里首先添加一个自定义变量PODS_ROOT,后面的挺多配置都需要这个环境变量的
b. 设置Pods头文件引用路径
c. 添加Pods的库引用(由于历史原因,我的Pods还是静态库,没改成动态framework)
最后,还有一些意想不到的事情。
a. 有个客户说使用我们的SDK之后,它的tabbar的样式总觉得不正常了。查看了代码才知道,原来我们APP里是通过这样的方式修改了全局的tarbar:)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SwizzleInstanceMethod(class,
@selector(viewDidLoad),
@selector(xx_viewDidLoad));
SwizzleInstanceMethod(class,
@selector(setSelectedViewController:),
@selector(xx_setSelectedViewController:));
SwizzleInstanceMethod(class,
@selector(setSelectedIndex:),
@selector(xx_setSelectedIndex:));
});
}
在判断SDK用不到这个功能的情况下,加了一个编译选项关闭了这个动作
b. 又有客户说,用了我们SDK之后,他的navigation bar的样式总觉得不正常了。查看了代码才知道,原来我们的APP里是通过[UINavigationBar appearance]修改了全局的样式。好吧,改成使用到SDK的界面才修改就好了。
c. 又有客户说,用了我们的SDK之后,每次进入某个界面再出来就crashed了。我那个去,还有这么神奇的事情?仔细观察界面,发现界面里有一个UITextView。有了a问题的经验,全局搜索“+(void)load”,有所发现了。我们APP用到了一个开源库:UITextView+Placeholder。这是一个给UITextView添加placeholder属性的category。其中有一个这样的实现
+ (void)load {
// is this the best solution?
method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),
class_getInstanceMethod(self.class, @selector(swizzledDealloc)));
}
- (void)swizzledDealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));
if (label) {
for (NSString *key in self.class.observingKeys) {
@try {
[self removeObserver:self forKeyPath:key];
}
@catch (NSException *exception) {
// Do nothing
}
}
}
[self swizzledDealloc];
}
它需要在dealloc里面remove一些它自己add进去的KVO。
巧的是,我们的客户也用到这个库。我们知道,一个类的多个category的+ (void)load都是会一一加载并执行的,于是SDK里的这个category执行一次load,客户的代码执行一次load。这样dealloc和swizzledDealloc就被调换了两次,相当于就是没有调换了。也就是说在UITextView的dealloc的时候,并没有调用到期望的swizzledDealloc方法,于是,UITextView释放的时候,注册了的KVO没有被释放,于是crash!
解决的办法?好吧,我当时只是简单的把这个库里注册KVO的代码注释了(项目时间紧,客户急,压力大呀)。
这个category里的KVO其实是为了实现在设置了placeholder之后,修改UITextView的字体,大小等属性的时候,placeholder能够相应的响应这些变化表现出一样的字体和大小来。
周末终于有一些自己的时间了,想了一个自己觉得能完美解决的方案,暂时提交到我自己的Github分支上UITextView+Placeholder。同时PR了一份给原作者。不过,作者已经写完这个库有好几个月了,不知道还会不会再看一眼这个项目了:)