引子
iOS開發中,我們封裝SDK給第三方使用通常採用.a或.framework + .bundle的形式。相信封裝過這種帶bundle資原始檔的SDK的同學們一定都會遇到這樣一個小麻煩。那就是載入自定義Bundle裡的資源的程式碼寫起來和我們平時開發App時載入mainBundle裡的資源的程式碼是不同的,前者寫起來要麻煩一些。
如果你正在封裝帶資源的SDK,那我相信BundleLoader應該可以幫助到你。它可以幫你消除這種呼叫上的不同,你只需要簡單的呼叫兩個方法就可以像載入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出馬,沒有搞不定的事情,哈哈。
特性
BundleLoader的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里載入檔案,找不到再去主包里載入。
如果這個庫對你有用,請各位賞個贊吧,謝謝。