BundleLoader:幫你無縫加載自定義Bundle裏的資源文件

引子

iOS開發中,咱們封裝SDK給第三方使用一般採用.a或.framework + .bundle的形式。相信封裝過這種帶bundle資源文件的SDK的同窗們必定都會遇到這樣一個小麻煩。那就是加載自定義Bundle裏的資源的代碼寫起來和咱們平時開發App時加載mainBundle裏的資源的代碼是不一樣的,前者寫起來要麻煩一些。css

若是你正在封裝帶資源的SDK,那我相信BundleLoader應該能夠幫助到你。它能夠幫你消除這種調用上的不一樣,你只須要簡單的調用兩個方法就能夠像加載App裏的資源那樣『無縫』的加載自定義Bundle裏的資源。既有代碼無需修改,後續代碼你也能夠繼續用最簡潔最熟悉的方式開發。git

項目地址: BundleLoadergithub

問題

最近,本人碰到了這樣一個需求。我是作直播APP的,老闆要求我從APP裏把直播間相關的部分分離出來封裝成SDK給第三方使用,而且從此要作到SDK和APP可以同步開發,同步更新。bash

這種狀況下,這種調用不一樣對我來講就是個大麻煩了。 其一,直播間及相關部分的代碼量很是龐大,各類資源各類形式的調用,改起來很麻煩。 其二,改動了之後從此同步開發也是個麻煩。markdown

要解決這個問題,咱們先來看看代碼上會有何不一樣。好比圖片,咱們知道加載App主包裏的圖片代碼只須要簡單的一句:oop

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];
複製代碼

或者簡化一點:spa

NSString *file2 = [[NSBundle mainBundle] pathForResource:@"myBundle.bundle/pic" ofType:@"png"];
UIImage *img2 = [UIImage imageWithContentsOfFile:file2];
複製代碼

再簡化一點:code

UIImage *img3 = [UIImage imageNamed:@"myBundle.bundle/pic"];
複製代碼

可是仍是都沒有mainBundle裏的簡單。因而,我就想,能不能不改代碼就能夠加載自定義Bundle裏的資源呢?方法確定有,OC強大的Runtime出馬,沒有搞不定的事情,哈哈。orm

特性

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

  1. 關聯對象 objc_setAssociatedObject
  2. 改變對象類型 object_setClass
  3. Method Swizzling method_exchangeImplementations

經過自定義的子類和自定義方法讓系統先從咱們的資源Bundle里加載文件,找不到再去主包里加載。

若是這個庫對你有用,請各位賞個贊吧,謝謝。

相關文章
相關標籤/搜索