背景
近期公司的项目开启了Swift
与ObjC
的混编,随之也将部分Xcode
的警告选项做了开启。开启后发现多出了很多的警告问题,大部分是代码不规范引起的低级的错误。于是在团队里成立了一个清理小组,专门立项对这些警告问题进行清理。这里总结一下我们在清理过程中发现的一些常见问题。
Warning2Error
Xcode的警告可以通过设置编译选项变为编译错误,参考文章。
具体操作方式如下:
-Werror: Turn warnings into errors.
-Werror=foo: Turn warning "foo" into an error.
-Wno-error=foo: Turn warning "foo" into an warning even if -Werror is specified.
-Wfoo: Enable warning foo
-Wno-foo: Disable warning foo
-w: Disable all warnings.
我们依靠人工的CodeReview很难做到没有遗漏,对于一些低级代码错误,还是需要借助自动化的方式来做,最好是在开发阶段实时报错提示,以此来避免出现低级失误,提高代码质量,守住研发底线。
所以这里先介绍将警告转换为错误的方法,在实际项目中对一些我们认为不应该出现的警告代码,设置为编译错误,减少这些风险代码的产生。这样的设置越早开启越好,在项目变大之后改起来成本会很高。
对于一些例外的情况,比如有的场景就是需要用一些会触发警告的代码,可以通过如下代码将Clang的诊断关闭:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
// 你自己的代码
#pragma clang diagnostic pop
常见问题
1. NS_DESIGNATED_INITIALIZER
警告
- 1: Method override for the designated initializer of the superclass '-init' not found
- 2: Convenience initializer missing a 'self' call to another initializer
- 3: Convenience initializer should not invoke an initializer on 'super
Clang选项
-Wobjc-designated-initializers
传送门
原因
以上3个警告是由于使用NS_DESIGNATED_INITIALIZER
不规范引起的。具体分析见这篇文章。
2. 方法形参命名重复
警告
- Redefinition of method parameter 'x'
原因
原因比较简单,在实现方法时出现了相同名称的形参,触发场景如下:
- (void)callMethodWithA:(NSString *)a andB:(NSString *)a
{
NSLog(@"%@", a);
}
这里有一点需要注意,在上述场景中,第二个变量a
的值会被忽略,即优先使用顺序靠前的实参的值。
//这里实际打印的是first
[self callMethodWithA:@"first" andB:@"second"];
这种警告改起来也很简单,确保参数命名不同即可。作为开发人员出现这种错误是不应该的,但是在实际项目中我们的确看到有很多这种警告,这种低级错误应该编译报错,而不是简单的给个警告。(需要调研Clang是否支持将该种类型警告变为错误)。
3. 方法重复声明
警告
- Multiple declarations of method 'x' found and ignored
Clang选项
-Wduplicate-method-match
传送门
原因
原因很直观,在类或者分类的声明中,出现了重复的方法名,触发场景如下:
@interface Warning : NSObject
- (void)callA;
- (void)callA;
@end
//或者分类中重复声明同一个方法
@interface Warning (Category)
- (void)callB;
- (void)callB;
@end
解决方案也简单,删除一个重复的命名即可,低级错误。
4. 无用变量
警告
- unused variable A
Clang选项
-Wunused-variable
-Wunused-const-variable
原因
声明的变量未使用,这种情况多数是代码迭代,逻辑变更时,没有考虑到要删除不再使用的变量。
5. 不会执行的代码
警告
- code will never be executed
Clang选项
-Wunreachable-code
传送门
原因
这个警告在我们项目中主要是在下面两个场景下触发:
//if 逻辑判断永远为false
if (a == b || c) {
//原意是a == b || a ==c,c是某个大于0的整数,写法错误导致else内的代码得不到执行
}
else {
// Warning
}
//方法逻辑不需要了,没有删除,为了省事直接前置return
- (void)someMethod
{
return;
//下面的代码逻辑不需要了
int a = 1;
NSLog(@"%d", a);
}
-
if
逻辑判断永远为false
- 代码逻辑废弃,前置
return
6. 方法有声明,没有实现
警告
- method definition for A not found
Clang选项
-Wincomplete-implementation
传送门
原因
类的定义中声明了某个方法,但是并没有实现。一般在定义协议时会声明方法,由遵循该协议的类去实现具体的方法。但是在我们的实际项目中,常见的场景是定义了基类,声明了方法,却不提供默认的实现,而仅仅在头文件里加了注释,告诉使用者需要子类来实现。
@interface SomeBaseClass : NSObject
- (void)methodA; //由子类实现
@end
@implementation SomeBaseClass
//no mehtod implementation for methodA
@end
这样的用法有些不伦不类,既不是协议,也不是一个完整的类,很容易给使用者造成困扰。
7. 隐式强引用self
警告
- block implicitly retains ‘self’; explicitly mention ‘self’ to indicate this is intended behavior
Clang选项
-Wimplicit-retain-self
传送门
原因
这个警告的常见出现场景是在block
内部直接使用实例变量_instance
这种方式:
@interface SomeClass : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation SomeClass
- (void)someMehtod
{
dispatch_async(dispatch_get_main_queue(), ^{
_name = @"John"; //没有明确使用self,直接通过下划线访问
});
}
@end
这种问题容易出现循环引用,需要block
捕获self
时,建议通过self->_name
或者self.name
的方式访问,不要直接访问_name
,将你的意图通过代码明确的表达出来,提升代码的可读性及可维护性。
8. 协议的方法没有实现
警告
- method A in protocol B not implemented
Clang选项
-Wprotocol
传送门
原因
遵循了协议B的某个类,没有实现协议B中声明的方法A。这个警告一般有两个场景会出现:
- 没有实现协议中要求为
@required
的方法 - 协议没有明确指定哪些方法是
@optional
的,默认情况下所有的方法都被认为是@required
这类警告要求我们在定义、实现协议时,要规范化:即定义时要明确哪些方法是必需实现的,哪些方法是可选的;实现时要保证实现所有必需的方法。
协议毕竟只是一份声明,一些类可能声明了自己遵循某个协议,但是实际并没有实现相关协议方法。这种情况下协议方法的调用者在执行某个方法前,不能简单的通过conformsToProtocol
来确保方法可调用,而是应该通过respondsToSelector
的方式来确认方法可调用。
9. 类型不匹配
警告
Clang选项
-Wincompatible-pointer-types
传送门
原因
这个警告是我们项目中发现最多的,原因很简单,就是变量类型不匹配,比如声明的UIImage *
类型变量,接受的却是一个UIView *
的返回值:
- (void)someMethod
{
UIImage *imageView = [self getImageView];
}
- (UIView *)getImageView
{
return [[UIView alloc] init];
}
在工程警告关闭的情况下,上述使用场景是不会报编译错误的,也不会有警告提示。开发人员在使用时没有意识到自己声明错类型了,依然将imageView
当作UIView
来使用,虽然在代码逻辑执行上没有什么问题,但是可读性、可维护性就相当差了。还有的案例如下:
@interface SomeClassA : NSObject
- (void)playWithVideoUrl:(NSString *)url;
@end
@implementation SomeClassA
- (void)playWithVideoUrl:(NSString *)url
{
self.video.url = url;//video.url实际是一个NSURL类型的变量
[self.video play];
}
@end
这里playWithVideoUrl
对外声明的是需要传入一个NSString *
类型的变量,但是内部实现时实际需要的是一个NSURL *
的类型,之所以上述代码在测试阶段没有发现问题,是因为在外部使用者调用时,传入的也是一个NSURL *
类型。简单来说错误的只有这个方法的声明,调用和实现两个部分都是按照预期进行的,这种错误给后续维护人员会带来很大的困扰。
我们的实际项目中关于NSArray
与NSMutableArray
来回赋值的类似例子有很多,一方面是对开发人员对类型的声明和实际传递不够关注;另一方面也是由于警告被关闭,对这类错误的容忍度很高,久而久之导致很多这种类型不匹配的问题出现,为后续进行警告清理带来很大的工作成本。所以强烈建议新的项目各种警告一定要开起来
总结
经过这次清理警告的过程,我认为一定要将警告重视起来,尤其是在团队协作的项目中,代码的质量也是团队的精神面貌的体现。总结起来有两点:
- 严格的把低级错误,由警告设置为错误,在开发阶段就将问题暴露。
- 在项目建立之初尽早的开启相关警告,避免问题积重难返。
最后也要建立起代码质量监控的手段,比如使用Sonar+Swift、Infer、OCLint、SwiftLint等工具,定期的分析项目的代码质量并解决相关风险代码问题。