關(guān)鍵詞:1. XIB在framework中加載失敶乇;2. imageNamed在framework中加載失斅袜汀;3. 第三方庫沖突屋摔;4. 然后手工添加Pods庫烁设;5. 一些意想不到的事情(感覺這里是本文最干的干貨,前面沒意思的钓试,可以直接拉到最后装黑,(__) 嘻嘻……)
如何創(chuàng)建一個(gè)iOS動(dòng)態(tài)framework的事情就不在本文贅述了,網(wǎng)上有很多的相關(guān)文章介紹弓熏。需要注意一點(diǎn)的是恋谭,網(wǎng)上有些文章會(huì)比較舊(iOS8以前的時(shí)代),會(huì)講一些過時(shí)了的方法硝烂,注意選擇正確的文章即可箕别。
為了更加方便讀者了解framework,放一個(gè)我認(rèn)為寫得用心的連接iOS 開發(fā)中的『庫』
本人主要講述實(shí)際做一個(gè)動(dòng)態(tài)framework作為SDK提供給第三方使用時(shí)候遇到的實(shí)際問題以及我的一些解決辦法滞谢。
情況是這樣的:項(xiàng)目原先已經(jīng)開發(fā)了一個(gè)APP了串稀,現(xiàn)在需要把其中的一些功能部件包裝成SDK給第三方使用。
為了方便同步開發(fā)SDK和APP狮杨,我選擇在原來的APP工程中添加一個(gè)target的方式來產(chǎn)生SDK母截。
XIB在framework中加載失敗
UIViewController的init方法默認(rèn)使用mainBundle加載與類同名的XIB文件。而動(dòng)態(tài)庫在APP里面是一個(gè)獨(dú)立的bundle橄教。這樣子的話喘漏,在動(dòng)態(tài)庫中用到這種方式加載的視圖都會(huì)失敗了。我解決的辦法是使用runtime修改了一下init的實(shí)現(xiàn):
@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的問題差不多华烟,因?yàn)閕mageNamed默認(rèn)也是從mainBundle加載翩迈,因此動(dòng)態(tài)庫里的圖片都加載不到了。
一開始盔夜,我使用了和1類似的方法负饲,想通過runtime修改imageNamed的默認(rèn)實(shí)現(xiàn),改成從[self class]的bundle中加載喂链,結(jié)果卻是不成功返十。
不成功的原因是,imageNamed是類方法椭微,它的[self class]是UIImage洞坑,而UIImage類所在的bundle是UIKit這個(gè)系統(tǒng)動(dòng)態(tài)庫中。
于是蝇率,更換另外一個(gè)實(shí)現(xiàn)方式如下:
+ (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是一個(gè)確定在動(dòng)態(tài)庫中的類迟杂。
這下子雖然解決了SDK里加載圖片的問題了,然而瓢剿,這個(gè)方案會(huì)使得SDK外面的imageNamed也被一起替換了逢慌,結(jié)果就是變成SDK外面的imageNamed找不到圖片了悠轩。
于是间狂,引入了一套更加復(fù)雜一些的配套操作:
@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加載的時(shí)候先記錄一下原始UIImage的imageNamed方法的實(shí)現(xiàn)函數(shù),然后在需要使用SDK的時(shí)候火架,替換實(shí)現(xiàn)方法鉴象,使用完SDK之后,還原實(shí)現(xiàn)方法何鸡。
這個(gè)方案對于使用SDK的過程有明確界限的情況還勉強(qiáng)可以纺弊,如果是使用SDK的同時(shí)還要使用其他代碼的情況,還是不能解決問題骡男。
這里請教讀者淆游,基于runtime有沒有可能完美解決此問題?
最后隔盛,為了完全解決imageNamed的問題犹菱,把項(xiàng)目的全部UIImage imageNamed方法統(tǒng)一改了一遍,改成調(diào)用[DWImageLoader imageNamed:]:
@implementation DWImageLoader
+ (UIImage *)imageNamed:(NSString *)name {
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
return [UIImage imageNamed:name
inBundle:bundle compatibleWithTraitCollection:nil];
}
@end
哈哈吮炕,也就是說在工程里面明確的定義一個(gè)類和imageNamed方法腊脱,使用這個(gè)類的bundle來加載,完全解決了龙亲。
第三方庫沖突
這個(gè)問題比較普遍陕凹,比如悍抑,我們自己的SDK使用的AFN這個(gè)開源庫。使用我們SDK的人同時(shí)也使用AFN這個(gè)開源庫杜耙,這樣搜骡,在運(yùn)行的時(shí)候會(huì)有告警,大致的意思是:現(xiàn)在runtime找到兩個(gè)AFN的類佑女,我警告你浆兰,只有其中一個(gè)類會(huì)被加載,至于是加載SDK里的AFN還是別的地方的AFN珊豹,我也保證不了:(
這個(gè)問題沒有解決簸呈,目前我的情況是,確保AFN這些第三方庫是相同的版本的話店茶,就可以忽略這個(gè)告警
補(bǔ)充說明一下蜕便,動(dòng)態(tài)庫方式的framework和靜態(tài)庫方式framework的庫沖突不同:靜態(tài)庫如果有庫沖突,編譯的時(shí)候就會(huì)編譯錯(cuò)誤贩幻,錯(cuò)誤是說符號重復(fù)了轿腺,必須要重新調(diào)整framework把其中重復(fù)的代碼去掉才行。動(dòng)態(tài)庫在編譯的時(shí)候不會(huì)報(bào)錯(cuò)丛楚,執(zhí)行的時(shí)候告警族壳。
手工添加Pods
因?yàn)閒ramework是自己手動(dòng)添加的target,其中使用到一些原APP已經(jīng)引入的Pods庫趣些。需要手工添加需要使用的Pods庫
有讀者知道這種情況下還可以用Pod install給我新加的target關(guān)聯(lián)使用Pods嗎仿荆?
具體需要做如下一些事情(中間過程磕磕碰碰,不一定能把完整的步驟復(fù)原出來坏平,如果有遺漏拢操,還請?zhí)釂枺?br> a. 在Setting里首先添加一個(gè)自定義變量PODS_ROOT,后面的挺多配置都需要這個(gè)環(huán)境變量的
b. 設(shè)置Pods頭文件引用路徑
c. 添加Pods的庫引用(由于歷史原因舶替,我的Pods還是靜態(tài)庫令境,沒改成動(dòng)態(tài)framework)
最后,還有一些意想不到的事情顾瞪。
a. 有個(gè)客戶說使用我們的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用不到這個(gè)功能的情況下惕橙,加了一個(gè)編譯選項(xiàng)關(guān)閉了這個(gè)動(dòng)作
b. 又有客戶說,用了我們SDK之后孵延,他的navigation bar的樣式總覺得不正常了吕漂。查看了代碼才知道,原來我們的APP里是通過[UINavigationBar appearance]修改了全局的樣式尘应。好吧惶凝,改成使用到SDK的界面才修改就好了吼虎。
c. 又有客戶說,用了我們的SDK之后苍鲜,每次進(jìn)入某個(gè)界面再出來就crashed了思灰。我那個(gè)去,還有這么神奇的事情混滔?仔細(xì)觀察界面洒疚,發(fā)現(xiàn)界面里有一個(gè)UITextView。有了a問題的經(jīng)驗(yàn)坯屿,全局搜索“+(void)load”油湖,有所發(fā)現(xiàn)了。我們APP用到了一個(gè)開源庫:UITextView+Placeholder领跛。這是一個(gè)給UITextView添加placeholder屬性的category乏德。其中有一個(gè)這樣的實(shí)現(xiàn)
+ (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進(jìn)去的KVO。
巧的是吠昭,我們的客戶也用到這個(gè)庫喊括。我們知道,一個(gè)類的多個(gè)category的+ (void)load都是會(huì)一一加載并執(zhí)行的矢棚,于是SDK里的這個(gè)category執(zhí)行一次load郑什,客戶的代碼執(zhí)行一次load。這樣dealloc和swizzledDealloc就被調(diào)換了兩次蒲肋,相當(dāng)于就是沒有調(diào)換了蘑拯。也就是說在UITextView的dealloc的時(shí)候,并沒有調(diào)用到期望的swizzledDealloc方法肉津,于是强胰,UITextView釋放的時(shí)候,注冊了的KVO沒有被釋放妹沙,于是crash!
解決的辦法熟吏?好吧距糖,我當(dāng)時(shí)只是簡單的把這個(gè)庫里注冊KVO的代碼注釋了(項(xiàng)目時(shí)間緊,客戶急牵寺,壓力大呀)悍引。
這個(gè)category里的KVO其實(shí)是為了實(shí)現(xiàn)在設(shè)置了placeholder之后,修改UITextView的字體帽氓,大小等屬性的時(shí)候趣斤,placeholder能夠相應(yīng)的響應(yīng)這些變化表現(xiàn)出一樣的字體和大小來。
周末終于有一些自己的時(shí)間了黎休,想了一個(gè)自己覺得能完美解決的方案浓领,暫時(shí)提交到我自己的Github分支上UITextView+Placeholder玉凯。同時(shí)PR了一份給原作者。不過联贩,作者已經(jīng)寫完這個(gè)庫有好幾個(gè)月了漫仆,不知道還會(huì)不會(huì)再看一眼這個(gè)項(xiàng)目了:)