UIViewController继承时父子类方法重名造成的一个crash

前言

最近在开发过程中,遇到了一个因为UIViewController继承时父子类方法重名造成的一个crash问题,本文是问题的原因分析和解决方法。

内容

1.一个crash问题的分析

有如下父子类,SubViewController继承于BaseViewController,父子类中均有-addConstraintsForSubviews这个重名的私有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//父类
@implementation BaseViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//添加子视图
[self base_lazyLoadSubviews];
//添加约束
[self addConstraintsForSubviews];
}
@end

//子类
@implementation SubViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//添加子视图
[self sub_lazyLoadSubviews];
//添加约束
[self addConstraintsForSubviews];
}
@end

实际运行过程中,遇到了如下的crash:

1
2
3
4
5
6
7
8
9
10
Terminating app due to uncaught exception 
'NSInvalidArgumentException', reason:
'NSLayoutConstraint for <UIImageView:
0x10bee7fa0; frame = (0 0; 0 0); opaque = NO;
userInteractionEnabled = NO; layer = <CALayer:
0x17023ba00>>: A multiplier of 0 or a nil
second item together with a location for the
first attribute creates an illegal constraint
of a location equal to a constant. Location
attributes must be specified in pairs.'

上述的-addConstraintsForSubviews方法中有用到AutoLayout做自动布局,crash也是因为自动布局导致的。
经过断点调试,也就是走了一遍ViewController的生命周期方法,分析出了这个crash的原因:

  • 1.子类执行-viewDidLoad时,调用了[super viewDidLoad],触发父类的-viewDidLoad
  • 2.父类的-viewDidLoad方法中实现了基础的UI加载和布局,也就是父类对应的-base_lazyLoadSubviews-addConstraintsForSubviews这两个方法。
  • 3.当父类执行这两个方法时,其实就已经有问题了,因为子类有同名的-addConstraintsForSubviews方法,导致父类的方法实现被子类覆盖了,此时父类会去调用子类的-addConstraintsForSubviews去布局。
  • 4.子类此时还没有执行-sub_lazyLoadSubviews方法,也就是子类的视图控件还没有添加到父视图上,此时执行AutoLayout,就造成了crash。

2.解决方法

在父类增加一些共有的UI组件,某些情况下可以简化开发,但其实更多的时候,并没有省却很多麻烦。个人建议还是不要在公有的父类中增加过多的UI特性,以免日后更多的不必要的麻烦,子类过多,已经足够引起你的重视了。当然如果只是项目中的基类,用作埋点或其他用途,那么增加其他的UI特性,更是不合适的了。
下面只是基于上述问题,提出对应的解决方案。

2.1.规范命名

这其实是算是一个命名不规范导致的问题,如果是父类私有的方法,还是增加前缀比较安全,所以最简单的解决方式就是改名:
-base_addConstraintsForSubviews
-sub_addConstraintsForSubviews

2.2.更加优雅的解决方式

上一种方法其实并不是很好,因为可能遇到其他开发者继承于你的父类的问题,如果每次都要告知对方去注意这些问题,就很容易出问题了。
这里提出一种参考系统的生命周期方法中回调父类的方式去解决这个问题。
在父类中的头文件,声明子类容易覆写的同类型方法:

1
2
3
4
@interface BaseViewController : UIViewController
- (void)lazyLoadSubviews NS_REQUIRES_SUPER;
- (void)addConstraintsForSubviews NS_REQUIRES_SUPER;
@end

方法声明过程中,使用了系统的宏NS_REQUIRES_SUPER来修饰,表示子类覆写该方法时,必须在方法内部调用super的这个方法,否则会有如下的警告(以系统的UITableViewCell-prepareForReuse方法为例):

UIViewController继承时父子类方法重名造成的一个crash-NS_REQUIRES_SUPE

这样即便子类使用者在不知情的情况下,覆写了父类的同名方法,也会有警告提示,只要执行了父类的同名方法,就可以避免上述的问题发生。

参考

  1. iOS 修饰符~ NS_UNAVAILABLE、NS_REQUIRES_SUPER
  2. NS_REQUIRES_SUPER

本文作者:霖溦
本文链接:https://kukumalucn.github.io/blog/2018/07/31/UIViewController继承时父子类方法重名造成的一个crash/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!

坚持原创技术分享,您的支持将鼓励我继续创作!