在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper

前言

最近更新了下以前写的TTGDeallocTaskHelper库,功能非常简单,就是在不改变原有代码的情况下,不用Runtime的Method Swizzling给任意对象添加任务Block,在对象dealloc的后期执行Block。使用时:

1
2
3
4
// 给object对象添加dealloc后期要执行的任务Block
[object ttg_addDeallocTask:^(__unsafe_unretained id target, NSUInteger identifier) {
// 执行任务
}];

Github地址:https://github.com/zekunyan/TTGDeallocTaskHelper

一张图说明原理:

总体结构

库的详细原理、限制、注意事项,为什么说“后期”执行,将在下文给出。

使用案例

先给出几个使用案例。

删除Notification监听

比如你给一个第三方类对象添加了Notification通知,要在对象release后删除Notification的监听,就可以:

1
2
3
4
[object ttg_addDeallocTask:^(__unsafe_unretained SomeClass *object, NSUInteger identifier) {
// 删除Notification监听
[[NSNotificationCenter defaultCenter] removeObserver:object];
}];

快速Debug内存泄漏

有时你想快速验证一下一个类有没有释放,存不存在内存泄漏,比如一个ViewController返回后,就可以这么做:

1
2
3
[someViewController ttg_addDeallocTask:^(__unsafe_unretained UIViewController *controller, NSUInteger identifier) {
NSLog(@"我正常释放了!哈哈哈!");
}];

事件打点

业务开发中,想监听某个事件什么时候销毁,就可以:

1
2
3
[someEvent ttg_addDeallocTask:^(__unsafe_unretained SomeEvent *event, NSUInteger identifier) {
// 打点。。。
}];

是不是很方便!没有对原有的代码做任何侵入。
接下来看看实现原理。

原理

在对象dealloc的时候做操作的四种办法

在对象dealloc的时候做操作,有好几种方法:

  • 继承:这个应该是最“正常”的方法了,继承对应的类,在子类中实现dealloc方法,执行相关的任务。缺点是要修改用到类的地方为自己定义的子类
  • Method Swizzling:方法交换,直接自己定义新的dealloc方法实现,实现任务的调用。但是坏处是所有的对象都会被替换,侵入性太大
  • ISA Swizzling:动态的生成对象的子类,在子类继承方法,实现调用任务,然后设置对象的isa指针,使其指向子类。这种方法的好处是只会影响到单个对象,但是系统KVO也是用这个方法,重复设置isa的话,会有问题。
  • 利用Associated Object:Associated Object会在对象释放时自动释放,所以可以用来“挂载”要执行的任务。

对比了上面的四种方法后,选定用Associated Object的方式实现TTGDeallocTaskHelper。

用Associated Object挂载任务

总的思路是:

  1. 给对象添加Associated Object
  2. 在Associated Object里面保存要执行的任务Block
  3. 实现Associated Object的dealloc方法,在Associated Object释放的时候,批量执行之前添加的Block

图示如下:

总体结构

Associated Object - TaskModel

属性

上图中的TaskModel,就是库代码里面的TTGDeallocTaskModel类,包含三个属性:

1
2
3
@property (nonatomic, assign) pthread_mutex_t lock;
@property (nonatomic, strong) NSMutableDictionary *tasksDict;
@property (nonatomic, unsafe_unretained) id target;
  • tasksDict保存要执行的任务Block
  • lock锁用来保证多线程下操作tasksDict的安全性
  • target指向的是原对象,是unsafe_unretained的,保证始终不为nil,所以也建议不要在任务Block里面调用target的方法

添加任务Block

TTGDeallocTaskModeltasksDict就是用来保存要执行的任务Block的,为了能在添加了以后删除,所以要以Key-Value的形式保存。
Key的类型是NSUInteger,全局自增,保证唯一。

OSAtomicIncrement64实现线程安全的自增整型Key-identifier

为了保证自增的Key的线程安全,同时平衡性能,所以此处用OSAtomicIncrement64实现原子整型数自增:

1
2
3
4
// 全局静态变量
static volatile int64_t globalIdentifier = 0;
// 自增+1,生成新的Key
NSUInteger newIdentifier = (NSUInteger)OSAtomicIncrement64(&globalIdentifier);

pthread_mutex_t保证操作字典的线程安全

为了保证NSMutableDictionary的多线程操作的安全,用pthread_mutex_t对添加Block的操作加锁:

1
2
3
pthread_mutex_lock(&_lock);
[_tasksDict setObject:[taskBlock copy] forKey:newIdentifierNumber];
pthread_mutex_unlock(&_lock);

删除任务Block

删除任务有两种,分别是删除一个identifier key对应的Block,和删除所有Block,同样,都要加锁保证线程安全:

1
2
3
4
5
6
7
8
// 加锁
pthread_mutex_lock(&_lock);
// 删除某个identifier对应的Block
[_tasksDict removeObjectForKey:identifierNumber];
// 或删除所有
[_tasksDict removeAllObjects];
// 解除锁
pthread_mutex_unlock(&_lock);

dealloc执行Block

最终在TaskModel被释放的时候,要批量执行所有添加的Block:

1
2
3
4
5
6
7
8
- (void)dealloc {
// 批量执行Block
[_tasksDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *identifier, TTGDeallocTaskBlock block, BOOL * _Nonnull stop) {
block(_target, identifier.unsignedIntegerValue);
}];
// 销毁锁
pthread_mutex_destroy(&_lock);
}

给NSObject添加Category

上面是TaskModel的部分,接下来是NSObject的。

给NSObject添加Categor NSObject (TTGDeallocTaskHelper),保存TTGDeallocTaskModel,封装调用。

封装-添加任务Block

Associated Object给对象添加TaskModel属性

objc_setAssociatedObject实现给任意对象添加对应的TaskModel对象。

@synchronized保证多线程下TaskModel的唯一性

首次添加任务Block,会创建TaskModel,为了保证多线程下的唯一性,所以创建的部分要用@synchronized保护起来。

所以,添加任务Block的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (NSUInteger)ttg_addDeallocTask:(TTGDeallocTaskBlock)taskBlock {
if (!taskBlock) {
// block为空
return TTGDeallocTaskIllegalIdentifier;
}

TTGDeallocTaskModel *model = nil;

// 线程安全
@synchronized (self) {
model = objc_getAssociatedObject(self, &TTGDeallocTaskModelKey);
if (!model) {
// 创建
model = [[TTGDeallocTaskModel alloc] initWithTarget:self];
// 保存
objc_setAssociatedObject(self, &TTGDeallocTaskModelKey, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}

// 添加新的Block
NSUInteger newIdentifier = [model addTask:taskBlock];

return newIdentifier;
}

封装-删除Block

直接获取当前对象的TaskModel对象,调用相应的方法即可,不再贴代码。

限制与注意事项

原理和实现说完了,接下来是库的限制与注意事项。

NSObject对象的dealloc过程

先来看看Associated Object在dealloc的什么时候释放。

NSObject对象dealloc的具体细节,sunnyxx在他的《ARC下dealloc过程及.cxx_destruct的探究》已经说的很清楚了。简单来说,就是对象dealloc的时候,最终会调用到objc_destructInstance方法上:

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
// 源码:objc4-706 objc-class.mm
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
* CoreFoundation and other clients do call this under GC.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa = obj->getIsa();

if (isa->hasCxxDtor()) {
// 1. 释放property
object_cxxDestruct(obj);
}

if (isa->instancesHaveAssociatedObjects()) {
// 2. 删除对象的associated objects
_object_remove_assocations(obj);
}

// 3. 设置所有指向此对象的weak指针为nil
objc_clear_deallocating(obj);
}
return obj;
}

主要是3步:

  1. object_cxxDestruct释放对象的ivar、property
  2. _object_remove_assocations释放对象的Associated Object
  3. objc_clear_deallocating重置所有指向对象的weak指针为空

不建议在Block里面调用target原对象的方法

从dealloc的流程可知,Associated Object是在第二步被释放的,这个时候,原对象的属性已被release,并且,Associated Object对象自己的dealloc不一定在什么时候会被调用(比如加到了autorelease pool里面),所以,不建议在任务Block里面调用target原对象的方法。

最后

什么时候Associated Object的释放能在对象属性release之前就好了=。=

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器