iOS的内存管理

0x01 Tagged Pointer

  当我们定义一个变量的时候,比如NSNumber *a = @123,内存中的分布如下

变量内存示意图

  这样就造成了资源浪费,定义一个变量,保存简单的123却用了16个字节,造成了内存碎片;而且为了维护它,还需要额外的开销用引用计数管理它。所以在64系统中,苹果为了解决这样的问题,提出了Tagged Pointer概念。

  Tagged Pointer直接将数据保存到指针中,其指针格式变为tag + data的形式,一般用于NSNumber、NSString、NSDate。我们以NSString为例,看下什么是Tagged Pointer。

  首先定义变量如下

1
2
3
4
5
NSString *a = [NSString stringWithFormat:@"%@", @"123"];
NSString *b = [NSString stringWithFormat:@"%@", @"1234567890123456"];

// 不能下面这样定义,这样定义的@"123"是常量
NSString *a = @"123";

  我们看到变量a的类型变为NSTaggedPointerString

NSTaggedPointerString示意图

  我们再打印下内存地址看下,很明显变量a的地址由33 + 32 + 31 + 字符串长度组成,其中3表示字符串类型的tag,后面的3、2、1就是保存的值。而变量b的地址很明显是个堆地址。

1
2
3
4
(lldb) p a
(NSTaggedPointerString *) $0 = 0xa000000003332313 @"123"
(lldb) p b
(__NSCFString *) $1 = 0x00000001d004b250 @"1234567890123456"

  那么,下面的代码会有什么问题?

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, copy) NSString *name;

- (void)viewDidLoad {
[super viewDidLoad];

for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123456789012"];
});
}
}

  我们会看到闪退,这是因为多线程环境下,调用setter方法的时候,可能多个线程同时进入到if语句内,导致同一个对象在引用计数只有1的情况下,release方法被重复调用,从而造成闪退。

1
2
3
4
5
6
- (void)setName:(NSString *)name {
if(_name != name) {
[_name release];
_name = [name retain];
}
}

  那么同样的,下面的代码会闪退吗?

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, copy) NSString *name;

- (void)viewDidLoad {
[super viewDidLoad];

for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123"];
});
}
}

  答案是不会,因为self.name此时是Tagged Pointer,不参与引用计数管理,其retainCount可以认为是个无限值,所以任由你release,能闪退算它输。

  我们可以通过CFGetRetainCount计算retainCount来证实下。

1
2
3
4
5
6
7
8
9
for (NSInteger i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"%@", @"123"];
CFIndex idx = CFGetRetainCount((__bridge CFTypeRef)(self.name));
});
}

// 打印的值
(CFIndex) idx = 9223372036854775807

  正是因为这样的特性,同样我们在下面的代码,打印变量a的值还是123,而不是nil

1
2
3
4
5
__weak NSString *a = nil;
@autoreleasepool {
a = [NSString stringWithFormat:@"%@", @"123"];
}
NSLog(@"%@", a);

  那么,我们看下系统是怎么判断是不是Tagged Pointer。

1
2
3
4
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) 
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

  对于宏_OBJC_TAG_MASK定义又如下

1
2
3
4
5
6
7
8
9
10
11
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL

// 对于OBJC_MSB_TAGGED_POINTERS定义如下
#if TARGET_OS_OSX && __x86_64__
# define OBJC_MSB_TAGGED_POINTERS 0 // macOS
#else
# define OBJC_MSB_TAGGED_POINTERS 1 // iOS或者模拟器
#endif

  所以我们可以看到,当时macOS系统的时候,就拿地址最后一位是不是1来判断是不是taggedPoint;而对于iOS来说,则判断最高位是不是为1,就拿之前例子里的0xa000000003332313为例,0xa表示为1010,最高位显然为1,所以是这个是TaggedPointer。

0x02 weak

  首先,看下如下测试代码

1
2
3
4
5
int main(int argc, char * argv[]) {
__weak NSObject *obj = [NSObject new];

return 0;
}

  接着,我们看下其汇编代码

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
testData`main:
0x102e1d808 <+0>: sub sp, sp, #0x40 ; =0x40
0x102e1d80c <+4>: stp x29, x30, [sp, #0x30]
0x102e1d810 <+8>: add x29, sp, #0x30 ; =0x30
0x102e1d814 <+12>: adrp x8, 9
0x102e1d818 <+16>: add x8, x8, #0xf8 ; =0xf8
0x102e1d81c <+20>: adrp x9, 8
0x102e1d820 <+24>: add x9, x9, #0xf38 ; =0xf38
0x102e1d824 <+28>: adrp x10, 7
0x102e1d828 <+32>: ldr x10, [x10, #0x68]
0x102e1d82c <+36>: add x11, sp, #0x18 ; =0x18
0x102e1d830 <+40>: stur wzr, [x29, #-0x4]
0x102e1d834 <+44>: stur w0, [x29, #-0x8]
0x102e1d838 <+48>: stur x1, [x29, #-0x10]
-> 0x102e1d83c <+52>: ldr x0, [x8]
0x102e1d840 <+56>: ldr x1, [x9]
0x102e1d844 <+60>: str x11, [sp, #0x10]
0x102e1d848 <+64>: blr x10
0x102e1d84c <+68>: ldr x8, [sp, #0x10]
0x102e1d850 <+72>: str x0, [sp, #0x8]
0x102e1d854 <+76>: mov x0, x8
0x102e1d858 <+80>: ldr x1, [sp, #0x8]
0x102e1d85c <+84>: bl 0x102e20e68 ; symbol stub for: objc_initWeak
0x102e1d860 <+88>: ldr x1, [sp, #0x8]
0x102e1d864 <+92>: mov x0, x1
0x102e1d868 <+96>: bl 0x102e20eb0 ; symbol stub for: objc_release
0x102e1d86c <+100>: stur wzr, [x29, #-0x4]
0x102e1d870 <+104>: ldr x0, [sp, #0x10]
0x102e1d874 <+108>: bl 0x102e20e14 ; symbol stub for: objc_destroyWeak
0x102e1d878 <+112>: ldur w0, [x29, #-0x4]
0x102e1d87c <+116>: ldp x29, x30, [sp, #0x30]
0x102e1d880 <+120>: add sp, sp, #0x40 ; =0x40
0x102e1d884 <+124>: ret

  可以发现,声明__weak后其内部实现过程中调用了objc_initWeakobjc_destroyWeak。但是如果测试例子再修改如下:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
__weak NSObject *obj = [NSObject new];
obj = [NSObject new];

return 0;
}

  汇编代码变化如下,发现再给__weak声明的变量重新赋值的时候调用的是objc_storeWeak

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
testData`main:
0x102cc5778 <+0>: sub sp, sp, #0x70 ; =0x70
0x102cc577c <+4>: stp x29, x30, [sp, #0x60]
0x102cc5780 <+8>: add x29, sp, #0x60 ; =0x60
0x102cc5784 <+12>: adrp x8, 9
0x102cc5788 <+16>: add x8, x8, #0xf8 ; =0xf8
0x102cc578c <+20>: adrp x9, 8
0x102cc5790 <+24>: add x9, x9, #0xf38 ; =0xf38
0x102cc5794 <+28>: adrp x10, 7
0x102cc5798 <+32>: ldr x10, [x10, #0x68]
0x102cc579c <+36>: sub x11, x29, #0x18 ; =0x18
0x102cc57a0 <+40>: stur wzr, [x29, #-0x4]
0x102cc57a4 <+44>: stur w0, [x29, #-0x8]
0x102cc57a8 <+48>: stur x1, [x29, #-0x10]
-> 0x102cc57ac <+52>: ldr x0, [x8]
0x102cc57b0 <+56>: ldr x1, [x9]
0x102cc57b4 <+60>: str x11, [sp, #0x30]
0x102cc57b8 <+64>: str x10, [sp, #0x28]
0x102cc57bc <+68>: str x8, [sp, #0x20]
0x102cc57c0 <+72>: str x9, [sp, #0x18]
0x102cc57c4 <+76>: blr x10
0x102cc57c8 <+80>: ldr x8, [sp, #0x30]
0x102cc57cc <+84>: str x0, [sp, #0x10]
0x102cc57d0 <+88>: mov x0, x8
0x102cc57d4 <+92>: ldr x1, [sp, #0x10]
0x102cc57d8 <+96>: bl 0x102cc8e44 ; symbol stub for: objc_initWeak
0x102cc57dc <+100>: ldr x1, [sp, #0x10]
0x102cc57e0 <+104>: mov x0, x1
0x102cc57e4 <+108>: bl 0x102cc8e8c ; symbol stub for: objc_release
0x102cc57e8 <+112>: ldr x8, [sp, #0x20]
0x102cc57ec <+116>: ldr x0, [x8]
0x102cc57f0 <+120>: ldr x9, [sp, #0x18]
0x102cc57f4 <+124>: ldr x1, [x9]
0x102cc57f8 <+128>: ldr x10, [sp, #0x28]
0x102cc57fc <+132>: blr x10
0x102cc5800 <+136>: str x0, [sp, #0x8]
0x102cc5804 <+140>: b 0x102cc5808 ; <+144> at main.m
0x102cc5808 <+144>: sub x8, x29, #0x18 ; =0x18
0x102cc580c <+148>: mov x0, x8
0x102cc5810 <+152>: ldr x1, [sp, #0x8]
0x102cc5814 <+156>: str x8, [sp]
0x102cc5818 <+160>: bl 0x102cc8ee0 ; symbol stub for: objc_storeWeak
0x102cc581c <+164>: ldr x1, [sp, #0x8]
0x102cc5820 <+168>: mov x0, x1
0x102cc5824 <+172>: bl 0x102cc8e8c ; symbol stub for: objc_release
0x102cc5828 <+176>: stur wzr, [x29, #-0x4]
0x102cc582c <+180>: ldr x0, [sp]
0x102cc5830 <+184>: bl 0x102cc8df0 ; symbol stub for: objc_destroyWeak
0x102cc5834 <+188>: ldur w0, [x29, #-0x4]
0x102cc5838 <+192>: ldp x29, x30, [sp, #0x60]
0x102cc583c <+196>: add sp, sp, #0x70 ; =0x70
0x102cc5840 <+200>: ret
0x102cc5844 <+204>: sub x8, x29, #0x18 ; =0x18
0x102cc5848 <+208>: mov x9, x1
0x102cc584c <+212>: stur x0, [x29, #-0x20]
0x102cc5850 <+216>: stur w9, [x29, #-0x24]
0x102cc5854 <+220>: mov x0, x8
0x102cc5858 <+224>: bl 0x102cc8df0 ; symbol stub for: objc_destroyWeak
0x102cc585c <+228>: ldur x0, [x29, #-0x20]
0x102cc5860 <+232>: bl 0x102cc8c94 ; symbol stub for: _Unwind_Resume

  查看源码比较下objc_initWeakobjc_storeWeak的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

id objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}

  虽然都是统一调用storeWeak函数,两者之间只有一个区分,就是objc_initWeak是没有旧值的,而objc_storeWeak是有旧值的,即DontHaveOldDoHaveOld的区别。

  最后会调用objc_destroyWeak函数进行销毁所有指向对象的弱引用对象。销毁的时候,告诉storeWeak函数没有新值。

1
2
3
4
5
void objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}

storeWeak

  我们直接来到源码,看下其实现

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

template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;

retry:
// 如果是旧值,拿到旧值的那个哈希表
if (haveOld) {
// *location 赋值给oldObj
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 如果是新值,拿到新值的那个哈希表
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 如果是旧值。之前*location赋值给过oldObj,如果不一致,说明有别的线程进来改过了,则重新尝试。
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// 如果是新值则需要判断有没有调用过initialized方法,如果没有这里进行调用
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));

previouslyInitializedClass = cls;

goto retry;
}
}

// 清理旧值,解除注册
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// 注册新值
if (haveNew) {
newObj = (objc_object *)
// newObject为所赋的值。比如例子中的[NSObject new], location为例子中的NSObject *obj
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);

// 设置isa的weakly_referenced位为true
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

*location = (id)newObj;
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

SlideTable

  定义如下

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
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; // 引用的数量
weak_table_t weak_table; // 弱引用表,保存着所有的关联着对象的弱引用地址

... ...
}

struct weak_table_t {
weak_entry_t *weak_entries; // 上面说的保存的地址就是保存在这里
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers; // 关联着对象的弱引用地址保存在这里
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}

weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

  具体解释是这样的,在我们的例子中__weak NSObject *obj = [NSObject new];[NSObject new]新构造了一个对象,这个对象有个SlideTable维护弱引用表,表里保存的就是NSObject *obj变量的地址。

weak_unregister_no_lock

  源码先行

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
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

weak_entry_t *entry;

if (!referent) return;

if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 解除引用关联,referer为引用对象的地址,即例子中的NSObject *obj变量的地址
remove_referrer(entry, referrer);
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
// 如果弱引用对象没有关联的对象了,表也可以删了
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
}

  具体如何解除引用过程如下,

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
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
......

size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 入参old_referrer在我们的例子中就是NSObject *obj变量的地址。然后查找表中的每个引用数据,如果找到的数据跟NSObject *obj变量的地址相等,就将其置为nil
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
entry->referrers[index] = nil;
entry->num_refs--;
}

weak_register_no_lock

  还是先看源码

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
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

if (!referent || referent->isTaggedPointer()) return referent_id;

// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}

if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}


weak_entry_t *entry;
// 结合例子来说,就是找到[NSObject new]新创建出对象的weak_entry_t,然后把NSObject *obj变量的地址加入到这个表中,这样做的目的就是,当[NSObject new]的对象销毁后,只要遍历这个对象里的弱引用表,就可以把所有指向这块内存的变量地址都可以置nil。
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}

return referent_id;
}

object_dispose

  当一个对象销毁时,会调用object_dispose函数

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
id object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
free(obj);

return nil;
}

void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// 是否c++,是的话调用析构函数
if (cxx) object_cxxDestruct(obj);
// 是否有关联属性,是的话移出关联属性
if (assoc) _object_remove_assocations(obj);
// 清理并销毁对象
obj->clearDeallocating();
}

return obj;
}

inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 如果是弱引用对象
clearDeallocating_slow();
}

assert(!sidetable_present());
}

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

  最终如果是弱引用对象会来到weak_clear_no_lock函数,里面遍历所有指向此对象的对象地址,然后都置为nil。

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
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
objc_object *referent = (objc_object *)referent_id;

weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
return;
}

weak_referrer_t *referrers;
size_t count;

if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}

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.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}

weak_entry_remove(weak_table, entry);
}

0x03 Copy

NSString

  首先,看下NSString相关的例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
NSString *a = [NSString stringWithFormat:@"1234567890"];
NSString *b = [a copy];
NSString *c = [a mutableCopy];

NSLog(@"%p -- %p -- %p", a, b, c);

return 0;
}

// 打印出的结果
0x1d02202c0 -- 0x1d02202c0 -- 0x1d0445cd0

  我们发现对于不可变的NSStringcopy出来的是不可变对象,并且是浅拷贝,因为地址相同;mutableCopy出来的是可变对象,而且是深拷贝,地址都不同了。

NSMutableString

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char * argv[]) {
NSMutableString *a = [[NSMutableString alloc] initWithString:@"1234567890"];
NSString *b = [a copy];
NSMutableString *c = [a mutableCopy];

NSLog(@"%p -- %p -- %p", a, b, c);

return 0;
}

// 打印出的结果
0x1d425d5e0 -- 0x1d422bfa0 -- 0x1d425d5b0

  对于可变的NSMutableStringcopy出来的是不可变对象,并且是深拷贝;mutableCopy出来的是可变对象,但也是深拷贝。

  NSArrayNSMutableArrayNSDictionayNSMutableDictionary其实也类似,具体例子不表述了,总结规律如下:

  • 不可变对象的copy出来的是不可变对象,且是浅拷贝;mutableCopy的是可变对象,且是深拷贝。
  • 可变对象的copy出来的是不可变对象,但是深拷贝;mutableCopy的是可变对象,且也是深拷贝

0x04 NSProxy

  在定时器相关的初始化方法中,如果使用的是下面这样的初始化方法,那么由于控制器self本身强引用定时器,而target又会把self传入定时器内部,而在定时器内部又会对self做强引用,这样就造成了循环引用。

1
2
3
self.timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerFire) userInfo:nil repeats:YES];

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFire)];

  解决方法,使用NSProxy,通过这个中间介打破强引用关系,然后再利用消息转发,将方法的实现还是转发回原来的target上。

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
@interface MyProxy : NSProxy

@property (nonatomic, weak) id target;

+ (id)proxyWithTarget:(id)target;

@end

@implementation MyProxy

+ (id)proxyWithTarget:(id)target {
MyProxy *proxy = [MyProxy alloc];
proxy.target = target;
return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

// 使用自定义的Proxy
self.timer = [NSTimer timerWithTimeInterval:1.f target:[MyProxy proxyWithTarget:self] selector:@selector(timerFire) userInfo:nil repeats:YES];

self.displayLink = [CADisplayLink displayLinkWithTarget:[MyProxy proxyWithTarget:self] selector:@selector(displayLinkFire)];

  那么为什么不是继承NSObject呢,这是因为这样就会走一遍消息转发流程,具体在runtime篇已经讲过,效率没有直接使用NSProxy高。

  当然,这样的使用场景也不止在定时器上,别的地方遇到类似的互相强引用,也可以使用这样的方法解决。

0x05 autoreleasepool

  我们的示例代码如下

1
2
3
4
5
6
7
8
int main(int argc, char * argv[]) {
@autoreleasepool {
for (NSInteger i = 0; i < 100; i++) {
NSObject *obj = [NSObject new];
}
}

return 0;

  转换汇编后,我们可以看到@autoreleasepool实际会被转换为objc_autoreleasePoolPushobjc_autoreleasePoolPop两个函数,而@autoreleasepool中间的代码则被包在这两个函数中间。

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
testData`main:
0x10019d7d0 <+0>: sub sp, sp, #0x70 ; =0x70
0x10019d7d4 <+4>: stp x29, x30, [sp, #0x60]
0x10019d7d8 <+8>: add x29, sp, #0x60 ; =0x60
0x10019d7dc <+12>: mov x8, #0x0
0x10019d7e0 <+16>: adrp x9, 9
0x10019d7e4 <+20>: add x9, x9, #0xf0 ; =0xf0
0x10019d7e8 <+24>: adrp x10, 8
0x10019d7ec <+28>: add x10, x10, #0xf30 ; =0xf30
0x10019d7f0 <+32>: adrp x11, 7
0x10019d7f4 <+36>: ldr x11, [x11, #0x68]
0x10019d7f8 <+40>: stur wzr, [x29, #-0x4]
0x10019d7fc <+44>: stur w0, [x29, #-0x8]
0x10019d800 <+48>: stur x1, [x29, #-0x10]
-> 0x10019d804 <+52>: stur x8, [x29, #-0x28]
0x10019d808 <+56>: str x10, [sp, #0x30]
0x10019d80c <+60>: str x11, [sp, #0x28]
0x10019d810 <+64>: str x9, [sp, #0x20]
0x10019d814 <+68>: bl 0x1001a0e14 ; symbol stub for: objc_autoreleasePoolPush
0x10019d818 <+72>: stur xzr, [x29, #-0x18]
0x10019d81c <+76>: str x0, [sp, #0x18]
0x10019d820 <+80>: mov x8, #0x64
0x10019d824 <+84>: ldur x9, [x29, #-0x18]
0x10019d828 <+88>: cmp x9, x8
0x10019d82c <+92>: cset w10, lt
0x10019d830 <+96>: tbnz w10, #0x0, 0x10019d838 ; <+104> at main.m
0x10019d834 <+100>: b 0x10019d87c ; <+172> at main.m
0x10019d838 <+104>: sub x0, x29, #0x20 ; =0x20
0x10019d83c <+108>: ldr x8, [sp, #0x20]
0x10019d840 <+112>: ldr x9, [x8]
0x10019d844 <+116>: ldr x10, [sp, #0x30]
0x10019d848 <+120>: ldr x1, [x10]
0x10019d84c <+124>: str x0, [sp, #0x10]
0x10019d850 <+128>: mov x0, x9
0x10019d854 <+132>: ldr x9, [sp, #0x28]
0x10019d858 <+136>: blr x9
0x10019d85c <+140>: stur x0, [x29, #-0x20]
0x10019d860 <+144>: ldr x0, [sp, #0x10]
0x10019d864 <+148>: ldur x1, [x29, #-0x28]
0x10019d868 <+152>: bl 0x1001a0f04 ; symbol stub for: objc_storeStrong
0x10019d86c <+156>: ldur x8, [x29, #-0x18]
0x10019d870 <+160>: add x8, x8, #0x1 ; =0x1
0x10019d874 <+164>: stur x8, [x29, #-0x18]
0x10019d878 <+168>: b 0x10019d820 ; <+80> at main.m
0x10019d87c <+172>: mov w0, #0x0
0x10019d880 <+176>: ldr x1, [sp, #0x18]
0x10019d884 <+180>: str w0, [sp, #0xc]
0x10019d888 <+184>: mov x0, x1
0x10019d88c <+188>: bl 0x1001a0e08 ; symbol stub for: objc_autoreleasePoolPop
0x10019d890 <+192>: ldr w0, [sp, #0xc]
0x10019d894 <+196>: ldp x29, x30, [sp, #0x60]
0x10019d898 <+200>: add sp, sp, #0x70 ; =0x70
0x10019d89c <+204>: ret

  而这两个函数的实现如下

1
2
3
4
5
6
7
8
9
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

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

  继续往下前,需要了解AutoreleasePoolPage的结构,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AutoreleasePoolPage 
{
... ...
static size_t const SIZE = PAGE_MAX_SIZE;
magic_t const magic;
id *next; // 指向能存放“autorelease对象的地址”的地址
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;

... ...
}

  首先,看到这个结构中有parentchild成员,第一反应这个结构是双向链表。这个结构的大小为PAGE_MAX_SIZE,这是一个宏,其定义如下

1
2
3
#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096

  一个AutoreleasePoolPage的大小为4096字节,除了用来存放其成员变量所占用的空间外,剩余的空间都是用来存放autorelease对象的地址。

  继续之前的源码分析,首先看下push方法的实现

1
2
3
4
5
6
7
8
9
10
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

  push方法里面接着调用了autoreleaseFast,并且传入了一个POOL_BOUNDARY对象,从字面意思理解这个对象是自动释放池的边界,那么它具体指的是什么?我们看到它的宏定义如下,其实就是个nil

1
#   define POOL_BOUNDARY nil

  再继续往下看下源代码,首先看看有没有已经创建了的自动释放池,并且如果没满的话调用AutoreleasePoolPageadd方法,如果满了的话就调用autoreleaseFullPage,最后如果没有创建过自动释放池就新建一个,则调用autoreleaseNoPage

1
2
3
4
5
6
7
8
9
10
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);
}
}

  首先看下add方法,直接将POOL_BOUNDARY加入进自动释放池中,并返回POOL_BOUNDARY的地址。

1
2
3
4
5
6
7
8
id *add(id obj) {
assert(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}

  接着看autoreleaseFullPage,前面说过自动释放池是双链表形式存在的,所以先遍历表,找到空的链表,然后把对象加入这个池子中;但是如果没子链表,那么就新建一个。

1
2
3
4
5
6
7
8
9
10
11
12
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *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);
}

  最后,看下autoreleaseNoPage,也是简单明了的代码,直接新建一个自动释放池,并将其设置为hot,因为自动释放池肯定有很多个,当前活跃的可添加自动释放对象的自动释放池为hot,否则为cold。

1
2
3
4
5
6
7
8
9
10
id *autoreleaseNoPage(id obj)
{
... ...
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

... ...

return page->add(obj);
}

  看完push方法源码,我们可以总结,当首次调用AutoreleasePoolPagepush方法时候,会将一个POOL_BOUNDARY入栈,并且会返回其存放的内存地址,这个地址也就是能存放自动释放对象的空间的首地址。所以,后面的autorelease对象的地址存放在这个POOL_BOUNDARY所在地址的后面。

AutoreleasePoolPage内存示意图

  如果4096的空间都被用完了,就会新创建一个AutoreleasePoolPage对象。

  看完push方法,接着看下pop方法,核心方法就是releaseUntil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void pop(void *token) {
AutoreleasePoolPage *page;
id *stop;

... ...

page = pageForPointer(token);
stop = (id *)token;

... ...

// 对自动释放池内的对象调用release方法,直到遇到POOL_BOUNDARY
page->releaseUntil(stop);

// 释放自动释放池
page->kill();
}

  接着看releaseUntil方法,遍历池子,在找到POOL_BOUNDARY之前的对象,都调用release方法进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void releaseUntil(id *stop) {

while (this->next != stop) {
... ...

page->unprotect();
// 之前说过,next指向下一个可存放自动释放对象的地址,所以往前就是已经存放过的那些自动释放对象了
id obj = *--page->next;

··· ···

if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}

setHotPage(this);
}

  总结,最后调用pop方法的时候,会把这个POOL_BOUNDARY所在地址作为参数传进去。然后从整个链表中所存放的最后一个autorelease对象的地址开始,依次往前调用对象的release方法,直到遇到前面传进来的POOL_BOUNDARY所在地址。

  如果是嵌套autorelease,因为没遇到一次@autorelease{}都会加入一个POOL_BOUNDARY,所以pop的时候,遇到POOL_BOUNDARY就停止了。所以对于对象来说,在最里面嵌套里的对象最先被释放掉。

  我们举个例子,会调用到私有函数_objc_autoreleasePoolPrint来帮助我们完成这个例子,然后我们需要将工程配置成MRC。

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
extern void _objc_autoreleasePoolPrint();

int main(int argc, char * argv[]) {

@autoreleasepool {
Animal *a1 = [[Animal new] autorelease];
Animal *a2 = [[Animal new] autorelease];

@autoreleasepool {
Animal *a3 = [[Animal new] autorelease];
_objc_autoreleasePoolPrint(); // 打印自动释放池
}

NSLog(@"第二个释放池结束");
}

NSLog(@"第一个释放池结束");

return 0;
}

// 运行结果
objc[53031]: ##############
objc[53031]: AUTORELEASE POOLS for thread 0x112b03380
objc[53031]: 5 releases pending.
objc[53031]: [0x7fdff9002000] ................ PAGE (hot) (cold)
objc[53031]: [0x7fdff9002038] ################ POOL 0x7fdff9002038 // 第一个AutoReleasePool的POOL_BOUNDARY
objc[53031]: [0x7fdff9002040] 0x60c00001dca0 Animal
objc[53031]: [0x7fdff9002048] 0x60c00001dc90 Animal
objc[53031]: [0x7fdff9002050] ################ POOL 0x7fdff9002050 // 第二个AutoReleasePool的POOL_BOUNDARY
objc[53031]: [0x7fdff9002058] 0x60c00001dc80 Animal
objc[53031]: ##############
2018-07-20 19:49:15.576802+0800 testData[53031:3013783] -[Animal dealloc] // 第二个AutoReleasePool内的对象被释放
2018-07-20 19:49:15.577441+0800 testData[53031:3013783] 第二个释放池结束
2018-07-20 19:49:15.577533+0800 testData[53031:3013783] -[Animal dealloc] // 第一个AutoReleasePool内的对象被释放
2018-07-20 19:49:15.577645+0800 testData[53031:3013783] -[Animal dealloc]
2018-07-20 19:49:15.577768+0800 testData[53031:3013783] 第一个释放池结束

  对于释放时机,在@autorelease{}是出了大括号就会进行释放。

  那么,我们知道主线程中默认会自动开启一个RunLoop,所以在这种情况下,如果只是调用autorelease方法,释放时机是根据RunLoop的,比如像这样的情况。

1
2
3
4
5
- (void)viewDidLoad {
[super viewDidLoad];

Animal *a1 = [[Animal new] autorelease];
}

  在主线程中,RunLoop注册了2个Observer,第一个,监听kCFRunLoopEntry,调用objc_autoreleasePoolPush();第二个,监听了kCFRunLoopBeforeWaiting和kCFRunLoopBeforeExit,前者会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush(),后者会调用objc_autoreleasePoolPop()。