野指針
指針指向的對象已經被回收转培,指針仍指向已經回收的內存地址恶导,那么這個指針就叫做野指針。
內存分配
申請1塊空間浸须,實際上是向系統申請1塊別人不再使用的空間惨寿。
釋放1塊空間邦泄,指的是占用的空間不再使用,這個時候系統可以分配給別人去使用。
-
在這個空間分配給別人之前缤沦,數據還是存在的
OC對象釋放以后,表示OC對象占用的空間可以分配給別人
但是再分配給別人之前這個空間仍然存在虎韵,對象的數據仍然存在
僵尸對象
僵尸對象一種用來檢測內存錯誤(EXC_BAD_ACCESS)
的對象,它可以捕獲任何對嘗試訪問壞內存的調用缸废。
如果給僵尸對象發(fā)送消息時,那么將在運行期間崩潰和輸出錯誤日志驶社。通過日志可以定位到野指針對象調用的方法和類名企量。
野指針訪問問題
-
使用野指針訪問對象,有時候會報錯(EXC_BAD_ACCESS)亡电,有時候不會
當野指針指向的內存未分配給別人時届巩,可以訪問到對象
當野指針指向的內存分配給別人后,訪問就會出問題
因此份乒,我們不允許通過野指針去訪問已經被釋放的對象恕汇。
開啟僵尸對象檢測
在 Xcode 中設置Edit Scheme -> Diagnostics -> Zombie Objects
源碼分析
新建一個終端項目(Command Line Tool),并開啟僵尸對象檢測
或辖,具體代碼如下:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
void printClassInfo(id obj)
{
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
NSLog(@"self:%s - superClass:%s", class_getName(cls), class_getName(superCls));
}
@interface People : NSObject{
int _age;
}
@end
@implementation People
- (void)dealloc {
[super dealloc];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *aPeople = [People new];
NSLog(@"before release!");
printClassInfo(aPeople);
[aPeople release];
NSLog(@"after release!");
printClassInfo(aPeople);
getchar();
}
return 0;
}
打印消息為:
Zombie-Object[66158:522009] before release!
Zombie-Object[66158:522009] self:People - superClass:NSObject
Zombie-Object[66158:522009] after release!
Zombie-Object[66158:522009] self:_NSZombie_People - superClass:nil
(1)由打印消息得知瘾英,People釋放后所屬的類型變?yōu)榱?code>_NSZombie_People,即對象釋放后變成了僵尸對象,保存當前釋放對象的內存地址颂暇,防止被系統回收缺谴。
這邊其實可以看到 _NSZombie_People 是沒有父類的,是一個根類耳鸯,并沒有實現任何方法湿蛔,因此所有發(fā)送給僵尸類的消息都要經過完整的消息轉發(fā)機制。這也是觸發(fā)僵尸對象機制會斷點在 forwarding 的原因县爬。
(2)從Xocde的Debug Memory Graph
也可以看出阳啥,沒有Person
類型的引用,但是多了_NSZombie_People
類型的引用财喳,也說明Person
對象釋放后變成了_NSZombie_People
類型的對象察迟。
對象釋放過程
我們先理解對象釋放的過程,讓Runtime 源碼告訴你:
/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
我們可以看看 objc_destructInstance 到底都干了些什么纲缓。從其注釋可以知道該方法做了下面幾件事:【C++ destructors】【ARC ivar cleanup】【Removes associative references】 并沒有釋放其內存卷拘,而是在free(obj)時才釋放了內存。
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
Zombie Object 的生成過程
讓我們在前面的項目中打個符號斷點祝高,去看看開啟僵尸對象檢測后對象的釋放過程
使用__dealloc_zombie符號斷點
0x1885150a8 <+56>: mov x0, x19
0x1885150ac <+60>: bl 0x18856262c ; symbol stub for: object_getClass
0x1885150b0 <+64>: str xzr, [sp, #0x10]
0x1885150b4 <+68>: bl 0x18856185c ; symbol stub for: class_getName
0x1885150b8 <+72>: str x0, [sp]
0x1885150bc <+76>: adrp x1, 618
0x1885150c0 <+80>: add x1, x1, #0x402 ; "_NSZombie_%s"
0x1885150c4 <+84>: add x0, sp, #0x10
0x1885150c8 <+88>: bl 0x18856161c ; symbol stub for: asprintf
0x1885150cc <+92>: ldr x0, [sp, #0x10]
0x1885150d0 <+96>: bl 0x18856247c ; symbol stub for: objc_lookUpClass
0x1885150d4 <+100>: mov x20, x0
0x1885150d8 <+104>: cbnz x0, 0x1885150f8 ; <+136>
0x1885150dc <+108>: adrp x0, 617
0x1885150e0 <+112>: add x0, x0, #0xdfa ; "_NSZombie_"
0x1885150e4 <+116>: bl 0x18856247c ; symbol stub for: objc_lookUpClass
0x1885150e8 <+120>: ldr x1, [sp, #0x10]
0x1885150ec <+124>: mov x2, #0x0
0x1885150f0 <+128>: bl 0x1885623ac ; symbol stub for: objc_duplicateClass
0x1885150f4 <+132>: mov x20, x0
0x1885150f8 <+136>: ldr x0, [sp, #0x10]
0x1885150fc <+140>: bl 0x188561d6c ; symbol stub for: free
0x188515100 <+144>: mov x0, x19
0x188515104 <+148>: bl 0x18856239c ; symbol stub for: objc_destructInstance
0x188515108 <+152>: mov x0, x19
0x18851510c <+156>: mov x1, x20
0x188515110 <+160>: bl 0x18856266c ; symbol stub for: object_setClass
總結起來的偽代碼大概是
// 獲取到即將deallocted對象所屬類(Class)
Class cls = object_getClass(self);
// 獲取類名
const char *clsName = class_getName(cls)
// 生成僵尸對象類名
const char *zombieClsName = "_NSZombie_" + clsName;
// 查看是否存在相同的僵尸對象類名栗弟,不存在則創(chuàng)建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
// 獲取僵尸對象類 _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
// 創(chuàng)建 zombieClsName 類
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//釋放字符串
free(zombieClsName)
// 在對象內存未被釋放的情況下銷毀對象的成員變量及關聯引用。
objc_destructInstance(self);
// 修改對象的 isa 指針工闺,令其指向特殊的僵尸類
object_setClass(self, zombieCls);
上面的偽代碼就是開啟僵尸對象檢測后乍赫,對象釋放的大致調用過程瓣蛀。執(zhí)行 objc_destructInstance(obj) 方法后,并沒有 free(obj) 這一步的調用雷厂。
從此處斷點可以大概看出 Zombie Object 的生成過程惋增。_NSZombie_%s
驗證了開啟僵尸對象檢測后的對象所指向的類改鲫。從這個調用棧也可以說明系統開啟僵尸對象檢測后不會釋放該對象所占用的內存诈皿,只是釋放了與該對象所有的相關引用像棘。
結論
系統在回收對象時,可以不將其真的回收缕题,而是把它轉化為僵尸對象截歉。這種對象所在的內存無法重用,因此不可遭到重寫烟零,所以將隨機變成必然。
系統會修改對象的 isa 指針宵睦,令其指向特殊的僵尸類,從而使該對象變?yōu)榻┦瑢ο笕壕=┦惸軌蝽憫械倪x擇器状飞,響應方式為:打印一條包含消息內容及其接收者的消息,然后終止應用程序书斜。
參考文檔
本文參考以下文章并根據個人實踐后完成,非常感謝文章作者
iOS Zombie Objects(僵尸對象)原理探索 http://www.reibang.com/p/493f581d336b
iOS-野指針與僵尸對象 https://www.cnblogs.com/junhuawang/p/9213093.html