继续上篇文章,通过定时器,引出常见的内存泄漏问题,可见内存管理的重要性。本文我们继续深入的分析内存管理的原理,看看曾经的MRC
年代,开发时需要做哪些内存管理相关的工作。
其实MRC
年代已经过去很久了,也许你觉得并没有必要去学习研究,很多工作都有编译器替开发者完成了,我们开发中并不需要去过多的关心,但是学习还是要抱着知其然也知其所以然
的态度。编译器替开发者做了很多工作,可是他做的到底是什么,又是在什么时刻(编译时or运行时)去做的呢?
希望你是带着上面的问题,和你在内存管理方便的一些疑惑开始阅读下面的内容。
先补充一个内存的基础知识:
内存布局
说了半天的内存管理,那内存到底是怎么分布的,我们开发中写的代码最终会存在内存的什么位置呢?这个问题面试中也会被问到,我们至少可以说出从低到高的内存布局。
根据上面这幅内存分布的图,可以看到,从低地址开始,首先是保留区域,保留区域暂时先不用关心,从保留区域往下开始,就是我们要了解掌握的东西了。
- 代码段:编译之后的代码就保存在这个区域,就是010101这些二进制
- 数据段:字符串常量、已初始化的数据、未初始化的数据从低到高排布
- 堆:通过alloc、malloc、calloc等动态分配的空间,地址有低到高排布
- 栈:函数调用的开销,比如函数中的局部变量,栈中的地址由高到低排布
再简单通过代码,验证一下上面图中数据段
、堆
、栈
的布局顺序是否正确:
NSString *str1 = @"123";
NSString *str2 = @"123";
NSLog(@"%p %p",str1, str2);//0x1049a0040 0x1049a0040
static int c = 20;
static int d;
int e;
int f = 20;
NSString *str = @"123";
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"\na=%p\nb=%p\nc=%p\nd=%p\ne=%p\nf=%p\nstr=%p\nobj=%p\nobj2=%p",&a,&b,&c,&d,&e,&f,str,obj,obj2);
根据打印的结果,再做一次内存地址从低到高的排序
a=0x10ef8b520
b=0x10ef8b5ec
c=0x10ef8b524
d=0x10ef8b5e8
e=0x7ffee0c77cc4
f=0x7ffee0c77cc0
str=0x10ef89040
obj=0x600001578090
就可以验证在数据段
的布局顺序从低到高依次是:
//字符串常量
str=0x10ef89040
//已初始化的全局变量、静态变量
a=0x10ef8b520
c=0x10ef8b524
//未初始化的全局变量、静态变量
d=0x10ef8b5e8
b=0x10ef8b5ec
//堆 alloc分配内存
obj=0x600001578090
//栈 先分配的内存地址高 后分配的地址低
f=0x7ffee0c77cc0
e=0x7ffee0c77cc4
Tagged Pointer
Tagged Pointer
是从64bit之后iOS引入的一项技术,用于优化NSNumber、NSDate、NSString等小对象的存储。我们知道,在没有使用Tagged Pointer
之前,所有的对象都需要动态分配内存,维护引用计数等。
以NSNumber
为例,使用Tagged Pointer
之前,NSNumber
的指针中存储的是堆中NSNumber对象的地址值,在使用时都要先根据指针找到对应的内存地址,再取出内存地址中存储的值;而使用Tagged Pointer
之后,NSNumber
的指针中存储的数据变成了Tag + Data
。Tag
是一个标志位,标识了对象的类型,Data
则就是NSNumber
的值,也就是将数据直接存储在了指针中。
所以,对应的objc_msgSend
也能识别Tagged Pointer
,可以直接从指针中提取数据,节省了调用的开销。
当然,如果数据比较大,指针不够存储数据的时候,依然是使用动态分配内存的方式来存储数据。
在runtime源码NSObject.mm
中,可以看到判断是否是Tagged Pointer
的方法
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
就是传入的指针ptr & _OBJC_TAG_MASK,_OBJC_TAG_MASK分为是iOS系统和Mac OS系统两个宏定义
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
总结:
Mac os:指针的最低有效位是1 就是tagged pointer 否则是对象
iOS:指针的最高有效位是1 就是tagged pointer 否则是对象
简单验证一下(iOS环境下)
NSNumber *number1 = @(2);
NSLog(@"%p",number1);//0xd749d63ace671147
0x d749 d63a ce67 1147
最高位d 转化为二进制是1101
最高位是1 就是tagged pointer
面试题
通过上面对Tagged Pointer
的分享,我们再看看这个面试题,是不是就不难做出来了。
思考一下两段代码的区别?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
两段代码的唯一区别就在于@"abcdefghijk"和@"abc",根据Tagged Pointer
的特点,就可以知道这道面试题考察的点是什么了。Tagged Pointer用于优化NSNumber、NSDate、NSString等小对象的存储,如果超过8个字节存不下,就会恢复堆空间存储
,正是本题的考点。
答案就是,字符串内容少一点就不会crash,因为tagged pointer不是oc对象;而crash的原因是坏内存访问。
要证明这两点也很容易,其实直接写两个字符串打印他们的类型和地址就可以知道了。
NSString *str = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"234abc"];
NSLog(@"%p %p",str, str2);//0x600001a27c00 0xa9a09f4b43fcc530
NSLog(@"%@ %@",[str class], [str2 class]);//__NSCFString NSTaggedPointerString
而坏内存访问呢,我们都知道setter方法的内部实现,其实是调用了release的,多线程同时调用release,导致了坏内存访问。
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release]; //多线程同时调用release 导致坏内存访问
_name = [name copy];
}
}
最后再复习一下之前学过的知识 ,说说这种crash如何解决 ?
方案一:改为atomic(当然不推荐使用)
方案二:加锁
MRC
其实写到这一节的时候,有点儿不知道怎么写下去了。本意是多掌握一点MRC下编码时需要做的工作,帮助我们理解ARC下,编译器帮我们做了什么。带着这个目的,写一写代码感受一下好了。
苹果在 iOS 5 中引入了ARC(Automatic Reference Counting)自动引用计数内存管理技术,通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器编译时在合适的地方为 OC 对象插入retain、release和autorelease代码,省去了在MRC(Manual Reference Counting)手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量。
在MRC下,当我们不需要一个对象的时候,要调用release或autorelease方法来释放它。调用release会立即让对象的引用计数减 1 ,如果此时对象的引用计数为 0,对象就会被销毁。调用autorelease会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release,所以autorelease相当于延迟了对象的释放。
下面通过代码测试分析一下,首先第一步是在XCODE中改一下配置,修改为MRC
release
先创建一个Person类作为测试的基础
Person *person = [[Person alloc] init];//alloc分配内存 init初始化
NSLog(@"%lu",(unsigned long)person.retainCount);
[person release];
MRC下其实就一个最重要的规则,有一个alloc就需要对应有一个release去释放对象,所谓内存泄漏,就是该释放的对象没有被释放。
除了上面这种一一对应的写法之外,还有autorelease,是在一个恰当的时候调用release,可以先理解为这个方法的作用域{}结束之后。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1 = [[[Person alloc] init] autorelease];
NSLog(@"1");
}
NSLog(@"2");
return 0;
}
这段代码的打印结果如下:
内存管理-MRC[1612:94630] 1
内存管理-MRC[1612:94630] -[Person dealloc]
内存管理-MRC[1612:94630] 2
注意这里是@autoreleasepool和autorelease的配合使用,如果只使用了autorelease则不是方法作用域{}结束后就释放。这里先简单带一句,后面打算在写文章专门分享一下autorelease的内容。
setter
MRC下,setter方法需要区分数据类型,如果是基础数据类型,不牵扯内存管理的,则直接赋值即可
- (void)setAge:(int)age {
_age = age;
}
如果是对象类型,则需要做对应的retain和release操作
- (void)setCar:(Car *)car {
if (_car != car) {
[_car release];
_car = [car retain];
}
}
@property
MRC下,对象类型的属性,@property修饰词要使用retain
。最初,@property只生成属性的setter和getter的声明,在.m文件中添加@synthesize才会自动生成方法实现,不需要在手动写setter和getter ,但是还是要实现dealloc方法。
- (void)dealloc
{
// 先释放属性,并把指针置为nil
[_dog release];
_dog = nil;
self.car = nil;
// 父类的dealloc放在最后调用
[super dealloc];
NSLog(@"%s",__func__);
}
类方法初始化
最后再补充一点,不是alloc、 init 或者new方法创建的,一般不需要release, 其内部实现已经调用了一次release。
self.data = [NSMutableArray array];
比如[NSMutableArray array]
这种类方法的创建方式,就不需要再手动调用release了。