Objectve-C内存管理

为什么进行内存管理?

由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当app所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空间,比如回收一些不再使用的对象和变量等。如果内存占用过大,用户会感觉界面卡顿,app运行速度慢等,甚至系统可能会强制退出app已释放资源。这时候app就会发生闪退现象,给用户造成影响。

那么问题来了,内存管理范围有那些呢?

  • 任何继承NSObject的对象,
  • 对其他的基本数据类型无效(int、char、float、double、struct、enum等 )。
  • 本质原因是因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被回收,指向对象的指针也被回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。

知道了需要管理内存的范围,我们就需要知道如何管理那些不需要使用的对象。这时候我们需要引入一个概念。

引用计数器
  • 每个OC对象都有自己的引用计数器
  • 它是一个整数
  • 从字面上, 可以理解为”对象被引用的次数”
  • 也可以理解为: 它表示有多少人正在用这个对象
  • 简单来说, 可以理解为:引用计数器表示有多少人正在使用这个对象 当没有任何人使用这个对象时, 系统才会回收这个对象。 也就是说,当对象的引用计数器为0时,对象占用的内存就会被系统回收;如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )。
  • 任何一个对象, 刚生下来的时候, 引用计数器都为1
  • 当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1
引用计数器的操作
  • 引用计数器的常见操作

    • 给对象发送一条retain消息,可以使引用计数器值+1(retain方法返回对象本身)
    • 给对象发送一条release消息, 可以使引用计数器值-1
    • 给对象发送retainCount消息, 可以获得当前的引用计数器值
  • 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1

dealloc方法基本概念

当对象销毁的时候,有一个常用的方法(dealloc)也需要我们了解。

  • 当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收对象即将被销毁时系统会自动给对象发送一条dealloc消息
    (因此, 从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
    一般会重写dealloc方法,在这里释放相关资源。一旦重写了dealloc方法, 就必须调用[super dealloc],并且放在最后面调用。

  • 使用注意

    • 不能直接调用dealloc方法(该方法是系统自动调用)
    • 一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

例子:

// 类声明文件
#import <Foundation/Foundation.h>

@interface Person : NSObject

@end
// 类实现文件
#import "Person.h"

@implementation Person

- (void)dealloc
{
    
    NSLog(@"%s", __func__);
    [super dealloc];
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    // 默认情况下所有的指针都是强指针
    Person *p = [[Person alloc] init];// 引用计数为1
    // 调用release方法,引用计数会减一,
    [p release]; // 引用计数为0,自动调用delloc方法
    return 0;
}

输出结果:
[849:245988] -[Person dealloc]
MRC和ARC
  • MRC:手动引用计数(Manual Referecen Counting)所有对象的内容都需要我们手动管理, 需要程序员自己编写release/retain等代码
    内存管理的原则就是有加就有减
    也就是说, 一次alloc对应一次release, 一次retain对应一次relese
#import <Foundation/Foundation.h>
@interface Person : NSObject

@end


#import "Person.h"

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
    [super dealloc];
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        // 只要创建一个对象默认引用计数器的值就是1
        Person *p = [[Person alloc] init];
       
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        
        // 只要给对象发送一个retain消息, 对象的引用计数器就会+1
        [p retain];
        
        NSLog(@"retainCount = %lu", [p retainCount]); // 2

        // 通过指针变量p,给p指向的对象发送一条release消息
        // 只要对象接收到release消息, 引用计数器就会-1
        // 只要一个对象的引用计数器为0, 系统就会释放对象
        [p release];
        // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        
        [p release]; // 0
        NSLog(@"--------");
    }
 
    return 0;
}

输出结果:
[928:316970] retainCount = 1
[928:316970] retainCount = 2
[928:316970] retainCount = 1
[928:316970] Person dealloc
[928:316970] --------
  • MRC注意点:
    1.如果一个对象的 retainCount = 0 了之后继续调用release方法会报错。也就是说不能过度release。

MRC手动引用计数管理在2011苹果推出ARC之后开发的过程中基本不再使用了,但是我们还是要了解其中的原理,方便我们对ARC的理解。

  • ARC: Automatic Reference Counting(自动引用计数)
    ARC机制可以说是WWDC2011和iOS5所引入的最大的变革和最激动人心的变化。它是LLVM编译器的特性。可以说ARC解决了让程序员一直以来感觉淡淡的忧伤的手动内存管理的麻烦。
    • ARC的判断原则
      • 只要还有一个强指针变量指向对象,对象就会保持在内存中
    • 强指针
      • 默认所有指针变量都是强指针
      • 被__strong修饰的指针
    • 弱指针
      • 被__weak修饰的指针
 Person *p1 = [[Person alloc] init];
 __strong  Person *p2 = [[Person alloc] init];
__weak  Person *p = [[Person alloc] init];

在工程中使用ARC非常简单:只需要像往常那样编写代码,只不过永远不写retain,release和autorelease三个关键字就好~这是ARC的基本原则。当ARC开启时,编译器在编辑阶段将自动在代码合适的地方插入retain, release和autorelease,而作为开发者,完全不需要担心编译器会做错(除非开发者自己错用ARC了),在默认情况下ARC机制是自动开启的。上边的MRC的代码用ARC写就简单多了。

#import <Foundation/Foundation.h>
@interface Person : NSObject

@end
#import "Person.h"
@implementation Person

@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    Person *p = [[Person alloc] init];
    NSLog(@"--------"); 
    return 0;
}

在main方法中,使用代码Person *p = [[Person alloc] init];创建了一个Person对象,拿一个指针p来指向这个对象的内存地址,此时对象的引用计数 = 1。开启了ARC手动内存管理机制后,在程序编译的时候编译器发现 Person这个对在初始化了之后没有其他的对象对它进行引用,那么编译器会自动在return之前添加一句[p release];的代码,引用计数 - 1 = 0,当main的大括号代码执行完的时候Person这个对象的内存会被释放。这一点和java的垃圾回收机制不太一样,在java中的垃圾回收是jvm中的gc进行处理的(不知道是否准确),而OC是编译器处理的。

  • ARC下的内存管理
  • 局部变量释放对象随之被释放
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
    } // 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    return 0;
}
  • 清空指针对象随之被释放
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
        p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
    }
    return 0;
}
  • 默认清空所有指针都是强指针
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p1和p2都是强指针
        Person *p1 = [[Person alloc] init];
        __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
}
  • 弱指针需要明确说明
    • 注意: 千万不要使用弱指针保存新创建的对象
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p是弱指针, 对象会被立即释放
        __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
}

了解更多请点击:苹果官网文档-关于内存管理

  • Xcode中ARC和MRC的配置
    • 要想手动调用retain、release等方法 , 就必须关闭ARC功能


    • ARC和MRC的混编
      转变为非ARC:-fno-objc-arc
      转变为ARC的:-f-objc-arc

      在Xcode设置方法


    • MRC转换ARC
      如果一个工程时间比较老,那时候应该是使用的MRC,在Xcode提供了功能将MRC的工程转换为ARC。如果工程比较简单还是可以使用的。如果比较复杂一般都会转换失败(原因有可能c和oc语言的桥接,dealloc方法,autorelease等等),这个有点坑。。。不多说了。


@property修饰符内存管理
  • 1.控制set方法的内存管理
    • retain : release旧值,retain新值(用于OC对象)
    • assign : 直接赋值,不做任何内存管理(默认,用于非OC对象类型)
    • copy : release旧值,copy新值(一般用于NSString *)
  • 2.控制需不需生成set方法
    • readwrite :同时生成set方法和get方法(默认)
    • readonly :只会生成get方法
  • 3.多线程管理
    • atomic :性能低(默认)
    • nonatomic :性能高
  • 非ARC

    • copy : 只用于NSString\block
    • retain : 除NSString\block以外的OC对象
    • assign :基本数据类型、枚举、结构体(非OC对象),当2个对象相互引用,一端用retain,一端用assign
  • ARC

    • copy : 只用于NSString\block
    • strong : 除NSString\block以外的OC对象
    • weak : 当2个对象相互引用,一端用strong,一端用weak
    • assgin : 基本数据类型、枚举、结构体(非OC对象)
autorelease
  • autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作

    • 注意,这里只是发送release消息,如果当时的引用计数(reference-counted)依然不为0,则该对象依然不会被释放。

  • autorelease方法会返回对象本身

Person *p = [Person new];
p = [p autorelease];
  • 调用完autorelease方法后,对象的计数器不变
Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 1
  • autorelease的好处
    • 不用再关心对象释放的时间
    • 不用再关心什么时候调用release
  • autorelease的原理
    • autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该 Object放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有Object会被调用Release。
自动释放池
  • 创建自动释放池格式:
    • iOS 5.0前
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池
  • iOS 5.0 开始
@autoreleasepool
{ //开始代表创建自动释放池

} //结束代表销毁自动释放池

  • 在iOS程序运行过程中,会创建无数个池子。这些池子都是以栈结构存在(先进后出)
  • 当一个对象调用autorelease方法时,会将这个对象放到栈顶的释放池
autorelease基本使用
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];

Person *p = [[[Person alloc] init] autorelease];

[autoreleasePool drain];
@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
} // 销毁自动释放池(会给池子中所有对象发送一条release消息)
autorelease使用注意
  • 并不是放到自动释放池代码中,都会自动加入到自动释放池
 @autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
}
  • 在自动释放池的外部发送autorelease 不会被加入到自动释放池中
    • autorelease是一个方法,只有在自动释放池中调用才有效。
 @autoreleasepool {
 }
 // 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
 Person *p = [[[Person alloc] init] autorelease];
 [p run];

 // 正确写法
  @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }

 // 正确写法
 Person *p = [[Person alloc] init];
  @autoreleasepool {
    [p autorelease];
 }
  • 自动释放池的嵌套使用
    • 自动释放池是以栈的形式存在
    • 由于栈只有一个入口, 所以调用autorelease会将对象放到栈顶的自动释放池
    • 栈顶就是离调用autorelease方法最近的自动释放池
@autoreleasepool { // 栈底自动释放池
        @autoreleasepool {
            @autoreleasepool { // 栈顶自动释放池
                Person *p = [[[Person alloc] init] autorelease];
            }
            Person *p = [[[Person alloc] init] autorelease];
        }
    }
  • 自动释放池中不适宜放占用内存比较大的对象
    • 尽量避免对大内存使用该方法,对于这种延迟释放机制,还是尽量少用
    • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
// 内存暴涨
    @autoreleasepool {
        for (int i = 0; i < 99999; ++i) {
            Person *p = [[[Person alloc] init] autorelease];
        }
    }
// 内存不会暴涨
 for (int i = 0; i < 99999; ++i) {
        @autoreleasepool {
            Person *p = [[[Person alloc] init] autorelease];
        }
    }

autorelease错误用法

  • 不要连续调用autorelease
 @autoreleasepool {
 // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
  • 调用autorelease后又调用release
 @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
 }
集合对象内存管理
  • 1.官方内存管理原则

    • 当调用alloc、new、copy(mutableCopy)方法产生一个新对象的时候,就必须在最后调用一次release或者autorelease
    • 当调用retain方法让对象的计数器+1,就必须在最后调用一次release或者autorelease
  • 2.集合的内存管理细节

    • 当把一个对象添加到集合中时,这个对象会做了一次retain操作,计数器会+1
    • 当一个集合被销毁时,会对集合里面的所有对象做一次release操作,计数器会-1
    • 当一个对象从集合中移除时,这个对象会一次release操作,计数器会-1
  • 3.普遍规律

    • 如果方法名是add\insert开头,那么被添加的对象,计数器会+1
    • 如果方法名是remove\delete开头,那么被移除的对象,计数器-1
  • 例子

#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        
        Person *p = [Person new]; // 1
        NSLog(@"reatinCount = %lu", [p retainCount]);
        NSMutableArray *arrM = [[NSMutableArray alloc] init];
        [arrM addObject:p]; // 2
        NSLog(@"reatinCount = %lu", [p retainCount]);
        
        [p release]; // 1
        // 当数组移除一个对象之后, 会给这个对象发送一条release消息
        [arrM removeObject:p];
    }
    return 0;
}
copy内存管理
  • 浅拷贝
    • 原对象引用计数器+1
    • 必须对原对象进行释放
    char *cstr = "this is a c string";
    NSString *str1 = [[NSString alloc] initWithUTF8String:cstr];
    NSLog(@"str = %lu", [str1 retainCount]);
    NSString *str2 = [str1 copy];
    NSLog(@"str = %lu", [str1 retainCount]);
    [str2 release];// 必须做一次release
  • 深拷贝
    • 必须释放新对象
    char *cstr = "this is a c string";
    NSString *str1 = [[NSString alloc] initWithUTF8String:cstr];
    NSLog(@"str = %lu", [str1 retainCount]);
    NSMutableString *str2 = [str1 mutableCopy];
    NSLog(@"str = %lu", [str1 retainCount]);
    [str2 release]; // 必须做一次release
block内存管理

block就是一段可以灵活使用的代码,你可以把它当变量传递,赋值,甚至可以把它声明到函数体里,更灵活的是你可以在里面引用外部的环境。 最后一条使得block要有更多的考虑,既然block可以引用外部环境,那如何保证block被调用的时候当时的环境变量不被释放呢?(block调用的时机可能是随意的)在说明block的内存管理之前需要先理解几个概念。

  • 函数指针
int sum(int value1, int value2)
{
    return value1 + value2;
}

int minus(int value1, int value2)
{
    return value1 - value2;
}

int main(int argc, const char * argv[]) {
    int (*sumP) (int, int) = sum;
    int res = sumP(10, 20);
    NSLog(@"res = %i", res);

    int (*minusP) (int , int) = minus;
    res = minusP(10, 20);
    NSLog(@"res = %i", res);
    return 0;
}

该代码中定义了指向函数的指针sumP和minusP,然后利用这两个指针调用对应的函数,返回函数计算后的结果。

  • 函数指针别名
typedef int (*calculate) (int, int);
int main(int argc, const char * argv[]) {
    calculate sumP = sum;
    int res = sumP(10, 20);
    NSLog(@"res = %i", res);
    calculate minusP = minus;
    res = minusP(10, 20);
    NSLog(@"res = %i", res);
    return 0;
}

typedef关键字:给函数或者变量起别名,就像给我们某个人起个外号一样,叫这个外号就是叫那个人。typedef的作用个人理解主要是为了简化代码,方法代码编写。
typedef int (*calculate) (int, int);给函数起别名,定义calculate是一个函数指针,有两个int类型的参数,并且有一个int类型的返回值。
calculate sumP = sum; 定义calculate类型的函数指针,该指针指向sum函数。
int res = sumP(10, 20);利用函数指针调用该函数,根据传递的参数值函数返回对应计算结果。

  • block和typedef
    知道了函数别名的概念后同样的代码使用block代码如下。
int main(int argc, const char * argv[]) {
    int (^sumBlock) (int, int) = ^(int value1, int value2){
        return value1 + value2;
    };
    int res = sumBlock(10 , 20);
    NSLog(@"res = %i", res);

    int (^minusBlock) (int, int) = ^(int value1, int value2){
        return value1 - value2;
    };
    res = minusBlock(10 , 20);
    NSLog(@"res = %i", res);
    return 0;
}
  • block别名
int main(int argc, const char * argv[]) {
    typedef int (^calculteBlock)(int , int);
    calculateBlock sumBlock = ^(int value1, int value2){
        return value1 + value2;
    };
    int res = sumBlock(10, 20);
    NSLog(@"res = %i", res);
    calculateBlock minusBlock = ^(int value1, int value2){
        return value1 - value2;
    };
    res = minusBlock(10, 20);
    NSLog(@"res = %i", res);

    return 0;
}

利用typedef给block起别名, 和指向函数的指针一样, block变量的名称就是别名。

  • Block注意事项
    • 在block内部可以访问block外部的变量
{
      int  a = 10;
      void (^myBlock)() = ^{
            NSLog(@"a = %i", a);
      }
      myBlock();
}
输出结果: 10
  • block内部使用static修饰符的全局变量
{
        static int base = 100;
        void (^sum)(int, int) = ^ void (int a, int b) {
            base++;
            NSLog(@"block内部base = %d",base + a + b);
        };
        
        base = 0;
        sum(1,2);
        NSLog(@"base = %d",base);
        输出结果:
        [1410:348974] block内部base = 4
        [1410:348974] base = 1
 }

static修饰的全局变量在内存中只有一份,在Block中使用全局静态变量的时候会去当前该变量的最新的值。

  • block内部也可以定义和block外部的同名的变量(局部变量),此时局部变量会暂时屏蔽外部(就近原则)
int  a = 10;
void (^myBlock)() = ^{
    int a = 50;
    NSLog(@"a = %i", a);
    }
myBlock();
输出结果: 50
  • 默认情况下, Block内部不能修改外面的局部变量
int b = 5;
void (^myBlock)() = ^{
    b = 20; // 报错
    NSLog(@"b = %i", b);
    };
myBlock();

  • Block内部可以修改使用__block修饰的局部变量
 __block int b = 5;
void (^myBlock)() = ^{
    b = 20;
    NSLog(@"b = %i", b);
    };
myBlock();
输出结果: 20

局部变量加上__block修饰符之后Block内部为什么可以改变局部变量的值呢?
默认情况下, 不可以在block中修改外界变量的值,因为block中的变量和外界的变量并不是同一个变量。
如果block中访问到了外界的变量, block会将外界的变量拷贝一份到堆内存中。因为block中使用的外界变量是copy的, 所以在调用之前修改外界变量的值, 不会影响到block中copy的值。

代码如下

    int a = 10;
    NSLog(@"外部a的地址:&a = %p", &a);
    void (^myBlock)() = ^{
//        a = 50;
        NSLog(@"Block内部a的地址:&a = %p", &a);
        NSLog(@"Block内部a的值:a = %i", a);
    };
    a = 20;
    NSLog(@"Block内部a的值:a = %i", a);

    myBlock();

    运行结果:
    [854:137804] 外部a的地址:&a = 0x7fff5fbff7fc
    [854:137804] Block内部a的地址:&a = 0x7fff5fbff7e8
    [854:137804] Block内部a的值:a = 10
    [854:137804] 外部a的值:a = 20


如果将上边代码做如下修改

int a = 10; => __block int a = 10;
运行结果如下:
[1029:184764] 外部a的地址:&a = 0x7fff5fbff7f8
[1029:184764] Block内部a的地址:&a = 0x7fff5fbff7f8
[1029:184764] Block内部a的值:a = 10
[1029:184764] 外部a的值:a = 20

运行结果我们可以发现,Block内部a的地址和外部a的地址是一样的,
因此我们可以说加上__block之后是地址传递,而不加__block使用外部的变量是做了某些操作分配了新的内存地址。而这个操作就是copy。

  • Block位于堆还是栈(ARC)
    根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。
    NSGlobalBlock:类似函数,位于text段;
    NSStackBlock:位于栈内存,函数返回后Block将无效;
    NSMallocBlock:位于堆内存。

直接看代码

typedef void (^blk) (void);
{
    __block int val = 10;
    __strong blk strongPointerBlock = ^{NSLog(@"val = %d", ++val);};
    NSLog(@"strongPointerBlock: %@", strongPointerBlock); //1
    __weak blk weakPointerBlock = ^{NSLog(@"val = %d", ++val);};
    NSLog(@"weakPointerBlock: %@", weakPointerBlock); //2
    NSLog(@"copyBlock: %@", [weakPointerBlock copy]); //3    
    NSLog(@"默认test %@", ^{NSLog(@"val = %d", ++val);}); //4
}
运行结果:
[1437:366203] strongPointerBlock: <__NSMallocBlock__: 0x7fbe7a6762e0>
[1437:366203] weakPointerBlock: <__NSStackBlock__: 0x7fff5746d9e0>
[1437:366203] copyBlock: <__NSMallocBlock__: 0x7fbe7a743240>
[1437:366203] 默认test <__NSStackBlock__: 0x7fff5746d9b8>

从运行结果可以看出
1>默认情况下,Block是在栈内存中。
2>strong指针指向的block已经放到堆上了。
3>weak指针指向的block还在栈上(这种声明方法只在block上有效,正常的weak指针指向堆上对象,直接就会变nil,需要用strong指针过一道。)
4>copy了Block之后就会从栈转移到堆上

在ARC下的另外一种情况,将block作为参数返回

- (__unsafe_unretained blk) blockTest {
    int val = 11;
    return ^{NSLog(@"val = %d", val);};
}
调用方
NSLog(@"block return from function: %@", [self blockTest]);

输出结果:
block return from function: <__NSMallocBlock__: 0x7685640>

5>在ARC环境下,当block作为参数返回的时候,block也会自动被移到堆上。

所有我们可以做出如下总结:
如果block在栈中, block中访问了外界的对象, 那么不会对对象进行retain操作。
如果block在堆中, block中访问了外界的对象, 那么会对外界的对象进行一次retain。(在ARC下没有retain的概念,这里只是为了后面循环引用的理解)。

了解更多:Objective-C Block的实现
[Block内存管理](http://www.tanhao.me/pieces/310.html/

  • block的循环引用防止
@property(nonatomic, readwrite, copy) completionBlock completionBlock;

self.completionBlock = ^ {
        if (self.success) {
            self.success(self.responseData);
        }
    }
};

//对象有一个Block属性,然而这个Block属性中又引用了对象的其他
//成员变量,那么就会对这个变量本身产生强应用,那么变量本身和他
//自己的Block属性就形成了循环引用。在ARC下需要修改成这样:
@property(nonatomic, readwrite, copy) completionBlock completionBlock;

__weak typeof(self) weakSelf = self;
self.completionBlock = ^ {
    if (weakSelf.success) {
        weakSelf.success(weakSelf.responseData);
    }
};

也就是生成一个对自身对象的弱引用,如果是倒霉催的项目还需要支持iOS4.3(好low啊),就用__unsafe_unretained替代__weak。如果是non-ARC环境下就将__weak替换为__block即可。non-ARC情况下,__block变量的含义是在Block中引入一个新的结构体成员变量指向这个__block变量,那么__block typeof(self) weakSelf = self;就表示Block别再对self对象retain啦,这就打破了循环引用。

循环引用问题
  • MRC下对象的循环引用
    如下代码
    Dog类
#import <Foundation/Foundation.h>
@class Person;
@interface Dog : NSObject
//@property(nonatomic, retain)Person *owner;
@property(nonatomic, assign)Person *owner;
@end

#import "Dog.h"
#import "Person.h"
@implementation Dog
-(void)dealloc
{
    NSLog(@"%s", __func__);
    [super dealloc];
}
@end

Person类

#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end

#import "Person.h"
#import "Dog.h"
@implementation Person

- (void)dealloc
{
    NSLog(@"%s", __func__);
    self.dog = nil;
    [super dealloc];
}
@end

main函数

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Dog.h"

int main(int argc, const char * argv[]) {

    Person *p = [Person new];
    Dog *d = [Dog new];
    p.dog = d; // retain
    d.owner = p; // retain  assign
    
    [p release];
    [d release];
    return 0;
}

Person要拥有dog对象, 而dog对应又要拥有Person对象, 此时会形成循环retain,在main执行完的时候,dealloc方法不会自动被调用,造成内存泄露。

引用关系如图


解决办法: 不要让A retain B, B retain A,让其中一方不要做retain操作即可。
替换前:@property(nonatomic, retain)Person *owner;
替换后:@property(nonatomic, assign)Person *owner;

  • 此处替换不用weak的原因,weak是ARC提供的防止循环引用的关键字。此处代码是MRC的,所有使用assign关键字。

修正后引用关系


  • ARC下对象的循环引用
    • ARC和MRC一样, 如果A拥有B, B也拥有A, 那么必须一方使用弱指针
@interface Person : NSObject

//@property (nonatomic, retain) Dog *dog;
@property (nonatomic, strong) Dog *dog;

@end

@interface Dog : NSObject

// 错误写法, 循环引用会导致内存泄露
//@property (nonatomic, strong) Person *owner;

// 正确写法, 当如果保存对象建议使用weak
//@property (nonatomic, assign) Person *owner;
@property (nonatomic, weak) Person *owner;
@end
其他
  • Objective-C指针与CoreFoundation指针之间的转换(Toll-Free Bridging)
1.MRC下的内存管理

倘若不使用ARC,手动管理内存,思路比较清晰,使用完,release转换后的对象即可。

//NSString 转 CFStringRef  
CFStringRef aCFString = (CFStringRef) [[NSString alloc] initWithFormat:@"%@", string];  
//...  
CFRelease(aCFString);  
  
  
//CFStringRef 转 NSString  
CFStringRef aCFString = CFStringCreateWithCString(kCFAllocatorDefault,  
                                                  bytes,  
                                                  NSUTF8StringEncoding);  
NSString *aNSString = (NSString *)aCFString;  
//...  
[aNSString release];  
2.ARC下的内存管理

ARC :只会对OC对象进行内存管理,苹果有句名言:ARC is only for NSObject。但是对c对象或是CF开头的对象,即存在于 Core Foundation框架 (CoreFoundation.framework 是一组C语言接口)中的对象无效,需要手动的retain 和release(CF中没有autorelease)。

CocoaFoundation指针与CoreFoundation指针转换,需要考虑的是所指向对象所有权的归属。ARC提供了3个修饰符来管理。

  • __bridge,__bridge_retained和__bridge_transfer三个转换关键字。
    • __bridge只做类型转换,但是不修改对象(内存)管理权;
    • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用CFRelease或者相关方法来释放对象;
    • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。

使用__bridge_retained 或者 CFBridgingRetain()

- (void)viewDidLoad  
{  
    [super viewDidLoad];  
      
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef aCFString = (__bridge_retained CFStringRef) aNSString;  
      
    (void)aCFString;  
      
    //正确的做法应该执行CFRelease  
    //CFRelease(aCFString);   
} 

使用__bridge_transfer 或者 CFBridgingRelease()

- (void)viewDidLoad  
{  
    [super viewDidLoad];  
      
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef aCFString = (__bridge_retained CFStringRef) aNSString;  
    aNSString = (__bridge_transfer NSString *)aCFString;  
} 

使用__bridge

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];
    CFStringRef aCFString = (__bridge CFStringRef)aNSString;
    
    (void)aCFString;
}
  • 3中转换方式总结
转换方式 转换结果
__bridge 不改变对象所有权
__bridge_retained 或者 CFBridgingRetain() 解除 ARC 所有权
__bridge_transfer 或者 CFBridgingRelease() 给予 ARC 所有权

了解更多:Toll-Free Bridging

iOS中内存分析
  • 静态内存分析
    静态内存分析是不运行程序,直接对代码进行内存分析。查看代码是否偶内存泄露。

    Analyze主要分析以下四种问题

    • 逻辑错误:访问空指针或未初始化的变量等;
    • 内存管理错误:如内存泄漏等;
    • 声明错误:从未使用过的变量;
    • Api调用错误:未包含使用的库和框架。

    优点:分析速度快,并且可以对所有的代码进行内存分析。
    缺点:分析不一定准确(没有真正的运行程序,根据代码的上下文语法结构)
    注意:如果有提示内存泄露,一定要检查代码,90%以上的都是准确的。

1>在XCode Product菜单下,点击Analyze对工程进行静态分析(shift+command+b)。

2>在开启arc的环境下,输入以下一段代码:

+(UIImage*)getSubImage:(unsigned long)ulUserHeader
{
    UIImage * sourceImage = [UIImage imageNamed:@"header.png"];
    CGFloat height = sourceImage.size.height;
    CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height);
 
    CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect);
    UIImage* smallImage = [UIImage imageWithCGImage:imageRef];
    //CGImageRelease(imageRef);
 
    return smallImage;
}

3>使用Analyze进行分析,在导航栏Analyze选择Analyzer查看分析结果:


  • 动态内存分析
    动态内存分析:程序真正运行起来的时候对程序进行内存分析、
    (查看内存的分配/内存泄露)。
    优点:分析非常准确,如果发现内存泄露,基本可以判断是代码问题。
    缺点:分析效率低(只有真正运行了一段代码,才能对该代码进行内存分析,如果程序中的某一段有内存泄露的没有执行到就不会被检测出来。)
    注意:如果发现内存泄露,基本都需要修改代码。

推荐阅读更多精彩内容