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 保证多线程可见性,但没有使用锁,存在一定的并发风险。实际使用时通常能接受。
相关推荐
—Qeyser5 分钟前
让 Deepseek 写电器电费计算器(html版本)
前端·javascript·css·html·deepseek
UI设计和前端开发从业者36 分钟前
从UI前端到数字孪生:构建数据驱动的智能生态系统
前端·ui
Junerver1 小时前
Kotlin 2.1.0的新改进带来哪些改变
前端·kotlin
千百元2 小时前
jenkins打包问题jar问题
前端
喝拿铁写前端2 小时前
前端批量校验还能这么写?函数式校验器组合太香了!
前端·javascript·架构
巴巴_羊2 小时前
6-16阿里前端面试记录
前端·面试·职场和发展
我是若尘2 小时前
前端遇到接口批量异常导致 Toast 弹窗轰炸该如何处理?
前端
该用户已不存在3 小时前
8个Docker的最佳替代方案,重塑你的开发工作流
前端·后端·docker
然我3 小时前
面试官最爱的 “考试思维”:用闭包秒杀递归难题 🚀
前端·javascript·面试
明月与玄武3 小时前
HTML知识全解析:从入门到精通的前端指南(上)
前端·html