0%

[UIViewController -loadView] 中的 Crash

一个 UIViewController 的 crash 问题,再探 loadView 机制

前情提要

在工程中,我们定义了一个用于展示报价的 ViewController,取名为 BaoJIaViewController。(注意这个JIa,并非本文笔误)

BaoJIaViewController 中定义了一个类名为 BaoJiaView 的自定义视图属性
BaoJiaView 类有个同名的 xib 文件 BaoJiaView.xib

在 ViewController 中对 BaoJIaViewController 的使用:

1
2
3
4
5
6
7
8
9
@implementation ViewController
- (void)viewDidLoad {
// ...
_baoJiaVC = [[BaoJIaViewController alloc] init];
[self addChildViewController:_baoJiaVC];
[self.view addSubview:_baoJiaVC.view];
// ...
}
@end

所有的代码跑的明明白白,直到我接收了项目…
BaoJIaViewController 这个名字怎么可以忍呢?改,现在就改

1
2
3
_baoJiaVC = [[BaoJiaViewController alloc] init];
[self addChildViewController:_baoJiaVC];
[self.view addSubview:_baoJiaVC.view];

于是乎,程序一跑,崩溃难找😭
(我用自己电脑运行了一下,居然有堆栈日志。。。公司的破机子上明明没有啊)

问题查找

鉴于公司电脑和自己电脑上的不同表现,此处以两条世界线展开

先以公司机子为例

公司电脑没有堆栈日志,毫无头绪。但是由于在调用 [BaoJiaViewController viewDidLoad] 方法之前就 Crash 了。我们尝试重写 loadView 方法。

1
2
3
4
5
6
@implementation BaoJiaViewController
- (void)laodView {
// 也不知道要写啥,先调用下 super,加个断点看看先
[super loadView];
}
@end

然后运行后发现,神奇地运行成功了。。。没有出现 Crash。。。
你这个 loadView 到底做了啥???

自己的电脑为例

自己的电脑上已经打印出来 Crash 时的堆栈,相比而言容易多了

crash_log

通过日志,我们发现 BaoJiaViewController 通过 _loadViewFromNibNamed:bundle: 方法加载了一个名为 BaoJiaView 的视图。
纳尼?你不是叫 BaoJiaViewController 么?还能够自己截去 ‘Controller’ 后缀,去加载前面的同名 View?

[UIViewController loadView] 探索

万事不决,先看文档。

先看看官方文档中对于 loadView 方法的描述:
建议看英文原文,对字母过敏的可以适量食用我的土味翻译。


You should never call this method directly. The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property.

你不要直接调用该方法。vc 会在它的 view 属性被使用且为 nil 的时候来调用该方法。这个方法会加载或者创建一个视图并复制给 vc 的 view 属性。

If the view controller has an associated nib file, this method loads the view from the nib file. A view controller has an associated nib file if the nibName property returns a non-nil value, which occurs if the view controller was instantiated from a storyboard, if you explicitly assigned it a nib file using the initWithNibName:bundle: method, or if iOS finds a nib file in the app bundle with a name based on the view controller’s class name. If the view controller does not have an associated nib file, this method creates a plain UIView object instead.

如果 vc 关联了一个 nib 文件,这个方法就会通过 nib 文件加载视图。如果 vc 的 nibName 属性返回了一个非空值,那么这个 vc 就有一个关联的 nib 文件。这通常是由于 vc 通过 storyboard 被初始化,或者你通过 initWithNibName:bundle: 方法指明了关联的 nib 文件,或者是因为 iOS 找到了一个基于 vc 类名的 nib 文件。
如果 vc 没有关联任何 nib 文件,这个方法会创建一个普通的视图对象。

If you use Interface Builder to create your views and initialize the view controller, you must not override this method.

如果你通过 IB 来创建了你的视图,并初始化了 vc,那么你就不应该重写(override)这个方法。

You can override this method in order to create your views manually. If you choose to do so, assign the root view of your view hierarchy to the view property. The views you create should be unique instances and should not be shared with any other view controller object. Your custom implementation of this method should not call super.

你可以通过重写这个方法来手动创建你的视图,记得把视图赋值给 vc 的 view 属性。该视图不应该共享给别的 vc,你的自定义实现中不应该调用 super。

If you want to perform any additional initialization of your views, do so in the viewDidLoad method.

如果你要做一些额外的视图初始化操作,请在 viewDidLoad 中进行。


我们发现,loadView 方法并不仅仅只是 self.view = [[UIView alloc] init]; 那么简单的实现。它会通过 nibName 属性,来加载相关的 xib 文件。

通过重写 BaoJiaViewControllernibName 的 getter 方法。

1
2
3
4
5
6
@implementation BaoJiaViewController
- (NSString *)nibName {
NSString *ret = [super nibName];
return ret; // ret = @“BaoJiaView”
}
@end

我们可以捕捉到当前的 nibName 属性居然是 @"BaoJiaView"

[UIViewController nibName] 探索

万事不决,再看文档。


This property contains the value specified at initialization time to the initWithNibName:bundle: method. The value of this property may be nil.

该属性的值是初始化时 initWithNibName:bundle: 方法指定的值。该属性可能为 nil。

If you use a nib file to store your view controller’s view, it is recommended that you specify that nib file explicitly when initializing your view controller. However, if you do not specify a nib name, and do not override the loadView method in your custom subclass, the view controller searches for a nib file using other means. Specifically, it looks for a nib file with an appropriate name (without the .nib extension) and loads that nib file whenever its view is requested. Specifically, it looks (in order) for a nib file with one of the following names:

如果你使用 nib 文件来存储你的 vc 的视图,推荐你在初始化 vc 的时候指明 nibName。如果你不使用 nib 文件,并且没有在你的自定义 vc 中重写 loadView 方法,vc 就会通过特定方式(顺序地)查找 nib 文件。

If the view controller class name ends with the word ‘Controller’, as in MyViewController, it looks for a nib file whose name matches the class name without the word ‘Controller’, as in MyView.nib.

如果 vc 的类名以 ‘Controller’ 结尾,例如 MyViewController,就会以 MyView 来查找 nib 文件 ‘MyView.nib’。

It looks for a nib file whose name matches the name of the view controller class. For example, if the class name is MyViewController, it looks for a MyViewController.nib file.

通过 vc 的类名直接查找,例如 MyViewController,就会直接查找 ‘MyViewController.xib’ 文件。

Note:
Nib names that include a platform-specific identifier such as ~iphone or ~ipad are loaded only on a device of the corresponding type. For example, a nib name of MyViewController~ipad.nib is loaded only on iPad. If your app supports both platform types, you must provide versions of your nib files for each platform.

还可以通过 ~iphone 或者 ~ipad 后缀来指定 nib 文件的平台。例如:’MuViewController~ipad.nib’ 只会在 iPad 平台上被加载。
如果你的 app 支持多平台,就可以通过这种方式来进行平台适配。


从中可以看出,如果不指定 nib 文件,且子类没有重写 loadView 方法,则会一次查找 MyView.xib 和 MyViewController.xib 来加载 vc 的视图。

怪不得如果我们重写了 loadView 方法,在内部调用 [super loadView],程序就不会 crash 了。

知道原因,就好改了。直接把 BaoJiaViewController refactor->rename 成 BaoJiaVC,搞定😄