1.2 iOS中内存分配的实现

 

  话说我昨天写了一篇告诉大家怎么泡妹子的文章,但是果然程序员都是不缺妹子的啊!!!好吧,既然不缺,那我们还是继续研究技术吧!还是同一句话,如果有交流技术的同学可以加我的qq:1583042987。

上一节的例子讲述了objective-c的内存管理方式,什么是引用计数,以及如何施行。对此有了一个粗略的概念之后,这一节,将使用代码来对此做具体的阐述。

   1.2.1 alloc/retain/release/dealloc/autorelease的实现

   通过之前的例子,可以清楚的了解到内存的管理步骤,但是具体在程序中如何实现呢?在objective-c中使用alloc/retain/release/dealloc/autorelease这些方法为基础,通过需求进行操作,接下来通过代码来进一步了解。

     /*

        * 活动开始,并已经有一名学生持有图书资源

        */

       id student = [[NSObject alloc]init];

         /**

          *  另一名学生持有图书资源

          */

      [student retain];

      /**

        *  打印学生持有资源总数

        */

     NSLog(@"How many books is %lu.",(unsigned long)[student retainCount]);

         /*

          * 一名学生释放资源

          */

       [student release];

       /**

        *  打印release后,学生持有资源总数

        */

     NSLog(@"After release,How many books is %lu.",(unsigned long)[student retainCount]);

      /**

      *  将资源注册到自动释放池,当释放池结束时自动调用release释放内存

      */

  [student autorelease];

       /**

        *  打印autorelease后,学生持有资源总数

        */

     NSLog(@"After autorelease,How many books is %lu.",(unsigned long)[student retainCount]);

 

打印结果为:How many books is 2.

 After release,How many books is 1.

After autorelease,How many books is 1.

从代码中可以看出,当调用alloc时对象向系统请求一块内存,生成并持有这块资源,再调用retain方法的时候,持有数量加1,引用计数数量打印结果为2,而调用release则减1,引用计数数量打印结果为2,在调用autorelease 时将此次操作记录到一个名为自动释放池的管理器中,暂时并不释放,引用计数不变。当释放池结束运行时,会将注册在里面的内存释放,如图1.2所示。

      

图1.2   自动释放池流程

在引用计数技术下,每一个对象都存有一个计数器,纪录当下对象持有资源数量,当数量为0时,调用dealloc彻底释放这块内存,为避免出现“dangling pointer”(悬挂指针,也称为迷途指针)现象,意为指针指向无效或者已被释放的内存地址,为避免这样的事情发生通常会在计数为0时将对象至为nil确保其安全释放。

如代码所示,对象的引用计数可以通过retaincount实例方法获得,但这个方法并不实用,原因有三点,一,虽然它保留了对象的计数,但保留计数的绝对值,且永远不会返回0,这就给开发者带来极大的不便,二,它只保留了对象在某一时间点的计数,当使用autorelease时并不能考虑到自动释放池运行结果,那就给开发者带来一个错误的信息,容易导致开发者过度释放,三,在处理如NSString,NSNumber这样基本数据类型时,返回值往往会很大,如下代码。

    /**

     *   为一个字符串赋值,并打印其引用计数

     */

    NSString *str = @"iOS性能优化";

    NSLog(@"str is retainCount = %lu.",(unsigned long)[str retainCount]);

     /**

          *  为一个数字类型的对象复制,并打印其引用技术

      */

      NSNumber *num = @1;

  NSLog(@"num is retainCount = %lu.",(unsigned long)[num retainCount]);

    打印结果为:str is retainCount = 18446744073709551615.

               num is retainCount = 9223372036854775807.

此时如果使用retainCount方法查看计数就会非常尴尬,另外在ARC中,retainCount方法已经被禁用。

在objective-c中还有两种方法可以增加引用计数copy(浅拷贝)和mutablecopy(深拷贝)。使用copy的类必须遵守NSCopying协议规定,该协议只有一个方法:

/*************** Basic protocols      ***************/

 

@protocol NSCopying

 

- (id)copyWithZone:(NSZone *)zone;

 

@end

而使用mutablecopy必须遵守NSMutableCopying协议规定,该类也是只有一个方法:

@protocol NSMutableCopying

 

- (id)mutableCopyWithZone:(NSZone *)zone;

 

@end

对于初学者而言,很难理解zone的含义。在很早的时候,开发系统程序时,会把内存分成很多不同的“区域”(zone),而对象则放在其中。当然现在苹果为了给开发者提供更快速,更方便的环境,将每个程序都只用了一个区域——“默认区域”(default zone)。所以开发者不用再纠结于zone这个参数了。

两者都可以生成并持有对象的副本,区别在于copy生成的副本不可变,而mutablecopy生成的副本是可变的。调用二者都会让对象的引用计数递增,于使用retain方法不同的是,retain是持有一个新的对象,而copy/mutablecopy则是持有对象本身的副本。那么使用copy以及mutablecopy对程序编写有什么好处呢?

(NSString mutablecopy) ===>  NSMutableString

 (NSMutableString copy)  ===>  NSString

如上面为代码所示,使用copy与mutablecopy可以使对象在可变与不可变之间互相切换。另外使用copy/mutablecopy可以将一些对象从“栈内存”(stack)复制到“堆内存”(heap),比如“闭包”(block),当然block这个对象还有很多使用规则,在本节不做过多介绍,后面将会对此进行详细讲解。

1.2.2  在开发项目中的循环引用问题

通过上面的介绍,对于objective-c的内存管理多少有一些认识,那接下来结合实际对内存的使用规则进行更深入的了解。

在使用引用计数机制时,最为需要注意的问题就是“循环引用”(retain cycle),有人也会称之为“内存保留环”,也就是两个或者多个对象间互相引用,导致内存泄露,如图1.3。

 

                                              图1.3   循环引用实例图

从图上可以看出来对象之间互相持有,这就造成至少有一个对象不能正常释放,在这个循环里,所有对象的保留计数都是1。这种情况,常见于类与类之间的联系,如“代理”(delegate)或是“闭包”(block)。下面通过不同代码,对代理与闭包分别展示一下。

BasicManage.h文件

#import <Foundation/Foundation.h>

@class BasicManage;

@protocol BasicManageDelegate <NSObject>

/**

 *  设置网络请求完成后回调

 *

 *  @param manage 网络请求管理器

 *  @param data   返回值

 */

-(void)netWorkManage:(BasicManage *)manage

      didReceiveData:(NSData *)data;

/**

 *  设置网络请求异常回调

 *

 *  @param manage 网络管理器

 *  @param error   错误信息

 */

-(void)netWorkManage:(BasicManage *)manage

    didFailWithError:(NSError *)error;

@end

@interface BasicManage : NSObject

 

@property(assign,nonatomic)id<BasicManageDelegate>delegate;

 

@end

BasicManage.m文件

#import "BasicManage.h"

 

@implementation BasicManage

 

@end

BasicModel.h文件

#import "BasicManage.h"

@interface BasicModel : NSObject<BasicManageDelegate>

 

/**

 *  归档网络请求数据

 *

 *  @param data 网络请求数据

 */

-(void)saveArchiveNetWorkData:(NSData *)data;

 

@end

BasicModel.m文件

#import "BasicModel.h"

 

@implementation BasicModel

-(instancetype)init

{

    self = [super init];

    if (self)

    {

        BasicManage *manage = [BasicManage new];

        manage.delegate     = self;

    }

    return self;

}

/**

 *  返回成功方法

 *

 *  @param manage 网络请求管理器

 *  @param data   请求返回值

 */

-(void)netWorkManage:(BasicManage *)manage didReceiveData:(NSData *)data

{

    [self saveArchiveNetWorkData:data];

}

-(void)netWorkManage:(BasicManage *)manage didFailWithError:(NSError *)error

{

   

}

-(void)saveArchiveNetWorkData:(NSData *)data

{

   

}

@end

 

此例中delegate属性用assign修饰,原因在于如果使用retain修饰的话,在设置代理方时manage对象被model对象持有,形成循环引用,导致最后dealloc时manage对象不能正常销毁,而使用assign只是简单赋值,并不持有,所以不会造成循环引用的问题。同样的情况也发生在block中,如下代码。

   BasicManage *manage = [BasicManage new];

        manage.delegate     = self;

        [manage receiveDataWithError:^(NSError *error) {

            self.title = [NSString stringWithFormat:@"%@",error];

        }];

由于在block中使用对象,当block从栈存储区域复制到堆时,对象同时被block强引用持有,此时self已经被block持有,很多初学者,对于block的内存管理不够了解,常常会造成这样的内存问题,导致内存暴增,如何解决这样问题?如下代码。

   BasicModel __weak *obj = self;

        [manage receiveDataWithError:^(NSError *error) {

            obj.title = [NSString stringWithFormat:@"%@",error];

        }];

这里先不对上述代码做过多解释,后面将通过对block一步步了解之后,解决这些问题。

通过上述代理对于循环引用的处理方法可以知道,采用简单赋值并不持有的方法,可以有效避免循环引用问题,或者从外界命令循环中的某个对象不在保留另一个对象,这两种方法都可以有效的解决循环引用的问题,从而避免内存泄漏。

posted @ 2016-08-12 14:51  徐栋  阅读(545)  评论(1编辑  收藏  举报