博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
BundleLoader:帮你无缝加载自定义Bundle里的资源文件
阅读量:6202 次
发布时间:2019-06-21

本文共 4437 字,大约阅读时间需要 14 分钟。

引子

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方法,分别是:

  1. 关联对象 objc_setAssociatedObject
  2. 改变对象类型 object_setClass
  3. Method Swizzling method_exchangeImplementations

通过自定义的子类和自定义方法让系统先从我们的资源Bundle里加载文件,找不到再去主包里加载。

如果这个库对你有用,请各位赏个赞吧,谢谢。

转载地址:http://xkaca.baihongyu.com/

你可能感兴趣的文章
DP-01背包 (题)
查看>>
WinForm中跨线程操作控件
查看>>
CODING 敏捷实践完全指南
查看>>
unittest测试框架和测试报告的输出实例(一)
查看>>
下MFC中对象、句柄、ID之间的区别.
查看>>
如何构建Win32汇编的编程环境(ONEPROBLEM个人推荐)
查看>>
Asp.Net MVC 分页、检索、排序整体实现
查看>>
Flymeos插桩适配教程
查看>>
还在用PS磨皮去皱?看看如何用神经网络高度还原你的年轻容貌!
查看>>
大端模式与小端模式、网络字节顺序与主机字节顺序
查看>>
微信支付申请90%的商户都卡在这儿了,申请微信支付,商户功能设置详细说明...
查看>>
制作一款微信表情
查看>>
高仿Instagram 页面效果android特效
查看>>
我的友情链接
查看>>
Juniper 基于路由的×××
查看>>
HDU - 2018 - 母牛的故事(dp)
查看>>
基于matlab的fft变换中参数的设置
查看>>
如何查找JSP页面中的错误
查看>>
2016 年总结
查看>>
Python学习开始
查看>>