内存管理-AutoreleasePool

内存管理-AutoreleasePool
Photo by Marius Niveri / Unsplash

AutoreleasePool(自动释放池)是Objective-C中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。

@autoreleasepool { ... }可以通过 clang -rewrite-objc 命令转成C++代码如下

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

}

然后可以从objc源码中找到__AtAutoreleasePool的定义

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

这个结构体先通过构造函数构(__AtAutoreleasePool)创建一个栈变量__autoreleasepool,然后在大括号结束的时候通过析构函数(~__AtAutoreleasePool)销毁__autoreleasepool变量,这里利用了变量声明和自动变量在代码块结束后自动销毁的特性。所以@autoreleasepool { ... }相当于以下代码

{
   atautoreleasepoolobj = objc_autoreleasePoolPush();
   //AutoreleasePool中的其他代码
   objc_autoreleasePoolPop(atautoreleasepoolobj);
}

这里分别调用了 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 函数。在源码中可以找到函数实现

void *objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

可以看到这里分别调用了AutoreleasePoolPage的push和pop类方法。而AutoreleasePoolPage继承于AutoreleasePoolPageData的结构体。

struct AutoreleasePoolPageData
{
	magic_t const magic;
	__unsafe_unretained id *next;
	pthread_t const thread;
	AutoreleasePoolPage * const parent;
	AutoreleasePoolPage *child;
	uint32_t const depth;
	uint32_t hiwat;

	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)
	{
	}
};
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    //省略
}

下面看下AutoreleasePoolPageData每一个成员变量的作用

magic:magic_t类型,是用来检查 AutoreleasePoolPage的内存没有损坏

struct magic_t {
	static const uint32_t M0 = 0xA1A1A1A1;
#   define M1 "AUTORELEASE!"
	static const size_t M1_len = 12;
	uint32_t m[4];

	magic_t() {
		OBJC_ASSERT(M1_len == strlen(M1));
		OBJC_ASSERT(M1_len == 3 * sizeof(m[1]));

		m[0] = M0;
		strncpy((char *)&m[1], M1, M1_len);
	}

	~magic_t() {
		volatile uint64_t *p = (volatile uint64_t *)m;
		p[0] = 0; p[1] = 0;
	}
}

可以看出构造的时候

m[0]= 0xA1A1A1A1 
m[1-3] = AUTORELEASE!

检查的时候方法如下,判断第一个字节是0xA1A1A1A1, 后面三个字节是AUTORELEASE! 如下

bool check() const {
    return (m[0] == M0 && 0 == strncmp((char *)&m[1], M1, M1_len));
}

next:类型是 __unsafe_unretained id *,存放的是下一个被 autorelease 对象的指针存放的地址。

thread:自动释放池对应的线程。

parentchild:用来保存前一个 AutoreleasePoolPage 和后一个 AutoreleasePoolPage,双向链表结构。

depth:链表长度

hiwat:一个在 DEBUG 时才有用的参数,表示最高记录过多少对象。

下面看下这些变量是怎么存储的,会发现AutoreleasePoolPage 重载了 new 操作符

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
static size_t const SIZE = PAGE_MIN_SIZE;

static void * operator new(size_t size) {
    return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}

这样一个新的AutoreleasePoolPage对象就需要 SIZE大小,可以看出SIZE的大小为4096,而AutoreleasePoolPage的成员变量大小加在一起也只有 56 字节,剩下多出的就是存储autorelease对象的地方。如下图

图片中 begin、end方法定义如下

id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this));
}

id * end() {
    return (id *) ((uint8_t *)this+SIZE);
}

接下来看AutoreleasePoolPage::push()方法,定义如下

static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

当slowpath(DebugPoolAllocation)的时候,会调用autoreleaseNewPage。也就是当Xcode中设置环境变量OBJC_DEBUG_POOL_ALLOCATION为YES的时候。

OPTION( DebugPoolAllocation,      OBJC_DEBUG_POOL_ALLOCATION,      "halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools")

EMPTY_POOL_PLACEHOLDER和POOL_BOUNDARY的定义如下

#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
#   define POOL_BOUNDARY nil

其他时候调用autoreleaseFast

static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page) return autoreleaseFullPage(obj, page);
    else return autoreleaseNoPage(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);
    }
}

两个方法不同的地方是,Debug的时候会返回一个全新的AutoreleasePoolPage。

看hotPage方法如下

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

这里是从tls中获取之前存入的page,tls是线程局部存储(Thread Local Storage)的缩写。如果是EMPTY_POOL_PLACEHOLDER的话,返回nil,后面会具体说明其作用。存入的方法如下

static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

继续看autoreleaseFullPage方法

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    ASSERT(page == hotPage());
    ASSERT(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

意思是,初始化个Page,添加为现有Page的子节点,再设置成hotPage,然后添加对象。

最后是autoreleaseNoPage

    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

可以看出,因为push的时候传的是POOL_BOUNDARY,这里并没有初始化新的page,而是设置了个EMPTY_POOL_PLACEHOLDER。

static inline id* setEmptyPoolPlaceholder()
{
    ASSERT(tls_get_direct(key) == nil);
    tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
    return EMPTY_POOL_PLACEHOLDER;
}

等有再有autorelease对象进来的时候,先判断有没有占位符,然后再把初始化page并且设置为hotPage,并把POOL_BOUNDARY添加到其中,最后再添加对象到page中。

对象的autorelease最终是调用到AutoreleasePoolPage的autorelease方法

static inline id autorelease(id obj)
{
    ASSERT(!_objc_isTaggedPointerOrNil(obj));
    id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
    return obj;
}

可以看出,这里主要是调用了autoreleaseFast方法,更上面push的时候调用的方法是一样的,只不过这里obj是对象地址,push的时候是POOL_BOUNDARY。

最后就是pop方法

    static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }

这里面主要是通过token获取需要pop的Page,真正的操作在popPage方法,模板传值为false,所以去除Debug相关的逻辑

template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    page->releaseUntil(stop);
    if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

先看下releaseUntil方法中做了什么

void releaseUntil(id *stop) 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage

    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
        // create an obj with the zeroed out top byte and release that
        id obj = (id)entry->ptr;
        int count = (int)entry->count;  // grab these before memset
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        if (obj != POOL_BOUNDARY) {
            // release count+1 times since it is count of the additional
            // autoreleases beyond the first one
            for (int i = 0; i < count + 1; i++) {
                objc_release(obj);
            }
        }
    }
    setHotPage(this);
}

大概逻辑上,向下遍历,然后通过内存平移获取对象,判断如果不是哨兵对象,会调用objc_release的操作。最后设置当前页面为hotPage。

最后看下kill

void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

上面是把对象都释放了,这里把不需要的page设置为nil。

综上所述:

1、自动释放池的压栈和出栈,是通过结构体的构造函数和析构函数触发的,压栈调用objc_autoreleasePoolPush函数,出栈调用objc_autoreleasePoolPop函数。

2、自动释放池是一个存储指针的栈结构,指针是一个要释放的对象或者是POOL_BOUNDARY自动释放池边界。

3、池页大小为4096字节,每一页都包含56字节的成员变量,一个Page最多可以容纳 (4096-56) / 8 = 505个autorelease的对象。

可以写代码验证下,写之前要先把工程设置为MRC。

void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (int i = 0; i < 505; i++) {
            NSObject *obj = [[[NSObject alloc] init] autorelease];
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

输入结果如下

objc[11901]: [0x101010000]  ................  PAGE (full)  (cold)
objc[11901]: [0x101010038]  ################  POOL 0x101010038
objc[11901]: [0x101010040]       0x100c277e0  NSObject
objc[11901]: [0x101010048]       0x100c272e0  NSObject
objc[11901]: [0x101010050]       0x100c26ba0  NSObject
objc[11901]: [0x101010058]       0x100c26160  NSObject
......
objc[11927]: [0x10100d000]  ................  PAGE  (hot) 
objc[11927]: [0x10100d038]       0x100ab2920  NSObject

可以看出,第一个是POOL_BOUNDARY,505个对象循环加入到page中,最后一个的时候page满了,所以最后一个加到了新的page中。

Read more

Category和关联对象

Category和关联对象

Category也就是分类,可以在没有类定义的情况下为现有类添加方法、属性、协议,众所周知是不能增加成员变量的,但是是可以通过关联对象给类增加的。 struct category_t { const char *name; classref_t cls; WrappedPtr instanceMethods; WrappedPtr classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsF

By LEMON
如何为macOS应用开发插件

如何为macOS应用开发插件

本文章只用于学习和交流,转载请注明出处。 玩过越狱手机的应该都了解,iOS的越狱插件是可以为系统或者应用添加一些特色的功能的。越狱插件的开发,已经有完整的工具链,并且插件的加载运行Substrate框架已经为我们做好。 更改应用的功能实现有两种方法,一种是通过反编译工具更改汇编代码后生成新的可执行文件。另一种是写一个动态库,然后注入到可执行文件。第一种适合简单的逻辑修改,毕竟用汇编实现功能是需要很大的工作量的。 第二种要怎么注入呢?了解Mach-O 二进制文件的应该知道,Mach-O 二进制文件Load Commands中的 LC_LOAD_DYLIB 标头告诉 macOS在执行期间要加载哪些动态库 (dylib)。所以我们只需要在二进制文件中添加一条LC_LOAD_DYLIB就可以。而insert_dylib工具已经为我们实现了添加的功能。所以现在唯一需要考虑的就是插件如何开发。 废话不多说,现在就以mac版的迅雷为例开发一款属于我们自己的插件。首先下载最新版本的迅雷(版本5.0.2)。然后用Xcode新建一个macOS下的Framework,如下 新建完目录结构如下,如果

By LEMON
深入分析objc_msgSend尾调用优化

深入分析objc_msgSend尾调用优化

无意间看到了这篇文章 iOS objc_msgSend尾调用优化详解,讲的是在Release模式下,当某函数的最后一项操作是调用另外一个函数,编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack),以实现栈帧复用。 栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。所以在每一次函数调用之前,需要保存下执行的环境,以确保正常返回到调用位置。下面是- (void)viewDidLoad方法的汇编代码 可以看出在调用[super viewDidLoad]之前,先申请栈空间,然后保存x29和x30寄存器的值,调用结束后恢复寄存器的值,然后平栈。x29寄存器也就是fp寄存器,保存栈底的地址。x30寄存器也就是lr寄存器,保存返回地址。那栈帧复用又是怎么实现的呢?本着知其然知其所以然的精神,深入研究下。用下文章中的例子 - (NSInteger)func1:(NSInteger)num{ if (num >= 2000000) { return num; } return

By LEMON
深入分析弱引用(__weak)

深入分析弱引用(__weak)

__weak 在开发中经常用到,主要是为了解决内存管理中的循环引用。__weak修饰的指针特性是其指向的对象销毁后,会自动置为 nil。 用到__weak基本上分两种情况,一种是先声明再赋值(@property (weak)也是这种),另一种是声明的时候赋值。 NSObject *obj = [NSObject new]; __weak id weak; weak = obj; __weak id weak1 = obj; 新建个项目,在项目中写入以上代码,然后再终端输入以下命令编译成C++源码 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 "替换为写代码的文件" 可得到以下内容,发现__weak 对应的类型属性都为__attribute((objc_ownership(

By LEMON
京ICP备15024336号-4