内存管理
所谓内存管理,就是对内存进行管理,涉及的操作有:
-
分配内存:比如创建一个对象,会增加内存占用
-
清除内存:比如销毁一个对象,能减少内存占用
内存管理的管理范围
-
任何继承了NSObject的对象
-
对其他非对象类型无效(int、char、float、double、struct、enum等)
内存管理的本质原因
-
OC对象存放于堆里边
-
非OC对象一般放在栈里边(栈内存会被系统自动回收)
内存管理的重要性
-
移动设备的内存极其有限,每个app所能占用的内存是有限制的
-
下列行为都会增加一个app的内存占用:
-
创建一个OC对象
-
定义一个变量
-
调用一个函数或者方法
-
-
当app所占用的内存比较多时,系统会发出内存警告,这时得回收一些不需要再使用的内存空间。比如回收一些不需要使用的变量、对象等。
-
如果app占用内存过大,系统可能会强制关闭app,造成闪退现象,影响用户体验。
堆和栈
栈:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其它操作方式类似于数据结构中的栈(先进后出)。
堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
示例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;//存放于栈中
int b = 20;//存放于栈中
//p对象存放于堆中,Person对象(计数器 == 1)
Person *p = [[Person alloc] init];
}
//经过上一行代码后,栈里边的变量a/b都会被回收,但是堆里边的Person独享还会留在内存中,因为它是计数器依然是1
return 0;
}
引用计数器
每个OC对象都有自己的引用计数器,它是一个整数,从字面上可以理解为”对象被引用的次数“,也可以理解为:它标识有多少人正在使用这个对象。
作用:
-
系统是根据对象的引用计数器判断什么时候需要回收一个对象所占用的内存。
-
当没有任何人使用这个对象时,系统才会回收这个对象,也就是说:”当对象的引用计数器为0时,对象占用的内存就会被系统回收,如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非程序已经退出)。“
-
任何一个对象,刚生下来的时候,引用计数器都为1,当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1。
操作:
-
给对象发送一条retain消息,可以使引用计数器值+1,并且返回对象本身。
//创建对象
Person *p = [[Person alloc] init];
//发送retain消息 +1
[p retain];//+1
-
给对象发送一条release消息,可以使引用计数器值-1,但release并不代表销毁/回收对象,仅仅是计数器-1。
//创建对象
Person *p = [[Person alloc] init];
//发送release消息 -1
[p release];//-1
-
给对象发送retainCount消息,可以获得当前的引用计数器值(不够准确)。
//创建对象
Person *p = [[Person alloc] init];
//获取当前对象的引用计数器值
[p retainCount];
总结:内存管理的原则就是有加就有减,也就是说,一次alloc对应一次release,一次retain对应一次release。
dealloc方法基本概念
当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收。
对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此,从dealloc方法有没有调用,就可以判断出对象有没有被销毁)
dealloc方法的重写:
-
一般会在.m文件中,也就是@implementation和@end之间重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言。
-
一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后调用
@implementation Person
- (void)dealloc
{
NSLog(@"Person dealloc");
[super dealloc];
}
@end
注意:xcode 4.2之后就不需要手动去管理内存了,这与ARC(自动管理)和MRC(手动管理)相关。
#import和@class
import基本概念
import是一个预编译指令,它会将双引号中的文件拷贝到import所在的位置。
弊端:
1.只要双引号中的文件发生了变化,那么import就会重新拷贝一次(更新操作),编译性能差。
2.对于循环依赖关系来说,比方A类引用B类,同时B类也引用A类,这种嵌套包含的代码编译会报错。
#import "B.h"
@interface A : NSObject{
@property B *b;
}
@end
#import "A.h"
@interface B : NSObject{
@property A *a;
}
@end
//上面这种代码编译会报错,因为A拷贝B,B又拷贝A,会形成死循环
因此诞生了@class来解决上面这两个问题。
@class的基本概念
@class可以简单地引用一个类,具体格式如下:
@class 类名;
//等价于
@class Person;
@class仅仅是告诉编译器,Person是一个类,并不会包含Person类的所有内容或者说拷贝Person类中的内容,具体使用如下:
-
在.h文件中使用@class引用一个类
//.h文件
#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
//声明属性狗对象
@property Dog *dog;
@end
-
在.m文件中使用#import包含这个类的.h文件
//.m文件
#import "Person.h"
#import "Dog.h"
@implementation Person
- (void) say{
Dog *dog = [[Dog alloc] init];
dog.name = @"小黄";
NSLog(@"我有一条狗,名字叫:%@",dog.name);
}
@end
注意:由于@class仅仅是告诉编译器,@class后面的名称是一个类,所以编译器并不知道这个类中有哪些属性和方法,所以在.m中使用这个类时需要import这个类才能使用。
Category(分类)基本概念
category是oc特有的语法,其他语言没有的语法。
category的作用:
-
可以在不修改原类的基础上,为这个类扩充一些方法
-
一个庞大的类可以分模块开发
-
一个庞大的类可以由多个人来编写,更有利于团队开发
通过分类给某一个类扩充方法,也分为生命和实现两个部分,具体格式如下。
category的格式:
/**
category分类的声明
ClassName:需要给哪个类扩充方法
CategoryName:分类的名称
NewMethod:扩充的方法
*/
@interface ClassName(CategoryName)
NewMethod;
...
@end
/**
category分类的实现
ClassName:需要给哪个类扩充方法
CategoryName:分类的名称
NewMethod:扩充的方法
*/
@implementation ClassName(CategoryName)
NewMethod;
...
@end
category的实例:
//声明类
@interface Person : NSObject
@property int age;
- (void) say;
@end
//实现类
@implementation Person
- (void)say{
NSLog(@"age = %i",_age);
}
@end
//Person分类的声明,分类名称:NJ
@interface Person (NJ)
//声明扩充方法
- (void) playGames;
@end
//Person分类的实现,分类名称:NJ
@implementation Person (NJ)
//实现扩充方法
- (void)playGames{
NSLog(@"玩游戏");
}
//程序入口
#import <Foundation/Foundation.h>
#import "Person.h"
//导入Person的category(分类)
#import "Person+NJ.h"
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
p.age = 30;
[p say];
//调用Person的category(分类)playGames方法。
[p playGames];
return 0;
}
category分类的注意事项:
-
分类是用于给原有类添加方法的,它只能添加方法,不能添加属性(成员变量)
-
分类中的@property只会生成getter/setter方法的生命,不会生成实现以及私有的成员变量
-
可以在分类中访问原有类中.h文件中的公有属性
-
如果分类中和原类中有同名的方法,会调用分类中的方法,也就是说会忽略原有类的方法,在开发中尽量不要这样写
-
如果多个分类中都有和原有类中同名的方法,那么调用该方法的时候执行顺序是由编译器决定会执行最后一个参与编译的分类中的方法
-
方法的调用顺序:分类>本类>父类
类扩展(Class Extension)
延展类别又称为类扩展(ClassExtension),Extension是Category的一个特例,可以为某个类扩充一些私有的成员变量和方法 。
-
写在.m文件中
类扩展具体格式
//对比分类,就少了一个分类名称,因此也称为”匿名分类“。
@interface 类名()
...
@end
//实现例子
// .h文件
//声明类
@interface Person : NSObject
@property int age;
- (void) say;
@end
// .m文件
//声明匿名分类,在这里声明的属性和方法都是私有的,不管有没有使用修饰符。
@interface Person ()
//声明私有属性
{
int _weight;
}
//声明私有方法
- (void) eat;
@end
//实现类
@implementation Person
- (void)say{
NSLog(@"age = %i",_age);
}
//实现私有方法
- (void) eat{
NSLog(@"吃东西!");
}
@end
Block基本概念
block是ios中一种比较特殊的数据类型,是苹果官方特别推荐使用的数据类型,应用场景比较广泛:
-
动画
-
多线程
-
集合遍历
-
网络请求回调
Block可以用来保存某一段代码,可以在恰当的时间再取出来调用,功能类似于函数和方法。
Block的应用场景:当发现代码的前面和后面都是一样的时候,就可以使用block
Block的定义格式:
返回值类型 (^block变量名)(形参列表) = ^(形参列表){};
没有返回值,没有参数的block例子:
//Xcode9之前的写法
//void (^blocksimple1) ();
//Xcode9之后的写法,如果使用xcode9之前的写法会出现警告:This block declaration is not a prototype
void (^blocksimple1) (void);
//下一行代码的()在没有参数的时候可以省略
blocksimple1 = ^(){
NSLog(@"我是没有返回值和形参的block代码区");
};
//调用block变量
blocksimple1();
注意:block没有参数的话,在给block变量赋值时的()可以不用写。
没有返回值,有一个参数的block例子:
void (^blocksimple2) (int);
blocksimple2 = ^(int num){
NSLog(@"我是带一个参数的block代码区,参数值为:%i",num);
};
//调用blocksimple2 block变量
blocksimple2(5);
没有返回值,有多个参数的block例子:
void (^blocksimple3) (int,NSString *);
blocksimple3 = ^(int age,NSString *name){
NSLog(@"%@,参数值为:%i",name,age);
};
//调用blocksimple3 block变量
blocksimple3(5,@"我是带多个参数的block代码区");
有返回值,有参数的block例子:
int (^blocksimple4) (int,NSString *);
blocksimple4 = ^(int age,NSString *name){
NSLog(@"%@,参数值为:%i",name,age);
return 10;
};
//调用blocksimple4 block变量
int temp = blocksimple4(5,@"我是带多个参数并且有返回值的block代码区");
NSLog(@"返回值 = %i",temp);
Block和typedef
需要定义比较多的block数据类型变量时,可以利用typedef给block起别名,block变量的名称就是别名。如下:
//定义别名
typedef void (^BlockSimple) (int,NSString *);
//使用别名
BlockSimple blocksimple3 = ^(int age,NSString *name){
NSLog(@"%@,参数值为:%i",name,age);
};
//调用blocksimple3 block变量
blocksimple3(5,@"我是带多个参数的block代码区");
Block的注意事项:
-
block中可访问外面的变量
int a = 10;
void (^myBlock)(void) = ^{
NSLog(@"block外面的变量 :%i",a);
};
myBlock();
-
block中可以定义和外界同名的变量,并且如果在block中定义了和外界同名的变量,在block中访问的是block的变量
int a = 10;
void (^myBlock)(void) = ^{
int a = 20;
NSLog(@"a变量 :%i",a);//这里的输出的日志 = a变量 :20
};
myBlock();
-
默认情况下,不可以在block中修改外界变量的值,因为block中的变量和外界的变量并不是同一个变量,如果block中访问到了外界的变量,block会将外界的变量拷贝一份到堆内存中,因为block中使用的外界变量是copy的,所在在调用之前修改外界变量的值,不会影响到block中copy的值。
-
如果想在block中修改外界变量的值,必须在外界变量前面加上__block
__block int a = 10;
void (^myBlock)(void) = ^{
a = 20;
NSLog(@"a变量 :%i",a);//这里的输出的日志 = a变量 :20
};
myBlock();
Protocol基本概念
protocol又称为“协议”,在写java的时候都会有接口interface这个概念,接口就是一堆方法没有实现,而在OCli
边interface是一个类的头文件的声明,并不是真正的意义上接口的意思,在OC中接口是由一个叫做协议的protocol来实现的。
protocol的作用:用来声明一些方法,也就是说一个Protocol是由一系列的方法声明组成的。
protocol语法格式:
@protocol 协议名称 <NSObject> //建议每个新的协议都要遵守NSObject协议(不强制)
//方法声明列表
@end
类遵守协议:
-
一个类可以遵守1个或多个协议
-
任何类只要遵守了Protocol,就相当于拥有了Protocol的所有方法声明
@interface 类名 : 父类 <协议名称1,协议名称2,...>
...
@end
protocol示例:
//声明SportProtocol协议
@protocol SportProtocol <NSObject>
//声明协议的踢足球方法
- (void) playFootball;
//声明协议的打篮球方法
- (void) playBasketball;
@end
//遵守了SportProtocol协议
@interface Student : NSObject <SportProtocol>
@end
@implementation Student
//实现协议playFootball方法
- (void) playFootball{
NSLog(@"踢足球");
}
//实现协议playBasketball方法
- (void) playBasketball{
NSLog(@"打篮球");
}
@end
Protocol注意事项:
-
协议只能声明方法,不能声明属性
-
父类遵守了某个协议,那么子类也会自动遵守这个协议
-
OC中的协议可以遵守其它协议,只要一个协议遵守了其它协议,那么这个协议中就会自动包含其它协议的声明
Protocol的@required和@optional关键字
协议中有2个关键字可以控制方法是否要实现(默认是@required,仅仅用于程序员之间交流)
-
@required:这个方法必须要实现,若不实现,编译器会发出警告。
-
@optional:这个方法不一定要实现
@protocol SportProtocol <NSObject>
@required
- (void) playFootball;
@optional
- (void) playBasketball;
@end
Protocol类型限制
将协议写在数据类型的右边,明确的标注如果想给该变量赋值,那么该对象必须遵守某个协议,格式如下:
数据类型<协议名称> 变量名;
//等价于
@interface Person : NSObject
//标注stu变量必须遵守SportProtocol协议,如果没有遵守协议则会报警告。
@property Student<SportProtocol> *stu;
@end
注意:虽然在接收某一个对象的时候,对这个对象进行了类限定(限定它必须实现某个协议),但是并不意味着这个对象就真正的实现了该方法,所以每次在调用对象的协议方法时应该进行一次验证,如下:
@implementation Person
//先判断学生类有没有实现playFootball(踢足球)的方法
if([self.stu respondsToSelector:@selector(playFootball)]){
//调用playFootball(踢足球)方法
[stu playFootball];
}
@end