iOS動態庫開發中遇到的問題小結

關鍵詞: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了一份給原作者。不過,作者已經寫完這個庫有好幾個月了,不知道還會不會再看一眼這個項目了:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容