业务场景:比如有一系列功能类似的app,只是包名不同,个别页面不同,但其他大部分功能和接口数据都相同时,又不想为每个项目单独创建工程,则可以在基于组件化的workspace中添加多target模式进行开发
针对这种需求,我的做法是先针对一个完整的app做模块化拆分并做成framework的形式,在pods中引用;转变为framework的方法在iOS组件化第一篇内容中有所讲解,这里建议做本地组件化工程,不建议将各个组件放在github或其他服务器上,因为组件修改需要上传再pod下来,比较繁琐也不利于后期的维护。
1.添加target后build settings中展现的样式如下
建议在创建target前,先配置好project下所有环境变量,这样在原有target基础上复制时,会将主工程配置项一起自动复制到新target中
2.每个target下的资源配置方法如下图
所添加的资源可以在xcode右边控制面板中的target membership中关联target(只有关联了target的文件在编译时才会被包含进安装包中)
注意:除了.m文件不能与组件中的重复之外,其他格式的文件均可重名;主要是因为项目中的组件都是使用了framework(称之为动态库)的形式加载,每个组件中自有的文件都只包含在其framework文件中(类似c++的命名空间的概率);不同于以往静态库.a文件(在主目录下与所有文件同级,没有文本域的概念,不可重名);
3.加载xib,图片文件等方式方法的探索
场景一:组件中已封装了制定好的viewcontroller.xib,但在某个target下该界面的布局会有所不同,但又无法在组件中做兼容性处理(因为未来项目变化是未知的),这时候需要在有变动的target下新建一个对应的viewcontroller.xib,这样以来,在主目录下和组件中各存在一个xib;这时就需要选择性加载xib;
在选择性加载xib前我们先看看程序在编译后生成的文件存储关系,按下图步骤去找
针对以上文件存储方式的初步了解后,我们来通过代码实现xib的选择性加载:
大家都知道uiviewcontroller初始化时可以通过方法initWithNibName:bundle:来选择性加载xib,而且即使只通过init方法初始化,其内部实现也会去寻找是否有匹配的xib,如果有则调用initWithNibName:bundle:方法;为了使项目中统一使用init方法自动优先选择指定的xib,我们需要通过runtime中的swizlling技术来hook掉initWithNibName:bundle:方法来指定加载xib的优先级;该hook方法中我们选择先寻找主工程目录中(上图中的target即为主工程目录文件)匹配的xib,如果没有再匹配各自framework中的xib。
所以我们创建了一个uiviewcontroller的分类,并添加如下代码
//注:tc_swizzleSelector方法为自行封装的swizzling方法,不懂的可以在网上搜索
@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变量实际上是没有值的,可能是底层已经知道需要去主工程目录找吧,在这里我们还是强制设置一个nib的name值,方便后面查找匹配
NSString*nibName = (nibNameOrNil?nibNameOrNil:NSStringFromClass(self.class));
//1.主工程目录下寻找xib(即上图中的target目录中)
//判断是否在主工程中存在(非多语言环境下会存在主工程目录下)
BOOL isNibExistInMainBundle = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle mainBundle] resourcePath],nibName]];
//检查多语言文件下是否有xib(多语言环境下xib会存在Base.lproj文件中)
if(!isNibExistInMainBundle) {
nibName = [NSStringstringWithFormat:@"Base.lproj/%@",nibName];
isNibExistInMainBundle = [[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithFormat:@"%@/%@.nib",[[NSBundle mainBundle] resourcePath],nibName]];
}
//如果在主工程目录下则直接调用swizzling的映射方法返回
if(isNibExistInMainBundle) {
return[selfinitWithRTNibName:nibNamebundle:[NSBundlemainBundle]];
}
//2.在framework中寻找xib(注意,[NSBundle bundleForClass:[self class]]是获取当前类所在bundle,可以理解为该bundle是一个framework)
nibName = (nibNameOrNil?nibNameOrNil:NSStringFromClass(self.class));
//判断是否在组件中存在(非多语言环境下查找)
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,直接调用swizzling的映射方法返回
return[selfinitWithRTNibName:nibNameOrNilbundle:nibBundleOrNil];
}
@end
场景二:uiimage类的图片读取有两种,①通过imageNamed:方法初始化,这种无非是hook imageNamed方法并指定bundle路径加载;②通过xib中UIimageview控件加载图片,这种模式如果要指定bundle图片,也需要hook,但要特殊处理;先讲一下xib加载的原理:xib在被解析时会执行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中加载图片来选择性加载图片资源
//注意:这里使用字符串拼接方式来组合成私有类名是为了避免苹果代码审核发现调用私有类
NSString *imgNibClassName = [[@"UIImage" stringByAppendingString:@"Nib"] stringByAppendingString:@"Placeholder"];
ClassimgNibClass =NSClassFromString(imgNibClassName);
//这里是借用uiimage共有类添加的自定义方法initWithCoderForNib ,用来与UIImageNibPlaceholder类的initWithCoder方法做swizzling呼唤(这里就是典型的hook私有类的方法的做法)
tc_swizzle2InstanceSelector(imgNibClass ,clazz ,@selector(initWithCoder:), @selector(initWithCoderForNib:));
});
}
//优先使用主工程资源
- (id)initWithCoderForNib:(NSCoder*)aDecoder {
NSString*resourceName = [aDecoderdecodeObjectForKey:@"UIResourceName"];
UIImage*image = [UIImageimageNamed:resourceName];
if(image) {
returnimage;
}
return [self initWithCoderForNib:aDecoder];
}
//优先使用主工程资源
+ (nullableUIImage*)rtc_imageNamed:(NSString*)name {
//1.获取主工程资源图片
UIImage *image = [UIImage imageNamed:name inBundle:[NSBundle mainBundle] compatibleWithTraitCollection:nil];
//2.如果主工程未获取到,再执行本方法回到来获取当前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初始化执行initWithNibName:bundle:方法后其内部还是会调用UINib类来完成最终的初始化,通过断点即可判断出来;那么就有疑问了,既然两个方法都设置了加载xib的顺序,岂不是重复了吗?首先需要解释一下UIViewController的初始化方法默认只会寻找mainBundle中的xib,初始化时默认bundle参数为空,且仅当mainbundle中存在xib时,才会去初始化UINib,所以我们需要在UIViewController中做hook处理确保bundle不为空,就会初始化UINib了。但为了兼容其他方式的xib加载,我们只能也hook UINib做选择性加载xib了)
而uitableviewcell或uiview相关的自定义xib,会默认去执行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);
});
}
//优先使用主工程中的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
总结:本节主要针对多target模式下,如何进行资源文件的加载操作,做了具体分解操作;并对底层加载原理做了浅显的探索,希望能给同行小伙伴们提供一些有价值的参考。