说一说基类 NSObject(三)

本节,我们继续学习NSObject,因为NSObject类是Cocoa框架下的基类,我们还是需要耐心学一学,认真的试一试,很多方法深深印在脑海里。

  1. 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;
}

看看他们的内存地址:


image.png

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


image.png

打印结果似乎印证了我们的猜想。我们再来看看字符串比较。

        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);

先来看看它们的内存地址。


image.png

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


image.png

不管我们用哪种方式创建的字符串对象,只要内容相同,尽管地址可能不同,但是使用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);

看看结果:


image.png

我们发现,即使数组的地址不同,比较结果也是相同的,说明比较的也是内容,这里不再做扩展。使用这些类型进行比较的时候,一定要注意一下。

嘿嘿···扯得有点远了,不过为了弄清楚疑惑,也是值得的。我们来继续本节内容的学习。

  1. description
    在NSObject中,有两个description,一个是只读属性,一个是类方法。


    image.png
image.png

先来看看类方法description。
+(NSString *)description;
返回消息接收者的所属类的内容,通常是类名。
测试一下:

    NSLog(@"NSObject: %@",[NSObject description]);
    NSLog(@"NSString: %@",[NSString description]);
    NSLog(@"ClassA: %@",[ClassA description]);
    NSLog(@"ClassB: %@",[ClassB description]);

看看结果:


image.png

再来看看属性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方法。
看看打印结果吧。

image.png

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


image.png
-(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);

打印结果:


image.png

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

image.png

结果:

image.png
  1. 消息发送
    -(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);

看看结果:


image.png

也是可以正确执行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];

看看打印结果:


image.png

发现不对了,我明明传入的是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];

打印结果:


image.png

这个特性挺有用的,通过SEL类型来指定要发送的消息,这也是OC消息发送的方式,也是通过这种方式实现了OC的动态性。

(3)不安全
performSelecor响应了OC语言的动态性:延迟到运行时才绑定方法。当我们在使用上面的方法时,编译阶段并不会去检查方法是否有效存在,可能会给出警告:


image.png

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

image.png
image.png

运行看看:


image.png

发生了崩溃。
只有声明,没有实现,使用performSelector执行的时候会发生崩溃,所以是不安全的。

注意:
使用performSelecor的时候要判断方法是否有效,以保证程序是安全健壮的。

我们修改一下,


image.png

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


image.png
(4) 可以执行私有方法

我们把程序修改如下:

    SEL method = [a1 isMemberOfClass:[ClassA class]] ? @selector(play) :@selector(work);
    if ([a1 respondsToSelector:method]) {
        [a1 performSelector:method];
    } else {
        NSLog(@"方法无效");
    }

我们把play方法声明以及实现注释掉,运行发现,程序打印“方法无效”。

我在实验的时候发现,只是注释掉play方法的声明,实现保留,还是可以顺利执行play方法的。


image.png
image.png

再次运行:


image.png

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

image.png

开始调用:


image.png

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


image.png

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


image.png

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


image.png

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


image.png

再次调用:


image.png

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


image.png

再次成功的调到了私有方法!

🤩!🤩!🤩!
现在看来,应该是可以的。那这个方法是不是太强大了,如果我知道了某个私有方法,是不是可以使用它调用了呢,想想也很激动呢。不过,万一被苹果发现私自调用私有方法,也许会被拒呢,也是有风险的哟。

实验证明,
-(id)performSelector:(SEL)aSelector;
可以执行私有方法。

😁😁

本想把performSelector的近亲,NSRunLoop中的


image.png

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

下次再见,祝大家生活愉快!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355