【iOS】ARC内存管理

内存管理的思考方式

怎么说呢。经典再放送咯。

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

iOS底层内存管理方式

iOS内存管理方案有三种,我们详细看下每种方案的实现及存在的意义。

1. tagged pointer

标签指针

没有这种管理机制会引起内存浪费,为什么呢?

我们来看下,假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示:

在这里插入图片描述

我们再来看看效率上的问题,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumberNSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。

所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:

在这里插入图片描述

当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。以上是关于Tag Pointer的存储细节。
Tagged Pointer的特点:

  • 我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:Tagged Pointer专门用来存储小的对象,例如NSNumberNSDate, 当然NSString小于60字节的也可以运用了该手段
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已,因为他没有isa指针。所以,它的内存并不存储在堆中,也不需要mallocfree
  • 在内存读取上有着3倍的效率,创建时比以前快106倍。

由此可见,苹果引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

2. non-pointer iSA–非指针型iSA

在64位系统上只需要32位来储存内存地址,而剩下的32位就可以用来做其他的内存管理

non_pointer iSA 的判断条件:

1 : 包含swift代码;

2:sdk版本低于10.11;

3:runtime读取image时发现这个image包含__objc_rawi sa段;

4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中;

5:某些不能使用Non-pointer的类,GCD等;

6:父类关闭。

3. SideTables,RefcountMap,weak_table_t

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。

因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables,所以不能对整个Hash表加锁。苹果采用了分离锁技术。

下边是SideTabel的定义:

SideTable
   struct SideTable {
     //锁
     spinlock_t slock;
     //强引用相关
     RefcountMap refcnts;
     //弱引用相关
     weak_table_t weak_table;
     ...
}

当我们通过SideTables[key]来得到SideTable的时候,SideTable的结构如下:

1、一把自旋锁。spinlock_t slock;

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

它的作用是在操作引用计数的时候对SideTable加锁,避免数据错误。

苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁。

2、引用计数器 RefcountMap * refcnts;

对象具体的引用计数数量是记录在这里的。

这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map呢?因为内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

引用计数器的数据类型是:

typedef __darwin_size_t size_t;

再进一步看它的定义其实是unsigned long,在32位和64位操作系统中,它分别占用32和64个bit。

苹果经常使用bit mask技术。这里也不例外。拿32位系统为例的话,可以理解成有32个盒子排成一排横着放在你面前。盒子里可以装0或者1两个数字。我们规定最后边的盒子是低位,左边的盒子是高位。

(1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001

(1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010

下面来分析引用计数器(图中右侧)的结构,从低位到高位。

(1UL<<0)???WEAKLY_REFERENCED

表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

(1UL<<1)???DEALLOCATING

表示对象是否正在被释放。1正在释放,0没有

(1UL<<(WORD_BITS-1))???SIDE_TABLE_RC_PINNED

其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。

3、维护weak指针的结构体 weak_table_t * weak_table;

第一层结构体中包含两个元素。

第一个元素weak_entry_t *weak_entries;是一个数组,上面RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry

(上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)

这个是因为weak的显著的特征来决定的: 当weak对象被销毁的时候,要把所有指向该对象的指针都设为nil。

第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。

第二层weak_entry_t的结构包含3个部分:

1、referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
2、referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
3、inline_referrers只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

上面我们介绍了苹果为了更好的内存管理使用的三种不同的内存管理方案,在内部采用了不同的数据结构以达到更高效内存检索。

内存管理有关修饰符

__strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。

不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。

对象的所有者和对象的生命周期

  1. 持有强引用的变量在超出其作用域时被废弃
  2. 随着强引用的失效,引用的对象会随之释放

__strong对象相互赋值

__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)	
obj1 = nil;//obj1不再强引用对象B	
obj0 = nil;//obj0不再强引用对象B	
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

方法参数中使用__strong

废弃Test对象的同时,Test对象的obj_成员变量也被废除
即成员变量的生存周期是与对象同步的。

__strong导致的循环引用

循环引用就是两个对象相互引用导致在该释放的时候没有释放,从而一直占着内存导致内存泄漏

内存泄漏:在内存该被释放的时候没有释放,导致内存被浪费使用了

举个例子:

#import <Foundation/Foundation.h>

@interface StrongTest : NSObject {
    id __strong _obj;
}
- (void) setObject:(id __strong) obj;

@end

#import "StrongTest.h"
@implementation StrongTest
- (void) setObject:(id)obj {
    _obj = obj;
}
@end

main函数:

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        StrongTest *testFirst = [[StrongTest alloc] init];//生成Test1
        StrongTest *testSecond = [[StrongTest alloc] init];//生成Test2
        
        [testFirst setObject:testSecond];
        [testSecond setObject:testFirst];
        
        //打印一下引用计数值
        NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)testFirst));       
    }
    return 0;
}

我们创建了两个对象testFirsttestSecond
每个对象内部都有一个成员变量_obj
其中一个对象的_obj持有另一个对象

最后就会造成下面的结果:

testFirst持有Test1testFirst.obj持有Test2testSecond持有Test2testSecond.obj持有Test1

失效阶段的表示:

what happenTest1引用计数Test1持有者Test2引用计数Test2持有者
初始状态2testFirst, testSecond.obj2testFirst.obj, testSecond
testFirst超出作用域1testSecond.obj2testFirst.obj, testSecond
testSecond超出作用域1testSecond.obj1testFirst.obj

这样一来,即使两个对象的指针都超出作用域,由于其中彼此的成员变量相互持有彼此的对象而造成循环引用。

__weak修饰符

__weak弱引用(引用计数不会加一 对象随时可能会被dealloc)
内部使用__autoreleasing来维持该对象不被dealloc(这个也是__weak修饰符修饰的对象在所指向的对象销毁之后会自动指向nil的关键所在)
对象的引用计数是记录在一张表上的,不在对象本身或者指针中,系统通过访问这张表来确定是否释放该对象。

将上面相互引用例子中的成员变量变为weak,即可避免循环引用。

weak还有个作用。在持有某对象的弱引用时,若该对象被废弃,则此若引用将自动失效且处于nil被赋值的状态(空弱引用)。

weak提供弱引用,弱引用不持有对象,NSObject对象会被销毁,所以会报一个警告

在这里插入图片描述

我们可以这样使用__weak修饰符的变量,将__strong修饰的对象赋值给__weak修饰的对象,这样就不会发生警告了:

	id __strong objTest = [[NSObject alloc] init];
    id __weak objTestSecond = objTest;

类似于“强弱共舞”

__weak修饰符的引用计数的问题

按以下例子,我们来看一下这两个的引用计数的值:

    id __strong obj = [[NSObject alloc] init];
    id __weak objTest = obj;
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)obj));
    NSLog(@"%lu", CFGetRetainCount((__bridge  CFTypeRef)objTest));

按照常理来说应该是1 1,因为__weak修饰符不持有对象,引用计数值两个都应该是1

打印一下结果:

在这里插入图片描述

在这里插入图片描述

对比一下就能发现 两行NSLog的汇编代码并不一样

第二个__weak修饰符的NSLog在于开始先loadWeakRetained
然后在打印结束后有一个release操作

在打印__weak的引用计数时NSLog先将其以强引用,防止没有打印就释放掉了造成程序的崩溃,在NSLog结束时,会调用objc_release使引用计数减一。

__unsafe_unretained

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。
附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

为什么是不安全的呢

weak 修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为 nil。 _unsafe_unretain 不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 __unsafe_unretain __weak 效率高。
悬垂指针 指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 迷途指针。野指针,没有进行初始化的指针,其实都是 野指针

附有__unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,生成的对象会立即释放。

在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃。

__autoreleasing修饰符

与MRC进行比较:
MRC中autorelease的使用方法:

  1. 生成并持有NSAutoreleasePool对象
  2. 调用已分配对象的autorelease方法(将对象注册到pool中)
  3. 废弃NSAutoreleasePool对象

在ARC环境下:

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init]; 1⃣️
[obj autorelease]; 2⃣️
[pool drain]; 3⃣️

等价于:

@autoreleasepool{ 1⃣️
id __autorelease obj2; obj2 = obj; 2⃣️
} 3⃣️

自动调用:

编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是将自动把返回值的对象注册到autoreleasepool

下面情况不使用__autoreleasing修饰符也能使对象注册到autoreleasepool中。

+ (id) array {
	return [[NSMutableArray alloc]init];
}

//如下:
+ (id) array {
	id obj = [[NSMutableArray alloc]init];
	return obj;
}

由于return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器也会自动注册到自动释放池

自动调用时的失效过程:

随着obj超出其作用域,强引用失效,所以自动释放自己持有的对象。
同时,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放。 因为对象的拥有者不存在,所以废弃对象。

weak修饰符与autoreleasing修饰符:

为什么访问__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢,这是因为__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象存在,因此,在使用附有__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象。

id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]);
与以下源代码相同
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [temp class]);

例1

	//这个例子中obj0没有加入到自动释放池中
    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

在这里插入图片描述

池中没有出现obj0。

例2

	//这个例子中obj0加入了自动释放池中
    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    NSLog(@"class = %@", [obj1 class]);

在这里插入图片描述

池中出现了obj0

例3

    id __autoreleasing obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

在这里插入图片描述

池中出现了obj0,且看到count为2

例4

    id  obj0 = [[NSObject alloc] init];
    id __weak obj1 = obj0;
    id __autoreleasing obj2 = obj1;
    NSLog(@"class = %@", [obj2 class]);

在这里插入图片描述

池中出现了obj0

总结: __weak修饰符并不会将对象加入到自动释放池,但是我们使用__weak修饰的对象一定要是本身已经加入到自动释放池的或者后续使用__autoreleasing将__weak所修饰的对象加入释放池

具体ARC规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法名规则
  • 不要显式调用dealloc
  • 使用@autorelease块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体的成员
  • 显式转换id和void*

重点:

不能显式调用dealloc

dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如:

  • 通知的观察者,或KVO的观察者
  • 对象强委托/引用的解除(例如XMPPMannerger的delegateQueue)
  • 做一些其他的注销之类的操作(关闭程序运行期间没有关闭的资源)
In the implementation of dealloc, do not call the implementation of superclass. You should try to avoid using dealloc to manage the lifetime of limited resources, such as file descriptors.
You never send a dealloc message directly. Instead, the dealloc method of the object is called by the runtime.
在dealloc的实现中,不要调用超类的实现。您应该尽量避免使用dealloc管理有限资源(如文件描述符)的生存期。
不要直接发送dealloc消息。与直接发送dealloc消息不同,对象的dealloc方法由运行时调用。

Special Considerations
When not using ARC, your implementation of dealloc must invoke the superclass’s implementation as its last instruction.
特别注意事项
当不使用ARC时,dealloc的实现必须调用父类(super)的实现作为它的最后一条指令 [super dealloc]

__bridge

  • bridge可以实现Objective-C与C语言变量和Objective-C与CoreFoundation对象之间的互相转换
  • _bridge不会改变对象的持有状况,既不会retain,也不会release
  • _bridge转换需要慎重分析对象的持有情况,稍不注意就会内存泄漏
  • _bridge_retained用于将口C变量转换为C语言变量或将OC对象转换为CoreFoundation对象
  • _bridge_retained类似于retain,“被转换的变量”所持有的对象在变量赋值给"转换目标变量“后持有该对象
  • _bridge_transfer用于将C语言变量转换为OC变量或将CoreFoundation对象转换为OC对象
  • bridge_transfer类似于release,“被转换的变量”所持有的对象在变量赋值给“转换目标变量”后随衣释热

属性关键字和所有权修饰符

属性关键字所有权修饰符
assign_unsafe_unretained
copy__strong
retain__strong
strong__strong
__unsafe_unretained__unsafe_unretained
weak__weak
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: iOS内存管理版本记录如下: 1. iOS 2.0及更早版本:使用手动管理内存的方式。 2. iOS 3.0:引入了基于引用计数的自动内存管理,使用retain和release函数来增加或减少对象的引用计数。 3. iOS 5.0:引入了ARC(自动引用计数)机制,ARC会在编译时自动插入retain和release代码,减少手动管理内存的工作。 4. iOS 7.0:引入了内存诊断工具Memory Usage Report,可以监测App内存使用情况,帮助开发者优化内存管理。 5. iOS 8.0:引入了一些新的API,如NSCache和NSURLSession,使得内存管理更加方便和灵活。 6. iOS 11.0:引入了基于图片大小的UIImage渲染机制,减少了内存占用。 7. iOS 13.0:引入了叫做“Scene”的多任务环境,使得内存管理更加复杂,需要更加小心谨慎地处理内存问题。 总的来说,随着iOS版本的不断更新,内存管理的机制也在不断地完善和优化,使得iOS应用能够更加高效地使用内存,提高用户体验。 ### 回答2: iOS内存管理是由操作系统自动管理的,在不同的版本中有所不同。 在iOS 5之前的版本中,内存管理主要依赖于手动管理引用计数(reference counting)来管理对象的生命周期。开发者需要手动调用retain和release方法来增加或减少对象的引用计数,以确保对象在不再需要时能够被正确释放。这种方式需要开发者非常谨慎地管理对象的引用,以避免内存泄漏或野指针等问题。 从iOS 5开始,iOS引入了自动引用计数(Automatic Reference Counting,ARC)的内存管理机制。ARC可以自动地插入retain、release和autorelease等方法的调用,使得开发者不再需要手动进行内存管理。开发者只需要关注对象的创建和使用,而不需要关心具体的内存管理细节。ARC减少了内存管理的工作量,提高了开发效率,并且减少了内存泄漏和野指针等问题的发生。不过,ARC并不是完全的自动化内存管理,开发者仍然需要遵循一些规则,比如避免循环引用等,以保证内存的正确释放。 随着iOS版本的不断更新,苹果不断改进和优化内存管理机制。每个新版本都带来了更好的性能和更高效的内存管理。开发者可以通过关注苹果的官方文档和开发者社区中的更新内容来了解每个版本中的具体变化和改进。 总结来说,iOS内存管理从手动的引用计数到自动引用计数的演变,极大地简化了开发者的工作,并提高了应用的性能和稳定性。随着不断的改进和优化,iOS内存管理会越来越高效和可靠。 ### 回答3: iOS内存管理版本记录是指苹果公司在不同版本的iOS操作系统中对于内存管理方面的改进和更新记录。随着iOS版本的不断迭代,苹果在内存管理方面进行了一系列的优化和改进,以提高系统的稳定性和性能。 首先,在早期的iOS版本中,苹果采用了手动内存管理的方式,即开发人员需要手动创建和释放内存,容易出现内存泄漏和内存溢出等问题。为了解决这些问题,苹果在iOS5版本中引入了自动引用计数(ARC)机制。ARC机制能够通过编译器自动生成内存管理代码,避免了手动管理内存带来的问题。 其次,iOS6版本引入了内存分页机制。这个机制能够将应用程序内存分成不同的页,将不常用的页置于闲置列表中,从而释放出更多的内存空间。这些闲置列表中的页能够在需要时快速恢复到内存中,减少了内存压力。 此外,iOS7版本中进一步提升了内存管理的能力。苹果在这个版本中引入了内存压缩技术,将内存中的数据进行压缩,从而提高了内存利用率。此外,iOS7还引入了资源清理功能,可以自动清理不再使用的资源,释放内存空间。 最后,在iOS13版本中,苹果进一步改进了内存管理策略。该版本中引入了后台内存优化功能,能够自动优化应用在后台运行时的内存占用,减少了后台应用对于系统内存的占用和影响。 综上所述,iOS内存管理版本记录反映了苹果在不同版本的iOS操作系统中对于内存管理方面的改进和优化。这些改进和优化使得iOS系统更加稳定和高效,并且提升了应用程序的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值