iOS 面向对象与类
至于未来会怎样,要走下去才知道反正路还很长,天总会亮。
1. 面向对象
1.1 什么是面向对象(OOP)
面向对象 Object Oriented Programming。在软件开发中,我们虽然用的是面向对象的语言,但我相信绝大多数入门或者工作经验不长的同学敲出来的代码依然是大段的面向过程的思想,我们只是把面向对象来当做 OC 语言的一个特性而已,具体是什么估计自己也说不明白到底是什么。那么到底该怎么去理解面向对象编程呢?
面向对象是一种程序设计的范型,同时也是一种程序开发的方法。面向对象是将现实世界中的事物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模,用更利于人的理解方式,对复杂的系统进行分析、设计与编程。
现代的程序开发几乎都是以面向对象为基础。而在面向对象广泛流行之前,软件行业中使用最广泛的设计模式是面向过程方式。面向过程的操作是以程序的基本功能实现为主,开发过程中只针对问题本身的实现,并没有很好的模块化设计,所以在代码维护的时候较为麻烦。而面向对象,采用的更多的是进行子模块化的设计,每一个模块都需要单独的存在,并且可以被重复利用。所以面向对象开发更像是一个具备标准模式的编程开发,每一个单独设计的模块都可以单独存在,需要时候只要通过简单的组装就可以使用。但是,面向对象的底层还是面向过程,这两种程序设计思想是可以互相依存的,也是贯穿我们整个程序开发周期的思想指导,我们在设计程序、开发程序中应该牢记“面向对象”、“面向过程”。面向对象中类和对象是最基本、最重要的组成单元。下面会讲到该怎么理解类和对象。
1.2 面向对象的三大特征
继承:继承是子类继承父类非私有数据结构和方法的机制,是类之间的一种关系。它是面向对象语言特有的特征,面向过程的语言不具有继承特性,而 OC 是单继承。继承提供了类的规范等级结构,使公共的特性能够共享,提高的软件的重用性。类的继承性使所建的软件具有开发性、可扩充性,简化了对象、类的创建工作量,提高了代码的重用性。
封装:在面向对象的语言中,对象、类、方法都是一种封装,对象是封装的最基本单位。
类的封装体现在每个类都有 .h 和 .m 两个文件,将定义与实现分开,.h 声明(用户可见的外部接口),.m实现(用户不可见的内部实现)。
方法的封装,是最常见的,每个方法中封装了一个小的功能,这是单一职责的很好体现,第三方框架和代码也是一种封装。
封装使程序的结构更加清晰,将实现的信息封装隐藏,用的时候直接调用封装好的方法或类,提高效率。此外,可以减少程序间的相互依赖。
多态:不同对象以自己的方式响应相同的消息的能力叫做多态。假设有一个类包含一个方法,由这个类派生出两个子类或者两个对象,其各自实现各自的方法,也就是不同的对象以自己的方式响应了相同的消息。多态增强了软件的灵活性和重用性。
1.3 对象的理解
什么是对象?世界万物皆对象,所有看到的看不到的都是对象,把对象引入到编程中,那就是面向对象编程(Object Oriented Programming,OOP,面向对象程序设计),在百度百科中有关于 OOP 详细的介绍。简单来说就是它将对象作为控件的基本元素, 利用对象和对象之间的相互作用来设计程序,说白了,一款软件的运行就是控件之间的相互作用。说了那么多,那到底啥是对象,对象是你,是我,是万物,我认为在程序中能 alloc 出来的都是对象。就拿人来说,你是人,我也是人,为啥咱俩不一样,要是放到程序里来说,是因为 alloc 分配的内存地址不一样。人有头发、眼睛、手鼻等,这些在 OC 里面称之为属性,人能跑、跳、投,等动作这些在 OC 里面称之为方法。其实,你在做项目的时候已经在用对象这个概念了,只不过你不知道罢了。举个简单的例子:点击 tableview 的 cell 让 cell 里面的控件变换颜色,我相信大家都能实现这个效果,是怎么做的呢?肯定是定位点击的哪个 cell,拿到当前 cell,那这个 cell 不就是对象,找到 cell 中的控件,控件不就是 cell 的属性吗?颜色也是控件的属性,控件即是对象也是属性,控件间的相互作用完成了这个功能,说到底也就是操作的对象。
1.4 类的理解
面向对象编程中,具体的事物是对象,将具有相同或相似性质的对象的属性或方法抽象出来便是类,类是对象的抽象化,对象便是类的具体实现。估计这不好理解,再拿网络请求数据来讲,数据都有相同或相似的数据,将这段数据的相同点抽离出来便是我们所建的 model 类,比如 person 类,属性有:name、height、weight 等,他们具有相同的属性将属性他们通过一个方法将数据进行转化,那这个方法不就是封装吗?一般都是在 .h 文件中声明方法,在 .m 中去实现,方法一般都是隐藏内部实现,预留一个稳定外部接口。面向对象程序设计中的方法可分为两种,一为上述的实体(对象)方法,二为类方法,主要的差异在于实体方法需要有一对象去引发,而类别方法可以由类别名称调用。
2. Objective-C 类
2.1 类概念
如同所有其他的面向对象语言,类是 Objective-C 用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。Objective-C 的类规格说明包含了两个部分:定义(interface)与实现(implementation)。定义(interface)部分包含了类声明和实例(成员)变量的定义,以及类相关的方法。实现(implementation)部分包含了方法的实现,以及定义私有(private)变量及方法。类的定义文件遵循 C 语言之惯例以 .h 为后缀,实现文件以 .m 为后缀。
2.2 类定义(Interface)
2.2.1 定义
定义文件遵循 C 语言之惯例以 .h 为后缀。定义部分,清楚定义了类的名称、数据成员和方法。 以关键字 @interface 作为开始,@end 作为结束。
下面定义一个叫做 XBCar 的类的语法,这个类继承自 NSObject 基础类。类名之后的(用冒号分隔的)是父类的名字。类的实例(成员)变量声明在被大括号包含的代码块中。实例变量块后面就是类声明的方法的列表。每个实例变量和方法声明都以分号结尾。
@interface XBCar : NSObject {
// 成员变量
float maxVelocity;
double money;
// 实例变量
NSString *name;
}
+ (void)class_method; // 类方法
- (void)instance_method1; // 实例方法
- (void)instance_method2:(int)p1;
@end
类定义具体内容包括:
- 类的声明,类声明总是由 @interface 编译选项开始,由 @end 编译选项结束;
- 实例(成员)变量的定义(用大括号包含的);
- 类相关方法的定义:类方法和实例方法;
2.2.2 实例变量和成员变量的理解
实例(Instance)是针对类(class)而言的。实例是指类的声明; 由此推理,实例变量(Instance Variable) 是指由类声明的对象。成员变量就是基本类型声明的变量。
严格来说 @interface{}
里定义的实例(成员)变量,它是这个类内部真正的全局变量。然而这个 instance variable 是不对外公开的,因此我们还需要一个对外公开的东西来调用,就是属性,关键字 @property。它其实是告诉大家,我这个类里,有一个变量的 seter/geter 方法。比如,@property NSString* string;就是说,本类里有一个 string/setString 供你们调用。属性具体的使用后面会讲到,这里就不展开了。
2.3 类实现(Implementation)
实现文件以 .m 为后缀。实现区块则包含了公开方法的实现,以及定义私有(private)变量及方法。 以关键字 @implementation 作为区块起头,@end结尾。
@implementation XBCar {
int private; // 私有成员变量
}
+ (void)class_method {
}
- (void)instance_method1 {
}
- (void)instance_method2:(int)p1{
}
值得一提的是不只 Interface 区块可定义实例(成员)变量,Implementation 区块也可以定义实例(成员)变量,两者的差别在于访问权限的不同,Interface 区块内的实体变量默认权限为 protected,implementation 区块的实例(成员)变量则默认为 private,故在 implementation 区块定义私有成员更匹配面向对象之封装原则,因为如此类别之私有信息就不需曝露于公开 interface(.h文件)中。在程序开发中我们会发现在实现文件(.m)中,有如下这样的代码:
@interface XBCar () // Class Extension
@end
那么问题来了,为什么 .h 文件和 .m 文件里各有1个 @interface 它们分别有什么用呢?
定义文件(.h 文件)里面的 @interface
,不用说,是典型的头文件,用来定义(声明)类的。
实现文件(.m 文件)里面的 @interface
,在 OC 里叫作 Class Extension,是 .h文件中 @interface
声明类的补充扩展。但是 .m 文件里的 @interface
,对外是不开放的,只在 .m 文件里可见。
2.4 创建对象
Objective-C 创建对象需通过 alloc 以及 init 两个消息。alloc 的作用是分配内存,init 则是初始化对象。 init 与 alloc 都是定义在 NSObject 里的方法,父对象收到这两个消息并做出正确回应后,新对象才创建完毕。
XBCar *car = [[XBCar alloc] init];
Objective-C 还可以通过 new 关键字来创建对象。
XBCar *blueCar = [XBCar new];
那么 alloc/init 和 new 有什么区别呢?首先功能上他两几乎是一致的,都是分配内存并完成初始化。差别在于,采用 new 的方式创建对象只能采用默认的 init 方法完成初始化,而通过 alloc 的方式可以采用其他定制的初始化方法完成初始化。另外 alloc 分配内存的时候使用了 zone。它是给对象分配内存的时候,把关联的对象分配到一个相邻的内存区域内,以便于调用时消耗很少的代价,提升了程序处理速度。
2.5 方法声明
当你想调用一个方法,你传递消息到对应的对象。这里消息就是方法标识符,以及传递给方法的参数信息。发送给对象的所有消息都会动态分发,这样有利于实现 Objective-C 类的多态行为。也就是说,如果子类定义了跟父类的具有相同标识符的方法,那么子类首先收到消息,然后可以有选择的把消息转发(也可以不转发)给他的父类。
消息被中括号( [ 和 ] )包括。中括号中间,接收消息的对象在左边,消息(包括消息需要的任何参数)在右边。例如,给 myArray 变量传递消息insertObject:atIndex: 消息,你需要使用如下的语法:
[myArray insertObject:anObj atIndex:0];
为了避免声明过多的本地变量保存临时结果,Objective-C 允许你使用嵌套消息。每个嵌套消息的返回值可以作为其他消息的参数或者目标。例如,你可以用任何获取这种值的消息来代替前面例子里面的任何变量。所以,如果你有另外一个对象叫做 myAppObject 拥有方法,可以访问数组对象,以及插入对象到一个数组,你可以把前面的例子写成如下的样子:
[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];
虽然前面的例子都是传递消息给某个类的实例,但是你也可以传递消息给类本身。当给类发消息,你指定的方法必须被定义为类方法,而不是实例方法。你可以认为类方法跟 C++ 类里面的静态成员有点像(但是不是完全相同的)。
类方法的典型用途是用做创建新的类实例的工厂方法,或者是访问类相关的共享信息的途径。类方法声明的语法跟实例方法的几乎完全一样,只有一点小差别。与实例方法使用减号作为方法类型标识符不同,类方法使用加号( + )。
下面的例子演示了一个类方法如何作为类的工厂方法。在这里,arrayWithCapacity 是 NSMutableArray 类的类方法,为类的新实例分配内容并初始化,然后返回给你。
NSMutableArray* myArray = nil; // nil 基本上等同于 NULL
// 创建一个新的数组,并把它赋值给 myArray 变量
myArray = [NSMutableArray arrayWithCapacity:0];
2.6 属性
属性(property)用于封装对象中的数据,iOS 开发中最常用最方便的变量声明方式,允许我们用点语法来访问对象的实例变量。
2.6.1 属性的实质是什么
@property 是声明属性的语法。可以快速为实例变量创建存取器。允许我们通过点语法使用存取器。
属性(@property) = 实例变量定义 + setter方法 + getter方法。
当我们声明一个属性属性 name
的时候,在编译阶段,编译器会自动给对象添加一个实例变量 name
和它的存取方法 - (void)setName:(NSString *)name
和 - (NSString *)name
。这个过程由于是在编译阶段自动合成的,所以我们在编辑阶段是看不到的。添加实例变量是有一个前提的,就是对象还没有同名的成员变量,就是如果已经有 _name 了,就不再添加了。我们可以用运行时验证一下:
- (void)propertyTest {
unsigned int count = 0;
Ivar *varList = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *varName = ivar_getName(varList[i]);
printf("成员变量----%s\n", varName);
}
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
SEL methodName = method_getName(methodList[i]);
NSLog(@"方法----%@",NSStringFromSelector(methodName));
}
}
打印的日志:
成员变量----_name
2021-07-20 16:55:59.492166+0800 001 - Class[772:380998] 方法----propertyTest
2021-07-20 16:55:59.492321+0800 001 - Class[772:380998] 方法----name
2021-07-20 16:55:59.492551+0800 001 - Class[772:380998] 方法----setName:
2021-07-20 16:55:59.492664+0800 001 - Class[772:380998] 方法----viewDidLoad
2.6.2 @synthesize
@synthesize 属于编译器指令,用来告诉编译器要做什么。
@property (strong,nonatomic) NSString *name;
@synthesize name = myname;
@dynamic name;
@synthesize
关键字主要有两个作用,在 ARC 下已经很少用了。
- 在 MRC 下,
@synthesize name
,用在实现文件中告诉编译器自动实现实例变量name
的存取(访问器 getter/setter)方法。不过在 ARC 下就不必了,无论你是否@synthesize name
,编译器都会自动合成 name 的存取方法。 - 如果你声明的属性是 name,系统自动给你添加的成员变量是
_name
,如果你对这个变量名字不满,可以这样@synthesize name = myname
;自己给个名字。这样系统给添加的成员变量就是 myname,而不是_name,但是变量的存取方法没有变化。不过我建议最好不要这么办,因为都按照约定成俗的方式来命名变量,代码的可读性较高,大家都理解,所以我建议大家最好不要用这个关键字。
2.6.3 @dynamic
@dynamic 关键字主要是告诉编译器不用为我们自动合成变量的存取方法, 我们会自己实现。即使我们没有实现,编译器也不会警告,因为它相信在运行阶段会实现。如果我们没有实现还调用了,就会报这个错误 '-[ViewController setName:]: unrecognized selector sent to instance 0x10040af10'
。
2.6.4 属性的特性(关键字)
1)属性特性概述
原子性:
- atomic(默认):atomic 意为操作是原子的,意味着只有一个线程访问实例变量(生成的 setter 和 getter 方法是一个原子操作)。atomic 是线程安全的,至少在当前的存取器上是安全的。它是一个默认的特性,但是很少使用,因为比较影响效率;
- nonatomic:意为操作是非原子的,可以被多个线程访问。它的效率比atomic 快。但不能保证在多线程环境下的安全性,开发中常用,所以我们在写代码的时候要尽量避非免线程安全的代码出现;
读写权限(存取器控制):
- readwrite(默认):readwrite 是默认值,表示该属性同时拥有 getter 和 setter;
- readonly: readonly 表示只有 getter 没有 setter;
- 有时候为了语意更明确可能需要自定义访问器的名字;
内存管理语义:
-
retain(MRC)/strong(ARC)
内存管理语义:强引用
系统默认:ARC 情况下是默认
作用:只能修饰对象。对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后都会-1;即使对象本身被释放了,只要还有对象在引用,就会持有不会造成什么问题;只有当引用计数为0时,就被dealloc析构函数回收内存了。对应变量权限修饰符为__strong。
-
weak
内存管理语义:弱引用
系统默认:否
作用:只能修饰对象。不改变对象的引用计数,当其指向对象被销毁时,它会自动置为nil;变量权限修饰符为__weak,常用于容易造成循环引用的地方不改变对象的引用计数。
-
assign
内存管理语义:赋值
系统默认:MRC 情况下是
作用:主要用于修饰值类型,如 int、float、double 和 CGPoint、CGFloat 等表示单纯的复制。还包括不存在所有权关系的对象,比如常见的 delegate。值类型变量的内存由编译器自动管理;修饰对象属性时,其指向一个对象后,不改变该对象的引用计数。即只引用已创建的对象,而不持有对象;assign 修饰的属性不持有对象,当其指向对象在别处释放后,该指针变为悬挂指针也叫野指针。对应的变量权限修饰符为
__unsafe_unretained
。 -
unsafe_unretained
用来修饰属性的时候,和 assing 修饰对象的时候是一模一样的。为属性设置新值的时候。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为 nil,这就会产生野指针,所以是不安全的。对应的变量权限修饰符为
__unsafe_unretained
。 -
copy
内存管理语义:复制/拷贝
系统默认:否
作用:只能修饰对象。一般情况下属性会持有该对象的一份拷贝,建立一个索引计数为1的新对象,然后释放旧对象。copy 一般用在修饰有可变对应类型的不可变对象上,如 NSString, NSArray, NSDictionary。
2)strong 和 copy 修饰属性的区别
不可变字符串:
@property (nonatomic,strong)NSString* strongedString;
@property (nonatomic,copy)NSString* copyedString;
// 当tempString为不可变字符串时候
NSString *tempString = @"Text";
self.strongedString = tempString;
self.copyedString = tempString;
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);
tempString = @"Change OK";
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);
// 打印结果:
BaseGrammar[40394:185339] tempString : Text 0x10d858e68 0x7ffee23adbf8
BaseGrammar[40394:185339] strongedString : Text 0x10d858e68 0x7ff634530440
BaseGrammar[40394:185339] copyedString : Text 0x10d858e68 0x7ff634530448
// 改变后:
BaseGrammar[40394:185339] tempString : Change OK 0x10d858ee8 0x7ffee23adbf8
BaseGrammar[40394:185339] strongedString : Text 0x10d858e68 0x7ff634530440
BaseGrammar[40394:185339] copyedString : Text 0x10d858e68 0x7ff634530448
结论:当 tempString 为不可变字符串时
- 不管是 strong 还是 copy 属性的对象,其指向的地址都是同一个,即为tempString 指向的地址。
- 如果我们换作 MRC 环境,打印 tempString 的引用计数的话,会看到其引用计数值是3,即 strong 操作和 copy 操作都使原字符串对象的引用计数值加了1。
- 当 tempString 的值发生改变时,两个对象的值也保持原来的值。
可变字符串:
// 可变字符串
NSMutableString *tempString = [NSMutableString stringWithFormat:@"Text"];
self.strongedString = tempString;
self.copyedString = tempString;
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);
// 改变tempString的值
[tempString appendString:@"Change OK"];
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);
// 打印结果:
BaseGrammar[42008:192582] tempString : Text 0x60000279c960 0x7ffee5983bf8
BaseGrammar[42008:192582] strongedString : Text 0x60000279c960 0x7fe20fe14380
BaseGrammar[42008:192582] copyedString : Text 0xb96aa7441ad7466e 0x7fe20fe14388
// 改变后:
BaseGrammar[42008:192582] tempString : TextChange OK 0x60000279c960 0x7ffee5983bf8
BaseGrammar[42008:192582] strongedString : TextChange OK 0x60000279c960 0x7fe20fe14380
BaseGrammar[42008:192582] copyedString : Text 0xb96aa7441ad7466e 0x7fe20fe14388
结论:当 tempString 为可变字符串时
-
此时 copy 属性字符串已不再指向 tempString 字符串对象,而是深拷贝了 tempString 字符串,并让 copyedString 对象指向这个字符串。
-
strongString 与 tempString 是指向同一对象,所以 strongString 的值也会跟随着改变(需要注意的是,此时 strongString 的类型实际上是NSMutableString,而不是 NSString);而 copyedString 是指向另一个对象的,所以并不会改变。
-
在 MRC 环境下,打印两者的引用计数,可以看到 tempString 对象的引用计数是2,而 copyedString 对象的引用计数是1。
由上面分析我们能得到一个结论:copy 对于不可变对象是浅拷贝,对于可变对象是深拷贝。在声明 NSString 属性时,到底是选择 strong 还是 copy,可以根据实际情况来定。不过,一般我们将对象声明为 NSString 时,都不希望它改变,所以大多数情况下,我们建议用 copy,以免因可变字符串的修改导致的一些非预期问题。即对于可变字符串以得到新的内存分配,而不只是原来的引用。
3)assign 和 weak 的区别
assign 的特点:
- 修饰基本数据类型和原子类类型。
- 修饰对象类型时,不改变其引用计数。
- 会产生悬挂指针(野指针),用 assign 修饰的对象被释放之后,assign指针仍指向原对象的地址内存,继续使用 assign 指针访问原对象,会产生悬挂指针导致内存泄漏。
weak 的特点:
- 只能修饰对象。
- 不改变修饰对象的引用计数。
- 所指对象在被释放之后会自动置为 nil。
4)深浅拷贝
浅拷贝:就是对内存地址的复制,让目标对象指针和原对象指针指向同一片内存空间。浅拷贝增加了被拷贝对象的引用计数,并没有产生新的内存分配空间。
深拷贝:就是让对象指针和原对象指针指向两片内容相同的内存空间。深拷贝没有增加被拷贝对象的引用计数,产生新的内存分配控件。
总结:
copy:对于可变对象为深拷贝,对于不可变对象为浅拷贝。拷贝出来的是不可变对象。
mutableCopy:始终是深拷贝。拷贝出来的是可变对象。
总的来说在 Objective-C 里面只有一种情况是浅拷贝,那就是不可变对象的copy,其它的都是深拷贝(包括不可变对象mutableCopy、可变对象的的 copy 和mutableCopy)。
3. 引用关键字
关键字说明:
-
#import
是 Objective-C 导入头文件的关键字。确保一个头文件只能被导入一次,这使你在递归包含中不会出现问题,所以 #import 比起#include 的好处是不会引起交叉编译。-
import<>
代表导入系统自带的框架; -
import""
代表导入我们自己创建的头文件;
-
-
#include
是C/C++导入头文件的关键字。 -
@class
一般用于声明某个字符串作为类名使用,它只是声明了一个类名,没有导入 .h 文件中的内容,不会引起交叉编译问题。
import 和 @class 的区别总结:
- Import 会包含这个类的所有信息,包括实体变量和方法(.h 文件中),而 @class 只是告诉编译器,其后边的声明的名称是类的名称,至于类是如何定义的,后边再说,这边不关心。或者说 @class 创建了一个前向引用,就是在告诉编译器,“相信我,以后你会知道这个类到底是什么,但是现在,你只需要知道这些就好”,如果有循环依赖关系;
- 在头文件中,一般只需要知道被引用的类的名称就可以了,不需要知道其内部的实体变量和方法,所以在头文件中一般使用 @class 来声明这个名称是类的名称,而在实现类里边,因为会用到这个引用类的内部的实体变量和方法,所以需要使用 #import 来包含这个被引用类的头文件;
使用总结:
-
如果不是 C/C++,尽量使用 #import;
-
能在实现文件中 #import,就不在头文件中 #import;
-
能在头文件中 @class 实现文件中 #import,就不再头文件中 #import;