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

发布于:2016-10-16 14:02,阅读数:2371,点赞数: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.blog.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.blog.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.blog.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条


返回列表

返回主页