说一说基类 NSObject(三)
本节,我们继续学习NSObject,因为NSObject类是Cocoa框架下的基类,我们还是需要耐心学一学,认真的试一试,很多方法深深印在脑海里。
-
isEqual
判断两个对象是否相等,该方法定义在NSObject协议中,返回BOOL值。
-(BOOL)isEqual:(id)object;
这个方法比较两个对象,是如何比较的?是根据地址是否相同判断的吗?如果地址不同,就一定返回NO吗?我们一起试试吧。
image.png
新建两个类,ClassA,ClassB,ClassB是ClassA的子类,分别如下:
ClassA.h
//
// Lesson8_3
//
// Created by wenhuanhuan on 2020/2/28.
// Copyright © 2020 weiman. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClassA : NSObject
@property(nonatomic, copy)NSString * name;
@property(nonatomic, assign)int age;
-(instancetype)initWithName:(NSString *)name age:(int)age;
@end
NS_ASSUME_NONNULL_END
ClassA.m
//
// ClassA.m
// Lesson8_3
//
// Created by wenhuanhuan on 2020/2/28.
// Copyright © 2020 weiman. All rights reserved.
//
#import "ClassA.h"
@implementation ClassA
-(instancetype)initWithName:(NSString *)name age:(int)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
@end
ClassB.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClassB : ClassA
@property(nonatomic, copy)NSString * sex;
@end
NS_ASSUME_NONNULL_END
ClassB.m
#import "ClassB.h"
@implementation ClassB
@end
main函数中
//
// main.m
// Lesson8_3
//
// Created by wenhuanhuan on 2020/2/28.
// Copyright © 2020 weiman. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ClassA.h"
#import "ClassB.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
ClassA * a = [[ClassA alloc] initWithName:@"小明" age:10];
ClassA * a2 = [[ClassA alloc] initWithName:@"小黄" age:6];
ClassA * a3 = [[ClassA alloc] initWithName:@"小黄" age:6];
ClassA * a4 = a3;
NSLog(@"a == a2? %@", [a isEqual:a2] ? @"相等" : @"不相等");
NSLog(@"a2 == a3? %@", [a2 isEqual:a3] ? @"相等" : @"不相等");
NSLog(@"a3 == a4? %@", [a3 isEqual:a4] ? @"相等" : @"不相等");
ClassB * b1 = [[ClassB alloc] initWithName:@"小红" age:5];
ClassB * b2 = [[ClassB alloc] initWithName:@"小明" age:10];
NSLog(@"b1 == b2? %@", [b1 isEqual:b2] ? @"相等" : @"不相等");
NSLog(@"a == b2? %@", [a isEqual:b2] ? @"相等" : @"不相等");
}
return 0;
}
看看他们的内存地址:

a3和a4的地址相同,其他的地址都是不同的,即使内容相同,比较结果也是不同的。我们一起看看打印结果:

打印结果似乎印证了我们的猜想。我们再来看看字符串比较。
NSLog(@"内容相同的两个字符串是否相等? %@", [@"1" isEqual:@"1"] ? @"相等" : @"不相等");
NSString * s1 = @"a";
NSString * s2 = @"a";
NSString * s3 = [NSString stringWithFormat:@"a"];
NSString * s3_2 = [NSString stringWithFormat:@"a"];
NSMutableString * s4 = [NSMutableString stringWithFormat:@"a"];
NSMutableString * s5 = [NSMutableString stringWithFormat:@"a"];
NSLog(@"s1 == s2? %@", [s1 isEqual:s2] ? @"相等" : @"不相等");
NSLog(@"s1 == s2? %@", [s1 isEqualToString:s2] ? @"相等" : @"不相等");
NSLog(@"s1 == s3? %@", [s1 isEqual:s3] ? @"相等" : @"不相等");
NSLog(@"s1 == s4? %@", [s1 isEqual:s4] ? @"相等" : @"不相等");
NSLog(@"s4 == s5? %@", [s4 isEqual:s5] ? @"相等" : @"不相等");
NSLog(@"s1 == s4? %@", [s1 isEqualToString:s4] ? @"相等" : @"不相等");
NSLog(@"s4 == s5? %@", [s4 isEqualToString:s5] ? @"相等" : @"不相等");
NSLog(@"s1 = %p, s2 = %p",s1, s2);
NSLog(@"s3 = %p, s3_2 = %p",s3, s3_2);
NSLog(@"s4 = %p, s5 = %p", s4, s5);
先来看看它们的内存地址。

似乎与我们自定义的对象的地址格式不太一样。这就引起了我的好奇心,它们是谁?如何存储?有什么特点呢?
我们再来看看打印结果:

不管我们用哪种方式创建的字符串对象,只要内容相同,尽管地址可能不同,但是使用isEqual或者isEqualToString比较,返回都是相同的。为什么呢?
小小扩展
先来看看字符串的地址信息吧。
__NSCFConstantString
字符串常量存储区,字面量相等的共用一块常量存储地址,常量区的copy操作是浅拷贝依然相同地址。
字符串常量,是一种编译时常量,它的 retainCount 值很大,是 4294967295,在控制台打印出的数值则是 18446744073709551615==2^64-1,测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。是创建之后便是放不掉的对象。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例。
这也就解释了s1和s2内容相同,地址也相同的原因了。
NSTaggedPointerString
理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 位足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。从他的引用计数可以看出,这货也是一个释放不掉的单例常量对象。在运行时根据实际情况创建。
对于 NSString 对象来讲,当非字面值常量的数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型,如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 __NSCFString 类型。
这种对象被直接存储在指针的内容中,可以当作一种伪对象。
__NSCFString
__NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。这种类型的对象地址在堆上。
这也就解释了为什么s1和s2的地址是一样的, s3和s3_2的地址是一样的,因为他们是单利对象。那么__NSCFString的对象,跟普通对象一样,引用计数会增加和减少,地址也不一样,那么内容相同的对象为什么是相等的呢?
我们猜测,可能是NSString内部重写了isEqual方法,比较的是字符串的字面量,如果字面量相同,就认为这两个字符串是相同的。这只是我的个人猜想,如有错漏,还请不吝赐教。
我们再试试NSArray
printf("\n\n");
NSArray * array = @[@1, @2];
NSArray * array2 = @[@1, @2];
NSMutableArray * array3 = @[@1, @2].mutableCopy;
NSLog(@"array == array2? %d", [array isEqual:array2]);
NSLog(@"array == array2? %d", [array isEqualTo:array2]);
NSLog(@"array == array2? %d", [array isEqualToArray:array2]);
NSLog(@"array == array3? %d", [array isEqual:array3]);
NSLog(@"array: %p, array2: %p", array, array2);
NSLog(@"array3: %p", array3);
看看结果:

我们发现,即使数组的地址不同,比较结果也是相同的,说明比较的也是内容,这里不再做扩展。使用这些类型进行比较的时候,一定要注意一下。
嘿嘿···扯得有点远了,不过为了弄清楚疑惑,也是值得的。我们来继续本节内容的学习。
-
description
在NSObject中,有两个description,一个是只读属性,一个是类方法。
image.png

先来看看类方法description。
+(NSString *)description;
返回消息接收者的所属类的内容,通常是类名。
测试一下:
NSLog(@"NSObject: %@",[NSObject description]);
NSLog(@"NSString: %@",[NSString description]);
NSLog(@"ClassA: %@",[ClassA description]);
NSLog(@"ClassB: %@",[ClassB description]);
看看结果:

再来看看属性description。
我们测试一下。
ClassA * a1 = [[ClassA alloc] initWithName:@"大熊猫🐼" age:3];
ClassA * a2 = [[ClassA alloc] initWithName:@"红腹锦鸡" age:2];
NSLog(@"a1: %@", a1.description);
NSLog(@"a2: %@", [a2 description]);
当然了,对于@property声明的属性,我们也是可以使用调用方法的方式进行调用,相当于调用getter方法。
看看打印结果吧。

属性description返回的是对象所属的类的类名以及类对象自身。
我们可以重写description这个属性的getter方法,来打印我们自己想打印的内容。
我们在ClassA的实现文件中,重写description。

-(NSString *)description {
NSString * desc = [NSString stringWithFormat:@"%@ name: %@, age: %d",self.class, self.name, self.age];
return desc;
}
再来验证一下:
ClassA * a1 = [[ClassA alloc] initWithName:@"大熊猫🐼" age:3];
ClassA * a2 = [[ClassA alloc] initWithName:@"红腹锦鸡" age:2];
ClassB * b1 = [[ClassB alloc] initWithName:@"朱鹮" age:1];
b1.sex = @"雌性";
NSLog(@"a1: %@", a1.description);
NSLog(@"a2: %@", [a2 description]);
NSLog(@"b1: %@", b1.description);
打印结果:

我们发现,b1的独有属性没有打印,如果需要打印b1的独有属性,我们需要在b1中再次重写description方法。

结果:

-
消息发送
-(id)performSelector:(SEL)aSelector;
-(id)performSelector:(SEL)aSelector withObject:(id)object;
-(id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
以上三个方法都是定义在NSObject协议中的,如图所示:
image.png
SEL类型
程序中的方法名在编译后会被一个内部标识所代替,这个内部标识所对应的数据类型就是SEL类型。简言之,就是方法名编译后的类型就是SEL类型的。
@selector()
为了操作编译后的方法名,定义了@selector()指令。通过@selector()指令,可以直接引用操作后的选择器。
消息发送的参数就是SEL类型的。
-(id)performSelector:(SEL)aSelector;
向对象发送aSelector代表的消息,并返回消息的执行结果。
-(id)performSelector:(SEL)aSelector withObject:(id)object;
向对象发送aSelector代表的消息,带一个参数object,返回消息的执行结果。
-(id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
向对象发送aSelector代表的消息,带两个参数object1和object2,返回消息的执行结果。
我们使用performSelector执行一下我们刚才重写的description方法看看。
printf("\n");
NSString * desc = [a1 performSelector:@selector(description)];
NSLog(@"使用performSelector执行,\n%@", desc);
看看结果:

也是可以正确执行description方法的。
本着公平公正的思想,我们也得试试下面的两个带参数的方法。☺️
我们先在ClassA中添加几个测试方法。
//
// ClassA.h
// Lesson8_3
//
// Created by wenhuanhuan on 2020/2/28.
// Copyright © 2020 weiman. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ClassA : NSObject
@property(nonatomic, copy)NSString * name;
@property(nonatomic, assign)int age;
-(instancetype)initWithName:(NSString *)name age:(int)age;
-(void)play;
-(void)eatWithFood:(NSString *)food;
/**
兴趣爱好和年数
*/
-(void)interest: (NSString *)obj1 years:(int)years;
@end
NS_ASSUME_NONNULL_END
实现:
//
// ClassA.m
// Lesson8_3
//
// Created by wenhuanhuan on 2020/2/28.
// Copyright © 2020 weiman. All rights reserved.
//
#import "ClassA.h"
@implementation ClassA
-(instancetype)initWithName:(NSString *)name age:(int)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
- (void)play {
NSLog(@"%@, %@ 想要玩耍", self.class, self.name);
}
-(void)eatWithFood:(NSString *)food {
NSLog(@"%@ 爱吃 %@", self.name, food);
}
-(void)interest:(NSString *)obj1 years:(int)years {
NSLog(@"%@ 的爱好是 %@, 坚持了%d年了", self.name, obj1, years);
}
-(NSString *)description {
NSString * desc = [NSString stringWithFormat:@"%@ name: %@, age: %d",self.class, self.name, self.age];
return desc;
}
@end
测试main中添加:
[a1 performSelector:@selector(eatWithFood:) withObject:@"竹子"];
[b1 performSelector:@selector(interest:years:) withObject:@"吃虫子" withObject:@3];
看看打印结果:

发现不对了,我明明传入的是3年,怎么执行结果变成了-921212013呢?因为performSelector的参数都要求是id类型的,也就是无法传入值类型的,这是一个不愉快的地方。
现在又有个疑问了,既然可以直接用对象进行方法的调用,又简单又好用,为什么还要用performSelector呢?它有什么特点呢?
(1)参数不能传入值类型
这一点在刚才的测试中已经看到了,不再赘述,至于如何传入值类型,网上有资料,这里不做扩展了。
(2)动态执行不同的方法
我们在ClassB中也添加一个方法。
@interface ClassB : ClassA
@property(nonatomic, copy)NSString * sex;
-(void)work;
@end
实现:
@implementation ClassB
-(void)work {
NSLog(@"%@, %@ 想要工作", self.class, self.name);
}
-(NSString *)description {
NSString * desc = [NSString stringWithFormat:@"%@ name: %@, age: %d, sex: %@",self.class, self.name, self.age, self.sex];
return desc;
}
@end
测试:
SEL method = [a1 isMemberOfClass:[ClassA class]] ? @selector(play) :@selector(work);
[a1 performSelector:method];
SEL method2 = [b1 isMemberOfClass:[ClassA class]] ? @selector(play) :@selector(work);
[b1 performSelector:method2];
打印结果:

这个特性挺有用的,通过SEL类型来指定要发送的消息,这也是OC消息发送的方式,也是通过这种方式实现了OC的动态性。
(3)不安全
performSelecor响应了OC语言的动态性:延迟到运行时才绑定方法。当我们在使用上面的方法时,编译阶段并不会去检查方法是否有效存在,可能会给出警告:

警告我们这个方法可能会引起内存泄漏,因为这个方法是未知的。
即使我们要执行的方法不存在,它在编译的时候也不会报错,只有在运行时,才会检查这个方法是否有效,如果没有实现,就会发生崩溃。
我们把play的实现注释掉,声明留下,看一看。


运行看看:

发生了崩溃。
只有声明,没有实现,使用performSelector执行的时候会发生崩溃,所以是不安全的。
注意:
使用performSelecor的时候要判断方法是否有效,以保证程序是安全健壮的。
我们修改一下,

再次运行,程序没有发生崩溃,而是走到了else里面,如我们预料的一般。

(4) 可以执行私有方法
我们把程序修改如下:
SEL method = [a1 isMemberOfClass:[ClassA class]] ? @selector(play) :@selector(work);
if ([a1 respondsToSelector:method]) {
[a1 performSelector:method];
} else {
NSLog(@"方法无效");
}
我们把play方法声明以及实现注释掉,运行发现,程序打印“方法无效”。
我在实验的时候发现,只是注释掉play方法的声明,实现保留,还是可以顺利执行play方法的。


再次运行:

断点在if中,也就是还可以执行play方法。这就很有意思了,明明已经注释了声明,这个方法变成私有的了,依然能够找到,是不是挺奇怪的。
那么我们直接写一个私有方法进行调用试试。

开始调用:

我们在书写程序的时候发现,编译器对privateMethodTest这个私有方法不提示,我们手动敲出来这个方法,编译器也会给出警告,提示我们这个方法没有声明。
我们运行看看

再次证明,私有方法也可以执行,神奇呀。

是不是说明,performSelector这个方法可以执行私有方法呢?简直不敢相信自己的眼睛,再试一次。
这一次我们新建一个全新的ClassC,如下所示:

然后我们定义一个私有方法。

再次调用:

警告依旧,执行看看,见证奇迹的时刻。

再次成功的调到了私有方法!
🤩!🤩!🤩!
现在看来,应该是可以的。那这个方法是不是太强大了,如果我知道了某个私有方法,是不是可以使用它调用了呢,想想也很激动呢。不过,万一被苹果发现私自调用私有方法,也许会被拒呢,也是有风险的哟。
实验证明,
-(id)performSelector:(SEL)aSelector;
可以执行私有方法。
😁😁
本想把performSelector的近亲,NSRunLoop中的

在本节一起学习下的,但是发现,本节内容有点多了,贪多容易消化不良,还是放在下一节吧,我们一起深入学习一下performSelector的其他几个兄弟姐妹,看起来都很厉害呢。
下次再见,祝大家生活愉快!