iOS中OC类构建的理论知识过一遍

我觉得学习iOS开发,首先对OC类和OC对象的学习,把里面的内容理清楚,至少理论知识的逻辑先理清楚,是很有必要的。

但是通过我写这篇博文的经验来看,这里面涉及到的知识点有相互有所交叉,很难理清一条顺序链,然后知识一环扣一环,做不到,唯一的办法可能就是需要看完之后再看第二遍,然后敲代码做实验。

类是面向对象程序设计(OOP)的构建块。实际上,通过OOP编写的程序主要有相互作用的类实例(即对象)的网络构成。面向对象编程是一种计算机程序设计方式,其重点是创建和使用对象编写程序。

构建一个类的时候,一定会选择一个继承的父类,子类与父类的继承关系,稍后补充,当构建一个类的时候,有接口文件.h和实现文件.m。图示如下:

iOS中OC类构建的理论知识过一遍

在接口文件中,一般写上“属性”和“方法”的声明,但是“实例变量”的声明不写在接口文件中,理由是:

在类的公有接口中声明实例变量会违法OOP的关键宗旨之一---封装。关于“实例变量”,尽管使用实例变量可以方便、直接地访问对象的状态,但是“实例变量”会暴露类的内部,因而会违法OOP的的封装原则。因此,只应在必要时声明实例变量,而且应将这些代码放在类的实现部分,而不是公有接口中。更好的公开对象内部状态的方式是使用“属性”。

也就是说,“实例变量”不仅不能写在接口文件中,在实现文件中都要少用,非必要情况下不要使用,而应该使用“属性”。虽然大多数情况下,使用属性的setter和getter无非也就是在访问_var和设置_var实例变量,为什么直接使用_var实例变量就不够封装呢?因为:

属性和实例变量的区别是,属性无法直接访问对象的内部状态,但提供了访问这类数据的方便机制(即读取getter和设置setter方法),因而可以含有其他逻辑。

也就是说,虽然对于实例变量的声明:

NSString *_name;

与它相对应的属性的setter方法和getter方法,默认的方法定义如下:

- (NSString *)name {
    return _name;
}

- (void)setName:(NSString *)name {
    _name = name;
}

但是,这仅仅是大部分默认情况下,之所以说getter方法和setter方法“无法直接”访问和设置对象的内部状态,那是因为在setter方法和getter方法定义时可以含有其他逻辑,比如:

- (NSString *)name {
    // 可以包含其他逻辑,而不是直接给出_name
    if ([_name isEqualToString:@"xiaoming"]) {
        return @"daming";
    }
    return _name;
}

- (void)setName:(NSString *)name {
    // 可以包含其他逻辑,而不是直接设置_name
    if ([name isEqualToString:@"xiaoming"]) {
        _name = @"daming";
    }
    _name = name;
}

接口文件中的“实例变量”的注意事项弄明白之后,就下来就要弄清楚“属性”。

iOS中OC类构建的理论知识过一遍

先把属性的各种特性关键字的描述先说明下:

原子性:atomic(原子性)/nonatomic(非原子性)

使用nonatomic特性可以在多线程并发的情况中,将访问器设置为非原子性的,因而能够提供不同的结果;如果不设置nonatomic,访问器就会拥有原子性,换言之,赋值和返回值结果永远都会完全同步。注意:如果不显示写出nonatomic,那默认就是原子性atomic。

设置器语义:assign

通过该特性可以在不使用copy和retain特性的情况下,使属性的设置器方法执行简单的赋值操作。这个特性是默认设置。

设置器语义:retain

在赋值时,输入值会被发送一条保留消息,而上一个值会被发送一条释放消息。

设置器语义:copy

在赋值时,输入值会被发送一条新消息的副本,而上一个值会被发送一条释放消息

设置器语义:strong

当属性使用ARC内存管理功能时,该特性等同于retain特性

设置器语义:weak

当属性使用ARC内存管理功能时,该特性的作用与assign特性类似,但如果引用对象被释放了,属性的值会被设置为nil。

可读写性:readwrite

使用该特性时,属性可以被读取也可以被写入,而且必须实现setter和getter方法。这个特性是默认设置。

可读写性:readonly

使用该特性时,会将属性设置为只读。必须实现getter方法。

方法名称:getter=getterName

将getter方法重命名为新读取器的名称。

方法名称:setter=setterName

将setter方法重命名为新设置器的名称。

上面的内容说明了属性的声明如何写,在头文件中把属性的声明写好,意味着getter方法和setter方法的声明。

属性的定义代码写在实现文件中,属性的定义意味着三个方面:

1)实例变量的声明

2)getter方法的定义,并在方法中使用这个实例变量

3)setter方法的定义,并在方法中使用这个实例变量

也就是说如果在接口文件中声明了一个属性:

#import <Foundation/Foundation.h> 

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *studentName;

@end

NS_ASSUME_NONNULL_END

那么在实现文件中需要做的事情就有三件:

#import "Person.h"

@implementation Person
{
    // 1 相应的成员变量的声明
    NSString *_studentName;
}

// 2 gette方法的定义
- (NSString *)studentName {
    return _studentName;
}

// 3 setter方法的定义
- (void)setStudentName:(NSString *)studentName {
    _studentName = studentName;
}

@end

然而,在实现文件中要做的这三件事,OC提供了4种定义属性的方式,即:

iOS中OC类构建的理论知识过一遍

1)自动补全

Clang/LLVM(4.2及更高版本)是苹果公司推荐使用的OC编译器,它支持对已声明属性进行自动补全。

“自动补全”意味着,在接口文件中声明属性后,在实现文件中需要做的三件事,编译器会自动“补充”源码进去。

但是,“自动补全”是有条件的,编译器可以自动补全以下已声明的属性:

a、没有使用关键字(如@synthesis)进行补全的属性

b、不是(通过@dynamic属性指令)动态生成的属性

c、没有用户编写的getter和setter方法的属性

2)显式定义

就是手动把刚刚上面说的那三件事手动代码敲出来,这里要注意,与该属性对应的实例变量的名称开头有一个下划线。这个名称反映了OC中属性实例变量的命名惯例,即实例变量的名称就是属性名称前加上一条下划线。

3)通过关键字补全

通过使用@synthesize关键字,可以使编译器自动生成属性定义。属性代码会在相应的类实现部分中被自动补全。通过关键字自动补全属性代码的语法如下:

@synthesize studentName [= _studentName];

如果省略了可选项[= 实例变量名称],编译器就会根据属性实例变量标准命名惯例,自动生成实例变量的名称。如果你设置了可选项,编译器就会使用该名称创建实例变量。因此,可以通过@synthesize关键字达到“为属性的实例变量起个别名”的作用。

“通过关键字补全”和“自动补全”两者是有区别的,一种很常见的情景就是,比如在接口文件中声明了studentName属性,然后在实现文件中手动写了该属性的getter方法和setter方法,在setter方法中使用“_studentName”时编译器就会报错,显示没有定义“_studentName”故不能使用。什么原因呢?首先如果用户编写了getter和setter方法,所以该属性就不会“自动补全”属性定义,因此三件事中的其中一件----“实例变量的声明”就没有做,这种情况下,有两种处理方式,一种就是采用“显式定义”的方式,程序员自己继续手动把该实例变量的声明代码敲上去;另一种方式,就是使用@synthesize关键字,通知编译器自动生成属性定义,如果程序员手动作做了部分属性定义的工作,那么就“劳烦”编译器把剩下的属性定义的工作“继续做完”。

#import "Person.h"

@implementation Person
{

}
@synthesize studentName = _studentName;

// gette方法的定义
- (NSString *)studentName {
    return _studentName;
}

// setter方法的定义
- (void)setStudentName:(NSString *)studentName {
    _studentName = studentName;
}

@end

4)动态生成

属性的访问器方法(getter、setter方法)可以在运行时被委托或动态创建。使用@dynamic属性指令可以阻止编译器通过这种方式自动生成访问器方法。这样,如果无法找到应用了@dynamic属性指令的属性访问器方法的实现代码,编译器就不会发出警告了。但是,开发人员必须通过下列方式创建这些访问器方法实现代码:直接手动编写这些代码,通过其他途径(如动态代码加载或动态方法决议)获得这些代码,以及使用能够动态生成这些方法的软件库(如苹果公司的Core Data框架)。

使用@dynamic关键字,其实就是让编译器不要给该属性做任何“属性定义”方面的事情,通过下图可以看出:

iOS中OC类构建的理论知识过一遍

如果不使用@dynamic关键字,按道理来说,编译器肯定会“自动补全”属性定义的工作。

上面的内容讲完了“属性的声明”和“属性的定义”相关的知识。

大多数属性都是由实例变量支持的,属性通过这种机制隐藏对象的内部状态(即不直接让外界访问和设置实例变量)。

OC有两种访问属性的机制:访问器方法 和 点语法。点语法只是访问器方法的一种简洁方式:点表达式。其实,访问属性都是调用属性对应的setter和getter方法。

一般而言,都应该使用这两种机制访问属性。然而,如果与属性关联的对象还没有完全创建好,就不要使用这些机制,而应该使用支持属性的实例变量,这就意味着类内部的init方法和dealloc方法会直接访问实例变量

接下来来梳理“方法”相关的知识,包含方法的声明、方法的定义。方法的种类又分为两种:类方法、实例方法。在接口文件中声明方法,然后需要在实现文件中定义这些方法。相关的说明如下:

方法定义了类和类实例(对象)在运行时展示的行为。他们直接与OC类(类方法)或对象(实例方法)关联。实例方法能够直接访问对象的实例变量。可以在类接口、协议或分类中声明方法,而已声明方法的定义在相应类的实现文件中实现。

类方法:表示该方法拥有类的作用范围,这意味着它使用类级的操作并且无法访问类的实例变量(除非这些变量被当做参数传给类方法)。

实例方法:表明该方法拥有对象的作用范围,这意味着它使用实例级的操作,并且可以直接访问对象及其父对象的实例变量(具体根据父类的实例变量上设定的访问控制而定)。

然后,关于方法的调用,在OC中,称之为“发送消息”。对象(发送器)通过发送消息与其他对象(接收器)进行交互,从而调用指定的方法。

如此一来,关于“类”的构建只有一个“实例变量的声明”这一个知识点了:

iOS中OC类构建的理论知识过一遍

实例变量是指类声明的变量,它们在相应类实例(即对象)的生命周期中存在并拥有值。当对象被创建时,系统会为实例变量分配内存,当对象被释放时系统也会释放变量占用的内存。实例变量拥有与对象对应的作用范围和命名空间。OC提供了控制实例变量直接访问方式的特性,还提供了获取和设置它们的值的方便机制。

实例变量的使用还是很容易的,一个是注意不是很有必要的情况下,尽量声明属性而不是声明实例变量;第二,就算要声明实例变量也决不要在接口文件中声明(否则违法OOP规范);声明实例变量时,实例变量的命名不一定要下划线开头,下划线开头是属性对象的实例变量的惯用命名方式,如果单从实例变量的命名来看,并无此硬性要求;最后一个就是要注意实例变量的“访问控制编译器指令”的使用。相关的访问控制编译器指令,有下面几种:

@private:将实例变量设置为只能在声明它的类以及与该类类型相同的其他实例中访问。

@protected:将实例变量设置为只能在在声明它的类及其子类的实例方法中被访问。在没有明确设置实例变量的访问控制等级时,这是实例变量默认的作用范围。

@public:将实例变量设置为可以被任何代码访问。

@package:将实例变量设置为可以被其他类实例和函数访问,但是在其所属程序包的外部,它会被视为私有变量。这种作用范围可以用于库或框架类。

比如设计三个类,Person(继承NSObjec)、Student(继承Person)、Grade(继承NSObject),然后在Perosn类的接口文件中定义personName这样一个实例变量,分别用上面的四个访问控制编译器指令进行修饰,结果如下:

iOS中OC类构建的理论知识过一遍

iOS中OC类构建的理论知识过一遍

iOS中OC类构建的理论知识过一遍

iOS中OC类构建的理论知识过一遍

iOS中OC类构建的理论知识过一遍

iOS中OC类构建的理论知识过一遍

上面说的都是将“实例变量”声明在接口文件中,如果声明在实现文件中呢,分为两种情况:

1)如果声明在大括号中{},就属于私有private的实例变量;

2)如果声明在大括号之外,就属于全局变量,意思是只要在该实现文件中可以访问。

个人觉得,本来根据OOP编程规范来说,就不推荐在接口文件中声明实例变量,在实现文件中声明实例变量,又只能在本实现文件中访问,既然如此,就无所谓上面提到的@private等4个关键词的学习了。因此,就可以形成编程习惯,实例变量只用于实现文件中使用。然而还有一个问题,在实现文件中,与属性相对应的实例变量,在读取和设置该实例变量时,究竟是使用属性的getter和setter方法,还是直接使用该实例变量呢?关于这点,《编写高质量iOS与OS X代码的52个有效方法》这本书中的第7条“在对象内部尽量直接访问实例变量”,指出的折中方案是:在写入实例变量时,通过其“设置setter方法”来做,而在读取实例变量时,则直接访问之。但是有两个地方要注意,特殊对待:

1)在初始化init方法中,应该总是直接setter实例变量;

2)如果使用了懒加载方法,那么实例变量的初始化工作是放在懒加载方法中的,因此要使用getter方法。

关于在实现文件中使用实例变量还是使用属性的setter与getter方法,的讨论,我觉得很是精彩:

·由于不经过OC的“方法派送”步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接方法保存对象实例变量的那块内存。

·直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。

·如果直接访问实例变量,那么不会触发KVO通知。这样做是否会产生问题,还取决于具体的对象行为。

·通过属性来访问有助于排查与之相关的错误,因为可以给getter方法和setter方法中新增“断点”,监控该属性的调用者机器访问时机。

 不同的属性限定符所生成的属性定义是不一样的,这个也是为什么要认真对待“究竟使用属性还是实例变量”这个问题。

-----未完待续,2021年6月16日

-----未完待续,2021年6月16日

-----未完待续,2021年6月17日