并非所有NSObject的子类的构造都会经过NSObject的构造方法

发布于:2016-10-16 14:02,阅读数:1659,点赞数:6


> 这是一次失败了的项目,Demo在[「GitHub」](https://github.com/trmbhs/ZYQLeaksDemo)中开源。
>
> 如果需要使用内存泄露监测工具,推荐使用Facebook的产品[「FBMemoryProfiler」](https://github.com/facebook/FBMemoryProfiler)。

# 开发环境

macOS 10.12 + Xcode 8 with Swift 3.0

# 导语

本项目最初的想法是利用方法交叉(`method swizzling`)替换掉`NSObject`的`init`和`deinit`方法来接受对象创建时和销毁时的消息。在Swift中,使用Objc-runtime有一点小坑,绕过坑的办法后面再讲。但最终,这个项目失败了。

先说结论:当我们创建新的NSObject子类时,在构造方法中通常会调用`super.init`,但研究结果发现,并非所有NSObject的子类的构造都会经过NSObject的init。

# 方法交叉
项目中用到了两个Objc-runtime的API,其用法跟Objc语法类似:

```swift
public func class_getInstanceMethod(_ cls: Swift.AnyClass!, _ name: Selector!) ->Method!
public func method_exchangeImplementations(_ m1: Method!, _ m2: Method!)
```

这两个方法是全局的,可以直接调用。这一步的目的是利用Objc的动态性将两个实例方法的方法的实现对调。需要注意的点:

- Swift的类请继承NSObject,来避免各种奇怪的问题。
- 两个对调的方法参数要对应。
- 在Swift中使用时,交换的方法需要通过ObjC选择器去执行才能调用到交换后的方法。

```objectivec
self.perform(#selector(NSObject.init(_:)))
```

- 在方法前面加入`dynamic`关键字可以绕过上面这个坑,直接使用C-Style的调用方法也能调用交换后的方法。

# 交换构造方法

交换init方法,由于参数需要对应的原则,使用默认值进行混淆,这个构造方法可以成功被交换。

```swift
convenience init(_ whatever : Int = 0) {
self.init(0)
ZYQLeaks.shared.objInit(cls: object_getClass(self))
}
```

交换的方法为:

```swift
let objectClass: AnyClass = NSObject.classForCoder()
let objectInit = class_getInstanceMethod(objectClass, #selector(NSObject.init))
let exchangeInit = class_getInstanceMethod(objectClass, #selector(NSObject.init(_:)))
method_exchangeImplementations(objectInit, exchangeInit)
```

交换掉构造函数后,就可以监控NSObject对象的子类了。监控是在AppDelegate中启动的。

![对象初始化监控](//cdn.yuusann.com/img/posts/16004_1.jpg)

可以看到控制台已经打印出了从AppDelegate中启动监控后所有NSObject子类创建时的日志了。最初我也是这么以为的,直到我替换掉了`deinit`方法,发现了问题。

# 交换析构方法

在Swift中,所有对象在释放前都会走`deinit{ }`方法。这是个很特殊的方法,不允许手动调用,只会在对象需要释放时被`ARC `自动调用

在`deinit`的[「官方文档」](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Deinitialization.html)中这样描述:

>Deinitializers `are called automatically`, just before instance deallocation takes place. You `are not allowed to call a deinitializer yourself`. Superclass deinitializers are inherited by their subclasses, and the superclass deinitializer `is called automatically` at the end of a subclass deinitializer implementation. `Superclass deinitializers are always called`, even if a subclass does not provide its own deinitializer.

关于析构函数:

- deinit会被自动调用,且不允许手动调用。
- 在调用完子类的析构方法后会自动调用父类的析构方法。
- 在执行析构方法时,所有的属性都还没有被释放掉,还是可以用的。

为什么说析构方法很特殊,因为它的写法为:

```swift
deinit {
//whatever
}
```

- 它是个方法,却没有参数,甚至没有括号。
- 不用也不能调用`super.deinit`,但`super.deinit`会自动被调用。意味着这个方法不仅仅执行表面上的代码,有很多表面上看不到的,没写出来的代码会被执行。

这些问题很可能影响到我们现在要做的方法交叉,抱着试试看的心理,开始交换析构方法,很遗憾的是,尝试了一下几种方法都出现了问题:

#### 方法1

将deinit看做普通函数,使用常规方法交换:

```swift
let objectDeinit = class_getInstanceMethod(objectClass, #selector(NSObject.deinit))
let exchangeDeinit = class_getInstanceMethod(objectClass,#selector(NSObject.zyqleaksDeinit))
method_exchangeImplementations(objectDeinit, exchangeDeinit)
```

```swift
dynamic func zyqleaksDeinit() {
ZYQLeaks.shared.objDeinit(cls: object_getClass(self))
}
```

![方法1](//cdn.yuusann.com/img/posts/16004_2.jpg)

这里的错误我没有找到原因,如果有大神知道请务必告诉我原因,在此谢过。

#### 方法2

使用类方法交换,虽然成功调用了,但是造成了内存溢出。所有的对象都没有被释放掉。显然NSObject的deinit中有一些看不到的代码被执行了,这些代码可能跟引用计数有关。

```swift
let objectDeinit = class_getInstanceMethod(objectClass, #selector(NSObject.deinit))
let exchangeDeinit = class_getClassMethod(objectClass,#selector(NSObject.zyqleaksDeinit))
method_exchangeImplementations(objectDeinit, exchangeDeinit)
```

```swift
static dynamic func zyqleaksDeinit() {
ZYQLeaks.shared.objDeinit(cls: self.classForCoder())
}
```

![方法2](//cdn.yuusann.com/img/posts/16004_3.jpg)

使用静态方法交换后产生了2个问题:

- 将`deinit`和`zyqleaksDeinit`交换后,`deinit`变成了`zyqleaksDeinit`,在对象需要`deinit`时调用了`zyqleaksDeinit`。而根据上面的推测`deinit`中可能包含了一些跟引用计数有关的代码,在执行完`zyqleaksDeinit`后我必须要执行`deinit`才能保证对象正常释放。而问题是,与`zyqleaksDeinit`是个类方法,如果我在这里调用`zyqleaksDeinit`,那么执行的仍然是`zyqleaksDeinit`,并不是被交换掉的`deinit`。因此导致引用计数出现问题,对象没有被正常标记释放,内存泄露。
- 在查看日志时发现`NSString`、`NSArray`、`NSDictionary`等,包括Mutable的这类对象,计数都是负数。这意味着只出现了`init`的日志,没有出现`deinit`的日志。 由此得出,这些类也是NSObject的子类,而他们构造时并没有经过NSObject的init。

# 最终的问题

对于上面提到的两个问题:

第一个问题的关键就是前面方法1的方案,如果可以正确处理方法1中崩溃,那么相信在交换实例方法后再调用一次`zyqleaksDeinit`就可以解决。

第二个问题,经过和殿神 [酷酷的哀殿](http://www.jianshu.com/users/486bf26e8dce/latest_articles) 的讨论,殿神的解释为:

> NSArray等长度可变类型,在最初的设计时,它是被设计为 开发者需要提前预估容量 的类型,所以,它的初始化方法需要最终调用到 带有 count 的初始化方法。

而Sunny大神 [@我就叫Sunny怎么了](http://weibo.com/u/1364395395) 的解释为:

> 这几个类是类簇,像 NSArray 这种是虚类,要 hook 的话去 hook 什么 _NSArrayI NSCFArray NSSingleObjectArray 的 init 才行,还得看你是用什么方式来 hook

两个人的答案基本是一致的 —— 我并没有hook到这些类的初始化。至此基本上可以得出的结论就是,并不是所有NSObject子类的初始化都走了NSObject的init。我hook了NSObject的init并不能在所有NSObject构造时都收到消息。因此用这个思路去设计内存泄露监测工具是错误的。

# 结语
在这两天的研究过程中,踩了不少坑。在hook`init`方法时对Cocoa的不熟悉,以为hook到NSObject就等于hook到了所有的子类。在`deinit`的交换过程中暴露了自己对对象销毁过程的不熟悉,至今都没有找到解决方法。但这两天的研究并没有白费,得到了这样的一个结论。同时也希望如果有大神知道`deinit`中崩溃的原因,能指导一下我。

# 更新
#### 2016年11月02日 参考文献补充
- [「Objc 对象的今生今世」](http://www.jianshu.com/p/f725d2828a2f)
这篇文章中对对象的`alloc`和`dealloc`过程有着更详细的解释。


评论:0条


返回列表

返回归档

返回主页