一、 背景和目标
在 Objective-C 中,对象被释放后,如果程序还访问该对象,就会出现"野指针"问题,导致程序崩溃(EXC_BAD_ACCESS)。为了更好地捕获和调试这种错误,通常会用"僵尸对象"技术:
- 目的 :替换对象的
dealloc
,在对象释放时记录该对象信息(地址、类名等)。 - 效果:当访问已释放对象时,可以查询这些信息,辅助定位问题。
其中KSCrashMonitor_Zombie这个类代码实现了这样一个僵尸对象监控模块。
二、 要数据结构和全局变量
arduino
typedef struct {
const void *object; // 被释放对象的地址
const char *className; // 被释放对象的类名
} Zombie;
static volatile Zombie *g_zombieCache; // 僵尸对象缓存区(哈希表)
static unsigned g_zombieHashMask; // 哈希掩码,用于快速索引缓存
static volatile bool g_isEnabled = false; // 是否启用监控
- g_zombieCache 是一个数组,缓存最近释放的对象信息。
- 使用哈希函数将对象地址映射到缓存槽位,支持快速查找。
- volatile 修饰保证多线程访问时的可见性。
三、 哈希索引函数
c
static inline unsigned hashIndex(const void *object) {
uintptr_t objPtr = (uintptr_t)object;
objPtr >>= (sizeof(object) - 1);
return objPtr & g_zombieHashMask;
}
- 将对象指针转换为整数,右移若干位(减少低位干扰),然后通过掩码取模,得到缓存索引。
g_zombieHashMask
= 缓存大小 - 1,缓存大小是2的幂,保证掩码操作等价于取模。
四、 替换 dealloc
实现(核心)
scss
#define CREATE_ZOMBIE_HANDLER_INSTALLER(CLASS) \
static IMP g_originalDealloc_##CLASS; \
static void handleDealloc_##CLASS(id self, SEL _cmd) { \
handleDealloc(self); \
typedef void (*fn)(id, SEL); \
fn f = (fn)g_originalDealloc_##CLASS; \
f(self, _cmd); \
} \
static void installDealloc_##CLASS() { \
Method method = class_getInstanceMethod(objc_getClass(#CLASS), sel_registerName("dealloc")); \
g_originalDealloc_##CLASS = method_getImplementation(method); \
method_setImplementation(method, (IMP)handleDealloc_##CLASS); \
}
- 这个宏为指定类(NSObject、NSProxy)生成替换
dealloc
的代码。 - 保存原始的
dealloc
方法实现指针。 - 新的
dealloc
会先调用handleDealloc(const void *self)
记录对象信息,再调用原始dealloc
完成释放。 - 对指指定类进行hook,
installDealloc_NSObject()
,installDealloc_NSProxy()
五、 记录释放对象信息
objectivec
static inline void handleDealloc(const void *self) {
volatile Zombie *cache = g_zombieCache;
if (cache != NULL) {
Zombie *zombie = (Zombie *)cache + hashIndex(self);
zombie->object = self;
Class class = object_getClass((id)self);
zombie->className = class_getName(class);
// 额外处理 NSException 对象,保存异常信息
for (; class != nil; class = class_getSuperclass(class)) {
if (class == g_lastDeallocedException.class) {
storeException(self);
}
}
}
}
- 将释放对象的地址和类名存入缓存对应槽位。
- 通过
object_getClass
和class_getName
获取类名,方便后续查询。 - 如果对象是
NSException
类型,调用storeException
保存异常的name
和reason
。
六、 异常信息保存
c
static void storeException(const void *exception) {
g_lastDeallocedException.address = exception;
copyStringIvar(exception, "name", g_lastDeallocedException.name, sizeof(g_lastDeallocedException.name));
copyStringIvar(exception, "reason", g_lastDeallocedException.reason, sizeof(g_lastDeallocedException.reason));
}
- 保存最近释放的异常对象信息。
- 通过
copyStringIvar
读取异常对象的name
和reason
成员变量字符串内容。
七、 读取对象成员字符串(辅助函数)
objectivec
static bool copyStringIvar(const void *self, const char *ivarName, char *buffer, int bufferLength) {
Class class = object_getClass((id)self);
if (class == nil) {
return false;
}
KSObjCIvar ivar = { 0 };
if (ksobjc_ivarNamed(class, ivarName, &ivar)) {
void *pointer;
if (ksobjc_ivarValue(self, ivar.index, &pointer)) {
if (ksobjc_isValidObject(pointer)) {
if (ksobjc_copyStringContents(pointer, buffer, bufferLength) > 0) {
return true;
}
}
}
}
return false;
}
- 通过运行时查询对象的 ivar(成员变量)地址,读取字符串内容。
- 这是为了安全地读取异常对象的
name
和reason
字符串。
八、查询僵尸对象信息接口
php
const char *kszombie_className(const void *object) {
volatile Zombie *cache = g_zombieCache;
if (cache == NULL || object == NULL) {
return NULL;
}
Zombie *zombie = (Zombie *)cache + hashIndex(object);
if (zombie->object == object) {
return zombie->className;
}
return NULL;
}
- 通过对象地址查找缓存槽位,如果缓存中有对应记录,返回类名。
- 方便崩溃报告或者调试工具调用,判断访问的对象是否是僵尸对象。
九、 启用与禁用监控
ini
static void setEnabled(bool isEnabled) {
if (isEnabled != g_isEnabled) {
g_isEnabled = isEnabled;
if (isEnabled) {
install();
} else {
g_isEnabled = true;
}
}
}
- 切换开关,开启时分配缓存、替换
dealloc
。 - 禁用时理论上应该恢复原始方法和释放缓存,但这里未实现。
十、 总结工作流程

十一、额外补充
- 为什么替换 NSObject 和 NSProxy 的 dealloc?
因为几乎所有 Objective-C 对象都继承自 NSObject 或 NSProxy,替换它们的dealloc
可以拦截绝大多数对象的释放。 - 哈希缓存的大小和冲突
缓存大小固定为 0x8000(32768),可能会有哈希冲突,后释放的对象会覆盖之前的记录。适合捕获最近释放的对象。 - 线程安全性
使用volatile
保证多线程可见性,但没有使用锁,存在一定的并发风险。实际使用时通常能接受。