0%

内存管理

内存管理方案

  1. ARC/MRC
  2. TarggedPointer: 专门用来处理小对象,比如NSNumber、NSDate、(NSString中有一种是targeed pointer)
  3. Nonpointer_isa:非指针类型的isa。主要用来优化64位地址。
  4. SideTables:散列表。主要有两种类型的表,引用计数表,弱引用表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// viewDidLoad添加
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
// alloc堆上,iOS优化之后变成 taggedpointer
// nameStr是NSTaggedPointerString
self.nameStr = [NSString stringWithFormat:@"aaa"];
NSLog(@"%@",self.nameStr);
});
}
}

// 点击
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
// nameStr是NSCFString
self.nameStr = [NSString stringWithFormat:@"aa-加油"];
NSLog(@"%@",self.nameStr);
});
}
}

运行上面的代码啊,发现在taggedPointerDemo方法中没有问题,但是点击屏幕,执行了touchesBegan就发生了崩溃。虽然在多线程时有过类似的例子,是由于多次释放造成的。但是这里的根本原因是nameStr在底层的类型不一致导致的,分别在两个赋值的方法处打上断点,看看是什么类型。

  • taggedPointerDemo方法中的nameStr类型是 NSTaggedPointerString,存储在常量区。因为nameStr在alloc分配时在堆区,由于较小,所以经过xcode中iOS的优化,成了NSTaggedPointerString类型,存储在常量区。
  • touchesBegan方法中的nameStr类型是 NSCFString类型,存储在堆上

NSString的类型

  1. NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区。
  2. NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上。
  3. NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。对于NSString对象来说,当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区。
  4. 当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区。

Tagged Pointer小对象

接下来看一下tagged pointer对象的引用计数相关逻辑。直接上源码:

void objc_setProperty -> reallySetProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

// copy和mutableCopy处理
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
// retain操作
newValue = objc_retain(newValue);
}

if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// release操作
objc_release(oldValue);
}

接下来我们看看retain和release内部做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
//判断是否是小对象,如果是,则直接返回对象
if (obj->isTaggedPointer()) return obj;
//如果不是小对象,则retain
return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
//如果是小对象,则直接返回
if (obj->isTaggedPointer()) return;
//如果不是小对象,则release
return obj->release();
}

如果是Tagged Pointer小对象,不会对引用计数做处理。

小对象地址分析

1
2
3
4
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"啊"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);

看一下输出结果:

1
2
0xd3c9816ac08c01c6-a
0x6000033631e0-啊

在类的加载时,其中的_read_images源码有一个方法对小对象进行了处理,即initializeTaggedPointerObfuscator方法。

查看一下源码:_read_images -> initializeTaggedPointerObfuscator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
// _OBJC_TAG_MASK 进行混淆
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}

全局搜索objc_debug_taggedpointer_obfuscator,找到了encode和decode方法。分别是对tagged pointer的编码和解码。

1
2
3
4
5
6
7
8
9
10
11
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

encode时进行了一次^操作,decode时也进行了一次^操作。可以对值进行还原。

那我们分别打印一下decode后的值:

1
2
3
4
5
6
7
NSString *str1 = [NSString stringWithFormat:@"a"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));

NSNumber *number1 = @1;
NSLog(@"%@-%p-%@",object_getClass(number1),number1,number1);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));

输出结果:

1
2
3
4
5
0xe4742f5bd16235e6-a
0xa000000000000611

__NSCFNumber-0xf4742f5bd16233e5-1
0xb000000000000012

在源码中有一个判断条件,是否为TaggedPointer:

1
2
3
4
5
6
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
//等价于 ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,即只是保留了最高位的值
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

所以0xa、0xb主要是用于判断是否是小对象taggedpointer,即判断条件,判断第64位上是否为1(taggedpointer指针地址即表示指针地址,也表示值)

0xa 转换成二进制为 1 010(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString类型

0xb 转换为二进制为 1 011(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的

这里可以通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString,3表示NSNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2, // NSString
OBJC_TAG_NSNumber = 3, // NSNumber
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,

// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,

// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,

OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,

OBJC_TAG_RESERVED_264 = 264
};

跟我们上面得到的结果是一样的。

总结

  • Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象指针不再是简单的地址,而是地址 + 值,即真正的值,它只是一个披着对象皮的普通变量而以。所以可以直接进行读取。优点是占用空间小,节省内存
  • Tagged Pointer小对象 不会进入retain 和 release,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收。
  • Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要malloc和free。
  • 对于NSString类型,建议直接使用@""初始化赋值。

SideTables 散列表

SideTables是一个hash表。在weak修饰时会存放在SideTables这个表中。

对于OC正常的对象来说当执行retain操作时,当引用计数达到一定的值(256)时,则会存放在SideTables中。

我们接下来看一下retain 的流程

retain流程

看一些源码:进入objc_retain -> retain -> rootRetain查看源码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
LWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;
//为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
//重点
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否为nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa,直接操作散列表sidetable
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//dealloc源码
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}


uintptr_t carry;
//执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断extra_rc是否满了,carry是标识符
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc
newisa.extra_rc = RC_HALF;
//给一个标识符为YES,表示需要存储到散列表
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

流程分析:

  1. isTaggedPointer直接返回。
  2. 不是nonpointer,直接存sideTable
  3. 是否正在释放,deallocating,返回nil
  4. 引用计数+1。
  5. 判断引用计数是否存满了,满了则变成一半,另一半存放在散列表中。

之所以不直接把引用计数存放在散列表中,是因为对表的操作,需要用到锁,这是耗时操作。
如果每一个对象都需要一个散列表,也会造成性能问题。如果所有对象公用一个散列表,则其他数据可能不安全,所以也不会公用一个表。真机上最多有8个表。

release流程

release流程与retain相反。

setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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;
//判断是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,则直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//进行引用计数-1操作,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此时extra_rc的值为0了,则走到underflow
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa;
//判断散列表中是否存储了一半的引用计数
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}

// Try to remove some retain counts from the side table.
//从散列表中取出存储的一半引用计数
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.

if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//进行-1操作,然后存储到extra_rc中
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
//此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
// Really deallocate.
//触发dealloc的时机
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

if (performDealloc) {
//发送一个dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

流程分析如下:

  1. 判断是否是Nonpointer isa,如果不是,则直接对散列表进行-1操作

  2. 如果是Nonpointer isa,则对extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中

  3. 如果此时的状态carray为0,则走到underflow流程

    underflow流程有以下几步:

    1. 判断散列表中是否存储了一半的引用计数,如果是,则从散列表中取出存储的一半引用计数,进行-1操作,然后存储到extra_rc中
    2. 如果此时extra_rc没有值,散列表中也是空的,则直接进行析构,即dealloc操作,属于自动触发

dealloc分析

dealloc是在retainCount为0时系统自动触发的。

dealloc -> _objc_rootDealloc -> rootDealloc查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline void
objc_object::rootDealloc()
{
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
if (isTaggedPointer()) return; // fixme necessary?

//如果没有这些,则直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//如果有
object_dispose((id)this);
}
}
  1. 根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
  2. 如果有,则进入object_dispose方法
1
2
3
4
5
6
7
8
9
10
11
id 
object_dispose(id obj)
{
if (!obj) return nil;
// 销毁实例而不会释放内存
objc_destructInstance(obj);
//释放内存
free(obj);

return nil;
}

objc_destructInstance为了消耗实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
  1. 在内部判断是否有析构函数,如果有则调用。
  2. 是否有关联对象,有的花移除关联对象。
  3. 执行clearDeallocating
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline void 
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是,则直接释放散列表
sidetable_clearDeallocating();
}
//如果是,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}

clearDeallocating的目的主要是为了清空散列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));

SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}

清空引用计数,情况弱引用表。

以上就是dealloc 的流程。

retainCount

1
2
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

上面输出的引用计数是多少?这是一个经典的面试题。

这里输出的结果是1。但是1是不对的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
👇
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);

return obj->rootRetainCount();
}
👇
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa,才有引用计数的下层处理
if (bits.nonpointer) {
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
//如果不是,则正常返回
sidetable_unlock();
return sidetable_retainCount();
}

当对象创建时,并没有存引用计数,bits.extra_rc还是0,当调用了retainCount时执行了1 + bits.extra_rc,所以就变成了1 。如果reatinCount=0,相当于创建成功之后就会被释放掉。

所以这里的答案应该是0。不管我有没有执行init操作,都是0 。在读的时候才会是1。