公司动态

JSDebugger 核心原理:libffi 如何动态调用任意 Objective-C 方法(附关键代码逐行解读)

📅 2026/8/18 15:15:21
JSDebugger 核心原理:libffi 如何动态调用任意 Objective-C 方法(附关键代码逐行解读)
JSDebugger 核心原理libffi 如何动态调用任意 Objective-C 方法附关键代码逐行解读【免费下载链接】JSDebuggerJavaScript-Based Debugger For Inspecting Running State Of Your Application项目地址: https://gitcode.com/gh_mirrors/js/JSDebuggerJSDebugger 是一款基于 JavaScript 的运行时调试工具它最惊艳的能力是无需手动声明 JSExport 协议就能在 JavaScript 中直接调用任意 Objective-C 方法。这个能力的背后正是 libffi 动态调用 Objective-C 方法的魔法。本篇文章将沿着一次 JS 调用的完整链路逐行解读JDFFIContext.m等核心源码帮你彻底理解 libffi、Objective-C Runtime 与 JavaScriptCore 三者是如何协同工作的。一、整体调用链路一次 JS 调用发生了什么在动手读代码之前先看一个最简单的例子。在 JSDebugger 的 demo.js 中你可以这样写var view UIView.alloc().init(); view.setBackgroundColor_(UIColor.redColor());这里setBackgroundColor_就是把 Objective-C 的setBackgroundColor:中的冒号替换为下划线得到的。当你敲下这行 JS 时背后发生了这样一条流水线步骤模块职责1JDEngine全局对象拦截属性访问把类名包装成 JS 对象2JDMethod4JS触发callAsFunction查找到 IMP 与方法签名3JDMethodBridge解析 NSMethodSignature得到参数类型编码列表4JDFFIContext把 JS 参数转成 C 内存、配置 libffi、发起调用5JDFFIContext把返回值从 C 内存转回 JSValue对应到源码入口在 JDEngine.m核心桥接在 JDFFIContext.m。下面我们逐层拆解。二、第一步把类名暴露给 JS 世界JDEngineJDEngine 创建了一个自定义的全局 JS 对象并实现了getProperty回调。当 JS 里访问UIView这个变量时它直接用 Runtime 的NSClassFromString去查对应的类并把类包装成JDClass4JS类型的 JS 对象返回static JSValueRef JDGlobalGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef *exception) { NSString *className JDCreateNSStringFromJSString(ctx, propertyName); Class cls NSClassFromString(className); if (!cls) return NULL; // 沿继承链继续查找 return JSObjectMake(ctx, JDClass4JS(), (__bridge void *)cls); }关键点JS 里访问的任意名字都会被翻译成一次 OC 类的查找。这就是为什么你可以凭空使用UIView、UIColor这些类而不需要任何预声明。三、第二步方法被调用时如何找到 IMP当你在 JS 中对一个类对象或实例对象调用方法时会触发 JDMethod4JS.m 中的JDMethodCallAsFunction。它的核心逻辑只有三步SEL sel JSObjectGetPrivate(function); // 1. 取出方法名对应的 selector Method m class_getInstanceMethod(toSearchClass, sel); // 2. 在类中查找 Method IMP imp method_getImplementation(m); // 3. 拿到真正的函数指针 IMP随后它用methodSignatureForSelector:拿到NSMethodSignature把它和实例、selector、IMP 一起封装成JDMethodBridge最后交给JDCallFunction。到这里调用哪个函数已经确定剩下的问题是怎么把 JS 参数塞进去——这正是 libffi 登场的时刻。四、第三步类型编码如何翻译成 libffi 类型Objective-C 的方法签名是运行时自描述的每一个参数都有一个类型编码Type Encoding比如i表示 int、表示对象、:表示 SEL。JDEncoding 先把它归一化成枚举见 JDEncoding.m。真正连接 libffi 的是JDConvertEncodingToFFI一个简单的 switch 映射static ffi_type *JDConvertEncodingToFFI(JDEncoding encoding) { switch (encoding) { case JDEncodingVoid: return ffi_type_void; case JDEncodingBool: return ffi_type_uint8; case JDEncodingInt32: return ffi_type_sint32; case JDEncodingFloat: return ffi_type_float; case JDEncodingObject: case JDEncodingClass: case JDEncodingSEL: case JDEncodingPointer: return ffi_type_pointer; ... } }一句话总结OC 类型编码 → JDEncoding 枚举 → ffi_type 指针三步完成类型对齐。对于结构体如 CGRectJDConvertStructToFFI会递归地把字段逐个转成ffi_type并组装成FFI_TYPE_STRUCT。五、第四步JS 值如何落进 C 内存参数转换libffi 要求把参数按类型写进连续的内存地址。函数JDSetJSValueToAddress干的就是这件事把 JS 的 number、string、array、object 分别转成对应 C 类型的字节。比如 JS number 转成 int32} else if (encoding JDEncodingInt32) { *(int32_t *)dst (int32_t)val; }最妙的是对 OC 对象的处理JSObjectGetPrivate取出之前包装时塞进去的 OC 对象指针直接写入内存如果是普通 JS 字典还会尝试按jd_canConvertToRect等规则转成 CGRect、CGPoint、CGSize 结构体。这一步让JS 传字典给 OC 方法成为可能。六、第五步核心魔法——ffi_cif 与 ffi_call 准备工作就绪后就到了整篇文章的主角JDCallFunction。它位于 JDFFIContext.m 的第 375 行附近。核心流程分四步1. 布局前两个隐藏参数每个 OC 方法实际都有self和_cmd两个隐藏参数ffiArgTypes[0] ffi_type_pointer; id instance methodBridge.instance; ffiArgs[0] instance; ffiArgTypes[1] ffi_type_pointer; SEL selector methodBridge.selector; ffiArgs[1] selector;2. 逐个转换剩余参数遍历 JS 传入的参数先取类型JDParameter 保存在methodBridge.argumentsType中再分配内存、转换值ffi_type *ffiType JDConvertEncodingToFFI(p.encoding); ffiArgTypes[i 2] ffiType; void *ffiArgPtr alloca(ffiType-size); JDSetJSValueToAddress(p.encoding, ctx, arguments[i], ffiArgPtr); ffiArgs[i 2] ffiArgPtr;3. 准备调用描述符cifcif 是 libffi 的核心概念它描述了函数签名让 libffi 知道该用哪套调用约定、参数和返回值长什么样ffi_cif cif; ffi_status ffiPrepStatus ffi_prep_cif_var(cif, FFI_DEFAULT_ABI, (unsigned int)methodBridge.argumentsType.count, (unsigned int)argCount, returnType, ffiArgTypes);注意这里用的是ffi_prep_cif_var因为它还支持 Objective-C 的可变参数variadic方法代码中会额外追加一个 NULL 结尾用于NS_REQUIRES_NIL_TERMINATION类方法。4. 真正发起调用ffi_call(cif, methodBridge.imp, returnPtr, ffiArgs);一行ffi_calllibffi 就会按照目标 CPU 的 ABI 规则把内存中的参数压栈/放入寄存器跳转到methodBridge.imp真实的 IMP 函数指针执行并把返回值写回returnPtr。七、第六步返回值如何变回 JS 对象调用结束只是完成了一半还要把 C 的返回值翻译回 JS。JDFillJSValueFromAddress按返回值类型分派case JDEncodingObject: { id obj (__bridge id)(*(void**)src); if (class_isMetaClass([obj class])) { return JSObjectMake(ctx, JDClass4JS(), (__bridge void *)obj); } else { return JSObjectMake(ctx, JDInstance4JS(), (__bridge void *)obj); } }如果返回值是 OC 对象就判断它是类还是实例重新包装成对应的 JS 包装对象如果是结构体则走JDBuildDictFromStruct把内存逐字段拆成字典再序列化成 JSON 字符串后交给JSValueMakeFromJSONString。至此一次完整的JS 调用 OC 方法闭环完成八、一张图看懂 libffi 动态调用 OC 方法的完整流程 ️JS 方法调用 │ ▼ JDMethod4JS.callAsFunction ──► 查 SEL / IMP / NSMethodSignature │ ▼ JDMethodBridge 解析签名 ──────► 得到参数与返回值的类型编码列表 │ ▼ JDConvertEncodingToFFI ───────► 类型编码 → ffi_type │ ▼ JDSetJSValueToAddress ────────► JS 值 → C 内存 │ ▼ ffi_prep_cif_var ffi_call ──► 按 ABI 规则动态调用 IMP │ ▼ JDFillJSValueFromAddress ─────► 返回值 → JSValue交还 JS 继续执行九、总结libffi 为什么是这一切的关键✨回顾整条链路JSDebugger 的巧妙之处在于把方法调用这个动作彻底数据化类型信息来自 Runtime 的自描述签名参数值通过统一的转换函数写进内存最后交给 libffi 这个万能跳板按 ABI 规则完成真正的函数调用。正因为有 libffi它才能做到✅ 无需预定义 JSExport运行时即可调用任意 OC 方法✅ 支持 int、float、指针、对象、结构体等多种类型✅ 支持可变参数variadic方法✅ 返回值可以是对象、类、SEL 甚至 C 指针如果你对源码感兴趣建议按这个顺序阅读JDEngine.m入口→JDMethodBridge.m签名解析→JDFFIContext.mlibffi 核心→JDEncoding.m类型系统再配合 demo.js 里的示例跑一遍你会对 libffi 动态调用 Objective-C 方法有更直观的体感。想亲自上手体验克隆仓库https://gitcode.com/gh_mirrors/js/JSDebugger打开 JSDebuggerDemo 工程改一改 demo.js 保存就能在模拟器里实时看到你的改动生效——这就是调试工具的终极乐趣 【免费下载链接】JSDebuggerJavaScript-Based Debugger For Inspecting Running State Of Your Application项目地址: https://gitcode.com/gh_mirrors/js/JSDebugger创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考