iOS组件化:2.多target模式下的组件配合使用(OC语言)

业务场景:比如有一系列功能类似的app,只是包名不同,个别页面不同,但其他大部分功能和接口数据都相同时,又不想为每个项目单独创建工程,则可以在基于组件化的workspace中添加多target模式进行开发

针对这种需求,我的做法是先针对一个完整的app做模块化拆分并做成framework的形式,在pods中引用;转变为framework的方法在iOS组件化第一篇内容中有所讲解,这里建议做本地组件化工程,不建议将各个组件放在github或其他服务器上,因为组件修改需要上传再pod下来,比较繁琐也不利于后期的维护。

1.添加target后build settings中展现的样式如下

建议在创建target前,先配置好project下所有环境变量,这样在原有target基础上复制时,会将主工程配置项一起自动复制到新target中

添加target

2.每个target下的资源配置方法如下图

主工程目录下添加的每个target配置信息

所添加的资源可以在xcode右边控制面板中的target membership中关联target(只有关联了target的文件在编译时才会被包含进安装包中)

注意:除了.m文件不能与组件中的重复之外,其他格式的文件均可重名;主要是因为项目中的组件都是使用了framework(称之为动态库)的形式加载,每个组件中自有的文件都只包含在其framework文件中(类似c++的命名空间的概率);不同于以往静态库.a文件(在主目录下与所有文件同级,没有文本域的概念,不可重名);

3.加载xib,图片文件等方式方法的探索

场景一:组件中已封装了制定好的viewcontroller.xib,但在某个target下该界面的布局会有所不同,但又无法在组件中做兼容性处理(因为未来项目变化是未知的),这时候需要在有变动的target下新建一个对应的viewcontroller.xib,这样以来,在主目录下和组件中各存在一个xib;这时就需要选择性加载xib;

在选择性加载xib前我们先看看程序在编译后生成的文件存储关系,按下图步骤去找


通过此处跳转进入安装目录


这里可看到target(即主工程目录中的内容)和pod中的framework文件是同级的


随便进入一个framework看到由xib加密后生成而来的nib


以及我们随便通过右键查看一个target包内容后看到的此图中的同样的一个nib,这个nib即是主工程目录中重写的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模式下,如何进行资源文件的加载操作,做了具体分解操作;并对底层加载原理做了浅显的探索,希望能给同行小伙伴们提供一些有价值的参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容