Skip to content

内存管理

FFur edited this page Sep 3, 2018 · 10 revisions

内存管理

内存区域

可编程内存在基本上分为这样的几大部分:静态存储区、堆区和栈区。他们的功能不同,对他们使用方式也就不同。

  1. 静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。

  2. 栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  3. 堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。

  4. 代码区:存放函数体的二进制代码

  5. 文字常量区:—常量字符串就是放在这里的。程序结束后由系统释放

栈区

  • 内存管理由系统控制
  • 存储的为非静态的局部变量,存放函数的参数值
  • 栈分配的内存,一旦出了作用域就会被销毁
  • 例如:函数参数,在函数中生命的对象的指针等。
  • 当系统的栈区大小不够分配时, 系统会提示栈溢出。

堆区

  • 内存管理由程序控制,
  • 存储的为malloc , new ,alloc出来的对象。
  • 如果程序没有控制释放,那么在程序结束时,由系统释放。但在程序运行过程中,会出现内存泄露、内存溢出问题。
  • 分配方式 类似于链表。

全局存储区(静态存储区)

  • 全局变量、静态变量会存储在此区域。
  • 事实上全局变量也是静态的,因此,也叫全局静态存储区。
  • 存储方式: 初始化的全局变量跟静态变量放在一片区域,未初始化的全局变量与静态变量放在相邻的另一片区域。
  • 程序结束后由系统释放。

文字常量区

在程序中使用的常量存储在此区域。程序结束后,由系统释放。在程序中使用的常量,都会到文字常量区获取。

程序代码区

存放函数体的二进制代码。 运行程序就是执行代码,代码要执行就要加载进内存。

内存详解

一句普通的代码:

NSObject *obj = [[NSObject alloc] init];

这行代码中写有两个NSObject ,但他们表示的意思是不一样的。

  • 等号左边表示:创建了一个 NSObject 类型的指针 obj 。(开辟一个 NSObject 类型大小的内存空间,并用指针变量 obj 指向它)
  • 等号右边表示:调用 NSObject 对象的类方法 alloc 进行内存空间的分配,调用实例方法init 进行构造工作,如成员变量的初始化等。
  • 等号右边的 NSObject 对象初始化完成之后将内存地址赋值给左边的 obj 。

为什么需要管理内存

程序在运行的过程中,以下情况程序的内存占用增加:

  • 创建一个OC对象

  • 定义一个变量

  • 调用一个函数或者方法

  • 而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的

  • 当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等

  • 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

那些对象才需要我们进行内存管理呢?

  • 任何继承了NSObject的对象需要进行内存管理

  • 而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理 这是因为

  • 继承了NSObject的对象的存储在操作系统的堆里边。

  • 非OC对象一般放在操作系统的栈里面

示例:

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int a = 10; //
        int b = 20; //
        // p : 栈
        // Person对象(计数器==1) : 堆
        Person *p = [[Person alloc] init];
    }
    // 经过上面代码后, 栈里面的变量a、b、p 都会被回收
    // 但是堆里面的Person对象还会留在内存中,因为它是计数器依然是1
    return 0;
}

内存的五大区域

在C/C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

  • 栈:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)

  • 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表,使用new方法生成的内存块

  • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。

  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

new方法和alloc&init

new方法的步骤:

  • 首先向内存申请一个空间,内部存放定义的属性

  • 将申请的空间内部的属性初始化为

  • 将初始化完以后的空间首地址返回出去.

    首先呢,创建的对象是在内存的堆区,可以看出有两个变量,然后第二步是给属性进行初始化,_age初始化为0,_name]初始化为空.然后就是第三步了,返回一个指针,然后返回出去的指针由p来接收.p也叫做对象名.再呢,就是如果你要调用方法,就是由调用者传入一个消息,传到p中,然后在内存中由isa指针取接收传来的消息,然后再由isa指针去内存代码区寻找相应的方法.比如上面那幅图找到得一个run的方法.所以,以上图就是创建对象到如何取内存中调用方法的图的分析.

Person * p = [Person new];

OC中new方法与alloc+init及构造方法 - CSDN博客

alloc & init

方法方便自定义构造函数,传入参数

int main( int argc, const char * argv[]) {
    @autoreleasepool {
        //这是创建一个对象,并且给内存中的属性赋,上自己想要赋的值
        Person *pe = [ [Person alloc] initWithName : @"小明" andAge:18] ;
        NSLog (@"名字叫%@的小孩,年龄是%d", pe. name , pe . age) ; //打印出结果
    }
}

引用计数(Reference Counting)

引用计数的工作原理:

  • 当我们创建(alloc)一个新对象A的时候,它的引用计数从零变为 1;

  • 当有一个指针指向这个对象A,也就是某对象想通过引用保留(retain)该对象A时,引用计数加

  • 当某个指针/对象不再指向这个对象A,也就是释放(release)该引用后,我们将其引用计数减

  • 当对象A的引用计数变为 0 时,说明这个对象不再被任何指针指向(引用)了,这个时候我们就可以将对象A销毁,所占内存将被回收,且所有指向该对象的引用也都变得无效了。系统也会将其占用的内存标记为“可重用”(reuse);

为什么使用来存储对象

那么为什么 Objective-C 会选择使用堆来存储对象而不是栈,来看看栈对象的优缺点。

优点:

创建速度和运行时速度快:相对于堆对象创建时间快几十倍;编译期能确定大部分内存布局,因而在运行时分配空间几乎不耗时 生命周期固定:对象出栈就会被释放,不会存在内存泄漏

缺点:

生命周期固定,可能会出现这种情况:一个栈对象被创建之后被传递到别的方法,当栈对象的创建方法返回时,栈对象会被一起 pop 出栈而释放,导致没法在别处被继续持有,此时 retain 会失效,因此,栈对象会给对象的内存管理造成相当大的麻烦。

空间:栈跟线程具有绑定关系,而栈的可用空间非常有限的。因此对象如果都在栈上创建不太现实,而堆只要物理内存不警告即可使用。

  • 512 KB (secondary threads)
  • 1 MB (iOS main thread)
  • 8 MB (OS X main thread)

综上,Objective-C 选择使用堆存储对象。

MRC

Manual Reference Counting

1. 引用计数器

系统是根据对象的引用计数器来判断什么时候需要回收一个对象所占用的内存

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

2. 引用计数器操作

  • 为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器值+1 ( retain 方法返回对象本身)
  • 当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
  • 给对象发送retainCount消息,可以获得当前的引用计数器值
  • 当对象的引用计数为0时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送dealloc消息发起这个过程。
  • 需要注意的是:release并不代表销毁\回收对象,仅仅是计数器-1

3. dealloc方法

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

ARC

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects. Rather than having to think about retain and release operations, ARC allows you to concentrate on the interesting code, the object graphs, and the relationships between objects in your application.

Transitioning to ARC Release Notes

  • ARC全称是 Automatic Reference Counting,是Objective-C的内存管理机制。简单地来说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

  • ARC的使用是为了解决对象retain和release匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

  • 以前需要手动的通过retain去为对象获取内存,并用release释放内存。所以以前的操作称为MRC (Manual Reference Counting)。

  • 自己生成的对象,自己所持有。

  • 非自己生成的对象,自己也可以持有。

  • 不再需要自己持有的对象时释放。

  • 非自己持有的对象无法释放。

对象操作 Objective-c方法
生成并持有对象 alloc/new/copy/mutableCopy方法
持有对象 retain方法
释放对象 release方法
废弃对象 dealloc方法

ARC & MRC

ARC管理原则:

  • 只要一个对象没有被强指针修饰就会被销毁,
  • 默认局部变量对象都是强指针,存放到堆里面

MRC了解开发常识:

  • MRC没有strong, weak,局部变量对象就是相当于基本数据类型
  • MRC给成员属性赋值,一定要使用set方法,不能直接访问下划线成员属性赋值

Autorelease

• Autorelease对象是在当前的runloop迭代结束时释放的 • ARC会在编译时为我们在合适的位置插入retain、release,释放不必要的内存。 autorelease作用:

autorelease不立即释放,而是注册到autoreleasepool(自动释放池)中,等到pool结束时释放池再自动调用release进行释放工作。

autorelease看上去很像ARC,但是实际上更类似C语言中的自动变量(局部变量),当某自动变量超出其作用域(例如大括号),该自动变量将被自动废弃,而autorelease中对象实例的release方法会被调用;[与C不同的是,开发者可以设定变量的作用域。]

AutoReleasePool

所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。 但是如果每次都放进应用程序的 main.m 中的 autoreleasepool 中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?

在一次完整的运行循环结束之前,会被销毁。

那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。

自定义的 NSOperation 和 NSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。

但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。

@autoreleasepool 当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送 release 消息,释放自动释放池中的所有对象。

如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

一、背景

要想深入了解autorelease pool的原理,推荐以下两片文章即可:

Using Autorelease Pool Blocks

Objective-C Autorelease Pool 的实现原理

要想掌握上文中的要点,还是要废不少劲的。对于这种原理比较抽象,和实际开发编码没有直接关系的原理性的东西,常常是看一遍过一阵子很快就忘得了,为了加深印象,还是有必要系统性地梳理一遍,简单化地总结一下,加深一下印象。以下笔记也是基于以上两处文献进行总结的。

二、Autorelease Pool使用场景

1、降低内存使用峰值:

这一点不用多说,当你使用类似for循环这样的逻辑需要产生大量的中间变量时,Autorelease Pool无意是最佳的一种解决方案;

2、如果是对NSArray操作,如果可以的话推荐使用OC提供的以下api:

  • (void)enumerateObjectsUsingBlock:

  • (void)enumerateObjectsWithOptions:(NSEnumerationOptions)opts usingBlock:

  • (void)enumerateObjectsAtIndexes:(NSIndexSet *)s options:(NSEnumerationOptions)opts usingBlock:

如果你debug一下源码就该知道为什么推荐使用它们了(内部封装了autoreleasepool),我们debug看一下:

然后在采取以下命令跟踪string_weak_的值变化,如下:

再点开enumerateObjectsUsingBlock的执行堆栈信息,看一下:

如果你再debug一下普通的for循环就不会有这些push和pop,既然enumerateObjectsUsingBlock内部有了autoreleasepool,为什么推荐使用它的原因就不多说了。

3、按照苹果给的文档说的,如果采取一些非cocoa创建的一些线程,将不会自动生成autoreleasepool给你,你需要手动去创建它。

三、Autorelease Pool的实现原理

1、Autoreleasepool的结构

每个Cocoa的线程都会默认标配一个Autorelease Pool,但是你也可以手动创建多个。从前面的操作中,也应该能隐约猜出来了些许,有push和pop操作,意味着每个pool的管理其实是一种类似栈结构的进栈出栈操作,当然pool的管理更复杂些,因为它可以创建多个,还可以嵌套创建删除。这种情况,普通的栈结构是无法满足这种需求的。如下的代码结构:

Pool的创建顺序:Pool 1 ---> Pool 2 ---> Pool 3,drain顺序是Pool 2 ---> Pool 1 --->Pool 3,如果要想实现这种顺序,采取FIFO做不到,普通的栈也不行。如果用链表操作可以做到,因为涉及到链表的首(Pool 2)或尾(Pool 3)插入,应该用双向链表来管理才合适。如下:

有人可能会有疑问,顺序为什么不是1、2、3,我觉得这些问题都不大,上面的顺序Push链表的复杂度为O(n),Pop的复杂度为O(1),反过来的话,

就是Push链表的复杂度为O(1),Pop的复杂度为O(n),如果纠结这个的可以去撸源码。

2、AutoreleasePoolPage的结构

上面介绍了,每个线程的Pool结构层次,其实是有多个PoolPage构成。

ARC下会对其中的对象会隐式执行autorelease操作,autorelease操作将一个指向对象实例的对象指针添加到PoolPage中。

添加的过程如下:

当当前PoolPage作用域一过,就会对从线程pool中执行pop操作,而pop操作,pop的过程,会遍历page堆栈,对指向的对象一一执行release操作,如果对象的retainCount变为0,即立即释放,如果对象的retaiCount大于0,不释放。当所有对象处理完(出栈完毕),最后完成pop操作。

四、为什么有了ARC还要Autorelease Pool?

这个问题之前我也想过,搜了下,没有感觉回答满意的,也没找到苹果的官方回答,这里只能自给妄自推断一下。提到OC的RC,首先要横向对比一下Android的GC,GC的内存回收是集中式回收(定期回收),而RC的回收是伴随整个运行时的,所以android机器有种时“卡”时“流畅”的感觉,而iOS总体比较均匀,缺乏像GC的集中式回收内存的类似机制,所以猜测Pool的产生也是弥补RC的这一不足,在RC基础上进行内存优化的一种手段。

利用@autoreleasepool优化循环

利用@autoreleasepool优化循环的内存占用,我觉得最有用的一点,下面就说说这个点。 如下面的循环,次数非常多,而且循环体里面的对象都是临时创建使用的,就可以用@autoreleasepool包起来,让每次循环结束时,可以及时的释放临时对象的内存。

//来自Apple文档,见参考

NSArray *urls = <# An array of file URLs #>;

for (NSURL *url in urls) {

    @autoreleasepool {
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:url
        encoding:NSUTF8StringEncoding error:&error];
        /* Process the string, creating and autoreleasing more objects.                 */
    }
}

这么做的效果是极其显著地,就如本文最开始的图一样,可以自己把示例工程下回来运行下试试~

释放时间:每个Runloop中都创建一个Autorelease pool(自动释放池),每一次的Autorelease,系统都会把该Object放入了当前的Autorelease pool中,并在Runloop周期的末尾进行释放,而当该pool被释放时,该pool中的所有Object会被调用Release。 所以,一般情况下,每个接受autorelease消息的对象,都会在下个Runloop周期开始前被释放。

在编译阶段,编译器将在项目代码中自动为分配对象插入retain、release和autorelease,且插入的代码不可见。

例子:

for(int i = 0; i <lagerNum; i++) {
   NSNumber *num = [NSNumber numberWithInt:i];      
   NSString *str = [NSString stringWithFormat:@"%d ", i];
   [NSString stringWithFormat:@"%@%@", num, str];    
}    

for循环里面,这个runloop是要整个for循环走完,里面放在堆内存的零时数据,才会被释放掉,如果这个for循环的循环次数非常的大,那么CPU就会爆炸性增长,如上这个例子,如果lagerNum = 80W,那么CPU内存将会超过400M,出现内存警告或app被kill掉,这个时候,就是我们在ARC模式中使用@autoreleasepool的最佳时机。

运营场景

  1. 写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
  2. 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
  3. 创建了新的线程。(非Cocoa程序创建线程时才需要)
  4. 长时间在后台运行的任务。

静态变量

当我们希望一个变量的作用域不仅仅是作用域某个类的某个对象,而是作用域整个类的时候,这时候就可以使用静态变量。

static

static关键字用来修饰变量的作用域. static修饰的变量只会分配一份内存.有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用的时候该变量已经有值。这时就应该指定该局部变量为静态变量,用关键字 static 进行声明。

static修饰局部变量

保证局部变量永远只初始化一次,在程序的运行过程中永远只有一份内存,生命周期类似全局变量了,但是作用域不变。这句话怎么理解呢?还是以代码例子来讲解吧。但是我们再看看局部变量i被关键字static修饰后的情况:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //声明一个局部变量i
  static  int i = 0;
    //每次点击view来到这个方法时让i自增
    i ++;
    //打印结果
    NSLog(@"i=%d",i);
}
2016-10-26 15:07:34.276 fff[2817:175155] i=1
2016-10-26 15:07:35.347 fff[2817:175155] i=2
2016-10-26 15:07:35.761 fff[2817:175155] i=3
2016-10-26 15:07:36.057 fff[2817:175155] i=4
2016-10-26 15:07:36.415 fff[2817:175155] i=5

打印日志中可以看到i的值一直在自增。什么,它不是每次进去都被初始化赋值为0了么,怎么能累加呢。这就是关键字static修饰的局部变量的作用,让局部变量永远只初始化一次,一份内存,生命周期已经跟全局变量类似了,只是作用域不变。

  1. 延长局部变量的生命周期, 程序结束才会销毁。
  2. 局部变量只会生成一份内存, 只会初始化一次。
  3. 改变局部变量的作用域。

static修饰全局变量

使全局变量的作用域仅限于当前文件内部,即当前文件内部才能访问该全局变量

iOS中在一个文件声明的全局变量,工程的其他文件也是能访问的,但是我又不想让其他文件访问,这时就可以用static修饰它了,比较典型的是使用GCD一次性函数创建的单例,全局变量基本上都会用static修饰。

下面是一个GCD一次函数创建的单例

@implementation LoginTool

//static修饰全局变量,让外界文件无法访问
static LoginTool *_sharedManager = nil;

+ (LoginTool *)sharedManager {
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _sharedManager = [[self alloc] init];
    });
    return _sharedManager;
}

static修饰函数

static修饰函数时,被修饰的函数被称为静态函数,使得外部文件无法访问这个函数,仅本文件可以访问。这个在oc语言开发中几乎很少用,c语言倒是能看到一些影子,所以不详细探讨。

  1. static不能用于修饰成员变量,它只能修饰局部变量、全局变量和函数
  2. static修饰局部变量表示将该局部变量存储到静态存储区
  3. static修饰全局变量用于限制该全局变量只能在当前源文件中访问
  4. static修饰函数用于限制该函数只能在当前源文件中调用
  5. static限制了变量的作用域为当前文件, 其他文件可以定义一个相同的static变量, 如果没有static修饰, 在其他文件中定义了相同名称的全局变量, 会报错.

静态常量

const

const简介: 之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。 修饰的变量是不可变的,如果需要定义一个时间间隔的静态常量,就可以使用const修饰。

  • const常被用来修饰字符串常量, 其作用和宏类似.
  • 宏定义是预编译指令, 在编译之前处理, 宏不做检查不会报编译错误, 只是简单的替换.而const会编译阶段, 会做编译检查报编译错误.
  • 宏可以用来定义一些函数和方法, 而const只能用来定义变量常量.
  • 在项目中大量使用宏定义, 会使项目的编译时长大大增加.

编译时刻:宏是预编译(编译之前处理),const是编译阶段。 编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。 宏的好处:宏能定义一些函数,方法。 const不能。 宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。

static const NSTimeInterval LMJTimeDuration = 0.5;

如果试图修改TimeDuration编译器则会报错。

如果我们定义一个字符串类型的静态常量就要注意了,这两种写法是一样的,而且是可以修改的

static NSString const * LMJName = @"iOS开发者公会";
static const NSString * LMJName = @"iOS开发者公会";

这两种写法const修饰的是* LMJName,*是指针指向符,也就是说此时指向内存地址是不可变的,而内存保存的内容时可变的。 所以我们应该这样写:

static NSString * const LMJName = @"iOS开发者公会";

当我们定义一个对象类型常量的时候,要将const修饰符放到*指针指向符后面。

全局变量

这个单词翻译过来是“外面的、外部的”。顾名思义,它的作用是声明外部全局变量。这里需要特别注意extern只能声明,不能用于实现。

全局变量 & 静态变量

  • 相同点:
    • 存储区域相同:全局变量和静态全局变量都存放在静态存储区。
    • 生命周期相同:全局变量和静态全局变量的都是在程序结束后或者所属对象被释放后才被释放。
  • 不同点:
    • 作用域不同:全局变量的作用域是这个程序的所有源文件,
    • 而静态全局变量的作用域是声明该静态变量的源文件。

extern

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

  • extern修饰的变量,是一个全局变量。只能用来获取全局变量的值, 不能用于定义变量
  • 先在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。
extern NSString * LMJName = @"iOS开发者公会;

extern修饰的变量也可以添加const进行修饰:

extern NSString * const LMJName = @"iOS开发者公会;

此时全局变量只能被初始化一次 extern定义的全局常量的用法和宏定义类似,但是还是有本质上的不同的。 extern定义的全局常量更不容易在程序中被无意窜改。

@property

Objective-C的属性(property)是通过用@property定义的公有或私有的方法。例如: 

@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@property 的本质是什么?

@property = ivar + getter + setter;

下面解释下:

“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为 Objective-C 2.0 的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正因为有了这种严格的命名规范,所以 Objective-C 这门语言才能根据名称自动创建出存取方法。其实也可以把属性当做一种关键字,其表示:

编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。 所以你也可以这么说: @property = getter + setter;

@synthesize和@dynamic分别有什么作用?

  • @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
  • @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
  • @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为@dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

在Xcode4.5及以后的版本中,可以省略@synthesize,编译器会自动帮你加上get 和 set 方法的实现,并且默认会去访问_age这个成员变量,如果找不到_age这个成员变量,会自动生成一个叫做 _age的私有成员变量。

iOSInterviewQuestions/《招聘一个靠谱的iOS》面试题参考答案(上).md at master · ChenYilong/iOSInterviewQuestions

@property修饰符

Strong(iOS4 = retain )

  • 它说“把它保存在堆中直到我不再指向它”
  • 换句话说,“我是拥有者,你不能在保留目标之前解除这个目标。”
  • 仅在需要保留对象时才使用strong。
  • 默认情况下,所有实例变量和局部变量都是强指针。
  • 我们通常对UIViewControllers使用strong(UI项目的父项)
  • 强与ARC一起使用,它基本上可以帮助你,不必担心对象的保留计数。完成后,ARC会自动为您释放它。使用关键字strong表示您拥有该对象。
01. atomic 			//default
02. nonatomic
03. strong=retain		//default
04. weak= unsafe_unretained
05. retain
06. assign 			//default
07. unsafe_unretained
08. copy
09. readonly
10. readwrite 		//default

ios - Objective-C ARC: strong vs retain and weak vs assign - Stack Overflow

iCoding: Variable property attributes or Modifiers in iOS

Copy拷贝机制

因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本. 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.

比如以下代码:

NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
NSString *stringCopy = [string copy];

查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行如下操作:

[string appendString:@"origion!"]

stringCopy 的值也不会因此改变,但是如果不使用 copy,stringCopy 的值就会被改变。 集合类对象以此类推。 所以,

集合类对象是指 NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

查看内容,可以看到 copyArray 和 array 的地址是一样的,而 mCopyArray 和 array 的地址是不同的。说明 copy 操作进行了指针拷贝,mutableCopy 进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看 mutable 对象拷贝的例子:

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

查看内存,如我们所料,copyArray、mCopyArray和 array 的内存地址都不一样,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。同样,我们可以得出结论:

在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示如下:

  • [immutableObject copy] // 浅复制
  • [immutableObject mutableCopy] //深复制
  • [mutableObject copy] //深复制
  • [mutableObject mutableCopy] //深复制

NSString 为什么要用copy

copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

copy  mutableCopy

  • copy 方法利用 基于NSCopying 方法约定,由各类实现的 copyWithZone: 方法生成并持有对象的副本。
  • 与copy方法类似,mutableCopy 方法利用基于 NSMutableCopying 方法约定,有各类实现的 mutableCopyWithZone: 方法生成并持有对象的副本。
  1. mutableCopy创建一个新的可变对象,并初始化为原对象的值,新对象的引用计数为 1;
  2. copy 返回一个不可变对象。分两种情况:    (1)若原对象是不可变对象,那么返回原对象,并将其引用计数加 1;    (2)若原对象是可变对象,那么创建一个新的不可变对象,并初始化为原对象的值,新对象的引用计数为 1。

对非集合类对象的copy操作:

在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示如下:

  • [immutableObject copy] // 浅复制
  • [immutableObject mutableCopy] //深复制
  • [mutableObject copy] //深复制
  • [mutableObject mutableCopy] //深复制

weak

weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

weak 实现原理

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

weak的实现原理可以概括一下三步:

  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
  2. 添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用clearDeallocating函数.clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

循环引用

循环引用是指2个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄漏的一种情况。举个例子:

class Father

@interface Father: NSObject
@property (strong, nonatomic) Son *son;

@end

class Son

@interface Son: NSObject
@property (strong, nonatomic) Father *father; 

@end

上述代码有两个类,分别为爸爸和儿子。爸爸对儿子强引用,儿子对爸爸强引用。这样释放儿子必须先释放爸爸,要释放爸爸必须先释放儿子。如此一来,两个对象都无法释放。

解决方法是将Father中的Son对象属性从strong改为weak。

内存泄漏可以用Xcode中的Debug Memory Graph去检查,同时Xcode也会在runtime中自动汇报内存泄漏的问题。

避免循环引用

随着ARC的引入,内存管理变得更容易了。然而,即使您不必担心何时保留和释放,但仍然有一些规则需要您知道,以避免内存问题。在这篇文章中,我们将讨论强引用循环。

什么是一个强引用循环?假设你有两个对象,对象A和对象B。如果对象A于对象B持有强引用,对象B于对象A有强引用,那么就形成了一个强引用循环。我们将讨论两种非常常见,需要注意循环引用的场景:Block和Delegate。

A->B: strong reference
B->A: strong reference

1. delegate

委托是OC中常用的模式。在这种情况下,一个对象代表另一个对象或与另一个对象协调。委派对象保留对另一个对象(委托)的引用,并在适当的时候向其发送消息。委托可以通过更新应用程序的外观或状态来响应。

(苹果的)API的一个典型例子是UITableView及其Delegate。在本例中,UITableView对其Delegate有一个引用,Delegate有一个返回UITableView的引用,按照规则,每一个都是(指向对方),保持对方活着,所以即使没有其他对象指向DelegateUITableView,内存也不会被释放。(所以需要弱引用)

#import <Foundation/Foundation.h>
 
@class ClassA;

@protocol ClassADelegate <NSObject>
 
-(void)classA:(ClassA *)classAObject didSomething:(NSString *)something;
 
@end
 
@interface ClassA : NSObject
 
@property (nonatomic, strong) id<ClassADelegate> delegate;

这将在ARC世界中生成一个保留循环。为了防止这一点,我们需要做的只是将对委托的引用更改为弱引用~

@property (nonatomic, weak) id<ClassADelegate> delegate;

Delegate模式

弱引用并未实现对象间的拥有权或职责,并不能使一个对象存活在内存中。如果没有其他对象指向delegate代理或者委托对象,那么delegate代理将被释放,随之delegate代理释放对委托对象的强引用。如果没有其他对象指向委托对象,则委托对象也将被释放。

2. Blocks

Block是类似于C函数的代码块,但除了可执行代码外,它们还可能包含堆栈中的变量。因此,Block可以维护一组数据,用于在执行时影响行为。因为Block保持代码的执行所需要的数据,他们是非常有用的回调。

官方文档:

BlockObjective-C对象,但是有些内存管理规则只适用于Block,而非其他Objective-C对象。

Block内对任何所捕获对象的保持强引用,包括Block自身,因此Block很容易引起强引用循环。如果一个类有这样一个Block的属性:

@property (copy) void (^block)(void);

在它的实现中,你有一个这样的方法:

- (void)methodA {
 
    self.block = ^{
 
        [self methodB];
    };
}
self->block: strong reference
block->self: strong reference

然后你就得到了一个强引用循环:对象selfblock有强引用,而block正好持有一个self的强引用。

Note: For block properties its a good practice to use copy, because a block needs to be copied to keep track of its captured state outside of the original scope.

注意:关于block的属性设置,使用copy是一个很好的方式,因为block需要被复制后用以在原始作用域外来捕获状态。

为了避免这种强引用循环,我们需要再次使用弱引用。下面就是代码的样子:

- (void)methodA {
 
    ClassB * __weak weakSelf = self;
 
    self.block = ^{
 
        [weakSelf methodB];
    };
}

通过捕获对自身的弱引用,block不会保持与对象的强引用。如果对象被释放之前的block称为weakself指针将被设置为nil。虽然这很好,因为不会出现内存问题,如果指针为nil,那么block内的方法就不会被调用,所以block不会有预期的行为。为了避免这种情况,我们将进一步修改我们的示例:

- (void)methodA {
 
    __weak ClassB *weakSelf = self;
 
    self.block = ^{
 
        __strong ClassB *strongSelf = weakSelf;
 
        if (strongSelf) {
 
            [strongSelf methodB];
        }
    };
}

我们在block内部创建一个Self对象的强引用。此引用将属于block,只要block还在,它将存活内存中。这不会阻止Self对象被释放,我们仍然可以避免强引用循环。

并不是所有的强引用循环都很容易看到,正如示例中的那样,当您的块代码变得更复杂时,您可能需要考虑使用弱引用。

这是两种常见的模式,它们可以出现强引用循环。正如您所看到的,只要您能够正确地识别它们,就很容易用弱引用来破坏这些循环。即便ARC让我们更容易管理内存,但是你仍需要注意。

附注:翻译中,为了靠近原文意思,强引用循环就是大家经常说的循环引用。

附:Block的一点碎碎念

  1. block要用copy修饰,还是用strong

NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary; block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks: block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道编译器会自动对 block 进行了 copy 操作,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你日用而不知

参考

  1. iOS开发笔记之六十一——Autorelease Pool的实现原理总结 - CSDN博客
  2. 自动释放池的前世今生 ---- 深入解析 Autoreleasepool - 简书
  3. 黑幕背后的Autorelease · sunnyxx的技术博客
  4. 深入理解RunLoop | Garan no dou
  5. 由"NSObject初始化"引发的一二事儿
  6. 【如何正确使用const,static,extern】|那些人追的干货 - 简书
  7. iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析) - 简书

拷贝

  1. 集合类对象的copy与mutableCopy

  2. iOS 集合的深复制与浅复制

循环引用

  1. Avoid strong reference cycles
  2. ChenYilong/iOSInterviewQuestions
Clone this wiki locally