美文网首页面试好文
iOS原理 AutoreleasePool的结构分析

iOS原理 AutoreleasePool的结构分析

作者: 东篱采桑人 | 来源:发表于2021-01-13 10:16 被阅读0次

    iOS原理 文章汇总

    上一篇文章介绍了AutoreleasePool的基本概念,本文将分析AutoreleasePool的内存结构。

    分析入口

    在OC中,使用@autoreleasepool {}代码块可以手动创建一个AutoreleasePool,若想知道这个代码块内部做了哪些处理,可以通过Clang编译汇编调试这两个方式来分析。

    • main函数中创建AutoreleasePool
    int main(int argc, char * argv[]) {
        
        @autoreleasepool {
            
        }
        
        return 0;
    }
    
    • 方式一:Clang编译

    在终端通过Clang命令行将main.m文件编译成main.cpp文件,可以看到底层编译如下:

    struct __AtAutoreleasePool {
        //构造函数
        __AtAutoreleasePool() {
                atautoreleasepoolobj = objc_autoreleasePoolPush();
        }
        //析构函数
        ~__AtAutoreleasePool() {
                objc_autoreleasePoolPop(atautoreleasepoolobj);
         }
          void * atautoreleasepoolobj;
    };
    
    int main(int argc, const char * argv[]) {
    
       //这里的{}表示autoreleasepool的作用域
       { 
             __AtAutoreleasePool __autoreleasepool; 
        }
    
        return 0;
    }
    
    //Clang命令行
    xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
    
    • 方式二:汇编调试

    在main函数中下断点,再设置Xcode的Debug -> Debug Workflow -> Always Show Disassembly,就可以查看汇编调用流程,如下图所示:

    从两种方式的分析结果可知,@autoreleasepool {}代码块实现逻辑如下:

    • {}作用域中通过构造函数objc_autoreleasePoolPush()创建了一个AtAutoreleasePool对象
    • 当超出作用域时,再通过析构函数objc_autoreleasePoolPop()销毁这个对象。

    因此,AutoreleasePool的底层原理,可以通过在objc源码中分析这两个函数的实现逻辑来得知。

    源码中对AutoreleasePool的描述

    在源码中,对AutoreleasePool作了简单的描述,有助于后面分析其结构。

    Autorelease pool implementation
    
    - A thread's autorelease pool is a stack of pointers.
    线程的自动释放池是指针的堆栈。
    
    - Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
    每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵对象,代表一个 autoreleasepool 的边界);
    
    - A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
    一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被释放。
    
    - The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
    这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
    
    - Thread-local storage points to the hot page, where newly autoreleased objects are stored.
    Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。
    

    通过描述,可知:

    • 自动释放池是一个栈的结构,被划分成了一个以page为结点的双向链表,根据需要添加和删除页面。
    • 每一页里存储着指向自动释放的对象或者pool_boundary哨兵的指针。
    • hot page表示当前活跃的页,存储新添加的自动释放的对象。
    • 自动释放池和线程有关联。

    分析自动释放池每页的结构

    在源码中,objc_autoreleasePoolPushobjc_autoreleasePoolPop的实现如下:

    void *
    objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    NEVER_INLINE
    void
    objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    可以看到,两个函数实际上是调用AutoreleasePoolPagepushpop方法,这个AutoreleasePoolPage就是自动释放池列表里的页。

    AutoreleasePoolPage
    //************宏定义************
    #define PROTECT_AUTORELEASEPOOL 0
    #define PAGE_MIN_SHIFT          12
    #define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)   //4096
    #define PAGE_MIN_MASK           (PAGE_MIN_SIZE-1)       //4095
    
    //************类定义************
    class AutoreleasePoolPage : private AutoreleasePoolPageData
    {
        friend struct thread_data_t;
    
    public:
        //页的大小,为PAGE_MIN_SIZE
        static size_t const SIZE =
    #if PROTECT_AUTORELEASEPOOL
            PAGE_MAX_SIZE;  // must be multiple of vm page size
    #else
            PAGE_MIN_SIZE;  // size and alignment, power of 2
    #endif
    
    private:
        
        ...
        static size_t const COUNT = SIZE / sizeof(id);
        
        //分配内存 
        static void * operator new(size_t size) {
            return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
        }
        //释放
        static void operator delete(void * p) {
            return free(p);
        }
    
        //构造函数
        AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
            AutoreleasePoolPageData(begin(),//开始存储的位置
                                    objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                    newParent,
                                    newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                    newParent ? newParent->hiwat : 0)
        {...}
        
        //析构函数
        ~AutoreleasePoolPage() {...}
    
        //杀掉
        void kill() {...}
    
        //other private funcs  
        ...  ...
    
    public:
    
        //自动释放
        static inline id autorelease(id obj){...}
    
        //入栈
        static inline void *push() {...}
    
        //出栈
        static inline void
        pop(void *token){...}
       
        //other public funcs
        ... ...    
    }
    

    AutoreleasePoolPage的结构中可以看出:

    • 每一页的大小为4096字节。
    • AutoreleasePoolPage继承自AutoreleasePoolPageData
    AutoreleasePoolPageData
    struct AutoreleasePoolPageData
    {
        //用来校验AutoreleasePoolPage的结构是否完整
        magic_t const magic;   //16字节
        //下次新添加的autoreleased对象的位置,初始化时指向begin()
        __unsafe_unretained id *next;   //8字节
        //当前线程
        pthread_t const thread;   //8字节
        //指向父节点,即上一个页面,第一个页面的parent值为nil
        AutoreleasePoolPage * const parent;   //8字节
        //指向子节点,即下一个页面,最后一个页面的child值为nil
        AutoreleasePoolPage *child;   //8字节
        //表示页面深度,从0开始,往后递增1
        uint32_t const depth;   //4字节
        //high water mark,表示最大入栈数量标记
        uint32_t hiwat;   //4字节
    
        //初始化
        AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
            : magic(), next(_next), thread(_thread),
              parent(_parent), child(nil),
              depth(_depth), hiwat(_hiwat)
        {
        }
    };
    
    //总共16字节
    struct magic_t {
           
        //静态变量不占用结构体的内存
        static const uint32_t M0 = 0xA1A1A1A1;
    #   define M1 "AUTORELEASE!"
        static const size_t M1_len = 12;
    
        //数组里有4个uint32_t类型的成员,总共16字节
        uint32_t m[4]; 
          
        //其他函数
        ... ...
    };
    

    AutoreleasePoolPageData结构中可知:

    • 结构体内存大小为56字节。
    • 结构体里存储了当前线程、下次新添加的对象的位置、父节点、子节点、页面深度、最大入栈数量等信息。

    由于自动释放池里的每一页都是一个AutoreleasePoolPage对象,AutoreleasePoolPage又继承自AutoreleasePoolPageData,因此可以得知,自动释放池每一页的结构如下:

    • 每一页内存大小为4096字节,页本身成员占用其中56字节的内存。
    • 每一页里都存储了next指针,指向下次新添加的autoreleased对象的位置。
    • 每一页里都包含父节点和子节点,分别指向上一页和下一页。第一页的父节点为nil,最后一页的子节点为nil
    • 每一页都有一个深度标记,第一页深度值为0,后面的页面递增1。
    • 每一页里还包当前线程、最大入栈数量。

    查看自动释放池的内存情况

    上面分析了page的结构,接下来通过具体Demo来打印自动释放池的内存。

    • 首先需要将Demo设置为MRC模式。将工程里的Build Settings -> Objectice-C Automatic Reference Counting,设置为NO

    • main函数里创建AutoreleasePool,添加自动释放的对象,并打印内存。

    //打印AutoreleasePool
    extern void _objc_autoreleasePoolPrint(void);
    
    int main(int argc, char * argv[]) {
        
        @autoreleasepool {
            
            //往自动释放池里添加count数量的对象
            NSInteger count = 对象数量;    
            for(NSInteger i=0; i<count; i++){
                
                NSObject *obj = [[NSObject alloc] autorelease];
            }
            
            //调用
            _objc_autoreleasePoolPrint();
        }
    
        return 0;
    }
    
    • 设置count = 3,即往释放池里添加3个对象,打印结果如下
    objc[85067]: ##############
    objc[85067]: AUTORELEASE POOLS for thread 0x1113a2dc0
    objc[85067]: 4 releases pending.
    //page地址,第一页的地址即为自动释放池的地址
    objc[85067]: [0x7fbfa380b000]  ................  PAGE  (hot) (cold)
    //哨兵
    objc[85067]: [0x7fbfa380b038]  ################  POOL 0x7fbfa380b038     
    //添加的3个对象,存放的是对象的地址指针
    objc[85067]: [0x7fbfa380b040]    0x600003e2c020  NSObject
    objc[85067]: [0x7fbfa380b048]    0x600003e2c030  NSObject
    objc[85067]: [0x7fbfa380b050]    0x600003e2c040  NSObject
    objc[85067]: ##############
    

    从打印结果可知,当往自动释放池里添加3个对象时:

    • 当前创建了一页page,为hot page
    • 哨兵地址和page地址相隔0x38,共56字节,说明哨兵前面存储的是AutoreleasePoolPage自身的成员。
    • page里存储的指针即为哨兵和自动释放对象的地址,为8字节。哨兵表示自动释放对象的边界。
    • 地址以0x7开头,说明自动释放池存储在栈区,page内部地址从低到高依次存储:AutoreleasePoolPage自身的成员哨兵自动释放的对象

    每页page的大小为4096字节,减去自身成员占用的56字节,能存储的指针数量为:(4096 - 56) / 8 = 505个,这些指针指向的是哨兵和自动释放对象的地址。

    • 设置count = 505 * 2,多往释放池里添加一些对象,再打印,结果如下
    objc[92734]: ##############
    objc[92734]: AUTORELEASE POOLS for thread 0x117e48dc0
    objc[92734]: 1011 releases pending.
    //第1页,page成员 + 1个哨兵 + 504个对象
    objc[92734]: [0x7fee2d00f000]  ................  PAGE (full)  (cold)
    objc[92734]: [0x7fee2d00f038]  ################  POOL 0x7fee2d00f038
    objc[92734]: [0x7fee2d00f040]    0x600001198030  NSObject
    objc[92734]: [0x7fee2d00f048]    0x600001198050  NSObject
    objc[92734]: [0x7fee2d00f050]    0x600001198060  NSObject
    ...  ...
    objc[92734]: [0x7fee2d00ffe8]    0x600001199f90  NSObject
    objc[92734]: [0x7fee2d00fff0]    0x600001199fa0  NSObject
    objc[92734]: [0x7fee2d00fff8]    0x600001199fb0  NSObject
    //第2页,page成员 + 0个哨兵 + 505个对象
    objc[92734]: [0x7fee2d011000]  ................  PAGE (full)     
    objc[92734]: [0x7fee2d011038]    0x600001199fc0  NSObject
    objc[92734]: [0x7fee2d011040]    0x600001199fd0  NSObject
    objc[92734]: [0x7fee2d011048]    0x600001199fe0  NSObject
    ... ...
    objc[92734]: [0x7fee2d011fe8]    0x60000119bf20  NSObject
    objc[92734]: [0x7fee2d011ff0]    0x60000119bf30  NSObject
    objc[92734]: [0x7fee2d011ff8]    0x60000119bf40  NSObject
    //第3页,page成员 + 0个哨兵 + 1个对象
    objc[92734]: [0x7fee2d00b000]  ................  PAGE  (hot) 
    objc[92734]: [0x7fee2d00b038]    0x60000119bf50  NSObject
    objc[92734]: ##############
    

    从这个打印结果可知,当往自动释放池里添加505 * 2个对象时,当前总共创建了三页page

    • 第1页,内部存放:page成员 + 1个哨兵 + 504个对象,状态为full page
    • 第2页,内部存放:page成员 + 505个对象,状态为full page
    • 第3页,内部存放:page成员 + 1个对象,状态为hot page

    因此,一个自动释放池只有一个哨兵,存放在第一页。每页最多存放505个指针,第一页满状态存放的是1个哨兵和504个对象地址,其他页存放的是505个对象地址。存满的页被标记为full page,正在操作的页被标记为hot page

    综上所述

    AutoreleasePool的结构

    经过分析,AutoreleasePool的内存结构如上图所示,特点如下:

    • 自动释放池是一个栈的结构,是一个以AutoreleasePoolPage为结点的双向链表,根据需要来动态添加或删除页面。

    • 每一页AutoreleasePoolPage的大小为4096字节,地址从低到高依次存储page自身成员哨兵对象指针。其中,自身成员占用56字节,且哨兵作为对象指针的边界,在释放池里只会有一个,因此:

      • 第一页,内部存放:page成员 + 1个哨兵 + 504个对象指针
      • 其它页,内部存放:page成员 + 505个对象指针
    • 已存满的页面被标记为full page,当前正在操作的页被标记为hot page

    • AutoreleasePoolPage继承自AutoreleasePoolPageData,内部成员情况如下:

      • magic:用来校验AutoreleasePoolPage的结构是否完整。
      • next :下次新添加的autoreleased对象的位置,初始化时指向begin()
      • thread:当前线程,说明自动释放池和线程有关联。
      • parent :指向父节点,即上一个页面,第一个页面的parent值为nil
      • child:指向子节点,即下一个页面,最后一个页面的child值为nil
      • depth :表示页面深度,从0开始,往后递增1。
      • hiwat :即high water mark,表示最大入栈数量标记

    相关文章

      网友评论

        本文标题:iOS原理 AutoreleasePool的结构分析

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