一天一点xib:10说说原理、优化方面的东西吧

引言

本来“一天一点xib”系列就九篇文章,但在留言中有一个朋友提出了两点疑问:

1.为什么获得重用cell的时候用的是dequeueReusableCellWithIdentifier:而没有用dequeueReusableCellWithIdentifier: forIndexPath:这个函数。

2.为什么从xib获得cell的时候用的是loadNibNamed: owner: options:而不是registerNib: forCellReuseIdentifier:这个函数?

其实这两个问题归根结底是一个问题,我当时考虑写该系列文章的初衷就是想让大家能慢慢了解xib,并喜欢上xib,所以当时并没打算向大家讲解Nib的生命周期是什么样的、苹果是如何优化Nib的等一系列理论性强的东西,怕大家认为xib过于复杂。

后面写高冷的xib的时候我的初衷没有变,还是希望大家能喜欢xib,所以还是从实用角度向大家讲解xib的用法,上面的两个问题要详细解释就要涉及到一个类——UINib,要讲这个类,我想有必要对原理、或细节加以解释,下面就来好好的说一说。这里再次感谢coderzcj这个朋友的留言反馈。

从nib的生命周期谈起

这里为什么用词是nib而不是xib,大家看一天一点xib:2初识xib就明白了。nib生命周期在官方文档中有详细说明,这里我们简单总结一下:

1.将nib文件加载入内存

我们之前讲过一种方式就是用bundle对象的loadNibNamed: owner: options:方法:

TestView *aTestView = [[NSBundle mainBundle] loadNibNamed:@"TestView" owner:self options:nil][0];

其实还有另一种更好的方式完成这个过程,那就是用UINib,这个我们后面会重点说。

2.“解固化”并实例化出nib文件里对应的对象

关于“固化”和“解固化”在一天一点xib:2初识xib中也有说明,这个过程中会调用initWithCoder: 方法,注意不会再调用 initWithFrame: 方法了,步骤1虽然将nib加载到了内存,但是它还是“数据”的形式,而这一步把步骤1中的“数据”变成了“对象”。

3.建立connections(outlets、actions)

outlets与actions就是我们之前提到的建立的IBOutlet与IBAction的“连接”。建立connections的顺序是先建立outliets连接,然后建立actions连接。outlet建立的过程用到了setValue:forKey:方法,同时建立outlet的过程也是支持KVO的,如果你有一个属性:

@property (strong, nonatomic) IBOutlet UIView *testView

那么你就可以注册该属性,通过KVO的回调得知outlet建立关系的时刻:

[self addObserver:self forKeyPath:@"testView" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];

这里注意:options必须有NSKeyValueObservingOptionInitial,因为这是初始化阶段,必须有NSKeyValueObservingOptionInitial才会发生回调,只用NSKeyValueObservingOptionNew是没有回调发生的,只有初始化之后再重新赋值的时候用NSKeyValueObservingOptionNew才会发生回调。

action关系的建立就是调用UIControl类的addTarget:action:forControlEvents:方法。我们给xib上的一个button建立一个outlet,属性名字叫testBtn,再建立一个action,函数名字叫btnPressed,则xib的source code中就会将两者放在<connections>标签中,nib加载到步骤3的时候就会根据这个标签去建立对应的关系。


4.调用awakeFromNib方法:
- (void)awakeFromNib {
    [super awakeFromNib]; //这个函数要先调super
    //...做一些初始化之后的事情
    //注意该函数只会在绑定xib的类中调用,不会在它的File's Owner及其内部的Object类中调用
}
5.将xib中可见的控件显示出来。

loadNibNamed: owner: options:的问题

了解了生命周期之后,再看我们平时用bundle对象的loadNibNamed: owner: options:方法会产生什么问题。如果我们显示一个tableView,一般正常情况手机屏幕可以同时显示8-10个cell(假设cell用了xib,而且cell的类型都是一样的),那么就会加载8-10个cell,每个cell加载过程都会走上面的1-5个步骤,能不能把这个过程优化一下呢?

我们来分析一下cell加载的5个步骤:步骤1:频繁的加载文件到内存,肯定有效率的问题。由于cell的类型都是样的,所以加载的xib文件是一个,这里是可以优化的,因为每次加载到内存的xib文件是相同的,所以可以把它缓存到内存中,每次加载cell的时候步骤1直接用缓存。苹果就是这样优化的,据说效率比优化前提升2倍,如何把xib文件缓存起来?

答案是:UINib


bundle与UINib对比

bundle对象无法与xib文件产生映射关系,所以每次加载cell都是读文件,而UINib对象与xib文件是映射关系,它就是内存中的xib文件,图中的两条红色箭头是理解的关键,所以我们可以通过UINib对象来达到缓存的目的

UINIb使用

UINib是iOS4就已经存在的类了,但是整个优化逻辑是iOS5、iOS6才完善的,UINib提供的方法很简单:

NS_CLASS_AVAILABLE_IOS(4_0) @interface UINib : NSObject 

+ (UINib *)nibWithNibName:(NSString *)name bundle:(nullable NSBundle *)bundleOrNil;

+ (UINib *)nibWithData:(NSData *)data bundle:(nullable NSBundle *)bundleOrNil;

- (NSArray *)instantiateWithOwner:(nullable id)ownerOrNil options:(nullable NSDictionary *)optionsOrNil;
@end

我们通常使用nibWithNibName: bundle: 这个方法来初始化,name就是xib的名称,bundle一般就是[NSBundle mainBundle],或者传nil系统自动就帮你指定mainBundle了。

instantiateWithOwner: options: 方法的参数和我们之前说的loadNibNamed: owner: options:中的参数是一样的

学习就是不断修正对事物认识的过程

我们之前在讲loadNibNamed: owner: options:方法的时候屏蔽了好多细节,目的是为了帮助大家在入门的时候好理解,现在是时候详细的把这个函数说明白了。

再说loadNibNamed: owner: options:

name参数就是xib的名字,owner当时说默认传self,其实这个owner就是指File's Owner,因为一般情况都是在File's Owner类中初始化话该xib的。

下面说最难说明白的参数options(一般传nil),还记得高冷的xib中的有个object的用法吗?是不是感觉很厉害?这个参数可以执行更厉害的。

我们建一个Xib10Demo的程序,建立PersonHandle类,继承自NSObject,建立PersonView类,继承自UIView,再建立PersonView.xib文件,在Identity inspector中指定class为PersonView,把view弄的小一点,设置个背景


在PersonView.xib中添加一个按钮,再添加IBAction事件:

@implementation PersonView
- (IBAction)personBtnPressed:(id)sender {
    NSLog(@"%@:我的button点击了", NSStringFromClass([self class]));
}
@end

在PersonHandle中添加如下代码:

@implementation PersonHandle
- (IBAction)personHandle:(id)sender {
    NSLog(@"%@可以处理personView中的button点击事件", NSStringFromClass([self class]));
}
@end

给PersonView.xib添加File's Owner:ViewController,再把里面的按钮拖动到ViewController中,添加IBAction事件:


- (IBAction)personViewBtnPressed:(id)sender {
    NSLog(@"%@:我的PersonView上的button点击了", NSStringFromClass([self class]));
}

在ViewController类中使用PersonView:

- (void)viewDidLoad {
    [super viewDidLoad];
    PersonView *aPersonView = [[NSBundle mainBundle] loadNibNamed:@"PersonView" owner:self options:nil][0];
    [self.view addSubview:aPersonView];
}

运行程序,点击按钮输出:

2016-01-28 22:02:38.673 Xib10Demo[772:29149] PersonView:我的button点击了
2016-01-28 22:02:38.674 Xib10Demo[772:29149] ViewController:我的PersonView上的button点击了

到这里一切正常,下面就是见证options参数奇迹的时刻!

选择PersonView.xib文件,在控件选择区域中选择External Object,并把它拖到左边栏中,在Identify inspector中将class设置成为PersonHandle,在Attributes inspector中将Identifier设置成testKey。再把PersonHandle中的personHandle:方法与PersonView里的button建立IBAction的“连线”



修改ViewController类中的代码:

@implementation ViewController{
    PersonHandle *_aPersonHandle;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _aPersonHandle = [PersonHandle new];
    NSDictionary *pramaDic = @{@"testKey" : _aPersonHandle}; //这里注意传入这个dic中的_aPersonHandle对象必须是全局变量
    NSDictionary *optionDic = @{UINibExternalObjects : pramaDic};
    PersonView *aPersonView = [[NSBundle mainBundle] loadNibNamed:@"PersonView" owner:self options:optionDic][0];
    //options这个参数只接收一个key为UINibExternalObjects的字典,这个字典的value也必须是个字典,这个value字典的key就是xib中设置的external object的identifier
    [self.view addSubview:aPersonView];
}

再运行,点击按钮输出:

2016-01-28 22:29:49.767 Xib10Demo[885:45056] PersonView:我的button点击了
2016-01-28 22:29:49.768 Xib10Demo[885:45056] PersonHandle可以处理personView中的button点击事件
2016-01-28 22:29:49.768 Xib10Demo[885:45056] ViewController:我的PersonView上的button点击了

这就是options参数的魔力,我无法用语言说明它到底是干什么的,希望通过这个例子大家自己去体会。

问题来了:这有什么用?

具体应用总是被你的思路所局限住,你的思路有多广,xib就有多灵活

给VC瘦身是我们每个人iOS程序员都在考虑的事情,你有没有想过把它和xib结合起来?有好多人是把tableView的delegate和datasource处理抽出到另一个类中从而减少VC代码,你有没有想过这个过程靠xib来实现?
看这样一个UI:


我们假设交互是这样的:我的 | 推荐 这里点击后选中的item会变色,另一个保持黑色,底部的指示条会会有滑动的动画,并且停止时有阻尼效果(左右滑动衰减最终停下),下面的整个View随着item的切换左右发生切换,并刷新数据。

我这里给大家一个路:既然 我的 | 推荐 这个View需求这么多,就从VC中抽出来,用一个带xib的view管理,类似于我们刚刚例子中的PersonView,它的交互和动画效果可以在自己类的IBAction函数中执行,而点击后下面整个View左右切换可以由VC自己来完成,毕竟这是它主要显示的View,方式就通过Files's Owner中的IBAction函数来执行,数据的显示就涉及到了tableView的delegate和datasource了,我们这里把这部分功能从VC中分出来,给一个继承自NSObject的类管理就行,类似于我们上面例子中的PersonHandle,它处理的事件就可以通过options参数的形式来实现,这里屏蔽了细节的实现,只说了说思路,仅仅起一个抛砖引玉的效果,希望大家能够灵活运用xib。

最后说说loadNibNamed: owner: options:方法返回NSArray的问题,之前和他家说固定取[0]就行,现在要再详细的说说了,最早的xib中是只允许有一个对象的,所以固定取[0]就行,后来我们可以向一个xib中拖多个对象,建立一个TwoViews.xib文件,向其中拖入两个对象:


xib中有两个对象
NSArray *twoViewsArr = [[NSBundle mainBundle] loadNibNamed:@"TwoViews" owner:nil options:nil];
NSLog(@"twoViewArr:%@", twoViewsArr);

打印结果:

2016-01-28 23:03:21.034 Xib10Demo[959:58681] twoViewArr:(
    "<UIView: 0x7fb1f8d073a0; frame = (0 0; 100 100); autoresize = W+H; layer = <CALayer: 0x7fb1f8d42570>>",
    "<UIImageView: 0x7fb1f8d49070; frame = (0 0; 200 200); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x7fb1f8d10cb0>>"
)

由此可以看出,如果你有多个对象的话,这个数组就不能固定取[0]了,要通过每个对象的Class,或者tag来区分你要用哪个对象。

这一点给我的启发是:可以把UI类似的对象放在一个xib文件中集中管理。

回到UINib

刚刚说了那么多关于loadNibNamed: owner: options:参数的问题就是为了说UINib的参数问题,因为他们的参数是一样的。

有了这些铺垫UINib用法就好说多了,一般来说UINib有两种用法:第一种就是用instantiateWithOwner: options:函数直接实例化对象使用,第二种是用registerNib:系列方法

用instantiateWithOwner: options:函数直接实例化对象

这个函数就不讲了,和上面用法一样,我们这里还是用最普通的用法,都传nil就行。假设我们有个VC中有个tableView,加载的cell都是一种样式的自定义cell,这里假设叫PersonCell,PersonCell类有一个PersonCell.xib文件,那么:

#import "SecondController.h"
#import "PersonCell.h"
@interface SecondController ()<UITableViewDelegate, UITableViewDataSource>
@end

@implementation SecondController {
    UINib *_personNib;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _personNib = [UINib nibWithNibName:@"PersonCell" bundle:nil]; //这句话就相当于把PersonCell.xib放到了内存里
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"person"];
    if (!cell) {
        cell = [_personNib instantiateWithOwner:nil options:nil][0];//最初屏幕显示的cell都没有重用,都是要创建的,但是此时创建的过程都是走的内存缓存,大大提高了效率
    }
    return cell;
}
@end
第二种是用registerNib:系列方法

registerNib:系列方法都是UITableView的,主要有:

- (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
- (void)registerNib:(nullable UINib *)nib forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
- (void)registerClass:(nullable Class)aClass forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);

如果我们把自定义cell对应的UINib对象注册到这些函数中去,也起到了缓存的效果,而且在返回UITableViewCell的函数中要调用dequeueReusableCellWithIdentifier: forIndexPath:这个方法来代替之前调用的dequeueReusableCellWithIdentifier: 方法,而且不需要判断cell如果是nil的话在创建cell的逻辑,为什么不需要这段逻辑了呢?

- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; 
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);

我们注意看这两个函数的返回值:nullable表示可以是空值,所以我们用它的时候如果没有cell的话要自己创建,而下面函数返回值中并没有nullable,证明它一定会存在,不需要我们再实现判断cell的逻辑了,__kindof用来说明返回值是UITableViewCell或者它的子类,类似于泛型,比之前用的id类型再强转要好多了。

之前OC是没有这些符号的,是swift推出后为了和swift在语言设计上保持一致,OC新版本中加入的,具体是哪个版本不清楚,但是是随着swift2和xcode7发布而出来的,这不是我们这篇文章讨论的问题,下面来看看具体用法:

#import "SecondController.h"
#import "PersonCell.h"
@interface SecondController ()<UITableViewDelegate, UITableViewDataSource>
@end

@implementation SecondController {
    UINib *_personNib;
    __weak IBOutlet UITableView *_pTableView;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _personNib = [UINib nibWithNibName:@"PersonCell" bundle:nil]; //这句话就相当于把PersonCell.xib放到了内存里
    [_pTableView registerNib:_personNib forCellReuseIdentifier:@"person"];
    //上面就是把_personNib注册到tableView里,由于后面并没有cell的判断,我们可以猜测这个函数其实就是用传入的_personNib参数实例化cell并把它放在重用池里,这样就一定存在cell,就不需要判断cell是否存在了。
    //这里其实也是有优化思想存在的,如果我们频繁在调用返回cell的函数中创建cell,会产生降低效率的隐患,所以我们提前把nib注册到tableView里,到时候直接从重用池中取就好了,但是注意这种用法的话,重用池中一定存在多个重用id相同的cell,此时就要用indexPath进行判断到底取哪个,这也就是为什么当我们用registerNib的时候苹果让我们用dequeueReusableCellWithIdentifier: forIndexPath: 函数替换dequeueReusableCellWithIdentifier:的原因。
    //本质上这种优化是把后面频繁多次调用datasource方法里要做的事情前置了。
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"person" forIndexPath:indexPath];
    return cell;
}
@end

注意上面代码中viewDidLoad里的注释是关键,是核心,要理解。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值