引子
iOS开发中,我们封装SDK给第三方使用通常采用.a或.framework + .bundle的形式。相信封装过这种带bundle资源文件的SDK的同学们一定都会遇到这样一个小麻烦。那就是加载自定义Bundle里的资源的代码写起来和我们平时开发App时加载mainBundle里的资源的代码是不同的,前者写起来要麻烦一些。
如果你正在封装带资源的SDK,那我相信应该可以帮助到你。它可以帮你消除这种调用上的不同,你只需要简单的调用两个方法就可以像加载App里的资源那样『无缝』的加载自定义Bundle里的资源。既有代码无需修改,后续代码你也可以继续用最简洁最熟悉的方式开发。
问题
最近,本人碰到了这样一个需求。我是做直播APP的,老板要求我从APP里把直播间相关的部分分离出来封装成SDK给第三方使用,并且今后要做到SDK和APP能够同步开发,同步更新。
这种情况下,这种调用不同对我来说就是个大麻烦了。 其一,直播间及相关部分的代码量非常庞大,各种资源各种形式的调用,改起来很麻烦。 其二,改动了以后今后同步开发也是个麻烦。
要解决这个问题,我们先来看看代码上会有何不同。比如图片,我们知道加载App主包里的图片代码只需要简单的一句:
UIImage *img = [UIImage imageNamed:@"pic"];复制代码
而加载自定义Bundle里的图片则要麻烦一些:
NSString *path = [[NSBundle mainBundle] pathForResource:@"myBundle" ofType:@"bundle"];NSBundle *bundle = [NSBundle bundleWithPath:path];NSString *file = [bundle pathForResource:@"pic" ofType:@"png"];UIImage *img = [UIImage imageWithContentsOfFile:file];复制代码
或者简化一点:
NSString *file2 = [[NSBundle mainBundle] pathForResource:@"myBundle.bundle/pic" ofType:@"png"];UIImage *img2 = [UIImage imageWithContentsOfFile:file2];复制代码
再简化一点:
UIImage *img3 = [UIImage imageNamed:@"myBundle.bundle/pic"];复制代码
但是还是都没有mainBundle里的简单。于是,我就想,能不能不改代码就可以加载自定义Bundle里的资源呢?方法肯定有,OC强大的Runtime出马,没有搞不定的事情,哈哈。
特性
的Demo里目前测试了下列几种情况的自定义bundle资源无缝加载:
- 图片
- xib
- storyboard
- xcssets图片
- 普通资源文件
xib或storyboard里用到的图片和xcssets图片也都可以正常显示。 同时,Demo还提供了一个简单的Framework + Bundle的工程模版,可以供大家参考。
其他资源,如CoreData模型,本地化字符串等应该也可以加载,如果不行的话大家也可以依葫芦画瓢,自行实现。
实现
具体的实现其实并不复杂,最关键的一点是:我发现,App里不论加载什么类型的资源,调用什么接口,系统内部都会去调用NSBundle的这个方法:
- (nullable NSString *)pathForResource:(nullable NSString *)name ofType:(nullable NSString *)ext;复制代码
这个方法就是突破口,我们只要在这个方法上去想办法,做文章,再用上灵活强大的Runtime,应该就能达到我们的目的。
实现的步骤如下:
- 获取自定义资源Bundle的对象
- 把这个对象关联到mainBundle对象上
- 把mainBundle对象的Class设为自定义Bundle子类的Class
- 在Bundle子类里重写
pathForResource:ofType:
方法 - 这个方法里拿到关联的自定义Bundle对象
- 判断自定义Bundle对象里该文件是否存在,存在则返回其路径
- 不存在则去mainBundle里找
上代码:
@implementation BundleLoader+ (void)initFrameworkBundle:(NSString*)bundleName { refCount++; NSBundle* bundle = objc_getAssociatedObject(self, NSBundleMainBundleKey); if (bundle == nil) { //获取自定义资源Bundle的对象 NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"]; NSBundle *resBundle = [NSBundle bundleWithPath:path]; //把这个对象关联到mainBundle对象上 objc_setAssociatedObject([NSBundle mainBundle], NSBundleMainBundleKey, resBundle, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //把mainBundle对象的Class设为自定义Bundle子类的Class object_setClass([NSBundle mainBundle], [FrameworkBundle class]); }}复制代码
@interface FrameworkBundle : NSBundle@end@implementation FrameworkBundle//系统底层加载图片,xib都会进这个方法- (nullable NSString *)pathForResource:(nullable NSString *)name ofType:(nullable NSString *)ext { NSBundle* bundle = objc_getAssociatedObject(self, NSBundleMainBundleKey); if (bundle) { NSString *path = [bundle pathForResource:name ofType:ext]; if (path) return path; } return [super pathForResource:name ofType:ext];}复制代码
运行代码,发现[UIImage imageNamed:@"crown"]
已经可以拿到UIImage对象了。原以为可以打完收工了,结果高兴的太早了。如果图片在xcassets里,那这样调用还是会失败。 加载自定义Bundle的xcassets方法只能用下面的方法:
[UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];复制代码
继续折腾,这次该Method Swizzling大法上场了。还不了解这个黑魔法的可以看。我们给UImage的imageNamed:
方法做了Method Swizzling。代码如下:
@implementation UIImage (FrameworkBundle)#pragma mark - Method swizzling+ (void)load { Method originalMethod = class_getClassMethod([self class], @selector(imageNamed:)); Method customMethod = class_getClassMethod([self class], @selector(imageNamedCustom:)); //Swizzle methods method_exchangeImplementations(originalMethod, customMethod);}+ (nullable UIImage *)imageNamedCustom:(NSString *)name { //Call original methods UIImage *image = [UIImage imageNamedCustom:name]; if (image != nil) return image; NSBundle* bundle = objc_getAssociatedObject([NSBundle mainBundle], NSBundleMainBundleKey); if (bundle) return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil];//加载bundle里xcassets的图片只能用这个方法 else return nil;}@end复制代码
先调用imageNamed:
获取图片,如果拿到则直接返回;失败则调用imageNamed:inBundle:compatibleWithTraitCollection:
方法去获取图片,并传入自定义Bundle对象。这样Bundle里的xcassets图片也可以简单加载了。
至于xib和storyboard也是同样的做法。
总结
实现还是比较简单的,用到了三个Runtime方法,分别是:
- 关联对象
objc_setAssociatedObject
- 改变对象类型
object_setClass
- Method Swizzling
method_exchangeImplementations
通过自定义的子类和自定义方法让系统先从我们的资源Bundle里加载文件,找不到再去主包里加载。
如果这个库对你有用,请各位赏个赞吧,谢谢。