業(yè)務場景:比如有一系列功能類似的app,只是包名不同藏鹊,個別頁面不同皮假,但其他大部分功能和接口數(shù)據(jù)都相同時,又不想為每個項目單獨創(chuàng)建工程衩椒,則可以在基于組件化的workspace中添加多target模式進行開發(fā)
針對這種需求蚌父,我的做法是先針對一個完整的app做模塊化拆分并做成framework的形式,在pods中引用毛萌;轉(zhuǎn)變?yōu)閒ramework的方法在iOS組件化第一篇內(nèi)容中有所講解,這里建議做本地組件化工程苟弛,不建議將各個組件放在github或其他服務器上,因為組件修改需要上傳再pod下來阁将,比較繁瑣也不利于后期的維護膏秫。
1.添加target后build settings中展現(xiàn)的樣式如下
建議在創(chuàng)建target前,先配置好project下所有環(huán)境變量做盅,這樣在原有target基礎上復制時缤削,會將主工程配置項一起自動復制到新target中
2.每個target下的資源配置方法如下圖
所添加的資源可以在xcode右邊控制面板中的target membership中關聯(lián)target(只有關聯(lián)了target的文件在編譯時才會被包含進安裝包中)
注意:除了.m文件不能與組件中的重復之外窘哈,其他格式的文件均可重名;主要是因為項目中的組件都是使用了framework(稱之為動態(tài)庫)的形式加載亭敢,每個組件中自有的文件都只包含在其framework文件中(類似c++的命名空間的概率)滚婉;不同于以往靜態(tài)庫.a文件(在主目錄下與所有文件同級,沒有文本域的概念帅刀,不可重名)让腹;
3.加載xib,圖片文件等方式方法的探索
場景一:組件中已封裝了制定好的viewcontroller.xib,但在某個target下該界面的布局會有所不同劝篷,但又無法在組件中做兼容性處理(因為未來項目變化是未知的)哨鸭,這時候需要在有變動的target下新建一個對應的viewcontroller.xib,這樣以來娇妓,在主目錄下和組件中各存在一個xib;這時就需要選擇性加載xib活鹰;
在選擇性加載xib前我們先看看程序在編譯后生成的文件存儲關系哈恰,按下圖步驟去找
針對以上文件存儲方式的初步了解后志群,我們來通過代碼實現(xiàn)xib的選擇性加載:
大家都知道uiviewcontroller初始化時可以通過方法initWithNibName:bundle:來選擇性加載xib着绷,而且即使只通過init方法初始化,其內(nèi)部實現(xiàn)也會去尋找是否有匹配的xib锌云,如果有則調(diào)用initWithNibName:bundle:方法荠医;為了使項目中統(tǒng)一使用init方法自動優(yōu)先選擇指定的xib,我們需要通過runtime中的swizlling技術來hook掉initWithNibName:bundle:方法來指定加載xib的優(yōu)先級桑涎;該hook方法中我們選擇先尋找主工程目錄中(上圖中的target即為主工程目錄文件)匹配的xib彬向,如果沒有再匹配各自framework中的xib。
所以我們創(chuàng)建了一個uiviewcontroller的分類攻冷,并添加如下代碼
//注:tc_swizzleSelector方法為自行封裝的swizzling方法娃胆,不懂的可以在網(wǎng)上搜索
@implementation UIViewController (runtime)
+ (void)load {
? ? static dispatch_once_t onceToken;
? ? dispatch_once(&onceToken, ^{
? ? ? ? Class class = [self class];
? ? ? ? //hook nib初始化方法,并選擇性加載bundle中的nib ? ? ? ?tc_swizzleSelector(class,@selector(initWithNibName:bundle:),@selector(initWithRTNibName:bundle:));
? ?});
}
- (id)initWithRTNibName:(nullableNSString*)nibNameOrNil bundle:(nullableNSBundle*)nibBundleOrNil {
//由于主工程目錄中如果存在xib等曼,那么nibNameOrNil變量實際上是沒有值的里烦,可能是底層已經(jīng)知道需要去主工程目錄找吧,在這里我們還是強制設置一個nib的name值禁谦,方便后面查找匹配
? ? NSString*nibName = (nibNameOrNil?nibNameOrNil:NSStringFromClass(self.class));
//1.主工程目錄下尋找xib(即上圖中的target目錄中)
? ?//判斷是否在主工程中存在(非多語言環(huán)境下會存在主工程目錄下)
? ? BOOL isNibExistInMainBundle = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle mainBundle] resourcePath],nibName]];
//檢查多語言文件下是否有xib(多語言環(huán)境下xib會存在Base.lproj文件中)
? ? if(!isNibExistInMainBundle) {
? ? ? ? nibName = [NSStringstringWithFormat:@"Base.lproj/%@",nibName];
? ? ? ? isNibExistInMainBundle = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle mainBundle] resourcePath],nibName]];
? ? }
//如果在主工程目錄下則直接調(diào)用swizzling的映射方法返回
? ? if(isNibExistInMainBundle) {
? ? ? ? return[selfinitWithRTNibName:nibNamebundle:[NSBundlemainBundle]];
? ? }
//2.在framework中尋找xib(注意胁黑,[NSBundle bundleForClass:[self class]]是獲取當前類所在bundle,可以理解為該bundle是一個framework)
? ? nibName = (nibNameOrNil?nibNameOrNil:NSStringFromClass(self.class));
? ? //判斷是否在組件中存在(非多語言環(huán)境下查找)
? ? BOOL isNibExist = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle bundleForClass:[self class]] resourcePath],nibName]];
? ? if(!isNibExist) {//檢查多語言文件下是否有xib
? ? ? ? nibName = [NSStringstringWithFormat:@"Base.lproj/%@",nibName];
? ? ? ? isNibExist = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle bundleForClass:[self class]] resourcePath],nibName]];
? ? }
? ? if(isNibExist) {//framework中存在則返回
? ? ? ? return[selfinitWithRTNibName:nibNamebundle:[NSBundlebundleForClass:[selfclass]]];
? ? }
//3.如果以上都未找到xib州泊,直接調(diào)用swizzling的映射方法返回
? ? return[selfinitWithRTNibName:nibNameOrNilbundle:nibBundleOrNil];
}
@end
場景二:uiimage類的圖片讀取有兩種丧蘸,①通過imageNamed:方法初始化,這種無非是hook imageNamed方法并指定bundle路徑加載拥诡;②通過xib中UIimageview控件加載圖片触趴,這種模式如果要指定bundle圖片氮发,也需要hook,但要特殊處理冗懦;先講一下xib加載的原理:xib在被解析時會執(zhí)行initWithCoder歸檔方法即解碼爽冕,那么xib中的控件實際上也會通過歸檔方式解壓處理,但我們看到的xib中的圖片控件雖然是uiimageview披蕉,但在解析時其中的image會映射為UIImageNibPlaceholder類颈畸,大致也可判斷UIimage或許是個類簇,所以我們需要hook掉 ?UIImageNibPlaceholder的initWithCoder方法來指定需要加載的圖片資源没讲;
下面貼出uiiamge的runtime代碼:
@implementationUIImage (FXExtensions)
//注:tc_swizzleClassSelector是封裝的在同一個類中做swizzling操作的方法眯娱;tc_swizzle2InstanceSelector是封裝的對兩個類中的兩個實例方法做swizzling操作的方法
+ (void)load {
? ? staticdispatch_once_tonceToken;
? ? dispatch_once(&onceToken, ^{
? ? ? ? //通過imageNamed方法選擇性加載圖片資源
? ? ? ? Class clazz = [self class];
? ? ? ? tc_swizzleClassSelector(clazz,@selector(imageNamed:),@selector(rtc_imageNamed:));
? ? ? ? //通過xib中加載圖片來選擇性加載圖片資源
//注意:這里使用字符串拼接方式來組合成私有類名是為了避免蘋果代碼審核發(fā)現(xiàn)調(diào)用私有類
? ? ? ? NSString *imgNibClassName = [[@"UIImage" stringByAppendingString:@"Nib"] stringByAppendingString:@"Placeholder"];
? ? ? ? ClassimgNibClass =NSClassFromString(imgNibClassName);
//這里是借用uiimage共有類添加的自定義方法initWithCoderForNib ,用來與UIImageNibPlaceholder類的initWithCoder方法做swizzling呼喚(這里就是典型的hook私有類的方法的做法)
? ? ? ? tc_swizzle2InstanceSelector(imgNibClass ,clazz ,@selector(initWithCoder:),? @selector(initWithCoderForNib:));
? ? });
}
//優(yōu)先使用主工程資源
- (id)initWithCoderForNib:(NSCoder*)aDecoder {
? ? NSString*resourceName = [aDecoderdecodeObjectForKey:@"UIResourceName"];
? ? UIImage*image = [UIImageimageNamed:resourceName];
? ? if(image) {
? ? ? ? returnimage;
? ? }
? ? return? [self initWithCoderForNib:aDecoder];
}
//優(yōu)先使用主工程資源
+ (nullableUIImage*)rtc_imageNamed:(NSString*)name {
//1.獲取主工程資源圖片
? ? UIImage *image = [UIImage imageNamed:name inBundle:[NSBundle mainBundle] compatibleWithTraitCollection:nil];
//2.如果主工程未獲取到爬凑,再執(zhí)行本方法回到來獲取當前framework中的圖片
if(!image) {
? ? ? ? image = [self rtc_imageNamed:name];
? ? }
? ? return image;
}
@end
場景三:關于UINib類的hook用途
這里講UINib是因為所有xib解析后都會通過uinib來完成ui的加載徙缴;
除了UIViewController的xib加載比較特殊外,其他自定義的xib均可通過UInib來指定加載方式;
所以需要hook掉UINib的初始化方法?initWithNibName:directory:bundle:
(針對UIViewController的xib加載例外做一個解釋:
大家應該注意到既然我可以通過hook Uinib的方法來指定xib的加載嘁信,為什么要要hook UIViewController的初始化方法呢于样?其實UIViewController初始化執(zhí)行initWithNibName:bundle:方法后其內(nèi)部還是會調(diào)用UINib類來完成最終的初始化,通過斷點即可判斷出來潘靖;那么就有疑問了穿剖,既然兩個方法都設置了加載xib的順序,豈不是重復了嗎卦溢?首先需要解釋一下UIViewController的初始化方法默認只會尋找mainBundle中的xib糊余,初始化時默認bundle參數(shù)為空,且僅當mainbundle中存在xib時,才會去初始化UINib单寂,所以我們需要在UIViewController中做hook處理確保bundle不為空贬芥,就會初始化UINib了。但為了兼容其他方式的xib加載凄贩,我們只能也hook UINib做選擇性加載xib了)
而uitableviewcell或uiview相關的自定義xib誓军,會默認去執(zhí)行UINib的初始化方法?initWithNibName:directory:bundle:,也不會存在像UIViewController這樣的問題疲扎;
下面貼出UINib的hook方法昵时,同樣是通過在分類中進行runtime操作
@implementationUINib (FXExtensions)
+ (void)load {
? ? staticdispatch_once_tonceToken;
? ? dispatch_once(&onceToken, ^{
? ? ? ? Classclazz = [selfclass];
? ? ? ? //@"initWithNibName:directory:bundle:"
? ? ? ? NSString *selStr = [[@"initWithNibName:" stringByAppendingString:@"directory:"] stringByAppendingString:@"bundle:"];
? ? ? ? SELoriginalSelector =NSSelectorFromString(selStr);
? ? ? ? SELswizzledSelector =@selector(initWithNibNameRTC:directory:bundle:);
? ? ? ? tc_swizzleSelector(clazz, originalSelector, swizzledSelector);
? ? });
}
//優(yōu)先使用主工程中的xib
- (id)initWithNibNameRTC:(NSString*)name directory:(id)dir bundle:(NSBundle*)bundle {
//1.查找mainbundle
? ? if ([UINib isNibExistInBundle:[NSBundle mainBundle] nibName:name]) {
? ? ? ? return [self initWithNibNameRTC:name directory:dir bundle:[NSBundle mainBundle]];
? ? }
//2.查找當前傳入的bundle
? ? if(bundle&&[UINib isNibExistInBundle:bundle nibName:name]) {
? ? ? ? return [self initWithNibNameRTC:name directory:dir bundle:bundle];
? ? }
//3.通過nib名稱獲取bundle并在其中查找
? ? NSBundle*currentBundle = [UINib fetchBundleWithNibName:name];
? ? if(currentBundle&&[UINibisNibExistInBundle:currentBundlenibName:name]) {
? ? ? ? return[selfinitWithNibNameRTC:namedirectory:dirbundle:currentBundle];
? ? }
? ? return [self initWithNibNameRTC:name directory:dir bundle:bundle];
}
//先查詢bundle目錄中的nib文件,如果沒有就查詢bundle下的Base.lproj下的nib文件
+ (BOOL)isNibExistInBundle:(nonnullNSBundle*)bundle nibName:(nonnullNSString*)nibName {
? ? BOOL isNibExist = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[bundle resourcePath],nibName]];
? ? if(!isNibExist) {
? ? ? ? isNibExist = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/Base.lproj/%@.nib",[bundle resourcePath],nibName]];
? ? }
? ? returnisNibExist;
}
//通過nibName生成獲取bundle
+ (NSBundle*)fetchBundleWithNibName:(nonnullNSString*)nibName {
? ? Classclazz =NSClassFromString(nibName);
? ? if(clazz) {
? ? ? ? return [NSBundle bundleForClass:clazz];
? ? }
? ? return nil;
}
@end
總結:本節(jié)主要針對多target模式下椒丧,如何進行資源文件的加載操作壹甥,做了具體分解操作;并對底層加載原理做了淺顯的探索壶熏,希望能給同行小伙伴們提供一些有價值的參考句柠。