iOS程序内存布局与管理

OC的对象都是指针形式表示,对于对像内存管理从四个方面来说(ARC和MRC,区别大部分是在于编译器帮我们添加retain和release,可以归到一起总结)

  1. 使用了TaggedPointer技术的对象指针,由于把数据存在了指针里,在函数栈调用结束时,就会自动释放,不涉及retain,release操作,即使ARC环境下,系统也不用加release
  2. 普通对象,使用了strong,copy策略声明的属性,编译器会在函数栈结束或者类调用dealloc的时候,加上release,释放对象指针,及内存。
  3. 类方法(array,[NSString stringWithFormat:]等)创建的对象,框架内部会把他加入到自动释放池,这种方法创建的对象和MRC下autorelease对像都是加到自动释放池,而自动释放池设计到内存分页管理,见下文。
  4. weak类型指针对象,被指向的对象,有一张哈希表,里面存着所有执向他的指针,在类dealloc时,会去释放这些指针。

iOS程序内存分为6个区:

  1. 保留区
  2. 代码区(__TEXT):保存编译之后的代码
  3. 数据段(__DATA):字符串常量:NSString *str = @"String";已初始化的全局变量,静态变量等;未初始化的全局变量,静态变量。
  4. 堆区(heap):函数调用开销,比如局部变量。分配的内存空间地址越来越小。
  5. 栈区(stack):通过alloc,malloc,calloc等动态分配的空间,分配的内存空间地址越来越大
  6. 内核区

数据对象调用copy和mutableCopy所发生的拷贝情况表格


iOS程序内存布局与管理_第1张图片
图-01

总结:不可变对象调用copy是浅拷贝,调用mutableCopy是深拷贝;可变对象调用copy和mutableCopy都是深拷贝。

Tagged Pointer 技术

从64bit开始,iOS引入Tagged Pointer技术用于优化NSNumber,NSdate,NSString等小对象的存储。
在没有使用Tagged Point对象之前, NSNumber等对象需要动态分配内存,维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址。
使用Tagged Pointer技术后,就变成了Tag + Data,也就是将数据直接存在了指针中, 但是如果指针存不下实再按原方法存到堆中。
当使用Tagged pointer计数后的对象时 调用
objc_msgSend()时,会直接去指针取之而不是去找什么isa,找对象.
把使用了Tagged pointer技术的对象赋值给一个对象,相当于把地址赋值给新对象。
判断是否是tagged pointer指针
iOS 平台:address & (1<<63):指针的最高有效位是否是1
Mac OSX 平台:address & 1:指针最低的有效位是否是1

MRC相关:

内存管理

  • 在iOS中,使用引用计数来管理OC对象的内存
  • 一个新建的OC对象引用计数默认是1,当引用计数为0,OC对象就会自动销毁释放其占用的内存。
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 可以通过以下私有函数来查看自动释放池的情况
    -objc_autoreleasePoolPrint(void);

平常声明属性时我们写的assig,retain,copy的含义相当于在MRC下是,如果我们调用set方法的属性没有对应的assig,retain,copy,就会报方法找不到。

//assign
-(void)setProperty:(id)Property
{
   _property = property;
}
//retain
-(void)setProperty:(id)Property
{
if(_property!=property)
  [_property release];
  _property = [property retain];
}
//copy
-(void)setProperty:(id)Property
{
if(_property!=property)
  [_property release];
  _property =[property copy];
}

调用类方法创建对象:[NSString stringWithFormat:@"123"];相当于在MRC下如下代码,不用我们去给对象写release。

[[NSString stringWithFormat:@"123"]autorelease];

自定义类对象实现copy

需要实现copyWithZone:(NSZone*)zone方法,因为copy也是调用这个方法实现,然后在copyWithZone里把想复制值的对象属性赋值一遍。

- (id)copyWithZone:(NSZone*)zone
{
  SomeClass* instance = [[SomeClass allocWithZone:zone]init];
  instance.property1 = self.property1;
  instance.property2 = self.property2;
  instance.property3 = self.property3;
  return instance;
}

引用计数的存储

在64位优化过后,isa指针是isa_t共用体类型,里面的extra_rc这个19位就是存储引用计数,当这19位装不下时,isa_t 共用体里的has_sidetable_rc就变成1,引用计数就存到一个SideTable的类的属性中refcnts

union isa_t
{
class cls;
struct {
  uintptr_t nonpointer    :1;//0代表普通指针,存Class,Meta-Class对象的内存地址
  uintptr_t has_assoc.    :1;//代表优化过,使用位域存储更多的信息
  uintptr_t has_cxx_dtor :1;//是否有C++的析构函数,如果没有释放更快
  uintptr_t shiftcls           :33;//存着Class,Meta-Class对象的内存地址信息
  uintptr_t magic.            :6;//调试时分辩对象是否未完成初始化
  uintptr_t weakly_referenced :1;//是否有被弱引用执行过,没有则释放更快
  uintptr_t deallocating   :1; //对象是否正在释放
  uintptr_t has_sidetable_rc :1;//extra_rc存不下的时候变为1,引用计数存到一个SideTable类的属性中
  uintptr_t extra_rc          :19;        //存引用计数
}
}
//存引用计数的结构体
struct SideTable
{
  spinlock_t slock;
  RefcountMap refcnts;//存着引用计数,是个散列表
  weak_table_t weak_table;
}

weak指针

weak 和unsafe_unretained区别
weak在所指向的对象销毁时,系统会将指针置nil,而unsafe_unretained不会自动清空的。
实现原理:
当一个对象要释放时,会自动调用dealloc,这个对象有张哈希表SideTable,这里存着指向这个对象的弱引都清掉,调用轨迹。

  1. dealloc
  2. _objc_rootDealloc
  3. rootDealloc
  • 如果是tagged指针,直接return
  • 如果这个对象的isa指针是个普通指针,并且没有弱引用,没有关联对象,没有C++析构函数没有弱引用列表就直接释放,
  • 不然就调用object_dispose
  1. object_dispose
  • 调用objc_destructInstance
  • free(objc)
  1. objc_destructInstance
  • 查看C++析构器返回是否是1,调用object_cxxDestruct清除成员变量
  • 查看是否有关联对象,调用_object_remove_assocations(obj);清楚关联对象
  • 调用clearDeallocating();//将指向当前对象的弱指针置为nil;
  1. free

autorelease

工程build Seting -> 搜索Automatic reference Counting设置NO 启用MRC模式。
在main.m的文件中加几句代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[[Person alloc]init]autorelease];
        NSLog(@"autorelease will end");
    }
    NSLog(@"autorelease end");

    return 0;
}

用clang编译器重写下main文件成main.cpp,看下原理。可以在重写后的main.cpp看到如下源码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    { 
__AtAutoreleasePool __autoreleasepool;
        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
      }
    return 0;
}

除去Person对象声明,可以看到这么一段东西
{
__AtAutoreleasePool __autoreleasepool;
}
这个就是我们写的
@autoreleasepool {}
相当于声明了个__AtAutoreleasePool对象,找到这个类的代码

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {                  atautoreleasepoolobj=objc_autoreleasePoolPush();
}
  ~__AtAutoreleasePool() {
   objc_autoreleasePoolPop(atautoreleasepoolobj);
}
  void * atautoreleasepoolobj;
};

在创建__AtAutoreleasePool对象的时候会调用构造函数,{...}作用域结束后,回收栈空间__AtAutoreleasePool对象会调用析构所以我们写的@autoreslease{}就相当于

{
void *atautireleasepooloobj  = objc_autoreleasePoolPush();
//中间调用autorelease的对象会被加入autorelease释放池
[someNSObject autorelease];
objc_autoreleasePoolPop(atautireleasepooloobj);//释放所有对象
}

关键的就是上面这三句代码

  1. objc_autoreleasePoolPush();
  2. [someNSObject autorelease];
  3. objc_autoreleasePoolPop(atautireleasepooloobj)

1. objc_autoreleasePoolPush();

转到objc源码里,在源码里正好能找到同名的函数,这个函数objc_autoreleasePoolPush()调用就相当于调用AutoreleasePoolPage的push方法,这个方法在源码里如下:

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
//先忽略inline关键字,这是内联函数,表示下面的函数直接转成代码,在汇编里而不是跳到一个函数地址
static inline void *push() {
        id *dest;
//判断是否有AutoreleasePoolPage对象,如果没有创建个新的,有的话,把POOL_BOUNDARY压进栈
        if (DebugPoolAllocation) {
               dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
              dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

上面的代码可以看出来会先去判断是否有autoreleaseNewPage,DebugPoolAllocation是个全局的,如果我们@autorelease循环嵌套的话并且当前的AutoreleasePage没满的话,第二个@autorelease只会把一个边界标记压入AutoreleasePoolPage栈里,如果当前的page满了才会创建下一页(注意:不是每个@autorelease都创建AutoreleasePoolPage对象,当前page只有一个,不同@autorelease靠bundary来区别)。

2. autorelease()

如下所示,

 static inline id autorelease(id obj)
 {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

一个对象调用autorelease方法时,存在三种情况

  1. 当前有AutoreleasePoolPage对象并且page对象没有放满,这样就把对象加入到page里
  2. page对象存在,但是满了,autoreleaseFullPage(obj, page);会新建个page,新的page里会指向旧的page,
  3. 当前没有page对象,autoreleaseNoPage(obj)里会创建page,并把对象加入到page里。

objc_autoreleasePoolPop(atautireleasepooloobj

接收objc_autoreleasePoolPush();返回的对象。从最后一页page开始,一个个把page里的对象release,直到遇到一个POOL_BOUNDARY,代表这个autorelease对象释放完毕。

多个AutoreleasePoolPage采用双向链表的形式存储。


iOS程序内存布局与管理_第2张图片
AutoreleasePoolPage结构图

autoreleasePool和runloop的关系,

iOS在主线程的Runloop注册了2个Observer,

  1. 第一个Observer
    监听了kCFRunloopEntry:
  • 调用objc_autoreleasePoolPush
  1. 第二个Observer
    监听了kCFRunloopBeforeWaiting事件:
  • 调用objc_autoreleasePoolpop ,objc_autoreleasePoolPush
    监听了kCFRunloopBeforeExit事件
  • 调用objc_autoreleasePoolPop()

可以看到是个结构体,里面有两个
autorelesase对象什么时候释放,是在所属Runloop循环中休眠之前调用pop释放
所以 :
我们的对象如果调用了autorelease 加入了自动释放池,则是在Runloop休眠或者退出时,被释放,而不是在所处作用域(函数或者{})结束时立即被释放。

我们的普通局部对象,则是因为编译器编译时会自动加上release,所以出了作用域,要是没有别的地方强引用着,则释放掉。

可以通过_objc_autoreleasePoolPrint(void)查看自动释放池情况
必须得先声明,extern void _objc_autoreleasePoolPrint(void);但是不用实现。

你可能感兴趣的:(iOS程序内存布局与管理)