总结
标号 | 主题 | 内容 |
---|---|---|
一 | OC的私有方法 | 私有变量/私有方法 |
二 | @property | 概念/基本使用/寻找方法的过程/查找顺序 |
三 | @synthesize | @synthesize概念/基本使用/注意点 |
四 | id | 静态类型和动态类型/有动态类型原因/id数据类型与静态类型 |
五 | new | new方法实现原理/alloc与init |
六 | 构造方法 | 重写init方法/使用注意/instancetype的作用 |
七 | 自定义构造方法 | 格式/继承中的构造方法 |
八 | 自定义类工厂方法 | 自定义类工厂方法/子父类中的类工厂方法 |
九 | 类的本质 | 类的本质/如何获取类对象/类对象的用法/存储/OC实例对象类对象元数据之间关系 |
一. OC的私有方法
1.OC中的私有变量
- 写在@implementation中的变量,默认是私有变量,并且与用@private修饰的成员变量不一样,@implementation中定义的成员变量在其它类中无法查看(即使是被@public来修饰,仍看不到),也无法访问
- @implementation中定义的私有变量只能在本类中访问
- 在类的实现(即.m文件)中也可以声明成员变量,但是因为在其他文件中通常都只是包含头文件而不会包含实现文件,所以在.m文件中声明的成员变量是@private的。在.m中定义的成员变量不能和它的头文件.h中的成员变量同名,在这期间使用@public等关键字也是徒劳的。
@implementation Dog { @public int _age;//在本类中是public,在其他类是private } @end
2.OC中的私有方法
- 私有方法:只有实现没有声明的方法
- 原则上:私有方法只能在本类的方法中才能调用。
- 注意: OC中没有真正的私有方法 因为OC是消息机制
@interface Dog : NSObject @end @implementation Dog - (void)eat { NSLog(@"啃骨头"); } @end int main(int argc, const char * argv[]) { Dog *d = [Dog new]; SEL s1 = @selector(eat); [d performSelector:s1]; OC中其它调用方式 id *d = [Dog new]; [d eat];//仍可以调用 [d performSelector:@selector(eat)];//仍可以调用 所以说OC中没有真正的私有方法 return 0; }
二[email protected]
- @property int age;//统称为属性
1.什么是@property
- @property是编译器的指令
- 什么是编译器的指令 ?
- 编译器指令就是用来告诉编译器要做什么!
- @property会让编译器做什么呢? +Xcode之前可以用@property在声明文件中告诉编译器声明成员变量的的访问器(getter/setter)方法
- 这样的好处是:免去我们手工书写getter和setter方法繁琐的代码
[email protected]基本使用
- 在@inteface中,用来自动生成setter和getter的声明
用@property int age;就可以代替下面的两行 - (int)age; // getter - (void)setAge:(int)age; // setter
- @property编写步骤
- 1.在@inteface和@end之间写上@property
- 2.在@property后面写上需要生成getter/setter方法声明的属性名称,注意因为getter/setter方法名称中得属性不需要,所以@property后的属性也不需要.并且@property和属性名称之间要用空格隔开
- 3.在@property和属性名字之间告诉需要生成的属性的数据类型,注意两边都需要加上空格隔开
[email protected]增强
- Xcode4.4之后对@property进行了一个增强,只要利用一个@property就可以对getter和setter进行声明和实现
- 如果没有将参数赋值给谁告诉@property,那么@property会默认将传入的属性赋值给_开头的成员变量
- 缺陷
- 没有对传入的数据进行过滤
- 如果想对数据进行过滤,那么就必须自己重写setter方法
- 注意
- 如果用@property生成的getter和setter方法,那么可以不写成员变量,因为property会在
.m
中自动生成一个以_开头的私有成员变量
- 如果重写了seter方法,那么property就只会生成getter方法
- 如果重写了geter方法,那么property就只会生成setter方法
- 如果同时重写setter和getter方法,property就不会自动生成私有的成员变量
- 如果用@property生成的getter和setter方法,那么可以不写成员变量,因为property会在
[email protected]修饰符
- 格式
- @property (属性修饰符) 数据类型 变量名称;
- 修饰是否生成getter方法的
- readonly 只生成getter方法,不生成setter方法
- readwrite 既生成getter 又生成setter方法(默认)
- 给属性值提供getter和setter方法,那么称为可读可写属性
- 如果只提供getter方法,那么称为只读属性
- 如果只提供setter方法,那么称为只写属性
@property (readonly) int age;
- 指定所生成的方法的方法名称
- getter = 你定制的getter方法名称
- setter = 你定义的setter方法名称(注意setter方法必须要有 :)
@property (getter = isMarried) BOOL married;
说明,通常BOOL类型的属性的getter方法要以is开头
三[email protected]
1.什么是@synthesize
- @synthesize是编译器的指令
- 什么是编译器的指令 ?
- 编译器指令就是用来告诉编译器要做什么!
- @synthesize会让编译器做什么呢?
- @synthesize用在实现文件中告诉编译器实现成员变量的的访问器(getter/setter)方法
- 这样的好处是:免去我们手工书写getterr和setter方法繁琐的代码
[email protected]基本使用
- 在@implementation中,用来自动生成setter和getter的实现
用@synthesize age = _age;就可以代替 - (int)age{ return _age; } - (void)setAge:(int)age{ _age = age; }
- @synthesize编写步骤
- 1.在@implementation和@end之间写上@synthesize
- 2.在@synthesize后面写上和@property中一样的属性名称,这样@synthesize就会将@property生成的什么拷贝到@implementation中
- 3.由于getter/setter方法实现是要将传入的形参给属性和获取属性的值,所以在@synthesize的属性后面写上要将传入的值赋值给谁和要返回哪个属性的值, 并用等号连接
- 以下写法会赋值给谁?
@interface Person : NSObject { @public int _age; int _number; } @property int age; @end @implementation Person @synthesize age = _number; @end int main(int argc, const char * argv[]) { Person *p = [Person new]; [p setAge:30]; NSLog(@"_number = %i, _age = %i", p->_number, p->_age);//_number = 30,_age = 0 return 0; }
[email protected]注意点
- @synthesize age = _age;
- setter和getter实现中会访问成员变量_age
- 如果成员变量_age不存在,就会自动生成一个@private的成员变量_age
- @synthesize age;
- setter和getter实现中会访问@synthesize后同名成员变量age
- 如果成员变量age不存在,就会自动生成一个@private的成员变量age
- 多个属性可以通过一行@synthesize搞定,多个属性之间用逗号连接
@synthesize age = _age, number = _number, name = _name;
四.id类型
1.静态类型和动态类型
- 静态类型
- 将一个指针变量定义为特定类的对象时,使用的是静态类型,在编译的时候就知道这个指针变量所包含的属性和方法以及所属的类,这个变量总是存储特定类的对象。
- 默认情况下,所有的数据类型都是静态数据类型
- 如果访问了不属于静态数据类型的属性和方法,那么编译器就会报错(解决方法:类型强制转换)
Person *p = [Person new];
- 动态类型
- 这一特性是程序直到执行时才确定对象所属的类
- 在编译的时候编译器并不知道变量的真实类型,只有在运行时才会知道
- 如果访问类不属动态数据类型的属性和方法,编译器不会报错
- 应用场景
- 多态,可以减少代码量,避免调用子类特有的方法需要强制类型转换
id == NSObject * 是万能指针 id obj = [Person new];
- 注意
- 通过静态数据类型定义变量,不能调用子类特有的方法
- 通过动态数据类型定义变量,可以调用子类特有的方法
- 通过动态数据类型定义变量,可以调用私有方法
- 访问类不属动态数据类型的属性和方法,编译时不保错,可能导致运行时错误
2.为什么要有动态类型?
- 我们知道NSObject是OC中的基类,那么任何对象的NSObject类型的指针可以指向任意对象.
- 因为NSObject是静态类型,如果通过它直接调用NSObject上面不存在的方法,编译器会报错。
- 你如果想通过NSObject的指针调用特定对象的方法,就必须把NSObject *强制转成特定类型,然后调用特有方法。 如下
//定义NSObject * 类型 NSObject* obj = [Cat new]; Cat *c = (Cat*)obj; [c eat];
- id 是一种通用的对象类型,它可以指向属于任何类的对象,也可以理解为万能指针 ,相当于C语言的 void *
- 因为id是动态类型,所以可以通过id类型直接调用指向对象中的方法, 编译器不会报错
/// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id; id obj = [Cat new]; [obj eat]; // 不用强制类型转换 [obj test]; //可以调用私有方法
- NSObject *与id的区别
- NSObject *是一个静态数据类型
- id是一个动态数据类型
- 注意:
- 在id的定义中,已经包好了*号。id指针只能指向OC中的对象
- 为了尽可能的减少编程中出错,Xcode做了一个检查,当使用id 类型的调用本项目中所有类中都没有的方法,编译器会报错
- id类型不能使用.语法, 因为.语法是编译时特性, 而id是运行时特性
3.id数据类型与静态类型
- 虽然说id数据类型可以存储任何类型的对象,但是不要养成滥用这种通用类型
- 如果没有使用到多态,尽量使用静态类型
- 静态类型可以更早的发现错误(在编译阶段而不是运行阶段)
- 静态类型能够提高程序的可读性
- 使用动态类型前最好判断其真实类型
- 动态类型判断类型
- 为了避免动态数据类型引发的运行时的错误,一般情况下如果使用动态数据类型定义一个变量,在调用这个变量的方法之前会进行一次判断,判断当前变量是否能够调用这个方法
- (BOOL)isKindOfClass:classObj 判断实例对象是否是这个类或者这个类的子类的实例
Person *p = [Person new]; Student *stu = [Student new]; //判断指定的对象是否是某一个类,或者是某一个类的子类 BOOL res = [p isKindOfClass:[Person class]]; NSLog(@"res = %i", res); // YES res = [stu isKindOfClass:[Person class]]; NSLog(@"res = %i", res); // YES
- (BOOL) isMemberOfClass: classObj 判断是否是这个类的实例
Person *p = [Person new]; Student *stu = [Student new]; BOOL res = [p isMemberOfClass:[Person class]]; NSLog(@"res = %i", res); // YES res = [stu isMemberOfClass:[Person class]]; NSLog(@"res = %i", res); // NO
- (BOOL) isSubclassOfClass:classObj 判断类是否是指定类的子类
BOOL res = [Person isSubclassOfClass:[Student class]]; NSLog(@"res = %i", res); // NO res = [Student isSubclassOfClass:[Person class]]; NSLog(@"res = %i", res); // YES
五. new
1.new方法实现原理
- 完整的创建一个可用的对象:Person *p=[Person new];
- new方法的内部会分别调用两个方法来完成3件事情:
- (1)使用alloc方法来分配
堆
存储空间,将所有的属性设置为0,返回分配的对象的地址; - (2)使用init方法来对对象进行初始化。默认情况下init的实现是什么都没有做,返回初始化后的实力对象地址
- (3)返回对象的首地址
- (1)使用alloc方法来分配
- 注意
- alloc与init返回的地址是同一个地址
This method is a combination of alloc and init. Like alloc, it initializes the isa instance variable of the new object so it points to the class data structure. It then invokes the init method to complete the initialization process.
- 可以把new方法拆开如下:
- (1)调用类方法+alloc分配存储空间,返回未经初始化的对象
Person *p1=[person alloc];
-
- (2)调用对象方法-init进行初始化,返回对象本身
Person *p2=[p1 init];
-
- (3)以上两个过程整合为一句:
Person *p=[[Person alloc] init];
- 说明:
- alloc 与 init合起来称为构造方法,表示构造一个对象
- alloc 方法为对象分配存储空间,并将所分配这一块区域全部清0.
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
-
- init方法是初始化方法(构造方法),用来对象成员变量进行初始化,默认实现是一个空方法。
An object isn’t ready to be used until it has been initialized. The init method defined in the NSObject class does no initialization; it simply returns self.
- init方法是初始化方法(构造方法),用来对象成员变量进行初始化,默认实现是一个空方法。
- 所以下面两句的作用是等价的
Person *p1 = [Person new]; Person *p = [[Person alloc] init];
- iOS 程序通常使用[[类名 alloc] init] 的方式创建对象,因为这个可以与其他initWithXX:...的初始化方法,统一来,使得代码更加统一.
六.构造方法
- 在OC中init开头的方法,称为构造方法
- 用途
- 用于初始化一个对象,让某个对象一创建出来就拥有某些属性和值
1.重写init方法
- 想在对象创建完毕后,成员变量马上就有一些默认的值就可以重写init方法
- 重写init方法格式:
- 1).必须先初始化父类,再初始化子类
- 2).判断父类是否初始化成功,只要初始化成功才能继续初始化子类
- 3).返回当前对象的地址
- (id)init { self = [super init]; if (self) { //初始化子类 // Initialize self. } return self; }
-
- [super init]的作用: 面向对象的体现,先利用父类的init方法为子类实例的父类部分属性初始化。
- self 为什么要赋值为[super init]: 简单来说是为了防止父类的初始化方法release掉了self指向的空间并重新alloc了一块空间。还有[super init]可能alloc失败,这时就不再执行if中的语句。
- 重写init方法其它格式
- (id)init { //不要把=写成== if (self = [super init]) { // Initialize self. } return self; }
2.练习
- 要求通过Person类创建出来的人初始化值都是10岁。
@implementation Person - (instancetype)init { if (self = [super init]) { _age = 10; } return self; } @end
- 让学生继承人类,要求学生对象初始化之后,年龄是10,学号是1,怎么办?
@implementation Person - (instancetype)init { //初始化父类 //如果初始化成功就返回对应的地址,如果失败就返回nil if (self = [super init]) { //设置属性的值 _age = 10; } //返回地址 return self; } @end @implementation Student - (instancetype)init { if (self = [super init]) { _no = 1; } return self; } @end
3.构造方法使用注意
- 子类拥有的成员变量包括自己的成员变量以及从父类继承而来的成员变量,在重写构造方法的时候应该首先对从父类继承而来的成员变量先进行初始化。
- 原则:先初始化父类的,再初始化子类的。
- 先调用父类的构造方法[super init];
- 再进行子类内部成员变量的初始化。
- 原则:先初始化父类的,再初始化子类的。
- 千万不要把
self = [super init]
写成self==
[super init] - 重写构造方法的目的:为了让对象方法一创建出来,成员变量就会有一些固定的值。
4.instancetype的作用
- instancetype与id相似,不过instancetype只能作为方法返回值,它会进行类型检查,如果创建出来的对象,赋值了其他的对象就会有一个警告信息,防止出错。
- 在以前init的返回值是id,那么将init返回的对象地址赋值给其他对象是不会报错的
5. instancetype与id的区别
- instancetype在编译时会报警告
- id可以用来定义变量,而instancetype只能用来作为返回值类型
- 以后但凡自定义构造方法,返回值类型尽量使用instancetype,不要使用id
// init此时返回值是id NSString *str = [[Person alloc] init]; // Person并没有length方法, 但是id是动态类型, 所以编译时不会报错 NSLog(@"length = %i", str.length); // init此时返回值是instancetype // 由于instancetype它会进行类型检查, 所以编译器会报警告 NSString *str = [[Person alloc] init]; NSLog(@"length = %i", str.length); instancetype *p = [[person alloc] init]; // 错误写法instancetype只能作为返回值
七. 自定义构造方法
1.自定义构造方法
- 自定义构造方法就是自定义一个init方法
- 要求
- 一定是对象方法
- 一定返回id/instancetype
- 方法名称一定以initWith开头
- 只要是构造方法就必须先初始化父类,再初始化子类
- 示例
@interface Person : NSObject @property int age; @property NSString *name; // 当想让对象一创建就拥有一些指定的值,就可以使用自定义构造方法 - (id/instancetype)initWithAge:(int)age; - (id/instancetype)initWithName:(NSString *)name; //自定义构造方法可以有1个或者多个参数 - (id/instancetype)initWithAge:(int)age andName:(NSString *)name; @end
- 一个类可以有0个或者多个自定义构造方法
2.继承中的自定义构造方法
- 不能在子类访问父类私有变量
@interface Person : NSObject @property int age; - (instancetype)initWithAge:(int)age; @end @interface Student : Person @property NSString *name; - (instancetype)initWithAge:(int)age andName:(NSString *)name; @end @implementation Student - (instancetype)initWithAge:(int)age andName:(NSString *)name andNo:(int)no { /* if (self = [super init]) { // 这个_age是父类中通过property自动在.m中生成的无法继承,不能直接访问 // _age = age; // [self setAge:age]; // _name = name; } return self; */ if(self = [super initWithAge:age andName:name]) { _no = no; } return self; } @end
- 父类的属性交给父类的方法来处理
@interface Person : NSObject @property int age; - (id)initWithAge:(int)age; @end @interface Student : Person @property NSString *name; - (id)initWithAge:(int)age andName:(NSString *)name; @end @implementation Student - (id)initWithAge:(int)age andName:(NSString *)name { if (self = [super initWithAge:age]) { _name = name; } return self; } @end
3.自定义构造方法的使用注意
- (1)自己做自己的事情
- (2)父类的属性交给父类的方法来处理,子类的方法处理子类自己独有的属性
- (3)自定义构造方法必须以intiWith开头,并且’W’必须大写
- (4)对象属性名称/方法名称不要以new开头,否则可能会引发未知错误
八. 自定义类工厂方法
1.自定义工厂方法
- 什么是工厂方法(快速创建方法)
- 类工厂方法是一种用于分配、初始化实例并返回一个它自己的实例的类方法。类工厂方法很方便,因为它们允许您只使用一个步骤(而不是两个步骤)就能创建对象. 例如new
- 概念
- 用于快速创建对象的类方法
- 作用
- 给对象分配存储空间和初始化这块存储空间
- 规范
- 一定是类方法
- 方法名称以类的名称开头,首字母小写
- 一定有返回值,返回值是id/instancetype
- 示例
+ (id)person; + (id)person { return [[Person alloc]init]; } + (id)personWithAge:(int)age; + (id)personWithAge:(int)age { Person *p = [[self alloc] init]; [p setAge:age]; return p; }
- apple中的类工厂方法
其实苹果在书写工厂方法时也是按照这样的规划书写
[NSArray array]; [NSArray arrayWithArray:<#(NSArray *)#>]; [NSDictionary dictionary]; [NSDictionary dictionaryWithObject:<#(id)#> forKey:<#(id<NSCopying>)#>]; [NSSet set]; [NSSet setWithObject:<#(id)#>];
2.子父类中的类工厂方法
- 由于之类默认会继承父类所有的方法和属性,所以类工厂方法也会被继承
- 由于父类的类工厂方法创建实例对象时是使用父类的类创建的, 所以如果子类调用父类的类工厂方法创建实例对象,创建出来的还是父类的实例对象
- 为了解决这个问题,以后在自定义类工厂时候不要利用父类创建实例对象,改为使用self创建,因为self谁调用当前方法self就是谁
- 示例
@interface Person : NSObject + (id)person; @end @implementation Person + (id)person { return [[Person alloc]init]; } @end @interface Student : Person @property NSString *name; @end @implementation Student @end int main(int argc, const char * argv[]) { Student *stu = [Student person];// [[Person alloc] init] [stu setName:@"lnj"]; // 报错, 因为Person中没有setName }
- 正确写法
@interface Person : NSObject + (id)person; @end @implementation Person + (id)person { // return [[Person alloc]init]; // 谁调用这个方法,self就代表谁 // 注意:以后写类方法创建初始化对象,写self不要直接写类名 return [[self alloc]init]; } @end @interface Student : Person @property NSString *name; @end @implementation Student @end int main(int argc, const char * argv[]) { Student *stu = [Student person];// [[Person alloc] init]类工厂方法 [stu setNo];//子类指针指向了父类,将类方法创建初始化对象写self而不要用类名 [stu setName:@"lnj"]; }
3.注意点
- 自定义类工厂方法,在类工厂创建对象时不要用类名来创建
- 一定要用self来创建
- self在类方法中就代表类对象,谁调用当前方法,self就代表谁
九. 类的本质
1.类的本质
- 类的本质其实也是一个对象(类对象),程序中第一次使用该类的时候被创建,在整个程序中只有一份.此后每次使用都是这个类对象,它在程序运行时一直存在。只要有类对象,就可以通过类对象来创建实例对象
- 类对象是一种数据结构,存储类的基本信息:类大小,类名称,类的版本,继承层次,以及消息与函数的映射表等
- 类对象代表类,Class类型,对象方法属于类对象
- 如果消息的接收者是类名,则类名代表类对象
- 所有类的实例都由类对象生成,类对象会把实例的isa的值修改成自己的地址,每个实例的isa都指向该实例的类对象
2.如何获取类对象
- 通过实例对象
格式:[实例对象 class ]; 如: [dog class];
- 通过类名获取(类名其实就是类对象)
格式:[类名 class]; 如:[Dog class]
3.类对象的用法
- 用来调用类方法
[Dog test]; Class c = [Dog class]; [c test];
- 用来创建实例对象
Dog *g = [Dog new]; Class c = [Dog class]; Dog *g1 = [c new];
4.OC实例对象/类对象/元对象之间关系
- Objective-C是一门面向对象的编程语言。
- 每一个对象 都是一个类的实例。
- 每一个对象都有一个名为isa的指针,指向该对象的类。
- 每一个类描述了一系列它的实例的特点,包括成员变量的列表,成员函数的列表等。
- 每一个对象都可以接受消息,而对象能够接收的消息列表是保存在它所对应的类中。
- 在Xcode中按Shift + Command + O 打开文件搜索框,然后输入NSObject.h和objc.h,可以打开 NSObject的定义头文件,通过头文件我们可以看到,NSObject就是一个包含isa指针的结构体.
NSObject.h @interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; } objc.h /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class; /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; };
- 按照面向对象语言的设计原则,所有事物都应该是对象(严格来说 Objective-C并没有完全做到这一点,因为它有int,double这样的简单变量类型)
- 在Objective-C语言中,每一个类实际上也是一个对象。每一个类也有一个名为isa的指针。每一个类都可以接受消息,例如[NSObject new],就是向NSObject这个类发送名为new的消息。
- 在Xcode中按Shift + Command + O , 然后输入runtime.h,可以打开Class的定义头文件,通过头文件我们可以看到,Class也是一个包含isa指针的结构体.
runtime.h struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
- 因为类也是一个对象,那它也必须是另一个类的实例,这个类就是元类 (meta class)。
- 元类保存了
类方法
的列表。当一个类方法
被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有则该元类会向它的父类查找该方法,直到一直找到继承链的头。 - 元类(metaclass)也是一个对象,那么元类的isa指针又指向哪里呢?为了设计上的完整,所有的元类的isa指针都会指向一个根元类(root metaclass)。
- 根元类(root metaclass)本身的isa指针指向自己,这样就行成了一个闭环。上面说??到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的isa指针在实际上很少用到。不过这么设计保证了面向对象的干净,即所有事物都是对象,都有isa指针。
- 由于
类方法
的定义是保存在元类(metaclass)中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以为了保证父类的类方法可以在子类中可以被调用,所以子类的元类会继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。
- 元类保存了
- 下面这张图或许能够 让大家对isa和继承的关系清楚一些
- 上图中,最让人困惑的莫过于Root Class了。在实现中,Root Class是指 NSObject,我们可以从图中看出:
- NSObject类对象包括它的对象实例方法。
- NSObject的元对象包括它的类方法,例如new方法。
- NSObject的元对象继承自NSObject类。
- 一个NSObject的类中的方法同时也会被NSObject的子类在查找方法时找到。
时间: 2024-10-10 04:04:09