1、Stack(栈)、Heap(堆)的概念
值类型是在stack上创建的,引用类型是在heap上创建的。那么stack和heap在内存上究竟是怎样的呢?
stack和heap保存在哪里?
stack和heap都是保存在随机存取存储器(RAM Random Access Memory),也就是我们平常所说的内存(条),RAM是与CPU直接交换数据的内部存储器,断电后会丢失信息,因为可读写,速度快,因此通常作为操作系统或程序运行时临时数据存放的地方。
我们知道,一个操作系统中的程序的运行活动就是进程,进程中可以包含若干线程(thread),一个标准的线程是由线程ID,当前指令指针、寄存器集合(CPU组成部分,因此,线程是消耗CPU资源)和堆栈(就是栈)组成的。
在多线程的应用程序中,每个线程都有其自己的stack(堆栈),但是,多个不同线程是共享资源的(也就是共享heap),这也意味的多线程需要一定的调控来防止不会再同一时间内访问相同的heap空间。
因此:
1.heap是堆,stack是栈。
2.stack的空间由操作系统自动分配和释放,heap的空间是手动申请和释放的,heap常用new关键字来分配。
3.stack空间有限,heap的空间是很大的自由区。在Java中,若只是声明一个对象,则先在栈内存中为其分配地址空间,若再new一下,实例化它,则在堆内存中为其分配地址。
4.举例:数据类型 变量名;这样定义的东西在栈区。如:Object a =null; 只在栈内存中分配空间new 数据类型();或者malloc(长度); 这样定义的东西就在堆区如:Object b =new Object(); 则在堆内存中分配空间
调用NSObject的alloc方法,如下
id obj = [NSObject alloc];
在GNUstep的实现是:
对象的引用计数retainCount的实现+(id)alloc{ return [self allocWithZone:NSDefaultMallocZone()]; }
+(id)allocWithZone:(NSZone *)z { return NSAllocateObject (self, 0, z);}
//通过allocWithZone:类方法调用NSAllocateObject 函数分配了对象
NSAllocateObject()函数的实现
struct obj_layout{ NSUInteger retained;};
inline id
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
int size = /* needed size to store the object */
id new = NSZoneMalloc(zone, size);
memset(new, 0, size);//将new指针所指的内存区域的前size个字节设置成0,函数原型如下:
extern void *memset(void *buffer, int c, int count);new = (id)&((struct obj_layout *)new)[1];把buffer所指内存区域的前count个字节设置成字符c,并且返回指向buffer的指针}NSAllocateObject()函数通过调用NSZoneMalloc函数来分配存放对象所需的内存空间,再将该空间置为0,最后返回作为对象而使用的指针
NSZone类,NSZone是最初用于防止内存碎片化的,通过把空间根据内存大小分成具有规则的块,可以使内存的使用效率提高,不过如今,ObjC的Runtime有更好的内存管理算法,因此,在Cocoa框架,这种使用NSZone来优化内存已被弃用。除去NSZone的相关代码,alloc方法可以重写为:
struct obj_layout{ NSUInteger retained; };//引用计数
+(id)alloc
{
int size = sizeof(struct obj_layout) + size_of_the_object;分配内存的大小还包括引用计数的空间
struct obj_layout *p = (struct obj_layout *)calloc(1,size);//不太理解
//calloc(1,size):创建一个大小为size的空间
//*p:把空间的首地址赋给obj_layout类型的指针便变量p
return (id)(p+1);//返回指针变量p只想下一个obj_layout类型元素的首地址
}
id obj = [[NSObject alloc] init];
NSLog(@"retainCount = %d",[obj retainCout]);
其实现方法如下:
retain的实现- (NSUInteger)retainCout { return NSExtraRefCount(self)+1; }//使用alloc动态分配的内存块被初始化为0,即retainCount=0,实际上分配内存空间后,应为1,所以加1
inline NSUInteger
NSExtraRefCount(id anObject)
{
return ((struct obj_layout *)anObject)[-1].retained;//-1使得指针指向内存空间块的首地址
}
[obj retain];
-(id)retain { NSIncrementExtraRefCount(self); return self; }
inline void
NSIncrementExtraRefCount(id anObject)
{
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
[NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained++;}
调用retain方法,实际上是让obj_layout结构体的变量retained自增。release方法呢?
[obj release];
- (void)release { if (NSDecrementExtraRefCoutWasZero(self) [self dealloc];};
Bool NSDecrementExtraRefCoutWasZero(id anObject)
{
if(((struct obj_layout *)anObject)[-1].retained == 0){
return YES;
}else{
(struct obj_layout*)anObject[-1].retained--;
return NO;
}
}
调用release方法时,先会获取obj_layout结构体变量retained的值,若值为0,则返回YES并调用dealloc方法, 否则,则对retained变量自减。
- (void)dealloc { NSDeallocateObject(self); }
inline void
NSDeallocateObject(id anObject)
{
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o)
}
上面可以知道GNUstep中对alloc、retain、release的实现,通过这认识,我们可以猜测苹果Cocoa框架的实现方式
通过Xcode的调试器lldb,可以看到关于alloc的实现:
+alloc-->+allocWithZone-->class_createInstance -->calloc
alloc的实现过程除了class_createInstance,其他和GNUstep差不多,而class_createInstance则是ObjC的Runtime特性。
苹果是通过哈希表(reference counter table)来控制引用计数(不同于GNUstep),所有的引用计数都是保存在哈希表的词目上。
这种管理方式的最大优点是监控和管理内存,在出现内存问题是,通过哈希表有助于程序的调试。
GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现则是保存在引用计数表的记录中。GNUstep和苹果各自的优点
GNUstep:
少量代码即可完成
能够统一管理引用计数用内存块和对象用内存块
苹果:
对象用内存的分配无需考虑内存块头部
引用计数表各记录中存有内存块地址,可从各个记录追溯到个对象的内存块
Autorelease内部的实现
demo:
NSAutoreleasePool * pool=[[NSAutoreleasePool alloc]init];
id obj=[[NSObject alloc]init];
[obj autorelease];
[pool drain];
[obj autorelease]实现如下:
-(id)autorelease
{
[NSAutoreleasePool addObject:self];
}autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject方法
GNUstep中的autorelease实际上是用一种特殊的方法实现的,在进行方法调用时,为了解决雷鸣,方法名以及取得方法运行时的函数指针,要在框架初始化时对其结果值进行缓存
NSAutoreleasePool的实现
+(void) addObject:(id) anObj
{
NSAutoreleasePool * pool=取得正在使用NSAutoreleasePool对象
if(pool!=nil) [pool addObject:anObj];
else .......
}
addObject:类方法调用正在使用的NSAutoreleasePool对象的addObject方法
-(void) addObject:(id) anObj
{ [array addObject:anObj];}实际的GNUstep实现使用的是连接列表,这同在可变数组中追加对象参数是一样的。如果调用NSObject类的autorelease实例方法,该对象呗追加到正在使用的NSAutoreleasePool对象的数组里
[pool drain];的实现
-(void) drain
{[self dealloc];}
-(void)dealloc
{[self emptyPool]; [array release];}
-(void) emptyPool
{for(id obj in array) [obj release];}
如果嵌套生成NSAutoreleasePool对象,理所当然会使用最内侧的对象
问题:如果autorelease NSAutoreleasePool对象会如何? 会发生异常
原因:通常在使用OC,即Foundation框架时,无论调用哪个对象的autorelease实例方法,实际上调用的事NSObject类的autorelease实例方法,但是对于NSAutoreleasePool类,autorelease实例方法已被重载,因此运行时就会出错。