iOS开发之内存管理




ios开发 内存爆炸 导出文件 ios开发内存管理_内存管理



文章目录

  • iOS开发之内存管理
  • 引用计数(Reference Count)
  • 内存管理的思考方式
  • 对象操作与对应的Objective-C方法
  • autorelease
  • 权限修饰符
  • __strong修饰符
  • 循环引用
  • __weak修饰符
  • __unsafe_unretained修饰符
  • __autoreleasing修饰符
  • 属性修饰符
  • assign修饰符
  • copy修饰符
  • 参考资料
  • 更新中


引用计数(Reference Count)

  iOS的内存管理基于引用计数这一机制,简单地说就是一个对象被其他对象持有时,引用计数+1,当不再被持有时-1,当引用计数减为零的时候对象不被任何人持有,就会被释放。



ios开发 内存爆炸 导出文件 ios开发内存管理_Test_02


内存管理的思考方式

  • 自己生成的对象自己持有
      id obj = [[NSObject alloc] init];
  • 不是自己生成的对象自己也能持有
      id obj = [NSMutableArray array];   [obj retain];
  • 不再需要自己所持有的对象时释放
      id obj = [[NSObject alloc] init];   [obj release]
  • 不是自己持有的对象不能释放
      id obj = [NSMutableArray array];   [obj release]   程序崩溃(obj并不持有NSMutableArray对象)

对象操作与对应的Objective-C方法

对象操作

Objective-C方法

对应的操作结果

生成并持有对象

Alloc,new,copy,mutableCopy 等方法

生成对象并设置引用计数=1

持有对象

retain

引用计数+1

释放对象

release

引用计数-1

释放对象

dealloc(系统自动调用)

引用计数=0时调用

  使用某个方法生成对象,其源码实现:

-(id)allocObject
    {
        id obj = [[NSObject alloc] init];    //生成并持有了NSObject对象
        return obj;
    }

  这里原封不动地将obj返回给方法的调用者,使得方法的调用者也持有该对象。
  我们考虑另一种情况:

-(void)object
    {
        id obj = [[NSObject alloc] init];   //生成并持有了NSObject对象
        [obj autorelease];                  //对象存在但不再持有
        return obj
    }

  这里使用autorelease方法将obj对象注册到autoreleasepool中,在对象超出指定的生存范围的时候能够自动正确地释放(调用release方法)

autorelease

  autorelease类似于C语言的自动变量的特性,当自动变量超出其作用域时,就会被自动释放。但是autorelease与C语言不通的是,你可以自己定义自动变量的作用域。
  autorelease的使用方法:
    ① 生成并持有NSAutoreleasePool对象
    ② 调用已分配的对象的autorelease实例方法
    ③ 释放NSAutoreleasePool对象



ios开发 内存爆炸 导出文件 ios开发内存管理_修饰符_03


  当我们在调用autorelease方法时,其内部其实是调用了[NSAutoreleasePool addObject:self]方法,从而将对象放入自动释放池中,当自动释放池被释放时,自动释放池里的对象也被一并释放了。

权限修饰符

__strong修饰符

  __strong修饰符是id类型和对象类型的默认修饰符

  id obj = [[NSObject alloc] init]id __strong obj [[NSObject alloc] init]等价
  __strong表示对对象的强引用,当变量超出其作用区域的时候,强引用会失效,此时对象不再被持有,就会被释放。

{
        id __strong obj = [[NSObject alloc] init];
        //此时obj对NSObject对象强引用
    }
    //超出作用区域,强引用失效,NSObject对象不再被持有,于是被释放

  使用__strong修饰的变量可以互相赋值

id __strong obj0 = [[NSObject alloc] init]; //对象A
    id __strong obj1 = [[NSObject alloc] init]; //对象B
    id __strong obj2 = nil
    obj0 = obj1 
    /* obj0被赋值,所以其对对象A的强引用失效,对象A不再被
     * 持有,于是被释放
     * obj1对对象B有一个强引用,现在obj1赋值给obj0,于是
     * obj0也有对象B的强引用,此时对象B的强引用对象为
     * obj1、obj0
     */
    obj2 = obj0
    //同样的此时的obj0对对象B有强引用,当obj0赋值给obj2
    //之后obj2也有对对象B的强引用,此时对象B的强引用变量
    //为obj0、obj1、obj2
    obj1 = nil
    //因为obj1被置为nil,所以obj1对对象B的强引用失效
    //此时对象B的强引用对象为obj0、obj2
    obj0 = nil
    //因为obj0被置为nil,所以obj0对对象B的强引用失效
    //此时对象B的强引用对象为obj2
    obj2 = nil
    //因为obj0被置为nil,所以obj0对对象B的强引用失效
    //此时对象B不再被任何人持有,于是被释放
循环引用

  下面我们用代码来重现循环引用,声明一个Test类

@interface Test:NSObject
    {
        id __strong obj_;
    }
    -(void)setObject:(id __strong)obj;
    @end
    @implemnetaion Test
    -(id)init
    {
        self = [super init];
        return self
    }
    -(void)setObject:(id __strong)obj
    {
        obj_=obj;
    }
    @end

  Test类有一个成员变量obj_,并且实现了obj_setter方法setObject:   下面为循环引用发生的代码

{
        id test0 = [[Test alloc] init]; //Test对象A
        //test0对对象A有强引用
        id test1 = [[Test alloc] init]; //Test对象B
        //test1对对象B有强引用
        [test0 setObject:test1];
        //对象A的成员变量obj_对对象B有强引用
        //此时对象B的强引用变量有test1、对象A的成员变量obj_
        [test1 setObject:test0]
        //对象B的成员变量obj_对对象A有强引用
        //此时对象A的强引用变量有test0、对象B的成员变量obj_
    }
    /* 出了作用域
     * test0对对象A的强引用失效,释放对象A,但是此时对象B的成员
     * 变量obj_还持有对对象A的强引用,导致对象A无法释放
     * test1对对象B的强引用失效,释放对象B,但是此时对象A的成员
     * 变量obj_还持有对对象B的强引用,导致对象B无法释放
     * 于是内存泄漏
     */



ios开发 内存爆炸 导出文件 ios开发内存管理_Test_04


  当只有一个对象时,也可以发生循环引用,例如

id test = [[Test alloc] init];
 [test setObject:test];

此时



ios开发 内存爆炸 导出文件 ios开发内存管理_内存管理_05


__weak修饰符

  __weak修饰符与__strong修饰符恰好相反,其提供弱引用,弱引用不能持有对象实例。

id __weak obj = [[NSObject alloc] init];

  在变量obj前加上__weak修饰符,此时编译器会报错,这是因为__weak修饰符使得obj变量持有NSObject对象的弱引用(并不持有NSObject对象),赋值过后,NSObject对象不再被任何人持有,故而release

{
    id __strong obj0 = [[NSObject alloc] init];
    //obj0 对NSObject对象请引用,并且持有该对象,引用计数+1
    id __weak obj1 = obj0
    //obj1弱引用obj0,对NSObject对象的引用计数没有贡献
}
    //出作用域之后 obj0对NSObject对象的强引用失效
    //此时没有任何变量持有NSObject对象,故而被释放

  所以前文中,我们将Test类中的obj变量用__weak来修饰,就可以规避循环引用现象的出现。

{
        id test0 = [[Test alloc] init]; //Test对象A
        //test0对对象A有强引用
        id test1 = [[Test alloc] init]; //Test对象B
        //test1对对象B有强引用
        [test0 setObject:test1];
        //对象A的成员变量obj_对对象B有弱引用
        //此时对象B的强引用变量仅有test1
        [test1 setObject:test0]
        //对象B的成员变量obj_对对象A有弱引用
        //此时对象A的强引用变量仅有test0
    }
    /* 出了作用域
     * test0对对象A的强引用失效,释放对象A,此时对象B的成员
     * 变量obj_持有对对象A的弱引用,但是对对象A的引用计数并无贡献
     * 因此对象A不再被任何变量持有,遂即被释放
     * test1对对象B的强引用失效,释放对象B,此时对象A的成员
     * 变量obj_持有对对象B的弱引用,但是对对象B的引用计数并无贡献
     * 因此对象B不再被任何变量持有,遂即被释放
     */



ios开发 内存爆炸 导出文件 ios开发内存管理_修饰符_06


  __weak的另一个特点就是,在持有某个对象的弱引用时,若对象被废弃,则此弱引用自动失效切被置为nil

__unsafe_unretained修饰符

  __weak只用用于iOS5以及OSX Lion以上版本,在之前的版本中可以使用__unsafe_unretained来修饰,两者的作用几乎一样只是__unsafe_unretained会出现悬垂指针的现象。

id __unsafe_unretained obj1 = nil;
    {
        id __strong obj0 = [[NSObject alloc] init];
        obj1 = obj0;
        NSLog(@"A:%@",obj1);
    }
    NSLog(@"B:%@",obj1);

打印输出:

A:<NSObject: 0x753e180>
    B:<NSObject: 0x753e180>

按道理说出了作用域之后打印,因为对象已经被释放,应该为nil,此时为什么还能打印出NSObject对象的内存地址呢?这里就是所说的悬垂指针,理论上讲应该输出B:(null),这是因为此时的内存地址还未被操作系统回收,只是恰巧我们还可以访问。

__autoreleasing修饰符

在ARC无效的情况下,我们使用autorelease:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool drain]

而在ARC有效的情况下我们这样使用autorelease:

@autoreleasepool{
        id __autoreleasing obj = [[NSObject alloc] init];
    }

属性修饰符

属性修饰符与所有权修饰符的对应关系

属性声明的属性

所有权修饰符

assign

__unsafe_unretained修饰符

copy

__strong修饰符(新建一块内存,对源数据进行拷贝)

retain

__strong修饰符

strong

__strong修饰符

Unsafe_unretained

__unsafe_unretained修饰符

weak

__weak修饰符

assign修饰符

assign可以修饰基本数据类型,也可修饰对象类型,但是在修饰对象类型的时候,可能会导致野指针的现象发生。而修饰基本数据类型时不会,这是因为值类型的数据是存储在栈空间中的,而栈空间的内存调度由操作系统管理;而对象类型的数据则是放入堆中需要程序员手动或通过ARC进行管理的。

copy修饰符

copy一般用来修饰有可变子类的对象,例如

  • NSString --> NSMutableString
  • NSArray --> NSMutableArray
  • NSDictionary --> NSMutableDictionary

下面比较一下用strong修饰NSString和用copy修饰NSString它们的区别在什么地方:

@property(nonatomic,strong) NSString *strongImmutableString;
@property(nonatomic,copy) NSString *copyImmutableString;

  NSMutableString *mutableStr = [NSMutableString stringWithString:@"BUSYLIFE"];
  self.immutableStrongStr = mutableStr;
  self.immutableCopyStr = mutableStr;
  NSLog(@"StrongStr:%@",_immutableStrongStr);
  NSLog(@"CopyStr:%@",_immutableCopyStr);
  
  [mutableStr appendString:@"1987"];
  NSLog(@"mutableStr:%@",mutableStr);
  NSLog(@"ChangedStrongStr:%@",_immutableStrongStr);
  NSLog(@"ChangedCopyStr:%@",_immutableCopyStr);
  
  NSLog(@"StrongStrAddress:%p",_immutableStrongStr);
  NSLog(@"CopyStrAddress:%p",_immutableCopyStr);
  NSLog(@"mutableStrAddress:%p",mutableStr);
  
  *********************运行结果************************
  StringStr:                BUSYLIFE
  CopyStr:                  BUSYLIFE
  mutableStr:               BUSYLIFE1987
  changedStrongStr:         BUSYLIFE1987
  changedCopyStr:           BUSYLIFE
  StrongStrAddress:         0x608000007b9c0
  CopyStrAddress:           0xa00be653e9cdcf58
  mutableStrAddress:        0x608000007b9c0

  通过上面的代码我们可以知道,通过strong修饰的strongImmutableString,当我们通过一个可变对象(NSMutableString)去进行赋值之后,对于可变对象的更改会直接修改strongImmutableString,并且通过打印的内存地址可以发现,修改前后的内存地址一致。而通过copy修饰的copyImmutableString在被可变对象赋值之后的修改,并未对原来的数据有所影响,通过打印的内存地址可以看出,此时的值是被赋值到了一块新的内存当中。
  因此,使用copy的目的是为了对象有更好的封装性,不受外部影响。无论外部传入可变或不可变对象,本身持有一个不可变的副本。

参考资料

  1. Objective-C高级编程 iOS与OSX多线程和内存管理


ios开发 内存爆炸 导出文件 ios开发内存管理_ios开发 内存爆炸 导出文件_07