一. 类概念
如同所有其他的面向对象语言,类是 Objective-C 用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。Objective-C 的类规格说明包含了两个部分:定义(interface)与实现(implementation)。定义(interface)部分包含了类声明和实例(成员)变量的定义,以及类相关的方法。实现(implementation)部分包含了方法的实现,以及定义私有(private)变量及方法。
类的定义文件遵循C语言之惯例以.h为后缀,实现文件以.m为后缀。
二. 类定义(Interface)
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. 实例变量和成员变量的理解
- 实例(Instance)是针对类(class)而言的。实例是指类的声明; 由此推理,实例变量(Instance Variable) 是指由类声明的对象。
- 成员变量就是基本类型声明的变量。
- 严格来说
@interface{}
里定义的实例(成员)变量,它是这个类内部真正的全局变量。然而这个instance variable是不对外公开的,因此我们还需要一个对外公开的东西来调用,就是属性,关键字@property。它其实是告诉大家,我这个类里,有一个变量的seter/geter方法。比如,@property NSString* string;就是说,本类里有一个string/setString供你们调用。属性具体的使用后面会讲到,这里就不展开了。
三. 类实现(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文件里可见。
四. 创建对象
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
。它是给对象分配内存的时候,把关联的对象分配到一个相邻的内存区域内,以便于调用时消耗很少的代价,提升了程序处理速度。
五. 方法声明语法
当你想调用一个方法,你传递消息到对应的对象。这里消息就是方法标识符,以及传递给方法的参数信息。发送给对象的所有消息都会动态分发,这样有利于实现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];
六. 属性
属性(property)用于封装对象中的数据,iOS开发中最常用最方便的变量声明方式,允许我们用点语法来访问对象的实例变量。
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. @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,而不是_str,但是变量的存取方法没有变化。不过我建议最好不要这么办,因为都按照约定成俗的方式来命名变量,代码的可读性较高,大家都理解,所以我建议大家最好不要用这个关键字。
3. @dynamic
@dynamic 关键字主要是告诉编译器不用为我们自动合成变量的存取方法, 我们会自己实现。即使我们没有实现,编译器也不会警告,因为它相信在运行阶段会实现。如果我们没有实现还调用了,就会报这个错误'-[ViewController setName:]: unrecognized selector sent to instance 0x10040af10'
。
4. 属性的特性(关键字)
-
原子性
- atomic(默认):atomic意为操作是原子的,意味着只有一个线程访问实例变量(生成的setter和getter方法是一个原子操作)。atomic是线程安全的,至少在当前的存取器上是安全的。它是一个默认的特性,但是很少使用,因为比较影响效率;
- nonatomic:意为操作是非原子的,可以被多个线程访问。它的效率比atomic快。但不能保证在多线程环境下的安全性,开发中常用;
-
读写权限(存取器控制)
- readwrite(默认):readwrite是默认值,表示该属性同时拥有setter和getter;
- readonly: readonly表示只有getter没有setter;
- 有时候为了语意更明确可能需要自定义访问器的名字;
-
内存管理
-
retain(MRC)/strong(ARC)
内存管理语义:强引用 系统默认:ARC情况下是 作用:只能修饰对象。对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后都会-1;即使对象本身被释放了,只要还有对象在引用,就会持有不会造成什么问题;只有当引用计数为0时,就被dealloc析构函数回收内存了。
-
assign
内存管理语义:赋值 系统默认:MRC情况下是 作用:主要用于修饰值类型,如int、float、double和NSInteger,CGFloat等表示单纯的复制。还包括不存在所有权关系的对象,比如常见的delegate。值类型变量的内存由编译器自动管理;修饰对象属性时,其指向一个对象后,不改变该对象的引用计数。即只引用已创建的对象,而不持有对象;assign修饰的属性不持有对象,当其指向对象在别处释放后,该指针变为悬挂指针也叫野指针。
-
weak
内存管理语义:弱引用 系统默认:否 作用:只能修饰对象。不改变对象的引用计数,当其指向对象被销毁时,它会自动置为nil;变量权限修饰符为__weak,常用于容易造成循环引用的地方不改变对象的引用计数。
-
copy
内存管理语义:复制 系统默认:否 作用:只能修饰对象。一般情况下属性会持有该对象的一份拷贝,建立一个索引计数为1的对象,然后释放旧对象。copy一般用在修饰有可变对应类型的不可变对象上,如NSString, NSArray, NSDictionary。
-
unsafe_unretained
用来修饰属性的时候,和assing修饰对象的时候是一模一样的。为属性设置新值的时候,设置方法既不会保留新值(新值的引用计数加一),也不会释放旧值(旧值的引用计数减一)。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为nil,这就会产生野指针,所以是不安全的。
-
-
assign和weak的区别
-
assign的特点:
1)修饰基本类型。
2)修饰对象类型时,不改变其引用计数。
3)会产生悬挂指针(野指针),用assign修饰的对象被释放之后,assign指针仍指向原对象的地址内存,继续使用assign指针访问原对象,会产生悬挂指针导致内存泄漏。
-
weak的特点
1)不改变修饰对象的引用计数。
2)所指对象在被释放之后会自动置为nil。
3)只能修饰对象。
-
-
深浅拷贝
-
浅拷贝(copy):就是对内存地址的复制,让目标对象指针和原对象指针指向同一片内存空间。浅拷贝增加了被拷贝对象的引用计数,并没有产生新的内存分配空间。
-
深拷贝(mutablecopy):就是让对象指针和原对象指针指向两片内容相同的内存空间。深拷贝没有增加被拷贝对象的引用计数,产生新的内存分配控件。
-
总结:
copy:对于可变对象为深拷贝,对于不可变对象为浅拷贝。拷贝出来的是不可变对象。
mutableCopy:始终是深拷贝。拷贝出来的是可变对象。
-