iOS Runtime模块隔离:为何协议方案可行但方法列表遍历失效?
模块隔离场景下Runtime方法遍历失效问题分析
项目结构背景
做代码模块隔离(类子工程架构)时,项目结构为1个主工程搭配两个Framework:
- 依赖关系:Framework A依赖Framework B
- 主工程同时添加了两个Framework的依赖,但仅显式导入Framework A,未导入Framework B
可正常运行的场景
- Framework A中定义公开协议:
public protocol AccountLotus { func methodA() }
- Framework B中定义协议实现类:
class AccountLotusoot: NSObject, AccountLotus { func methodA(){} }
主工程通过Runtime反射,用字符串ModeleB.AccountLotusoot拿到对应的NSObject子类,实例化后转成AccountLotus协议类型,调用methodA可以正常执行——哪怕这个方法的实现位于未被主工程显式导入的Framework B中。
失效场景
同一套架构下:
- Framework A中定义公开类:
public class Maid: NSObject{}
- Framework B中给Maid类添加扩展:
extension Maid{ dynamic func loginServiceName() -> String{ return "ModeleB.AccountLotusoot" } }
主工程调用下面的方法遍历Maid类的方法列表时,无法获取到Framework B扩展中实现的loginServiceName方法:
func printMethodNamesForClass(cls: AnyClass) { var methodCount: UInt32 = 0 let methodList = class_copyMethodList(cls, &methodCount) if let methodList = methodList{ let arr = Array(arrayLiteral: methodList) for (i, m) in arr.enumerated(){ let name: String = methodName(m: m.pointee) ?? "Not Known" print("#\(i): \(name)") } free(methodList) } } func methodName(m: Method) -> String? { let sel = method_getName(m) let nameCString = sel_getName(sel) return String(cString: nameCString) }
失效原因
两个核心问题共同导致失效:
- dyld惰性加载导致扩展方法未注册到目标类
Swift类扩展桥接到Objective-C Runtime时,本质是通过OC的Category机制实现的。Category中的方法不会被直接编译到目标类的方法列表中,需要在所属Framework的二进制镜像被dyld完整加载时,由Runtime的加载回调主动调用class_addMethod把扩展方法追加到目标类的方法列表里。
主工程没有显式导入Framework B、代码中也没有任何直接引用Framework B内符号的逻辑时,dyld会对Framework B开启惰性加载优化:App启动阶段不会执行Framework B的初始化逻辑,包括+load方法执行、Category方法注册。此时遍历Maid类的方法列表,Framework B中实现的扩展方法根本还没被挂载到类上,自然查询不到。
这也是第一个反射场景能正常运行的核心原因:NSClassFromString查找类的过程会触发系统遍历所有可加载的镜像匹配类名,这个过程会强制加载Framework B,等实例化完成调用方法时,对应的方法实现已经被正确注册,所以调用可以成功。 - 方法遍历代码存在逻辑bug
代码中let arr = Array(arrayLiteral: methodList)的写法是错误的:class_copyMethodList返回的是C风格的连续Method指针数组,Array(arrayLiteral:)只会把整个指针数组作为单个元素存入Swift数组,遍历的时候最多只能拿到方法列表的第一个元素,后续所有方法都不会被遍历到,哪怕方法已经完成注册,这个写法也拿不到完整的方法列表。
解决方案
按稳定性从高到低排序:
- 首先修正方法遍历的代码逻辑,通过C指针偏移的方式遍历连续内存中的方法,不要用Array字面量错误包装C指针,修正后的遍历代码如下:
func printMethodNamesForClass(cls: AnyClass) { var methodCount: UInt32 = 0 guard let methodList = class_copyMethodList(cls, &methodCount) else { return } for i in 0..<Int(methodCount) { let method = methodList[i] let name = methodName(m: method) ?? "Not Known" print("#\(i): \(name)") } free(methodList) }
- 组件服务注册不要依赖主工程主动遍历类方法列表,改成在Framework B内部通过
@objc class func load()方法(对应OC的+load),主动向Framework A暴露的全局注册中心注册服务,这种方式只要Framework被加载就会自动执行注册逻辑,完全不依赖主工程的显式导入,是组件化架构下的标准实现方案。 - 如果确实需要主工程能遍历到所有跨Framework的扩展方法,可以在App启动阶段主动加载Framework B的Bundle:拿到Framework B对应的Bundle实例后调用
load()方法,强制触发Framework的初始化和Category注册流程,加载完成后再遍历方法列表即可拿到扩展中的loginServiceName方法。 - 在Framework A的初始化逻辑中引用Framework B的公开空类/空符号,打破dyld的惰性加载判断,让Framework B在App启动阶段就被完整加载,避免出现方法未注册的问题。
内容的提问来源于stack exchange,提问作者DNG
相关产品推荐
相关产品推荐

