黑马程序员---OC基础知识⑦

时间:2023-02-19 15:49:54

※内存管理很重要

1.为什么要管理内存
首先看这样一段代码:

<span style="font-size:18px;">int main(int argc, const charchar * argv[])  
{
int a = 10;
BOOL b = YES;
char c = 'w';
return 0;
} </span>

像a、b、c这样的基本数据类型的局部变量存放在栈上,函数执行结束时这些存储单元自动被释放。
在看看这样的代码:

<span style="font-size:18px;">int main(int argc, const charchar * argv[])  
{
Person *p = [[Person alloc] init];
[p setAge:10];
NSLog(@"%d", [p age]);
return 0;
} </span>
像Person这样的对象类型,分配在堆上,在上面的代码中,[[Person alloc] init]在堆上创建了一个
Person类型的对象 Person类型的指针p指向了这个对象,而指针p类型保存在栈上,当程序结束时p所
占用的内存被清空,而存储在堆上得对象没有被释放,依然放在内存中。OC项目中的代码中处处都是
这样的代码,如果不进行内存管理,程序运行过程会不断地加大内存开销,最终可能导致程序崩溃。


2.引用计数器

①  每个OC对象都有自己的引用计数器,是一个整数,表示“对象被引用的次数”,即有多少人正在
使用这个OC对象
②  每个OC对象内部专门有4个字节的存储空间来存储引用计数器
③  当使用alloc、new或者copy创建一个新对象时,新对象的引用计数器默认就是1
④  当一个对象的引用计数器值为0时,对象占用的内存就会被系统回收。换句话说,如果对象的计数
器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收,除非整个程序已经退出

我们可以使用引用计数器来进行内存管理,比如:
当程序结束时,p不存在了,而p指向的对象的对象的引用计数器由1变成了0,意味着没有引用它的指
针了,这时候可以将堆中对象占用的内存回收,进行合理的内存管理。现在的问题就是如何进行管理对象的引用计数器
OC语言中定义了下面几种对引用计数器的操作:
①   给对象发送一条retain消息,可以使引用计数器值+1(retain方法返回对象本身)
②   给对象发送一条release消息,可以使引用计数器值-1
③   可以给对象发送retainCount消息获得当前的引用计数器值
由此可以得到启发,每当创建对象或者复制对象时可以使用retain将其"引用计数器"的值+1,而不再使

用引用它的指针时,将对象的"引用计数器"的值-1,问题就来了:如何在对象的"引用计数器"的值为0

的时候将对象销毁呢:

对象的销毁
①   当一个对象的引用计数器值为0时,那么它将被销毁,其占用的内存被系统回收
②  当一个对象被销毁时,系统会自动向对象发送一条dealloc消息
③   一般会重写dealloc方法,在这里释放相关资源,dealloc就像对象的遗言
④  一旦重写了dealloc方法,就必须调用[superdealloc],并且放在最后面调用
⑤  不要直接调用dealloc方法
⑥  一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)
下面对Person类的deallloc方法进行重写:


@implementation Person  
// 当一个Person对象被回收的时候,就会自动调用这个方法  
- (void)dealloc  
{  
    NSLog(@"Person对象被回收");  
    // super的dealloc一定要调用,而且放在最后面  
    [super dealloc];  
}  


对retain、release、retainCount做一些简单的测试:
#import <Foundation/Foundation.h>  
#import "Person.h"  
int main(int argc, const charchar * argv[]) {  
      
    Person *p = [[Person alloc] init]; // 执行后p指向的对象retainCount为1  
      
    NSUInteger c = [p retainCount]; // 获取p指向的对象的retainCount  
      
    NSLog(@"计数器:%ld", c);  
      
      
    [p retain];  // 执行完这句之后retainCount变为2     
    [p release];  // retainCount为1  
    [p release];   // retainCount为0 系统自动调用Person的dealloc方法   
    return 0;  
}  
程序运行后输出的结果是:
 计数器:1  
Person对象被回收  


使用引用计数器要特别注意:
注意点一:给已经释放的对象发送了一条setAge消息-------->闪退
注意点二:给空指针发送消息不会报错 OC中不存在空指针错误

<span style="font-size:18px;">#import <Foundation/Foundation.h>  
#import "Person.h"
int main(int argc, const charchar * argv[]) {
//
Person *p = [[Person alloc] init];
[p retain];
//
[p release];
[p release];
p.age = 10; // 给已经释放的对象发送了一条setAge消息-------->闪退
return 0;
} </span>

这时候说指针p指向了僵尸对象(对象的内存已经被释放了),此时p被成为野指针。
这时候有这样一种解决方案将p赋值为ni,因为对空指针(值为nil的指针,基本数据类型而言值为0)
调用方法时,程序什么也不做。代码如下:
<span style="font-size:18px;">#import <Foundation/Foundation.h>  
#import "Person.h"
int main(int argc, const charchar * argv[]) {
//
Person *p = [[Person alloc] init];
[p retain];
//
[p release];
[p release];

p = nil;// 对象计数器rc减为0时调用 以此来消除野指针

p.age = 10;// 给空指针发送消息不会报错 OC中不错在空指针错误
[p release];// 给空指针发送消息不会报错 OC中不错在空指针错误
return 0;
} </span>



3.多对象内存管理
在实际开发中,可能遇到这样的情况,一个类A的成员变量类型也是一个类B,对成员变量B类型对象的
引用就不只是B类型,可能还有A类型。这样容易引发这样两种问题,①B类型的对象被销毁了A成员仍
然在引用它,②A类型的对象销毁顺带着将B类型的成员变量销毁,但仍有B类型的指针指向该对象。只
要按照以下原则使用引用计数,就可以对内存进行合理的管理


原则1. 谁创建谁release
①  如果你通过alloc、new或[mutable]copy来创建一个对象,那么你必须调用release或autorelease
②  换句话说,不是你创建的,就不用你去[auto]release 


原则2.       谁retain,谁release
①  只要你调用了retain,无论这个对象是如何生成的,你都要调用release 
例如Person类中包含了一个Book类,规范的设计代码应该是这样:

<span style="font-size:18px;">#import "Person.h"  

@implementation Person
- (void)setBook:(Book *)book
{
_book = [book retain];
}
- (Book *)book
{
return _book;
}

- (void)dealloc
{
[_book release];
NSLog(@"Person对象被回收");
[super dealloc];
}
@end </span>

<span style="font-size:18px;">#import <Foundation/Foundation.h>  
#import "Book.h"
#import "Person.h"
int main(int argc, const charchar * argv[]) {

Book *b = [[Book alloc] init];

Person *p1 = [[Person alloc] init];

[p1 setBook:b];


[p1 release];
p1 = nil;

[b release];
b = nil;
return 0;
}
</span>

上面的程序对多个对象内存管理很好地进行了总结:
 1.你想使用(占用)某个对象,就应该让对象的计数器+1(让对象做一次retain操作)// 如 Person 要使用Book
 2.你不想再使用(占用)某个对象,就应该让对象的计数器-1(让对象做一次release) 
 3.谁retain,谁release // Person retain Book ,Person就要release Book
 4.谁alloc,谁release // Book alloc Book对象,Book就要release Book对象


4.set方法完善---针对内存管理
在刚刚的多对象内存管理中,仍让存在一些不合理的地方:
假设有这样的场景,人拥有一辆车,按照刚才的方法先创建一辆车carA,设置人的车为carA,但是当
人要换车时人并没有对carA进行release(因为人 没有调用dealloc方法,但实际上人已经不再引用carA
了,应当将carA的rc减一)。解决的方案是在Person的set方法中将旧的车release一次:

- (void)setCar:(Car *)car  
{     
    // 对当前正在使用的车(旧车)做一次release  
    [_car release];  
    // 对新车做一次retain操作  
    _car = [car retain];      
}  
这样似乎能解决问题,但是如果新传入的汽车仍然是carA(现在的车),且在执行[p setCar:carA];之前
carA被release了一次(也就是说carA现在的rc是1),此时执行setCar方法,就会引起野指针问题。因此
再对set方法进行完善:

<span style="font-size:18px;">- (void)setCar:(Car *)car  
{
if (car != _car)
{
// 对当前正在使用的车(旧车)做一次release
[_car release];
// 对新车做一次retain操作
_car = [car retain];
}
} </span>


总结
内存管理代码规范:
1.只要调用了alloc,必须有release(autorelease)
2.set方法的代码规范
  1>.基本数据类型:直接赋值

- (void)setAge:(int)age  

{    // 基本数据类型 不需要管理内存  
     _age = age;  
}  
  

2>.OC对象类型
- (void)setCar:(Car *)car  
{  
    // 1.先判断是不是新传进来的对象  
    if(car != _car)  
    {  
        // 2.对旧的对象做一次release  
        [_car release];  
  
        // 3.对新传进来的对象做一次retain  
        _car = [car retain];  
    }  
}  


 3.dealloc方法的代码规范
1>一定要调用[super dealloc]且放在最后
2>.对当前对象所拥有的其他对象做一次release
- (void)dealloc  
{  
    [_car release];  
    [super dealloc];  
}  

利用@property参数对内存进行管理
默认情况下@property生成set方法只是简单的赋值操作:
在Person类的声明中添加这行代码

@property Book *book;  
意味着编译器在实现中帮助生成的set方法是这样的

- (void)setBook:(Book *)book  
{  
    _book = book;  
}  
这样显然是不合理的(刚刚讨论的),若要为@property添加retain关键字:
@property (retain) Book *book;  
编译器便会添加这样的set方法:

- (void)setBook:(Book *)book  
{  
    if (book != _book)  
    {  
        [_book release];  
        _book = [book retain];  
    }  
}  

可见retain参数的作用: 生成的set方法里面,release旧值,release新值。
因此开发中对于成员变量是类类型时@property要添加retain参数,需要注意的是使用NSString作为
成员变量类型时也要添加,因为NSString也是了类类型。


5.property参数详解
1).内存管理相关的参数(三择一)
retain: release旧值、retain新值(适用于OC对象类型)
assign:直接赋值(默认,适用于非OC对象类型)
copy:release旧值, copy新值

2).是否要生成set方法
readwrite:同时生成setter和getter的声明和实现(默认)
readonly:只会生成getter的声明和实现

3).多线程管理
nonatomic:性能高(一般就用这个)
atomic:性能低(默认)

4).setter和getter方法的名称
setter:决定了set方法的名称,一定要由个冒号:
getter:决定了get方法的名称(一般用在BOOL类型的属性值)
注意:
当成员变量类型为BOOL类型时,get方法方法名一般以is开头

@property (getter=isRich) BOOL rich;  

6.类的循环引用
两个类循环引用时(A引用B,B也引用A)
在两个类中在.h分别用@class声明另一个类 在m文件中使用#import
 @class仅仅是告诉编译器这是一个类
@class和#import的区别
 #import方式会包含被引用类的所有信息,包括被引用类的变量和方法;
@class方式只是告诉编译器在A.h文件中 B *b只是类的声明,具体这个类里有什么信息,这里不需要
知道,等实现文件中真正要用到时,才会真正去查看B类中信息
如果有上百个头文件都#import了同一个文件,或者这些文件依次被#improt,那么一旦最开始的头

文件稍有改动,
后面引用到这个文件的所有类都需要重新编译一遍,这样的效率也是可想而知的,而相对来 讲,使用
@class方式就不会出现这种问题了
 在.m实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import方式引入
被引用类

老师总结
 1.@class的作用:仅仅告诉编译器,某个名称是一个类
 
 2.开发中引用一个类的规范
 1>.在.h文件中用@class声明类
 2>.在m文件中用#import来包含类的所有东西
 
 3.两端循环引用解决方案
 1>.一端用retain
 2>.一端用assign
例如Person拥有Card类型的属性,Card类型拥有Person类型的属性:
在Person.h中

<span style="font-size:18px;">#import <Foundation/Foundation.h>  
// @class仅仅是告诉编译器这是一个类
@class Card;
@interface Person : NSObject
@property (nonatomic, retain) Card *card;
@end </span>

在Person.m中
<span style="font-size:18px;">#import "Person.h"  
#import "Card.h"

@implementation Person
- (void)dealloc
{

NSLog(@"Person被销毁");
[_card release]; // 使用retain参数的对象 需要这一句
[super dealloc];
}
@end </span>

Card.h中
<span style="font-size:18px;">#import <Foundation/Foundation.h>  
@class Person;
@interface Card : NSObject
@property (nonatomic, assign) Person *person;
@end </span>

在Card.m中
<span style="font-size:18px;">#import "Card.h"  
#import "Person.h"

@implementation Card
- (void)dealloc
{
NSLog(@"Car被销毁");
// [_person release]; // 因为使用的时assign参数 所以不用这一句
[super dealloc];
}
@end </span>

7.autorelease的使用
1).autorelease的基本用法
 1>.会将对象放到自动释放池
 2>.当自动释放池销毁时,会对池子里面的所有对象做一次release操作
 3>.方法会返回对象本身
 4>.调用完autorelease方法后,对象的计数器不变
 
 2).autorelease的好处
 1>.不用担心对象释放的时间
 2>.不用关心什么时候调用release
 
3).autorelease的使用注意
 1>.占用内存较大的对象不要随便使用autorelease
 2>.占用内存较小的对象使用autorelease,没有太大影响
 
 4).错误写法
 1>.alloc之后调用了autorelease,又调用release 
@autoreleasepool  
 {  
    Person *p = [[[Person alloc] init] autorelease];  
    [p release];  
 }  


  2>.连续调用多次autorelease
@autoreleasepool  
{  
    Person *p = [[[[Person alloc] init] autorelease] autorelease];  
}  

 5).自动释放池
 1>.在IOS程序运行过程中,会创建无数个池子,这些池子都是以栈结构存在(先进后出)
 2>.当一个对象调用autorelease方法时,会将这个对象放到栈顶的释放池
 
 6).自动释放池的创建方式
 1>.IOS5.0之前 
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  
 Person *p = [[[Perosn alloc] init] autorelease];  
 [pool release];// [pool drain];  
 
 2>.IOS5.0开始
@autoreleasepool  
 {  
    Person *p = [[[Person alloc] init] autorelease];  
 }  


autorelease应用
1.系统自带的方法里面没有包含alloc、new、copy说明返回的对象都是autorelease的
  [NSString stringWithFormat:...];

2.开发中经常会提供一些类方法,快速创建一个已经autorelease过的对象
1>.创建对象时候不要直接用类名,一般用self
+ (instancetype)person  
{  
    return [[[self alloc] init] autorelease];  
}  
2>.添加带参数的方法时,先使用1>中创建的方法初始化再赋值
+ (instancetype)personWithAge:(int)age  
{  
    Person *p = [self person];  
    p.age = age;  
    return p;  
}  

8.ARC机制
arc是一种编译器特性:编译时编译器帮助完成代码 ,不能和垃圾回收混为一谈
而JAVA垃圾回收是运行时特特性。
 强弱指针:指针分2种
 1>强指针: 默认情况下,所有的指针都是强指针 __strong
 2>弱指针: __weak
ARC所做的工作:
使用arc机制,每当使用alloc时,编译器都会添加release;
每一个类的dealloc方法中都会增加对类成员变量的release;
ARC的判断准则:只要没有强指针指向对象,就会释放对象

// 错误写法(没有意义的写法)  
__weak Person *p = [[Person alloc] init]; // 创建 随即就被销毁了 相当于把空值nil赋给了p  

ARC特点(使用时需要注意的地方)
 1>.不允许调用release、retain、retainCount
 2>.允许重写dealloc,但是不允许调用[super dealloc];
 3>.@property的参数
 strong:成员变量是强指针 (适用于OC对象类型)
 weak:成员变量是弱指针 (使用于OC对象类型)
 assign:适用于非OC对象类型
 4>.以前的retain改为用strong,其他不变

设置单个文件是否使用arc (对于导入其他第三方开发框架的情况)
Build Phases->Compile Sources->选择文件->修改文件编译参数Compile Flags
-fno-objc-arc//不需要arc
-f-objc-arc//需要arc
将整个非arc的工程转为arc工程  Edit->Refactor->Convert to Objective-C ARC

使用ARC时如何处理循环引用:
 当两端循环引用的时候,解决方案:
 1.使用ARC
 修改@property参数:一端用strong,一端用weak
 2.非ARC
修改@property参数: 一端用retain另一端用assign