iOS 初始化(init、initWithNibName、initWithCoder、initWithFrame、loadNibNamed)

3,087 阅读6分钟

很多朋友如果是初学iOS开发,可能会被其中的几个加载方法给搞得晕头转向的,但是这几个方法又是作为iOS程序员必须要我们掌握的方法,下面我将对这几个方法做一下分析和对比,看看能不能增加大家对几个方法的理解和使用。

代码段

// alloc init。(alloc: 创建对象,分配空间。 init/initWithNibName: 初始化对象及数据。)
- (instancetype)init

// 指定初始化方法。子类化UIViewController必须调用这个的超级实现方法,即使没有使用NIB。(为方便起见,默认的init方法会为您执行此操作,并为这两个方法参数指定nil)
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;

// 是当从 NIB 文件中加载对象的时候会调用
- (instancetype)initWithCoder:(NSCoder *)coder;

// 代码创建View时调用,是懒加载,只有到需要显示时,子控件才不是 nil
- (instancetype)initWithFrame:(CGRect)frame;

// 这个方法既可以用来创建 UIViewController,也可以创建 UIView 
- (NSArray *)loadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary<UINibOptionsKey, id> *)options;

// 控制器的View为空的时候调用,帮控制器加载View。子类应该在这里创建它们的自定义视图层次结构,永远不应该直接调用。
- (void)loadView;

UIViewController初始化方式

1. storyboard绑定:

这种就是我们新建一个工程的时候的默认方法,即 Main.storyboard 中绑定的 ViewController,我们在创建它的时候并未使用任何一行代码,除此之外在 storyboard 根据 segue 自动跳转也是这种情况。

2. instantiateViewControllerWithIdentifier:

这种的话是在 storyboard 中新建了ViewController 控件,并且设置了它的标示 id,之后就可以在代码中使用该方法创建控制器。

3. loadNibNamed:

这个方法既可以用来创建 ViewController,也可以创建UIView。

4. initWithNibName:

这个也是比较常用的一个初始化方法,值得注意的是在调用控制器的init方法的时候会在[super init]中自动调用initWithNibName:,方法的参数是该控制器类的类名字符串。

5. init

用了xib: 跟4是同一种,只是多了一步。

6. init

纯代码: 这种指的是完全使用代码创建控制器,和 xib 还有故事板无关。

总结

序号创建方法调用initWithCode调用awakeFromNib调用initWithNibName
1storyboard绑定
2instantiateViewControllerWithIdentifier
3loadNibNamed
4initWithNibName
5init
6init

UIView初始化方式

纯代码

一般来说我们的自定义类继承自UIView,首先在initWithFrame:方法中将需要的子控件加入view中。注意,这里只是加入到view中,并没有设置各个子控件的尺寸。

为什么要在initWithFrame:方法而不是在init方法?

因为使用纯代码的方式创建自定义类,在以后使用的时候可能使用init方法创建,也有可能使用initWithFrame:方法创建,但是无论哪种方式,最后都会调用到initWithFrame:方法。在这个方法中创建子控件,可以保证无论哪种方式都可以成功创建。

为什么要在initWithFrame:方法里面只是将子控件加到view而不设置尺寸?

前面已经说过,两种方式最后都会调用到initWithFrame:方法。如果使用init方法创建,那么这个 view 的 frame 有可能是不确定的:

UIView *myView = [[UIView alloc] init];
myView.frame = CGRectMake(0, 0, 100, 100);
...

如果是这种情况,那么在init方法中,frame是不确定的,此时如果在initWithFrame:方法中设置尺寸,那么各个子控件的尺寸都会是0,因为这个view的frame还没有设置。(可以看到是在发送完 init 消息才设置的)。

所以我们应该保证 view 的 frame 设置完才会设置它的子控件的尺寸。

layoutSubviews 方法中就可以达到这个目的。第一次 view 将要显示 的时候会调用这个方法,之后当 view 的尺寸(不是位置)改变时,会调用这个方法。

Xib方式

使用xib的方式可以省去initWithFrame:layoutSubviews中添加子控件和设置子控件尺寸的步骤,还有在view controller里面设置view的frame,因为添加子控件和设置子控件的尺寸以及整个view的尺寸在xib中就已经完成。 (注意整个view的位置还没有设置,需要在控制器里面设置。) 我们只需对外提供数据接口,重写setter方法就可以显示数据。

注意要将xib中的类设置为我们的自定义类,这样创建出来的才是自定义类,而不是默认的父类。

当然,用xib这种方式是需要加载xib文件的。加载xib文件有两种方法:

// 第一种方法(较为常用)
TDView *view = [[[NSBundle mainBundle] loadNibNamed:@"TDView" owner:nil options:nil] firstObject]; 
// TDView代表TDView.xib,代表TDView这个类对应的xib文件。这个方法返回的是一个NSArray,我们取第一个Object或最后一个(因为这个数组只有一个TDView没有其他对象)就是需要加载的TDView。

// 第二种方法
UINib *nib = [UINib nibWithNibName:@"TDView" bundle:nil];
NSArray *objectArray = [nib instantiateWithOwner:nil options:nil];
TDView *view = [objectArray firstObject];

补充

如果使用代码的方式创建控件,那么在创建时一定会调用initWithFrame:方法;如果使用xib/storyboard方式创建控件,那么在创建时一定会调用initWithCoder:方法。

initWithCoder:里面访问属性,比如self.button,会发现它是nil的,因为此时自定义控件正在初始化,self.button可能还未赋值(self.button是一个IBOutlet,IBOutlet本质上就相当于Xcode找到这个对应的属性,然后UIButton button = … , [self.view addSubview: button]这种操作,而这一切的操作都是相当于在TDView *view = [[TDView alloc] initWithCoder: nil]方法之后执行的。上面的代码就相当于用代码的方式实现Xcode在storyboard中加载TDView),所以如果在这个方法中进行初始化操作是可能会失败的。

所以建议在awakeFromNib方法中进行初始化的额外操作。因为awakeFromNib是在初始化完成后调用,所以在这个方法里面访问属性(IBOutlet)就可以保证不为nil

事实上使用xib创建自定义控件,我们可以将加载xib的过程封装到自定义的类中,只对外暴露一个初始化方法,这样外界就不知道内部是如何创建的自定义控件了。如下:

+ (instancetype)viewWithBook:(Book *)book {
    TDView *view = [[[NSBundle mainBundle] loadNibNamed: NSStringFromClass(self) owner: nil opetions: nil] firstObject];
    view.book = book;
    return view;
}

这样外界只需用viewWithBook:方法传入一个book,就可以创建一个TDView的对象,而具体是怎么创建的,只有TDView才知道。

如果我们想,无论是通过代码的方式,还是通过xib的方式,都会初始化一些值,那么我们可以将初始化的代码抽到一个方法里面,然后在initWithFrame:方法和awakeFromNib方法中分别调用这个方法。