1、类
1)根类:因为类 NSObject 是层次结构的最顶层,因此称为根类。
- 可以将类称为子类(subclass)和父类(superclass),也可以将类称为子类和超类。
2)分类/类别(category):允许以模块的方式向现有类定义添加新的方法(默认不能添加实例变量)。扩展自己或他人以前实现的类,使它适合自己的需要。
分类的名称括在类名之后的一对圆括号“( )”中。
@interface QCStudent (Print)@end@implementation QCStudent (Print)@end
分类文件名使用符号“+”来分隔类和分类的名字(Xcode 会自动生成)。
QCStudent+Print.mQCStudent+Print.h
分类用它可以将类的定义模块化到相关方法的组或分类中。它还提供了扩展现有类定义的简便方式,并且不必访问类的源代码,也无需创建子类。
分类可以覆写该类中的另一个方法,但是通常认为这种做法是拙劣的设计习惯。一个类可以拥有多个分类。使用分类添加新方法来扩展类不仅会影响这个类,同时也会影响它的所有子类。分类为现有类添加新方法可能对你有用,但它们可能和该类的原始设计或意图不一致。对象/分类命名对必须是唯一的。
iOS 开发中,分类默认不允许添加属性。但是如果在自己开发的框架中,希望在分类中动态添加属性,可以通过 OC 运行时的关联对象功能添加,详见 iOS - OC Category 分类。
3)类的扩展:有一种特殊的情况是创建一个未命名的分类,并且括号“( )”之间不指定名字。
这种特殊的语法定义称为类的扩展。定义一个像这样的未命名的分类时,可以通过定义额外的实例变量和属性来扩展类,这在有命名的分类中是不允许的。
@interface QCStudent ()@end@implementation QCStudent@end
未命名的分类中声明的方法需要在主实现区域实现,而不是在分类的的实现区域。
未命名的分类的方法都是私有的。如果需要写一个类,而且数据和方法仅供这个类本身使用,未命名分类比较合适。
4)抽象类:有时创建类只是为了更容易创建子类。因此,这些类名为抽象类,或等价的称为抽象超类。在该类中定义方法和实例变量,但不希望任何人从这个类创建实例。
5)类与类之间的关系:
- 类的包含(复合):
- 一个类中有另一个类的
@class
声明。一个类中包含有#import
另一个类的.h
头文件。 - 一般在
.h
头文件中用@class
声明一个类,在.m
文件中使用到该类时再在.m
文件中包含该类的.h
头文件。
#import:文件包含。这种方式会包含被引用类的所有信息,包括被引用类的变量和方法;@class :类的声明。这种方式只是告诉编译器另一个类的声明,具体这个类里边有什么信息,这里不需要知道,等实现文件中具体用到时,才会真正的去查看引用类的信息,因为编译器不需要引入和处理整个文件,只需知道是一个类名,在 .h 文件中使用 @class 指令提高了效率。
- 一个类中有另一个类的
- 类的包含(复合):
6)类的加载:
1>、类加载时自动调用方法:
+ (void)load
;+ (void)load {NSLog(@"%@",@"Student ------------- load");}
2>、类首次使用时自动调用方法:
+ (void)initialize
;+ (void)initialize {NSLog(@"%@",@"Student ------------- initialize");}
3>、使用 %@ 打印对象时会自动调用方法:
- (NSString *)description
;// description 覆写- (NSString *)description {return [NSString stringWithFormat:@"age : %d, name : %@", self.age, self.name];}
7)是在子类中使用的实例变量,必须先在接口部分声明,而不是在实现部分声明。在实现部分声明和合成的实例变量是私有的,子类中不能直接访问,需要明确定义或合成取值方法,才能访问实例变量的值。
8)类前缀:
使用 Objective-C 开发 iOS 程序时,最好在每个类名前面加一个前缀,用来标识这个类。
- 目的是防止 N 个人开发了一样的类,出现冲突。
- 比如 Jake Will、Kate Room 在同一个项目中都各自开发了个 Button 类,这样的程序是不能运行起来的。
- 解决方案:Jake Will 的类名叫做 JWButton,Kate Room 的类名叫做 KRButton。
类前缀的设置
Xcode 6 之前:
在创建项目时设置。
Xcode 6 之后:
创建完项目后设置。
设置完后,再创建新的文件时会自动添加上设置的类前缀。
2、对象、方法
1)类的独特存在就是一个实例(对象),对实例执行的操作称为方法。
2)合成对象:可以定义一个类包含其它类的一个或多个对象,这个新类的对象就是所谓的合成对象,因为它是由其它对象组成的。
- 作为创建子类的代替方法,可以定义一个新类,它包含要扩展类的实例变量。然后,只需在新类中定义适合该类的方法。
3)实例初始化
1>、初始化方式:
alloc :方法保证对象的所有实例变量都变成初始状态。 创建对象。init :方法用于初始化类的实例变量。 初始化对象。new :可以将 alloc 和 init 的结合起来。 创建并初始化对象。
- 但用两步来实现创建和初始化的方式通常更好,这样可以在概念上理解正在发生两个不同的事件:首先创建一个对象,然后对它初始化。
2>、构造方法:实例初始化常见的编程习惯是类中所有初始化方法都以 init 开头。如果希望在类对象初始化时做一些事情,可以通过重载 init 方法达到这个目的。下面是重载 init 方法的一个标准模板。
不带参数:- (instancetype)init {self = [super init];if (self) {// 初始化代码}return self; }带参数:- (instancetype)initWithAge:(int)age andNo:(int)no { self = [super init];if (self) {_age = age;_no = no; }return self;}类方法:+ (instancetype *)studentWithAge:(int)age {Student *stu = [[Student alloc] init];stu.age = age;return stu;}
执行父类的初始化方法,使得继承的实例变量能够正常的初始化。如果父类初始化成功,返回的值将是非空的。self 用来指明对象是当前方法的接收者。必须将父类 init 方法的执行结果赋值给 self,因为初始化过程改变了对象在内存中的位置(意味着引用将要改变)。
特殊类型 instancetype 表明从 init 方法返回的类型与它的初始化类(也就是初始化消息的接收者)相同。 init 被定义为返回 instancetype 类型,这是编写可能被继承的类 init 方法的一般规则。当编译器遇见 instancetype 作为返回类型,它就知道返回的类型是发送消息的对象。
4)消息:请求一个类或实例来执行某个操作时,就是在向它发送一条消息,消息的接受者称为接收者。
- OC 采用特定的语法对类和实例应用方法:
[类/实例 方法];
- OC 采用特定的语法对类和实例应用方法:
- 5)类方法(静态方法):是对类本身执行某些操作的方法。
实例方法(动态方法):对类的实例执行一些操作。
创建方法名时,参数名实际上是可选的,参数名可以省略。如:
- (int)set :(int)name :(int)age;
方法(函数)不返回任何值时,无需在方法的末尾执行一条 return 语句。或者也可以执行一条不带任何指定值的 return 语句:
return;
。
6)重写(覆盖):在子类中新建一个与父类中的方法同名的方法。子类中的新方法必须具有相同的返回类型,并且参数的数目和覆写的方法相同。
- 如果需要来扩展继承的方法。子类中包含对
if (self = [super init])
的判断。
- 如果需要来扩展继承的方法。子类中包含对
7)重载:在类中,相同名字不同参数的方法的写法有一个专门的术语来描述,叫做重载。
8)懒加载
- 对象在用到时再去加载,而且只加载一次。加载的数据比较大时可以节省内存。
一般重写 getter 方法实现对象的懒加载。
@property (strong, nonatomic) NSArray *shops;- (NSArray *)shops {// 加载数据if (_shops == nil) {NSString *filePath = [[NSBundle mainBundle] pathForResource:@"shops" ofType:@"plist"];_shops = [NSArray arrayWithContentsOfFile: filePath];}return _shops;}
3、数据封装(实例变量)
1)数据封装:将实例变量隐藏起来的这种做法实际上涉及一个关键概念 --“数据封装”。
- 不能在类的外部编写方法直接设置或获取实例变量的值,而需要编写设置方法和取值方法来设置和获取实例变量的值,这便是数据封装的原则。
- 必须通过使用一些方法来访问这些通常对“外界”隐藏的数据,这种做法集中了访问实例变量的方式,并且能够阻止其它一些代码直接改变实例变量的值。如果可以直接改变,会让程序很难跟踪、调试和修改。
2)实例变量的定义作用域
@public 全局都可以访问,实例对象可以使用符号 “->” 直接访问实例变量。 @protected 只能在类内部和子类中访问 (访问器方法 默认) 。@private 只能在类内部访问 (合成取值方法 默认)。@package 常用于框架类的实例变量,同一包内能用,跨包就不能访问。
3)访问器方法(accessor):取值方法和设值方法通常称为访问器方法。通常实例变量声明时以下画线( _ )字符开头,此实例变量默认为保护(
@protected
)的。在类内部和子类中都可以访问。设值方法(setter):设置实例变量值的方法通常总称为设值方法。定义时在实例变量名前加上 set。如:
// ARC- (void)setAge:(NSNumber *)age {_age = age;}// MRC- (void)setAge:(NSNumber *)age {if (_age) { [_age release]; } _age = [age retain];}
取值方法(getter):用于检索实例变量值的方法叫做取值方法。定义时直接使用实例变量名。如:
- (NSNumber *)age {return _age;}
4)合成取值方法:通常实例变量声明时不以下画线( _ )字符开头,以字母开头,并且此实例变量是私有(
@private
)的。只能在类内部访问。在接口部分中使用 @property 指令标识属性,声明实例变量的 setter 和 getter 方法。 - 如:
@property int numerator, denominator;
在实现部分中使用 @synthesize 指令标识属性,实现实例变量的 setter 和 getter 方法。 - 如:
@synthesize numerator, denominator;
1>、如果使用了 @property 指令,就不需要在实现部分声明相应的实例变量。当然也可以再声明相应的实例变量,但是那不是必须要做的,编译器会有一些提示。
- 可以不使用 @synthesize 指令,使用 @property 指令就足够了,编译器会自动为你生成 setter 和 getter 方法(声明并实现),但是注意如果你不使用 @synthesize ,那么编译器声明的实例变量会以下划线( _ )字符作为其名称的第一个字符。
2>、@property 的修饰
在不写任何修饰时,Xcode 会自动生成标准的 setter 和 getter 方法,写修饰时 Xcode 会自动生成带内存管理的 setter 方法,标准 getter 方法。
参数分类:
读写属性:readwrite/readonlysetter 处理:assign/retain/copy原子性:atomic/nonatomic方法名:setter = method / getter = method引用型:strong/weak可选性:nonnull/nullable/null_unspecified/null_resettable // Xcode 7 新增特性readwrite:可读写,生成 setter 和 getter 方法。默认。readonly :只读,只生成 getter 方法。assign :修饰普通类型,在 setter 方法中直接赋值。默认。简单赋值,不更改引用计数。如:@property (nonatomic, assign)int age;retain :修饰 OC 对象,在 setter 方法中 release 旧值,retain 新值。释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的引用计数为 1。如:@property (nonatomic, retain)Dog *dog;copy :修饰 NSString 类型,在 setter 方法中 release 旧值,copy 新值。建立了一个相同的对象,地址不同(retain:指针拷贝 copy:内容拷贝)。如:@property (nonatomic, copy)NSString *name;atomic :原子性,默认。是 OC 使用的一种线程保护技术,防止在写入未完成的时候被另外一个线程读取,造成数据错误。给 setter 和 getter 方法加锁,保证多线程安全。nonatomic:非原子性,禁止多线程,变量保护,提高性能。不给 setter 和 getter 方法加锁,执行相对快点。setter = method:指定 setter 方法的方法名。 如:@property (nonatomic, setter = setIsRich)BOOL rich; 将 rich 的 setter 方法重命名为 setIsRich 。getter = method:指定 getter 方法的方法名。 如:@property (nonatomic, getter = isRich)BOOL rich; 将 rich 的 getter 方法重命名为 isRich 。strong :强引用,在 OC 中对象默认都是 strong。(ARC 下的)和(MRC)retain 一样 (默认)。viewController 对根视图是强引用,view addSubviews 方法是向数组中添加子视图,数组会对子视图强引用。weak :弱引用,weak 的作用,一旦没有强引用,会被立即释放。(ARC 下的)和(MRC)assign 一样。苹果从 StoryBoard 拖线默认是 weak。weak 当指向的内存释放掉后自动 nil 化,防止野指针。nonnull :不可为空nullable :可以为空null_unspecified:不确定是否可以为空(极少情况)null_resettable :set 方法可以为 nil,get 方法不可返回 nil,只能用在属性的声明中。
5)点运算符(点语法):访问的是方法(setter/getter 方法),不是实例变量。
合成取值方法中可以使用点运算符访问属性,也可以对自定义的方法使用点运算符,如语句
myFraction.print
,并未考虑编码风格是否良好。点运算符通常用在属性上,用于设置或取得实例变量的值。做其它工作的方法通常不是由点运算符执行的,而是使用传统的方括号形式的消息表达式作为首选的语法。
6)尖运算符(->):当实例变量定义为
@public
类型时,实例对象可以使用符号 “->” 直接访问实例变量。 如:car -> _speed = 80; int a = car -> _speed;
- 7)局部变量:是基本的 C 数据类型,并没有默认的初始值,所以在使用前要先赋值。
局部对象变量:默认初始值为 nil 。
- 方法的参数名也是局部变量。执行方法时,通过方法传递的任何参数都被复制到局部变量中。因为方法使用参数的副本,所以不能改变通过方法传递的原值。
- 如果参数是对象,可以更改其中的实例变量值。当你传递一个对象作为参数时,实际上是传递了一个数据存储位置的引用。
静态变量:在局部变量声明前加上关键字 static ,可以使局部变量保留多次调用一个方法所得的值。静态变量的初始值为 0 。
- 很多情况下想要将变量定义为全局变量,但不是外部变量,可以在包含这个特定类型实现的文件中将这个变量定义为 static 。
外部变量:在方法外定义的变量不仅是全局变量,而且是外部变量。
使用外部变量时,必须遵循下面这条重要原则:变量必须定义在源文件中的某个位置。即在所有的方法和函数之外定义变量,并且前面不加关键字 extern 。在所有的函数之外声明变量,在声明前面加上关键字 extern 。
处理外部变量时,变量可以在许多地方声明为 extern ,但是只能定义一次。
4、继承、多态
1)继承的概念作用于整个继承链。
类的每个实例(对象)都拥有自己的实例变量,即使这些实例变量是继承来的。
继承通常用于扩展一个类。不能通过继承删除或减少方法。
为什么需要创建子类:(1)希望继承一个类的函数,也需加入一些新的方法和/或实例变量。(2)希望创建一个类的特别版本。(3)希望通过覆写一个或多个方法 来改变类的默认行为。
2)多态:使不同的类共享相同方法名称的能力称为多态。能够使来自不同类的对象定义相同名称的方法。
- 3)动态类型:id 类型的对象。在运行时而不是编译时确定对象所属的类,能使程序直到执行时才确定对象所属的类。
- 静态类型:将一个变量定义为特定类的对象时,使用的是静态类型。静态指的是对存储在变量中的对象类型进行显示的声明。
动态绑定:在运行时而不是编译时确定对象需要调用的方法,能使程序直到执行时才确定实际要调用的对象方法。
- OC 系统总是跟踪对象所属的类。
id 类型的对象先判定对象所属的类(动态类型),然后在运行时确定需要动态调用的方法,而不是在编译的时候(动态绑定)。
为什么还要关心静态类型:(1)它能更好的在程序编译阶段而不是运行时指出错误。(2)它能提高程序的可读性。
如果使用动态类型来调用一个方法,需要注意一下规则:如果在多个类中实现名称相同的方法,那么每个方法都必须符合各个参数的类型和返回值类型,这样编译器才能为消息表达式生成正确的代码。
处理动态类型的方法:
以下总结了 NSObject 类所支持的一些基本方法,其中,class-object 是一个类对象(通常是由 class 方法产生的),selector 是一个 SEL 类型的值(通常是由
@selector
指令产生的)。- (BOOL)isKindOfClass:class-object // 对象是不是 class-object 或其子类的成员- (BOOL)isMemberOfClass:class-object // 对象是不是 class-object 的成员+ (BOOL)isSubclassOfClass:class-object // 某个类是否是指定类的子类- (BOOL)respondsToSelector:selector // 对象是否能够响应 selector 所指定的方法+ (BOOL)instancesRespondToSelector:selector // 指定的类实例是否能响应 selector- (id)performSelector:selector // 应用 selector 指定的方法- (id)performSelector:selector withObject:object // 应用 selector 指定的方法,传递参数 object- (id)performSelector:selector withObject:object1 withObject:object2 // 应用 selector 指定的方法,传递参数 object1 和 object2[Square class] // 从名为 Square 的类中获得类对象[mySquare class] // 知道对象 mySquare 所属的类@selector(alloc) // 为名为 alloc 的方法生成一个 SEL 类型的值。
4)消除 performSelector: 方法警告
#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks"// performSelector: 方法#pragma clang diagnostic pop
5、协议、代理
1)协议:是多个类共享的一个方法列表。协议中列出的方法没有相应的实现,计划由其他人来实现。协议中列出的方法,有些是可以选择实现,有些是必须实现。
1>、如果你定义了自己的协议,那么不必由自己实现它。但是,这就告诉其他程序员,如果要采用这项协议,则必须实现这些方法。这些方法可以从超类继承。
协议不引用任何类,它是无类的。
分类也可以采用一项协议。
2>、定义一个协议很简单:只要使用 @protocol 指令,后面跟上你给出的协议名称。定义一项协议时,可以扩展现有协议的定义。
@protocol PlayerDelegate <NSObject>- (void)end;@end
3>、协议的修饰
@optional:该指令之后列出的所有方法都是可选的。@required:该指令之后列出的所有方都是必须实现的,默认。由于 OC 是弱语法,虽然字面上是必须,但编译器并没有强求实现。
4>、协议的声明
@protocol protocol-name
5>、协议的检查
// 检查一个对象是否遵守某项协议。- (BOOL)conformsToProtocol:(Protocol *)aProtocol;// 用于获取一个协议名称,并产生一个 Protocol 对象,conformsToProtocol: 方法期望这个对象作为它的参数。@protocol(Drawing)// 检查对象是否能够响应 selector 所指定的方法。- (BOOL)respondsToSelector:selector// 为名为 alloc 的方法生成一个 SEL 类型的值。@selector(alloc)
2)非正式协议:实际上是一个分类,列出了一组方法但并没有实现它们。非正式协议通常是为根类定义的,有时,非正式协议也称为抽象协议。
声明非正式协议的类自己并不实现这些方法,并且选择实现这些方法的子类需要在它的接口部分重新声明这些方法,同时还要实现这些方法中的一个或多个。
指令 @optional 添加到 OC 2.0 语言中,用于取代非正式协议的使用。
3)代理:协议也是一种两个类之间的接口定义。定义了协议的类可以看作是将协议定义的方法代理给了实现它们的类。
- 代理设计模式的作用:
- 1、A 对象监听 B 对象的一些行为,A 成为 B 的代理
- 2、B 对象想告诉 A 对象一些事情,A 成为 B 的代理
- 代理设计模式的总结:
- 1、如果你想监听别人的一些行为,那么你就要成为别人的代理
- 2、如果你想告诉别人一些事情,那么就让别人成为你的代理
- 代理设计模式的开发步骤:
- 1、拟一份协议(协议名字的格式:控件名 + Delegate),在协议里面声明一些代理方法(一般代理方法都是 @optional)
- 2、声明一个代理属性:@property (nonatomic, weak) id delegate;
- 3、在内部发生某些行为时,调用代理对应的代理方法,通知代理内部发生什么事
- 4、设置代理:xxx.delegate = yyy;
- 5、yyy 对象遵守协议,实现代理方法
- 代理设计模式的作用:
6、为什么 Objective-C 的方法调用要用方括
为什么 Objective-C 的方法调用要用方括号 [obj foo],而不是别的语言常常使用的点 obj.foo ?
首先要说的是,Objective-C 的历史相当久远,如果你查 wiki 的话,你会发现:Objective-C 和 C++ 这两种语言的发行年份都是 1983 年。在设计之初,二者都是作为 C 语言的面向对象的接班人,希望成为事实上的标准。最后结果大家都知道了,C++ 最终胜利了,而 Objective-C 在之后的几十年中,基本上变成了苹果自己家玩的玩具。不过最终,由于 iPhone 的出现,Objective-C 迎来了第二春,在 TOBIE 语言排行榜上,从 20 名开外一路上升,排名曾经超越过 C++,达到了第三名(下图),但是随着 Swift 的出现,Objective-C 的排名则一路下滑。
Objective-C 在设计之初参考了不少 Smalltalk 的设计,而消息发送则是向 Smalltalk 学来的。Objective-C 当时采用了方括号的形式来表示发送消息,为什么没有选择用点呢?我个人觉得是,当时市面上并没有别的面向对象语言的设计参考,而 Objective-C 「发明」了方括号的形式来给对象发消息,而 C++ 则「发明」了用点的方式来 “发消息”。有人可能会争论说 C++ 的「点」并不是真正的发消息,但是其实二者都是表示「调用对象所属的成员函数」。
另外,有读者评论说使用方括号的形式是为了向下兼容 C 语言,我并不觉得中括号是唯一选择,C++ 不也兼容了 C 语言么?Swift 不也可以调用 C 函数么?
最终,其实是 C++ 的「发明」显得更舒服一些,所以后来的各种语言都借鉴了 C++ 的这种设计,也包括 Objective-C 在内。Objective-C 2.0 版本中,引入了 dot syntax,即:
a = obj.foo 等价于 a = [obj foo]obj.foo = 1 则等价于 [obj setFoo:1]
Objective-C 其实在设计之中确实是比较特立独行的,除了方括号的函数调用方式外,还包括比较长的,可读性很强的函数命名风格。
我个人并不讨厌 Objective-C 的这种设计,但是从 Swift 语言的设计来看,苹果也开始放弃一些 Objective-C 的特点了,比如就去掉了方括号这种函数调用方式。
所以,回到我们的问题,我个人认为,答案就是:Objective-C 在 1983 年设计的时候,并没有什么有效的效仿对象,于是就发明了一种有特点的函数调用方式,现在看起来,这种方式比点操作符还是略逊一筹。
大多数语言一旦被设计好,就很难被再次修改,应该说 Objective-C 发明在 30 年前,还是非常优秀的,它的面向对象化设计得非常纯粹,比 C++ 要全面得多,也比 C++ 要简单得多。