美文网首页
iOS程序内存布局与管理

iOS程序内存布局与管理

作者: 分流替躺欧阳克 | 来源:发表于2019-06-15 22:58 被阅读0次

    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所发生的拷贝情况表格


    图-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采用双向链表的形式存储。


    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程序内存布局与管理

          本文链接:https://www.haomeiwen.com/subject/hdvkfctx.html