KSCrash中僵尸对象监控原理与实现

一、 背景和目标

在 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_getClassclass_getName 获取类名,方便后续查询。
  • 如果对象是 NSException 类型,调用 storeException 保存异常的 namereason

六、 异常信息保存

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 读取异常对象的 namereason 成员变量字符串内容。

七、 读取对象成员字符串(辅助函数)

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(成员变量)地址,读取字符串内容。
  • 这是为了安全地读取异常对象的 namereason 字符串。

八、查询僵尸对象信息接口

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 保证多线程可见性,但没有使用锁,存在一定的并发风险。实际使用时通常能接受。
相关推荐
Mr_Mao2 小时前
Naive Ultra:中后台 Naive UI 增强组件库
前端
前端小趴菜054 小时前
React-React.memo-props比较机制
前端·javascript·react.js
摸鱼仙人~5 小时前
styled-components:现代React样式解决方案
前端·react.js·前端框架
sasaraku.6 小时前
serviceWorker缓存资源
前端
RadiumAg7 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo7 小时前
ES6笔记2
开发语言·前端·javascript
yanlele7 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子8 小时前
React状态管理最佳实践
前端
烛阴8 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子8 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端