函数和内存管理

OC每一个方法名是一个SEL ,根据SEL可以查找到函数体存放的内存地址IMP。查找的原理请参考iOS OC运行时详解

让我们跟着一个方法,一步步探究以下问题

  • 函数执行前后做了什么
  • 函数内部成员的内存如何管理
@interface TestObject :NSObject
@property (nonatomic , strong) NSArray *array;
@end

@implementation TestObject

- (instancetype)init
{
    if (self = [super init]) {
         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
            self.array = [NSArray array];
            NSLog(@"dispatch_after");

        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            self.array = [NSArray array];
            NSLog(@"dispatch_after main");
            
        });
    }
    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc %@",_array);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self testTemporaryVariable];
}

- (void)testTemporaryVariable
{
    TestObject *obj = [[TestObject alloc] init]; // 临时变量
    obj.array = [NSArray arrayWithObject:@"test"]; // 引用一次
    // 函数结束,obj会在什么时候释放?
}

@end

跟踪TestObject的dealloc调用栈发现objc_object::sidetable_release(bool)回调了dealloc

TestObject的dealloc调用栈

接下查看objc源码来看sidetable_release是做什么用的。

objc_object结构体中可以发现这个函数。说明每个OC对象都有
id sidetable_retain();
uintptr_t sidetable_release(bool performDealloc = true);
两个函数

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

// The order of these bits is important.
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)
// rdar://20206767
// return uintptr_t instead of bool so that the various raw-isa 
// -release paths all return zero in eax
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    // static StripedMap<SideTable>& SideTables() {
    //     return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    // }
    SideTable& table = SideTables()[this];// 找到当前对象的管理表,包含引用计数map和当前对象的弱引用表

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);// 引用计数表中以当前对象的地址为Key查找到当前对象的引用计数表
    if (it == table.refcnts.end()) { // 引用计数了
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.可能有弱引用,
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE; // 引用计数 -= SIDE_TABLE_RC_ONE
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {// 在此处调用dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

struct SideTable {
    spinlock_t slock;
// refcnt的详细解释
// ZeroValuesArePurgeable=true is used by the refcount table.
// A key/value pair with value==0 is not required to be stored   in the refcount table; it could correctly be erased instead.
// For performance, we do keep zero values in the table when the  true refcount decreases to 1: this makes any future retain faster.
// For memory size, we allow rehashes and table insertions to  remove a zero value as if it were a tombstone.
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

通过上面的分析大致能够了解sideTable_release(bool)判断了对象的引用计数,达到释放阈值并进行标记。未达到释放阈值的计数器-1,不调用dealloc函数。直到当前对象调用release,并达到阈值,才会被释放。
这里可以在上面的代码中得到证实。延时函数内部强引用了obj对象,直到dispatch_after的block执行完成后才会继续走dealloc.

根据现有知识,OC调用某函数,其实给这个函数地址发消息。比如:

  • ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);

阅读objc-msg-arm64.s源码可以发现, objc_msgSend的汇编实现。
GetClassFromIsa_p16根据对象的isa指针缓存中获取类,缓存不命中去执行常规查找,找到执行完,加入方法缓存。

下面分析执行sideTable_release函数之前干了什么。调用来源有下面三个

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
........
     }while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));
........
}

// Base release implementation, ignoring overrides.
// Does not call -dealloc.
// Returns true if the object should now be deallocated.
// This does not check isa.fast_rr; if there is an RR override then 
// it was already called and it chose to call [super release].
inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

inline bool 
objc_object::rootReleaseShouldDealloc()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(false);
}

可以猜想一个对象调用了release操作,引用计数达到了阈值,就会回调dealloc。

让我们继续思考。MRC下函数的局部成员,谁分配谁释放。ARC把这个过程集成到了Clang中。Objective-C Automatic Reference Counting (ARC).

  • 编译器到底什么时候把retain和release加入代码的?

这就涉及到llvm里讲的,所有权归属操作。

Methods in the `alloc`, `copy`, `init`, `mutableCopy`, and `new` [families](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-method-families) are implicitly marked `__attribute__((ns_returns_retained))`. This may be suppressed by explicitly marking the method `__attribute__((ns_returns_not_retained))`.

It is undefined behavior if the method to which an Objective-C message send statically resolves has different retain semantics on its result from the method it dynamically resolves to. It is undefined behavior if a block or function call is made through a static type with different retain semantics on its result from the implementation of the called block or function.
// 我们 通过这个简单的函数看一下app可执行文件的方法到底变成了什么
- (id)testTemporaryVariable
{
    TestObject *obj = [[TestObject alloc] init];
    obj.array = [NSArray arrayWithObject:@"test"];
    return obj;
}

工程run 成功后,hopper查看生成的可执行文件,找到对应方法如下图:

image.png

可以看出给obj赋值操作(setArray:)先调用了objc_retainAutoreleasedReturnValue方法,相当于帮我们retain一次,因为arc下的返回对象都是autorelease对象。该方法其实是对arrayWithObject:返回对象的引用计数管理,可以在NSObject.mm源码找到。如下

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

其实就是调用了objc_object::rootRetain(bool tryRetain, bool handleOverflow)方法。

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
   if (isTaggedPointer()) return (id)this;

   bool sideTableLocked = false;
   bool transcribeToSideTable = false;

   isa_t oldisa;
   isa_t newisa;

   do {
       transcribeToSideTable = false;
       oldisa = LoadExclusive(&isa.bits);
       newisa = oldisa;
       if (slowpath(!newisa.nonpointer)) {
           ClearExclusive(&isa.bits);
           if (!tryRetain && sideTableLocked) sidetable_unlock();
           if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
           else return sidetable_retain();
       }
.......
   } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
.......
}

这就走到了与sidetable_release()对应的sidetable_retain()方法。暂时略过这里。
函数最后执行了objc_storeStrong方法,相当于与调用了一次release。通过查看源码可以发现这个函数做了哪些操作。

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location; // 取出该地址的对象
    if (obj == prev) { // 对象没有变化return
        return;
    }
    objc_retain(obj); // retain新对象
    *location = obj; // 新对象存入该地址
    objc_release(prev);// 释放老对象
}

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.
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.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.

Autoreleasepool 是一个以栈为节点的双向链表结构。


class AutoreleasePoolPage 
{
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
// 双向链表结构,AutoreleasePoolPage为每个节点结构
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    // SIZE-sizeof(*this) bytes of contents follow

    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);
    }
......
}

在@autoreleasepool {}结构中alloc的对象会被加入自动释放池中

- (void)testAutoreleasePool
{
    @autoreleasepool {
        TestObject *obj = [[TestObject alloc] init];
    }
}

image.png

可见先调用了objc_autoreleasePoolPush 函数执行完又调用了objc_autoreleasePoolPop 。

是push入栈了什么?push操作发生了什么?

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);
        }
    }

    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

可见push的返回值是加在栈头的哨兵nil的地址,在这之后加入page的指针指向的对象都会在pop时释放掉。

那autoreleasepool和autorelease/runloop什么关系呢?
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
加上Autorelease Pool后就是出了作用域就释放。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容