【IOS 知识点】内存管理

IOS 内存管理

1.垃圾回收机制 和引用计数机制

  • 垃圾回收机制:定期查找不再使用的对象,释放对象占用的内存
  • 引用计数机制:采用引用计数来管理对象的内存,当需要持有一个对象时,使它的引用计数 +1;当不需要持有一个对象的时候,使它的引用计数 -1;当一个对象的引用计数为 0,该对象就会被销毁

2. 五大内存区域

1.1栈区
创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区
里面的变量通常是局部变量函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

@interface TestObject()

@end
@implementation TestObject
- (void)testMethodWithName:(NSString *)name
{
   //方法参数name是一个指针,指向传入的参数指针所指向的对象内存地址。name是在栈中
  //通过打印地址可以看出来,传入参数的对象内存地址与方法参数的对象内存地址是一样的。但是指针地址不一样。
  NSLog(@"name指针地址:%p,name指针指向的对象内存地址:%p",&name,name);


  //*person 是指针变量,在栈中, [Person new]是创建的对象,放在堆中。
  //person指针指向了[Person new]所创建的对象。
  //那么[Person new]所创建的对象的引用计数器就被+1了,此时[Person new]对象的retainCount为1
  Person *person = [Person new];
}

1.2堆区
就是那些由 new alloc 创建的对象所分配的内存块,它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0时系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个 release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。

//alloc是为Person对象分配内存,init是初始化Person对象。本质上跟[Person new]一样。
Person *person = [[Person alloc] init];

1.3全局/静态存储区
全局变量静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,
未初始化的全局变量与静态变量在相邻的另一块区域,
同时未被初始化的对象存储区可以通过 void* 来访问和操纵,
程序结束后由系统自行释放),在 C++ 里面没有这个区分了,
他们共同占用同一块内存区。

1.4常量存储区
这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。一般值都是放在这个地方的

1.5代码区
存放函数的二进制代码

NSString *string1;//string1 这个NSString 类型的指针,未初始化存在于<全局区>的<BBS区>

NSString *string2 = @"1234";//string2 这个NSString类型的指针,已初始化存在与<全局区>的<data数据区>,@“1234”存在与堆区,因为@代表了对象。 

static NSString *string3;//string3 这个NSString 类型的指针存在于<全局区>的<BBS区>

static NSString *string4 = @"1234";//string4 这个NSString类型的指针存在与<全局区>的<data数据区>,@“1234”存在与堆区,因为@代表了对象。stiring2和string4的值地址是一样的

static const NSString *string5 = @"654321";//const 修饰后  string5不能修改值。 其他的与string4一样

- (void)test
{
int  a;//a这个int类型的变量 是存在与<栈区>的
a = 10;//10这个值是存在与 <常量区>的

NSStirng *str;//str这个NSString类型的指针 存在于<栈区>
str = @“1234”;//@“1234”这个@对象存在于 <堆区>

static NSString *str1;//str1这个NSString类型的指针 存在于<全局区>的<BBS区>
static NSString *str2 = @"4321';//str2这个NSString类型的指针 存在于<全局区>的<data区>

NSString *str3;//str3这个NSString类型的指针 存在于<栈区>
str3 = [[NSString alloc]initWithString:@"1234"];//[[NSString alloc]initWithString:@"1234"]这个对象 存在于<堆区>

}

引用计数的存储

Objective-C 中的 “对象” 通过引用计数功能来管理它的内存生命周期。那么,对象的引用计数是如何存储的呢?它存储在哪个数据结构里?

isa指针用来维护对象和类之间的关系,并确保对象和类能够通过isa指针找到对应的方法、实例变量、属性、协议等;

在 arm64 架构之前,isa就是一个普通的指针,直接指向objc_class,存储着ClassMeta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;

从 arm64 架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。将 64 位的内存数据分开来存储着很多的东西,其中的 33 位才是拿来存储classmeta-class对象的内存地址信息。要通过位运算将isa的值& ISA_MASK掩码,才能得到classmeta-class对象的内存地址。

// objc.h
struct objc_object {
    Class isa;  // 在 arm64 架构之前
};

// objc-private.h
struct objc_object {
private:
    isa_t isa;  // 在 arm64 架构开始
};

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes
# if __arm64__  // 在 __arm64__ 架构下
#   define ISA_MASK        0x0000000ffffffff8ULL  // 用来取出 Class、Meta-Class 对象的内存地址
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;  // 0:代表普通的指针,存储着 Class、Meta-Class 对象的内存地址
                                          // 1:代表优化过,使用位域存储更多的信息
        uintptr_t has_assoc         : 1;  // 是否有设置过关联对象,如果没有,释放时会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        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;  // 如果为1,代表引用计数过大无法存储在 isa 中,那么超出的引用计数会存储在一个叫 SideTable 结构体的 RefCountMap(引用计数表)散列表中
        uintptr_t extra_rc          : 19; // 里面存储的值是对象本身之外的引用计数的数量,retainCount - 1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
......  // 在 __x86_64__ 架构下
};

Autoreleasepool

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

整个 iOS 的应用都是包含在一个自动释放池的 Block 中。

@autoreleasepool {} 是语法糖,他的本质是一个结构体:

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

这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。所以替换后 main 函数是这样的:

int main(int argc, const char * argv[]) {
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();

        // do whatever you want

        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

AutoreleasePool 是什么

要解释他是个什么东西其实只要看下两个函数是怎么实现了:

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

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

源码面前了无秘密,原来 AutoreleasePool 内部是一个叫做 AutoreleasePoolPage 的数据结构哈!通过 push 和 pop 两个方法可以猜想可能是一个栈的数据结构。猜的不错哦,他是一个类似于栈的东西,但是是用一个双向链表实现的哈!

class AutoreleasePoolPage : private AutoreleasePoolPageData {

    // 对当前page的完整性的校验
    magic_t const magic;
    // 指向page中可以存放Autorelease对象的内存区域
    __unsafe_unretained id *next;
    // 保存了当前页所在的线程
    pthread_t const thread;
    // 双向链表的前一个节点指针
    AutoreleasePoolPage * const parent;
    // 双向链表的后一个节点的指针
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    id * begin();

    id * end();

    id *add(id obj);

    static inline AutoreleasePoolPage *hotPage();

    static inline AutoreleasePoolPage *coldPage();

    static inline void *push();

    static inline void pop(void *token);
}

每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000,也就是虚拟内存一页的大小)

从上面的图中可以看出每一个自动释放池就是一个由若干个 AutoreleasePoolPage 组成的双向链表。

AutoreleasePool 中的栈

假设我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:

其中有 56 bit (7个字节的空间)用于存储 AutoreleasePoolPage 的成员变量,剩下的字节都是用来存储加入到自动释放池中的对象的。其中 begin() 和 end() 可以快速获取这个分页栈上的头尾边界地址。next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:

AutoreleasePool 中的哨兵

上面的内存结构图里面有一个很特殊的对象:POOL_SENTINEL,我们把他叫做哨兵对象。这个哨兵对象是个非常有趣的设计思路,由于有了这个对象系统可以很方便的知道当一个嵌套的子作用域结束的时候,pop 要释放的对象到什么时候结束。

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL哨兵对象。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();

        // do whatever you want

        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

上面的 atautoreleasepoolobj 就是一个 POOL_SENTINEL。

而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 遇到的哨兵(POOL_SENTINEL):

objc_autoreleasePoolPush 方法

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

static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

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

原理也很简单,就是调用 AutoreleasePoolPage 的 push 方法,这个方法里面会向当前分页插入一个哨兵对象,插入之前会先查找下 hot 的那个分页(热分页),也就是当前活跃的那个分页,找到并且这个 hot 分页还没满,就往栈顶插入哨兵,满了的话就再创建一个新的分页(链表新的节点)。

objc_autoreleasePoolPop 方法

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

我们一般都会在这个方法中传入一个哨兵对象 POOL_SENTINEL,如下图一样释放对象:

autorelease 方法

如何正确理解 autorelease 消息,先来看一下这个消息的调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

当向一个对象发送一个 autorelease 消息时,系统就会将该对象的一个引用加到 AutoreleasePoolPage 中。有一点要注意的是,这个时候它仍然是个正当的对象(并没有真正释放掉,他的生命周期其实是被延长了),只有当自动释放池 pop 的时候(这个时候会收到 drain 消息),才会真正对它所管理的所有对象发送 release 消息,对象才真正被释放。

ARC 很好的帮我们做了所有的事情,所以大多数情况下既不用主动给对象发 Autorelease 消息,也不用显示的调用 AutoreleasePool,但是我们还是要知道这背后的所有黑幕哈。

SideTable

SideTable主要存放了OC对象的引用计数和弱引用相关信息

struct SideTable {
    spinlock_t slock;           // 自旋锁,防止多线程访问冲突
    RefcountMap refcnts;        // 对象引用计数map
    weak_table_t weak_table;    // 对象弱引用map

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

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

    // 锁操作 符合StripedMap对T的定义
    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);
};
SideTable的定义很清晰有三个成员:

● spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable。
● RefcountMap refcnts :以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。
● weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC weak功能实现的核心数据结构。

除了三个成员外,苹果为SideTable还写了构造和析构函数:

// 构造函数
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    //析构函数(看看函数体,苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误)
    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
  }

通过析构函数可以知道,SideTable是不能被析构的。

最后是一堆锁的操作,用于多线程访问SideTable, 同时,也符合我们上面提到的StripedMap中关于value的lock接口定义:

// 锁操作 符合StripedMap对T的定义
    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);

SideTables是一个64个元素长度的hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。
因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。

img

前面絮絮叨叨说了一大堆,其实真正现在才抛出本次讨论的问题。

1、如何实现的引用计数管理,控制加一减一和释放?

2、为何维护的weak指针防止野指针错误?

三、数据结构分析( SideTables、RefcountMap、weak_table_t)

咱们先来讨论最顶层的SideTables

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。

因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables,所以不能对整个Hash表加锁。苹果采用了分离锁技术。

分离锁和分拆锁的区别

降低锁竞争的另一种方法是降低线程请求锁的频率。分拆锁 (lock splitting) 和分离锁 (lock striping) 是达到此目的两种方式。相互独立的状态变量,应该使用独立的锁进行保护。有时开发人员会错误地使用一个锁保护所有的状态变量。这些技术减小了锁的粒度,实现了更好的可伸缩性。但是,这些锁需要仔细地分配,以降低发生死锁的危险。

如果一个锁守护多个相互独立的状态变量,你可能能够通过分拆锁,使每一个锁守护不同的变量,从而改进可伸缩性。通过这样的改变,使每一个锁被请求的频率都变小了。分拆锁对于中等竞争强度的锁,能够有效地把它们大部分转化为非竞争的锁,使性能和可伸缩性都得到提高。

分拆锁有时候可以被扩展,分成若干加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁。

因为是使用对象的内存地址当key所以Hash的分部也很平均。假设Hash表有n个元素,则可以将Hash的冲突减少到n分之一,支持n路的并发写操作。

SideTable

当我们通过SideTables[key]来得到SideTable的时候,SideTable的结构如下:

1,一把自旋锁。spinlock_t??slock;

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

它的作用是在操作引用技术的时候对SideTable加锁,避免数据错误。

苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁,我在这里只能说"双击666,老铁们! 没毛病!"

2,引用计数器 RefcountMap??refcnts;

对象具体的引用计数数量是记录在这里的。

这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map?其实苹果采用的是分块化的方法。

举个例子

假设现在内存中有16个对象。

0x0000、0x0001、...... 0x000e、0x000f

咱们创建一个SideTables[8]来存放这16个对象,那么查找的时候发生Hash冲突的概率就是八分之一。

假设SideTables[0x0000]和SideTables[0x0x000f]冲突,映射到相同的结果。

SideTables[0x0000] == SideTables[0x0x000f]  ==> 都指向同一个SideTable复制代码

苹果把两个对象的内存管理都放到里同一个SideTable中。你在这个SideTable中需要再次调用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)来找到他们真正的引用计数器。

这里是一个分流。内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

引用计数器的存储结构如下

注意这里讨论的是table.refcnts.find(this)得到的value的结构,至于RefcountMap是什么结构我们在下一篇文章中讨论

引用计数器的数据类型是:

typedef __darwin_size_t        size_t;复制代码

再进一步看它的定义其实是unsigned long,在32位和64位操作系统中,它分别占用32和64个bit。

苹果经常使用bit mask技术。这里也不例外。拿32位系统为例的话,可以理解成有32个盒子排成一排横着放在你面前。盒子里可以装0或者1两个数字。我们规定最后边的盒子是低位,左边的盒子是高位。

  • (1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001
  • (1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010

下面来分析引用计数器(图中右侧)的结构,从低位到高位。

  • (1UL<<0)????WEAKLY_REFERENCED

表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

  • (1UL<<1)????DEALLOCATING

表示对象是否正在被释放。1正在释放,0没有。

  • REAL COUNT

图中REAL COUNT的部分才是对象真正的引用计数存储区。所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四,因为真正的计数是从2^2位开始的。

  • (1UL<<(WORD_BITS-1))????SIDE_TABLE_RC_PINNED

其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。

3,维护weak指针的结构体 weak_table_t ??weak_table;

上面的RefcountMap??refcnts;是一个一层结构,可以通过key直接找到对应的value。而这里是一个两层结构。

第一层结构体中包含两个元素。

第一个元素weak_entry_t *weak_entries;是一个数组,上面的RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry。

(上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)

第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。

第二层weak_entry_t的结构包含3个部分

1,referent:

被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。

2,referrers

可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。

3,inline_referrers

只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

OK大家来看着图看着伪代码走一遍流程

1,alloc

这时候其实并不操作SideTable,具体可以参考:

深入浅出ARC(上)

Objc使用了类似散列表的结构来记录引用计数。并且在初始化的时候设为了一。

2,retain: NSObject.mm line:1402-1417

//1、通过对象内存地址,在SideTables找到对应的SideTable
SideTable& table = SideTables()[this];

//2、通过对象内存地址,在refcnts中取出引用计数
//这里是table是SideTable、refcnts是RefcountMap
size_t& refcntStorage = table.refcnts[this];

//3、判断PINNED位,不为1则+4
if (! (refcntStorage & PINNED)) {
    refcntStorage += (1UL<<2);
}复制代码

3,release NSObject.mm line:1524-1551

table.lock();
引用计数器 = table.refcnts.find(this);
//table.refcnts.end()表示使用一个iterator迭代器到达了end()状态
if (引用计数器 == table.refcnts.end()) {
    //标记对象为正在释放
    table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (引用计数器 < SIDE_TABLE_DEALLOCATING) {
    //这里很有意思,当出现小余(1UL<<1) 的情况的时候
    //就是前面引用计数位都是0,后面弱引用标记位WEAKLY_REFERENCED可能有弱引用1
    //或者没弱引用0

    //为了不去影响WEAKLY_REFERENCED的状态
    引用计数器 |= SIDE_TABLE_DEALLOCATING;
} else if ( SIDE_TABLE_RC_PINNED位为0) {
    引用计数器 -= SIDE_TABLE_RC_ONE;
}
table.unlock();
如果做完上述操作后如果需要释放对象,则调用dealloc复制代码

4,dealloc NSObject.mm line:1555-1571

dealloc操作也做了大量了逻辑判断和其它处理,咱们这里抛开那些逻辑只讨论下面部分sidetable_clearDeallocating()

SideTable& table = SideTables()[this];
table.lock();
引用计数器 = table.refcnts.find(this);
if (引用计数器 != table.refcnts.end()) {
    if (引用计数器中SIDE_TABLE_WEAKLY_REFERENCED标志位为1) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //从refcnts中删除引用计数器
    table.refcnts.erase(it);
}
table.unlock();复制代码

weak_clear_no_lock()是关键,它才是在对象被销毁的时候处理所有弱引用指针的方法。

weak_clear_no_lock objc-weak.mm line:461-504

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //1、拿到被销毁对象的指针
    objc_object *referent = (objc_object *)referent_id;

    //2、通过 指针 在weak_table中查找出对应的entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p
", referent);
        return;
    }

    //3、将所有的引用设置成nil
    weak_referrer_t *referrers;
    size_t count;

    if (entry->out_of_line()) {
        //3.1、如果弱引用超过4个则将referrers数组内的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        //3.2、不超过4个则将inline_referrers数组内的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }

    //循环设置所有的引用为nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.
", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }

    //4、从weak_table中移除entry
    weak_entry_remove(weak_table, entry);
}复制代码

weak 原理

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

weak 的实现原理可以概括一下三步:

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

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

推荐阅读更多精彩内容

  • 一、说一下悬垂指针、野指针的区别 垂悬指针指针指向的内存已经释放,但是指针还存在,这就是 垂悬指针 或者 迷途指针...
    枫叶无处漂泊阅读 620评论 0 9
  • indexed标识isa是否仅仅为一个内存指针,如果为1的话就仅是一个内存指针,如果为0的话则意味着内存的64位不...
    木子奕阅读 1,079评论 0 2
  • iOS中内存管理机制是开发中一项很重要的知识,了解iOS中内存管理的规则不管是在开发中还是在学习中都能很大程度的帮...
    JQShan1993阅读 1,814评论 0 7
  • 文章目录 一.内存管理准则 二.属性内存管理修饰符全解析 三.block中的weak和strong 四.weak是...
    YouKnowZrx阅读 1,054评论 5 10
  • 如果有不好的地方或者不全面的地方请留言批评指正,拜谢~~~ 引发反思栈怎么清除?会引发什么状况?怎么使栈溢出?堆空...
    代码守望者阅读 301评论 0 1