你有注意过objc办法声明处和办法完成处参数类型不一致的情况吗,就像这样:

@interface Person : NSObject
- (void)frothTime:(NSInteger)regionTime value1:(BOOL)value;
@end
@implementation Person
- (void)frothTime:(NSInteger)regionTime value1:(NSString *)value;
@end

这2个办法除了第2个参数的类型不相同,其它都相同,但一旦调用这个办法就会发生一个坏内存拜访的溃散,这是为什么呢?

这是我在实在项目中遇到的1个很有意思的问题,只需调用分类中的某个办法就百分百溃散,而且控制台没有任何有用的报错信息,被调用的办法里边的代码也都没有履行,非常难调试,我花了一些时间才弄懂了其间的原理,整理后分享出来,希望能帮到你,溃散如下图所示:

objc方法声明和实现由于参数类型不一致所引发的崩溃

以下是我简写后的代码,它是一份完整的代码而且能够直接运转。

@interface Person : NSObject
- (void)frothTime:(NSInteger)regionTime value1:(BOOL)value;
@end
@interface Person (Category)
- (void)frothTime:(NSInteger)regionTime;
- (void)frothTime:(NSInteger)regionTime value1:(NSString *)value;
@end
@implementation Person
- (void)frothTime:(NSInteger)regionTime value1:(BOOL)value {
    NSLog(@"%s", __func__);
}
@end
@implementation Person (Category)
- (void)frothTime:(NSInteger)regionTime {
    [self frothTime:regionTime value1:@"111"];
}
- (void)frothTime:(NSInteger)regionTime value1:(NSString *)value {
    NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
    Person *p = [[Person alloc] init];
    [p frothTime:123];
    return 0;
}

分析

运转代码后,会在 - (void)frothTime:(NSInteger)regionTime value1:(NSString *)value 这行代码处发生一条 EXC_BAD_ACCESS 溃散问题,经过打印和断点,能够看出办法内的代码并没有履行,阐明是调用这个办法时发生的溃散,所以能够扫除是办法内的代码问题。

溃散前的代码方位是 [self frothTime:regionTime value1:@"111"];,这行代码从表面上看没有任何问题,假如你把示例代码粘贴到 xcode 中,编译器或许会在这行代码后边给出1个正告: “Incompatible pointer to integer conversion sending ‘NSString *’ to parameter of type ‘BOOL’ (aka ‘signed char’)”,意思是说办法接纳的是一个 BOOL 类型的参数,而你传了一个 NSString * 类型。

仔细看一下代码,你会发现 Person 类中声明晰 - (void)frothTime:(NSInteger)regionTime value1:(BOOL)value;,而且分类中也有一个类似的声明 - (void)frothTime:(NSInteger)regionTime value1:(NSString *)value;,它们除了第2个参数类型不相同,其它都是相同的;熟悉objc的同学应该都知道,objc是没有办法重载的概念,也就是说分类中的办法其实和类中的办法,它们的办法签名都是 frothTime:value1:

现在有2个同名的办法完成,那么 [self frothTime:regionTime value1:@"111"]; 到底调用哪个办法呢?依照 xcode 给出的提示,似乎是调用 - (void)frothTime:(NSInteger)regionTime value1:(BOOL)value; 这个办法,因为编译器提示第2个参数类型不一致。

有些同学在这里或许有一个疑问,分明有2个办法,而且分类中的办法明显更适合调用方,为什么编译器认为咱们调用的是类中的办法而不是分类中的办法;有2点原因,第1是因为objc没有办法重载的概念,所以这2个办法对编译器来说其实都是相同的;第2是因为objc的分类是运转时加载的,编译器在编译时并不知道分类以及分类办法的存在。

和其它言语不相同,objc的办法声明和完成能够重复,只是不能在一个效果域中重复,例如在 @interface 和 @end 就不能一起存在 - (void)frothTime:(NSInteger)regionTime value1:(BOOL)value;- (void)frothTime:(NSInteger)regionTime value1:(NSString *)value;,即便它们的参数类型并不是彻底相同;可是能够在分类中写出和类中相同的办法声明或完成,即便你在分类中写出 - (void)frothTime:(NSInteger)regionTime value1:(BOOL)value; 这种和类中的办法彻底一模相同的办法也不会有任何报错信息,假如你不小心在分类中完成了和类中同名的办法,那么运转时会永远调用分类中的办法完成,不清楚为什么的同学自行上网寻觅答案。

现在咱们弄明白了为什么编译器会给出正告,也知道了实践调用的其实是分类中的办法完成,但分类中的办法参数类型和咱们传递的参数类型分明是一致的,那为什么还会溃散呢?

原因在于编译器在对代码进行编译时对 @"111" 这个参数是依照 BOOL 类型而不是 NSString 类型处理的,请看下图:

objc方法声明和实现由于参数类型不一致所引发的崩溃

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件途径 -o 输出的文件途径.cpp 将objc代码编译为C++代码。

能够看到编译器把参数强转成了 bool 类型,可是办法完成处却是依照 NSString 类型进行接纳的,依照 NSString 类型去拜访一个 bool 类型的内存,这就是溃散的真正原因

补充

假如你测验将 - (void)frothTime:(NSInteger)regionTime value1:(BOOL)value; 修正为 - (void)frothTime:(NSInteger)regionTime value1:(NSObject *)value;(其实能够把value的参数类型修正为任意objc目标类型,只需不是根底数据类型就行),注意:这里我只修正了办法声明处的参数类型,并没有修正办法完成处的参数类型;然后运转项目;正常运转并输出;编译后的代码截图如下:

objc方法声明和实现由于参数类型不一致所引发的崩溃

从截图中能够看到参数虽然仍是被强转成了 NSObjet 类型,可是据我观察,只需是objc目标都没关系,你能够把它改为 NSArray 等任何 objc 目标类型,虽然有编译正告,可是并不影响运转。

另外,你也能够将 [self frothTime:regionTime value1:@"111"]; 修正为 [self performSelector:@selector(frothTime:value1:) withObject:@(regionTime) withObject:@"111"];,项目也能够正常运转,原因和上面相同,因为 withObject 的参数类型是 id。

总结

  • 从上面的比如能够看出来,objc是一门非常动态的言语,这有许多好处,但也有许多坑,假如你不了解这些细节,那么就很或许会遇到各种奇奇怪怪的问题。
  • 由于 objc 没有办法重载的概念,所以在分类中写办法时必定必定必定要加前缀(即便办法的参数类型不相同也不行),因为你不知道它会不会掩盖类中的私有办法。
  • objc编译器会自动将传递的参数强转为办法声明中的参数类型,假如办法完成处声明处的参数类型不一致,编译器会以办法声明中的参数类型为准,可是运转时会以办法完成处的参数类型进行接纳。