iOS开发:NSZombie的实现

iOS

写在前面

在iOS开发调试内存问题时,Xcode提供了一个内存管理调试选项:Zombie Objects。打开这个选项,可以在访问已释放内存对象的方法时,进行警告。这里探讨下这个僵尸对象内部实现原理,以及动手去实现自定义的一个僵尸对象类。相关知识点主要涉及Objc的对象模型及消息机制。

属性修饰符

Objc中对象是基于引用计数进行内存管理的。并且编译器支持由开发者通过声明的方式定义属性,而由编译器自动合成相应的访问方法。对象的属性特性由我们声明的、用于控制由编译器生成的存取访问接口的特性(attribute),正确的使用特性修饰符才能有效避免对象内存管理错误。

Objc的对象属性声明主要包括以下四种特性修饰符:

  1. 原子性

    主要包括atomicnonatomic两个修饰,默认情况下编译器会添加加锁代码来保证原子性访问。如果自行编写访问器代码,则自行控制加锁提供原子性访问。

  2. 读写控制

    主要包括readonlyreadwrite控制是否可读写,即是否相应提供gettersetter访问方法。

    需要注意的是,这仅是对由编译器生成的属性存取接口的控制,对属性所指向的对象本身没有约束。举例,如若一个属性指向一个NSMutableArray类型对象,并且声明为readonly,那么只提供了getter属性访问器,但是对应的属性对象本身是可变的。

  3. 内存管理

    属性封装的是数据,而数据必须有明确的所有权管理,这也是只影响setter访问器的,也就是在setter方法中,是否需要持有新值亦或是单纯的赋值给相应的实例变量。主要包括:

    • assign:仅用于对数值类型的值的简单赋值;
    • strong:声明了持有关系,当一个新值被设置时,新值首先被retain持有,旧值被release释放,然后执行赋值;
    • weak:声明了非持有关系,新值被设置时,新值不会被retain,旧值也不会被release,基本与assign相同,但当对象被释放时,该值还会被设置为nil
    • unsafe_unretained:类似于assign,声明了非持有关系,但是用于对象类型,对象被释放时不会被置空;
    • copy:声明了持有关系,但不同于strong的是,它不对新值做持有而是做了复制;常用于NSString这种内部可变的类型。

    还有对应实例变量的几个修饰符:__strong__weak__unsafe_unretained__autoreleasing

  4. 方法名称

    也就是用于指定gettersetter的名称。

需要关注的也就是上述的“内存管理”的属性特性修饰,通常因为不当的使用assignunsafe_unretained导致野指针及对野指针访问crash。

僵尸对象的内存诊断

当访问野指针时,如果野指针所在的内存区域尚未被覆盖使用,则一切方法调用如常;一般情况下该野指针原先占用内存区域会被覆盖,此时访问野指针行为将是不确定的。比如,开发过程中可能会遇到这样的情况:crash并报错”[classXXX methodYYY]….unrecognized selector sent to instance 0x608000001fc0”,而classXXX没有这样的方法声明,相反selector是另外一个类的方法。这种情况是,野指针所指向区域刚巧覆盖了一个classXXX的实例对象。而大多数情况可能是其他未知类型数据,所报的则是“EXC_BAD_ACCESS”之类错误。

Xcode所加的Zombie Object的内存诊断则可以明确的指出错误原因是因为实例对象被释放、野指针访问的内存问题。测试报错如下:“ -[NTLObject call]: message sent to deallocated instance 0x608000036040”。

既然测试对象NTLObject已经被“释放”了,那具体报错信息是怎么得到的?怎么知道野指针原来指向的对象类型?以及当前所调用导致出错的方法?

事实上测试对象并未被释放(这导致调试时大量对象得不到释放会有内存问题),因为编译器替换了根对象的释放方法,并将原对象的isa指针指向了动态生成的一个僵尸类,来实现对旧对象指针访问时的报错处理。这是基本的处理思路。

NSZombie实现原理

在测试例子中,添加调试代码输出释放前后对象的类信息(包括类名、父类、元类、实例变量、方法列表等),可以看到,对于测试对象NTLObject,在对象“释放”后,对象isa指向了一个_NSZombie_NTLObject的类,其父类为空,元类为_NSZombie_。下面我们来重建自定义的僵尸类如NTLZombie,实现:

  • 对象僵尸化,也就是对象释放后会变成一个“僵尸对象”
  • 方法捕获,因为对象重新指向了新的类,新的类不可能全部实现原类的方法;
对象的内存释放

对象僵尸化,我们首先需要阻止对象被释放,否则对野指针的调用直接导致内存访问错误(这正是NSZombie要监测的问题)。

Objc中的对象(继承NSObject)都是在堆中分配的内存(关于这点另有撰文介绍),libobjc(实际上还有Core Foundation框架)将对象实例其实现为一个objc_object的结构体(参考objc4-647源码):

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
@interface NSObject <NSObject> {
Class isa;
}

struct objc_object {
private:
isa_t isa;

public:
// object may have -.cxx_destruct implementation?
bool hasCxxDtor();

// Optimized calls to retain/release methods
id retain();
void release();
id autorelease();

// Implementations of retain/release methods
id rootRetain();
bool rootRelease();
// ...

// Implementation of dealloc methods
bool rootIsDeallocating();
void clearDeallocating();
void rootDealloc();
// ...

private:
// ... Side-table 方法
};

对于对象的创建,所有对象创建时都调用基类NSObject+alloc方法(不自定义构建函数的话),将通过:

  • (id)alloc
    • _objc_rootAlloc(self)
      • callAlloc(cls, false /*checkNil*/, true /*allocWithZone*/)

申请内存后进行isa的初始化及构建函数的调用。

对于对象的释放,则是编译器添加代码,自动调用各继承层次的-dealloc方法,进行非自动管理内存的释放;对于实例变量及本身堆内存的释放则是在根类调用:

  • [NSObject dealloc]
    • _objc_rootDealloc(self);
      • obj->rootDealloc();
        • object_dispose((id)this);
          • _object_dispose(obj);
            • objc_destructInstance(anObject);
            • free(anObject)

释放内存,并将原内存块数据isa指向一个“已释放类”(freedObjectClass,12字节的被特殊置位的内存块),避免消息发送时对Nil类的检查。关键步骤是objc_destructInstance,其实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *objc_destructInstance(id obj) 
{
if (obj) {
Class isa = obj->getIsa();

if (isa->hasCxxDtor()) {
object_cxxDestruct(obj);
}

if (isa->instancesHaveAssociatedObjects()) {
_object_remove_assocations(obj);
}

if (!UseGC) objc_clear_deallocating(obj);
}

return obj;
}

主要包含三个步骤:

  1. object_cxxDestruct,对象的析构
  2. _object_remove_assocations,移除关联对象,从AssociationsManager中移除相应的ObjectAssociationMap
  3. objc_clear_deallocating,清理弱引用

步骤1,沿继承链向上逐级调用析构方法(类中对应selector为SEL_cxx_destruct的方法,.cxx_destruct),这个方法是编译器添加的实现,大概过程是遍历声明中的所有实例变量,调用objc_storeStrong函数进行release及置空操作。(注:SEL_cxx_destruct对应的是.cxx_destruct方法,这个是编译器添加的代码,后来搜索到了swift开源代码中的CodeGenModule.cpp,再然后发现sunny也做过相应的介绍,可以参考sunny的介绍)

步骤3,另外对于对象弱引用,libobc是将其保存到一个SideTable的类对象(C++)中,步骤3调用objc_object::sidetable_clearDeallocating()进行弱引用清理,具体可见后文的附1。

因此,为避免对象被真的释放了,我们需要替换根类的-dealloc方法。

对象僵尸化

我们对象原本被释放后再访问需要触发警告,首先原本的方法调用不能正常进行,再者要能记录调用的方法进行报警。被“释放”后的对象需要能实现这点,就需要我们所谓对象僵尸化,将被“释放”的对象的isa指向另一个类型,否则原本的类实例依然能查找到相应的方法实现(后文有介绍),达不到警告的需求。

所以所谓的对象僵尸化,是我们将被“释放”的对象的isa指向一个新的“僵尸”类,并由僵尸类处理方法调用异常问题。这里有一个问题,我们如何保存对象原来的类信息?既然原本的对象已经被“释放”了,内存空间作废了,能不能利用上?

对象实例objc_object的内存布局中,首先是isa指针,紧跟着各继承层次的ivar实例变量。假如使用实例变量所在内存区域来保存原来的类的信息的话,是没有保证的(有些类并没有声明实例变量)。因此,原来类信息无法保存到实例对象内存区域,僵尸对象类也不能是一个固定的类,否则实例对象无法识别原来类信息。因此对于每一个被僵尸化的类,需要动态注册一个类,并保存原来的类信息。通过log出来可以看到,苹果的做法是,将原来类名作为僵尸类的后缀,并且替换了旧的dealloc方法为__dealloc_zombie

具体实现,可以通过objc/runtime提供的接口,动态创建一个僵尸类:(代码应加锁,代码示例参考mikeash)

1
2
3
4
5
6
7
8
9
10
11
12
13
Class makeZombieClass(Class class) {
NSString *className = NSStringFromClass(class);
NSString *zombieClassName = [@"_NSZombie_" stringByAppendingString:className];
Class zombieClass = NSClassFromString(zombieClassName);
if(zombieClass) return zombieClass;

zombieClass = objc_allocateClassPair(nil, [zombieClassName UTF8String], 0);

// ... 消息捕捉处理

objc_registerClassPair(zombieClass);
return zombieClass;
}

替换dealloc方法,修改对象的isa类型指针:

1
2
3
4
void __dealloc_zombie(id obj, SEL _cmd) {
Class cls = makeZombieClass(object_getClass(obj));
object_setClass(obj, cls);
}

并在僵尸化时进行方法替换:(注意,不是对僵尸类添加dealloc方法)

1
2
Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
method_setImplementation(m, (IMP)__dealloc_zombie);

此后当对象被释放时,其isa将指向_NSZombie_XXXObject类对象,由后者进行消息传递的管理。

消息捕获

新的僵尸类,未实现任何方法,在未做任何处理时,向原来的对象指针发送消息,会导致异常抛出。当然也不能继承原有类,否则消息方法的IMP还是能被搜索到。(注:实际上苹果的Zombie实现中,动态生成的僵尸类,其superclassnil

再来回顾一下objc的消息机制(消息查找具体可见源码lookUpImpOrForward方法实现),举例:调用[object message]Object对象发送消息message

  1. 首先编译器层面会转换为objc_msgSend函数的调用:((void (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("message"));,对父类发消息或者根据函数返回值不同objc_msgSend会被替换为objc_msgSendSuperobjc_msgSend_stret

  2. 根据isa继承链,在类信息中进行方法查找,若能找到相应的方法IMP则调用该方法,否则按以下3-6进行动态消息解析、消息转发(及异常抛出)。方法IMP的查找见下面附3详述。

  3. 动态消息解析(Method Resolution)

    当步骤2中无法查找得到方法实现时, 将调用+resolveInstanceMethod: 或者 +resolveClassMethod:方法,可以动态提供一个函数实现。如果你添加了函数并返回 YES, 那运行时系统就会重新启动一次消息发送的过程(triedResolver保证仅一次)。对对象进行respondsToSelector 自省也可能会导致动态消息解析。(题外话,动态消息解析可以用在ORM方案中,我们可以在类初始化或者调用时进行方法的绑定)

    1
    2
    3
    4
    5
    6
    7
    8
    if (resolver  &&  !triedResolver) {
    rwlock_unlock_read(&runtimeLock);
    _class_resolveMethod(cls, sel, inst);
    // Don't cache the result; we don't hold the lock so it may have
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
    }

    在无法动态解析方法时,将在类对象的方法缓存中加入占位的方法缓存入口,进入消息转发。(CoreFoundation中forwarding_prep_0_ -> _forwarding_

  4. 快速消息转发(Fast forwarding)

    对象实现-forwardingTargetForSelector:方法时,若返回非空对象,则消息被转发到该对象。(注:可以利用这个作为proxy)

    若返回空的快速转发对象,则进行正常的消息转发。

  5. 消息转发(Normal forwarding)

    基类的实现是:调用基类的-doesNotRecognizeSelector抛出异常。

    这通常是我们对消息处理的最后时机。runtime会构建一个NSInvocation的对象作为参数,调用方法forwardInvocation

    • 首先,调用-methodSignatureForSelector:(CF实现)获得函数的方法签名(参数及返回值类型),若无法获得方法签名、返回nil,则直接导致-doesNotRecognizeSelector: 错误;否则可以对NSInvocation进行唤起:-invokeWithTarget:。(注:可以自行提供类型编码字符串获得方法签名[NSMethodSignature signatureWithObjCTypes:"v@:"]
    • NSInvocation 是对一个消息的描述,包括selector 以及参数等信息,还可以在 -forwardInvocation: 里修改传进来的 NSInvocation 对象,然后发送 -invokeWithTarget: 消息给它,传进去一个新的目标等,可以控制消息的转发。

    methodForSelector 获取IMP时,在没有对应方法时,返回的是 _objc_msgForward ,但不会有消息转发的过程。

因此我们可以在methodSignatureForSelector方法中捕获到方法调用进行处理,或者提供相应的方法签名,在forwardInvocation中处理。

僵尸对象方法调用

主要是修改方法签名的获取方法:

1
2
3
4
5
6
7
8
NSMethodSignature *MethodSignatureForSelector_zombie(id obj, SEL _cmd, SEL selector) {
Class class = object_getClass(obj);
NSString *className = NSStringFromClass(class);
className = [className substringFromIndex: [@"_NSZombie_" length]];
NSLog(@"Selector %@ sent to deallocated instance %p of class %@",NSStringFromSelector(selector), obj, className);

abort();
}

并在上文代码“// … 消息捕捉处理”中添加方法:

1
class_addMethod(zombieClass, @selector(methodSignatureForSelector:), (IMP)MethodSignatureForSelector_zombie, "@@::");

僵尸对象的处理即完成。

其他

附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
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
void objc_object::sidetable_clearDeallocating()
{
SideTable *table = SideTable::tableForPointer(this);

// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
spinlock_lock(&table->slock);
RefcountMap::iterator it = table->refcnts.find(this);
if (it != table->refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
// 引用表清理
weak_clear_no_lock(&table->weak_table, (id)this);
}
table->refcnts.erase(it);
}
spinlock_unlock(&table->slock);
}

// SideTable
class SideTable {
private:
static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE];

public:
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
};

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

/// The address of a __weak object reference
typedef objc_object ** weak_referrer_t;

/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line==0, the set is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};

/**
* 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是用来保存对象引用计数及弱引用的类,其中包括3个公开属性自旋锁slock、引用计数散列表refcnts、弱引用表weak_table,并使用一个静态数组table_buf保存所有SideTable实例,提供工厂方法tableForPointer查找对应的table。

  1. 首先是查找refcnts得到其引用计数,它是一个DenseMap类型,其值为site_t类型(其低位第0位是标记是否被弱引用),若存在弱引用则执行weak_clear_no_lock清理:
  2. weak_table_t以被引用对象指针哈希化(ptr_hash)之后作为索引,weak_entry_t类型作为值,weak_entry_t保存了所有weak引用了该对象的指针的指针。对象dealloc后执行到该步骤,会将该对象的所有弱引用指针置为nil,并从weak_table中移除该entry。

另外,上面所提到的table_buf是在可执行文件被操作系统加载、链接符号绑定之后的初始化进行创建的,具体调用可查看入口点_objc_init方法。

附2:objc_class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
// ...
}

以上是Objc-2.0以前的类对象结构定义,Objc-2.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
34
35
36
37
38
39
40
41
42
43
44
45
//Class的实际结构
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable

//class_data_bits_t可理解为class_rw_t加一些标志位的组合
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;
};

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

主要是对类信息等数据的封装,从概念上没有太大差异。

附3:IMP查找

objc_msgSend()会调用class_getMethodImplementation(cls, sel)继而调用lookUpImpOrNil进行方法查找,若查找失败,则返回_objc_msgForward的IMP,这个实现了具体的消息转发过程(见上文)。

1
2
3
4
5
6
7
8
9
10
imp = lookUpImpOrNil(cls, sel, nil, 
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);

IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}

lookUpImpOrForward正是上文提到的消息查找:

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
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;

runtimeLock.assertUnlocked();

// 优化的快速缓存查找
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}

runtimeLock.read();

if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();

// 类初始化+initialize方法
realizeClass(cls);

runtimeLock.unlockWrite();
runtimeLock.read();
}

if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
// +initialize等
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}


retry:
runtimeLock.assertReading();

// 当前类 缓存查找
imp = cache_getImp(cls, sel);
if (imp) goto done;

// 当前类 方法列表遍历查找并缓存(Try this class's method lists)
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

// 父类 缓存及方法列表查找 Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}

// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}

// 方法动态解析 No implementation found. Try method resolver once.

if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}

// No implementation found, and method resolver didn't help.
// 消息转发 Use forwarding.

imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);

done:
runtimeLock.unlockRead();

return imp;
}

上述代码中的realizeClass主要处理类对象初始化过程:

  • 父类(superclass)、元类(metaclass)及根类处理
  • 实例大小设置
  • 构造函数调用
  • 附加类别方法(到方法列表)(所以方法的查找以类别的优先,类别的加载在符号绑定后的回调)

另外需要说明的是SEL类型,这实际上可以看做是一个整型值,实际有一份字符串与SEL之间的映射表,详细可查看__sel_registerName方法。这个在方法缓存中也可以看到,缓存的存取是以SEL为key的,key的类型被定义为typedef uintptr_t cache_key_t;typedef unsigned long uintptr_t;

Author: Jason

Permalink: http://blog.knpc21.com/ios/ios-nszombie-implementation/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments