ARC下的Dealloc原理解读

需求告一段落,需要学习的东西实在太多了,最近崩溃数据监测到时有崩溃是在.cxx_destruct时候崩溃,看着像是dealloc的东西,所以抽空给解读一下dealloc。

一、一些总结性的东西

1、前段时间出现过这样的一个崩溃,在dealloc中 写了这样几句代码

- (void)dealloc{
    self.mainTableView.dataSource = nil;
    self.mainTableView.delegate = nil;
    self.refreshHeaderView.delegate = nil;
    self.inputTextView.delegate = nil;
    self.editView.delegate = nil;
}

是在一个ViewController里面写的,进入页面的按钮没有做防连击,导致连续进入两次,造成崩溃。前面有过一次总结,今天在研究dealloc的时候补充总结一下。在调用getter的时候,创建对象之后,立马指定了 self 为代理,并且是 weak 的,设置了weak修饰的代理后,objc 底层对 self 进行了处理,不管是使用 weak 修饰的属性,还是使用 __weak 修饰的变量,objc 都会对这个变量处理,都会造成崩溃。

理论上讲,ARC之后,dealloc中只需要对一些非OC的对象进行过手动释放(比如CoreFoundation的对象),对KVO监听移除之外(iOS9之后也不需要手动移除了),基本上就没别的了。但是ARC的内存销毁具有一定的滞后性,也可将一些变量手动置空,告诉系统这些变量个已经使用完毕可以释放了。为节省资源,在确保属性没有利用价值的时候可以手动清空。如果不做任何操作,系统会自动释放这些成员变量或者属性。在dealloc的.cxx_destruct方法中会处理。 所以我最后的解决办法就是不手动释放了,交给系统自己去释放吧。其实也可以把self去掉,直接改成这样

- (void)dealloc{
    _mainTableView.dataSource = nil;
    _mainTableView.delegate = nil;
    _refreshHeaderView.delegate = nil;
    _inputTextView.delegate = nil;
    _editView.delegate = nil;
}

MRC下的dealloc就要手动释放、置空变量、iOS9以下移除KVO监听等操作还要调用父类的dealloc。在ARC下,释放顺序是,先调用子类的dealloc,子类的dealloc调用完成之后,会自动调用父类的dealloc方法,(自动调用父类的dealloc方法是由编译器自动插入的,类似于.cxx_desctruct方法由编译器自动插入)。这个稍后分析源码

2、dealloc实在什么线程被执行调用的?

我也一直认为是主线程,其实是取决于最后在什么线程中release,在什么时候release在什么线程调用

3、还是针对第一点的总结,不要在dealloc中调用self.属性

在《Effective Objective-C 2.0》这本书中也提到了,在对象内部尽量直接访问实例变量。直接调用_name = nil 比 调用self.name = nil 的执行效率要高一些,但绝不是因为效率高,当然这也是一个因素。至于为什么效率高一会看代码。

在dealloc中调用

-(void)dealloc{
	self.name = nil;
}

调用self.name 意为调用name的setter方法对_name进行赋值

-(void)setName:(NSString*)name{
	_name = name;
}

即将nil作为参数传递到setName:方法中,其方法内部本身执行的仍然是_name = nil; 这样看和在dealloc方法中 写 _name = nil 没有什么区别,但是setter方法并没有看起来那么简单,如果是在ARC中通常setter方法如下

-(void)setName:(NSString *)name{
	if(_name !=name) {
		[_name release];
		_name = [name retain];
	}
}

对于setter赋值方法采用的是释放旧值保留新值的方式,直接调用_name=nil 避免指针转移问题且避免Objective-C的“方法派发”操作,因此直接调用_name=nil 相比 self.name = nil执行效率要高一些。且,其他的setter方法,或者属性是懒加载的方式实现的话,可能会在懒加载里面存在weak操作,极易造成崩溃,就是最一开始我写的案例。所以在dealloc中调用self.属性 容易造成崩溃。

以上是借用掘金上一位前辈的思路,链接在最下面贴出来了。

下面这个就是在调用self.属性名 = nil的时候,由于setter方法里面有自定义方法,不是简单的赋值操作,引起的崩溃

首先创建一个父类

@interface Animal : NSObject
@property (nonatomic,copy) NSString *basicString;
@end

#import "Animal.h"
@implementation Animal
- (void)dealloc{
    self.basicString = nil;
}
@end

写一个子类,重写父类的basicString的setter方法

#import "Animal.h"
@interface Dog : Animal
@end

#import "Dog.h"
@implementation Dog
- (void)setBasicString:(NSString *)basicString{
    NSLog(@"%@",[NSString stringWithString:basicString]);
}
@end

方法调用

- (void)testDealloc{
    Dog *dog = [Dog new];
    dog.basicString = @"子类属性";
}

运行,崩溃了,贴一下崩溃信息
在这里插入图片描述
最后崩溃定位到这一行
ARC下的Dealloc原理解读_第1张图片
[NSString stringWithString:basicString],这个方法当basicString 为空的时候就会报上面的崩溃,必现。所以,千万不要粗心大意啊!
在testDealloc方法中,出了这个作用域,dog就被释放了,就会调用dog的dealloc方法,调用完dog的dealloc方法,就会调用Animal的dealloc,在Animal的dealloc的方法的时候,写了self.basicString = nil,当写这句代码的时候,就是在调用 basicString 的set方法,将basicString 赋值为nil,但是在子类里面重写了set方法,并且调用了

[NSString stringWithString:basicString]

这个方法是不允许传nil参数的,所以崩溃就出来了。当然你也可以不用这个方法就不会崩溃了,但是没有办法保证有哪个方法就崩了。
不知道父类会做什么,就算继承的是NSObject,也不能精确的知道父类里面做了什么,所以,不要在dealloc中使用 self.属性 这样的格式

4、dealloc中使用__weak

- (void)dealloc{
   __weak __typeof(self)weak_self = self;
   NSLog(@"%@", weak_self);
}

运行,崩溃,崩溃信息

objc[24629]: Cannot form weak reference to instance (0x600002ce8d60) of class Dog. It is possible that this object was over-released, or is in the process of deallocation.

clang(LLVM编译器)具有转换为我们可读源代码的功能,可以将OC转换为C++的源代码,而C++其实也近视使用struct结构,其本质是C语言源代码
cd 到当前文件夹
clang -rewrite-objc main.m

如果以来了UIKit

ZBMAC-C02VP1301:DeallocProject nvshen$ clang -rewrite-objc main.m
In file included from main.m:10:
./AppDelegate.h:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
        ^~~~~~~~~~~~~~~
1 error generated.
ZBMAC-C02VP1301:DeallocProject nvshen$ 

找啊找,下面找到答案

You have to specify the development sysroot of the particular OS you’re targetting, or else the compiler and linker cannot find the headers and libraries. Add -isysroot to the compiler and linker flags, where SYSROOT should be the full path of the iPhoneOSXXX.sdk directory (XXX is the version number of iOS).

必须要指定sysroot ,不然compoler 和 linker找不到头文件和库,加上 -isysroot,SYSROOT 这里sysroot 是包含你的目标系统头文件及库的目录 ,原谅我蹩脚的英语

大概就是这个意思,你缺一个路径,加上这个路径,才能找到你的头文件可你引用的库,不然找不到。路径如下

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

修改后:

ZBMAC-C02VP1301:DeallocProject nvshen$ clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
/var/folders/g_/_jwf02p50j36cn5430m6_f2jx9_7jr/T/main-cde08c.mi:38294:9: warning: 
      'EAGLContext' is deprecated: first deprecated in iOS 12.0 - OpenGLES API
      deprecated. (Define GLES_SILENCE_DEPRECATION to silence these warnings)
      [-Wdeprecated-declarations]
typedef EAGLContext *CVEAGLContext;
        ^
/var/folders/g_/_jwf02p50j36cn5430m6_f2jx9_7jr/T/main-cde08c.mi:35096:12: note: 
      'EAGLContext' has been explicitly marked deprecated here
@interface EAGLContext : NSObject
           ^
/var/folders/g_/_jwf02p50j36cn5430m6_f2jx9_7jr/T/main-cde08c.mi:60112:27: error: 
      cannot create __weak reference because the current deployment target does
      not support weak references
        id __attribute__((objc_ownership(weak))) objc = object;
                          ^
1 warning and 1 error generated.
ZBMAC-C02VP1301:DeallocProject nvshen$ 

还是报错,换了个错报,clang一下这么麻烦的嘛
clang 是把objc代码“导出”为C++代码,而__weak 是在objc 不仅仅需要静态编译,还需要运行时的支持,如果不指定会报错,所以需要额外添加参数 fobjc-runtime=ios-8.0.0,而且__weak 是在ARC下才支持的,所以我们还需要指定使用arc, -fobjc-arc

执行

ZBMAC-C02VP1301:DeallocProject nvshen$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 Skill.m -o Skill.cpp

结果

2020-06-28 14:31:46.401 xcodebuild[77397:33628345] [MT] PluginLoading: Required plug-in compatibility UUID BAB79788-ACEE-4291-826B-EC4667A6BEC5 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/VVDocumenter-Xcode.xcplugin' not present in DVTPlugInCompatibilityUUIDs

Xcode插件失效了,升级插件

当有一个weak属性的时候,编译器会自动创建一下方法

//初始化
objc_initWeak(&obj1,obj);
//释放
objc_destroyWeak(&obj1);

查看崩溃的堆栈信息,崩溃是在 objc_initWeak 函数中,这里要查看Runtime源码查看 objc_initWeak 函数是怎么定义的

id
objc_initWeak(id *location, id newObj)
{
	//查看对象是否有效,无效直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    //三个bool值
	//DontHaveOld-- 没有旧对象
	//DoHaveNew -- 有新对象
	//DoCrashIfDeallocating -- 如果newObj已经释放了就Crash提示
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

location 是weak指针地址,newObj是被指向的对象的地址,调用了storeWeak 函数 ,有个名称是DoCrashIfDeallocating ,也就是说释放过程中过存储,就会crash, 最终会调用register 函数,

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id, bool crashIfDeallocating)
{
...
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最终会调用objc_initWeak函数进行注

下面会有weak原理解析

5、dealloc中使用GCD

定时器的创建和释放必须在同一个线程,因为定时器销毁只能从启动定时器的runloop中移除,然后runloop和线程是一一对应的,所以要确保释放和创建是在同一个线程处理,否则可能会出现释放不了的情况
程序代码默认是在主线程执行,dealloc中异步执行主队列,再释放定时器,GCD中会强引用self,此时dealoc已经执行完成了,那么self其实已经被释放掉了,此时销毁内部再调用self就会访问野指针
具体看这个吧 ARC下,Dealloc还需要注意什么?

野指针:指向未知内存的指针

二、源码分析

1、dealloc过程

dealloc是在 runtime 的 NSObject.mm文件中实现

//调用dealloc的时候, 调用_objc_rootDealloc() 方法
- (void)dealloc {
    _objc_rootDealloc(self);
}
//在调用_objc_rootDealloc方法,再调用 rootDealloc() 方法
void
_objc_rootDealloc(id obj)
{
	//assert-断言,满足条件返回真值,程序继续运行,返回假值,终止程序
    assert(obj);

    obj->rootDealloc();
}

//调用rootDealloc() 方法在  objc-object.h 文件中,如下,再调用  object_dispose()
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
//object_dispose函数在objc-runtime-new.mm文件中,最终调用objc_destructInstance()
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
//objc_destructInstance() 系统自动释放属性或者成员变量,就是在这个函数里面处理
void *objc_destructInstance(id obj)
{
    if (obj) {
        Class isa_gen = _object_getClass(obj);
        class_t *isa = newcls(isa_gen);

        // Read all of the flags at once for performance.
        bool cxx = hasCxxStructors(isa);
        bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);

        // This order is important.
        //1、执行object_cxxDestruct ,对象拥有成员变量的时候,编译器会自动插入.cxx_descruct方法,用于自动释放
        if (cxx) object_cxxDestruct(obj);
        //2、去除和这个对象assocate的对象,常用于category中添加带变量的属性,所以不需要手动remove
        if (assoc) _object_remove_assocations(obj);
		//3、清空引用计数表并清除弱引用表,将所有的weak引用指向nil,所以weak变量能安全置空
        if (!UseGC) objc_clear_deallocating(obj);
    }
    return obj;
}
//在这里面移除成员变量或者属性 object_cxxDestruct()
void object_cxxDestruct(id obj)
{
    if (!obj) return;
    //如果是[isTaggedPointer](https://blog.devtang.com/2014/05/30/understand-tagged-pointer/),不处理 
    if (obj->isTaggedPointer()) return;
    
    object_cxxDestructFromClass(obj, obj->ISA());
}
//object_cxxDestructFromClass()函数
static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.
	//沿着继承链逐层向上搜寻SEL_cxx_destruct 这个方法,找到 函数实现并执行
    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id)lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            (*dtor)(obj);
        }
    }
}

有文档写:
ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

ARC实际上创建了一个 .cxx_destruct 的方法,这个方法用来释放实例变量,这个方法是在销毁对象的时候自动调用C++ 的析构函数的时候创建的

在《Effective Objective-C 2.0》中提到
When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

当编译器监测到对象包含了C++对象的时候,会创造一个.cxx_destruct方法,ARC借用这个方法插入代码实现了自动释放的工作

所以对象的实例变量是在.cxx_destruct内完成的,在.cxx_destruct进行形如objc_storeStrong(&ivar, null)的调用后,这个实例变量就被release和设置成nil了

相关用法举例

1、ARC下销毁一个对象,如果执行cxx_destruct的时候,仍然有访问其成员变量的情况,就会导致崩溃

1、信号量崩溃

//重新赋值或者将semephore = nil都会造成崩溃,因为此时信号量还在使用中
self.semephore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(self.semephore, DISPATCH_TIME_FOREVER);
self.semephore = nil;

比如在一个ARC里面,写了如下代码

//当dispatch_semaphore_t正处于dispatch_semaphore_wait的时候,释放这个信号量,将会引起崩溃 
//原因:在semaphore调用dispose的时候不能让currentValue < originValue, 如果出现就会导致指令执行错误, 系统发出sigill信号中断程序执行,导致信号量在释放的时候, currentValue为-1, 最终程序强制终止。
- (void)dealloc{
    NSLog(@"%s",__func__);
    _semephore = nil; //这样会在这里崩溃
}
- (instancetype)init{
    self = [super init];
    if (self) {
        _semephore = dispatch_semaphore_create(1);
        dispatch_semaphore_wait(self.semephore, DISPATCH_TIME_FOREVER);
    }
    return self;
}

2、不移除观察者造成崩溃
iOS9之前,需要手动移除观察者,iOS9之后,一般的通知,不需要手动移除,系统会自动在dealloc的时候 调用 [[NSNotificationCenter defaultCenter]removeObserver:self]

iOS9以前观察者注册的时候,通知中心并不会对观察者对象做retain操作,而是进行了unsafe_unretained引用,所以在观察者被回收的时候,如果不对通知进行手动改移除,那么指针指向被回收的区域就会成为野指针,这时,如果再发送通知,便会造成程序崩溃。

从iOS9开始通知中心会对观察者进行weak弱引用,这时即使不对通知进行手动移除,指针也会在观察者被回收后自动置空,这时再发送通知,向空指针发送消息是不会有问题的。

2、weak原理

未完,补充中…

参考文献
ARC下,Dealloc还需要注意什么?
iOS编译指令
升级Xcode插件失效解决方法
深入理解Tagged Pointer
iOS 通知 观察者移除注意点!!!

你可能感兴趣的:(iOS,ios)