在 iOS 开发中,利用 Objective-C Runtime 实现切片编程(AOP,Aspect-Oriented Programming)的核心原理是 Method Swizzling。通过动态交换方法的实现(IMP),可以在不修改原始代码的情况下插入自定义逻辑。以下是详细原理和代码示例:
一、核心原理
-
Method Swizzling
- 通过
class_getInstanceMethod
和method_exchangeImplementations
交换两个方法的实现。 - 将原始方法替换为自定义方法,在自定义方法中插入切片逻辑后调用原始实现。
- 通过
-
动态消息转发
- 使用
class_addMethod
动态添加方法实现,避免因原方法未实现导致的 Crash。
- 使用
-
关联对象(可选)
- 通过
objc_setAssociatedObject
存储切片逻辑的 Block,实现更灵活的 AOP。
- 通过
二、完整代码示例
1. 创建 AOP 工具类 AspectUtility
scss
#import <objc/runtime.h>
@implementation AspectUtility
+ (void)hookClass:(Class)targetClass
originalSelector:(SEL)originalSelector
swizzledSelector:(SEL)swizzledSelector {
Method originalMethod = class_getInstanceMethod(targetClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector);
// 尝试给原方法添加实现(避免原方法未实现)
BOOL didAddMethod = class_addMethod(targetClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 添加成功:替换新方法的实现为原始实现
class_replaceMethod(targetClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 添加失败:直接交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
2. 实现具体切片逻辑
以 Hook UIViewController
的 viewDidLoad
方法为例:
objectivec
// UIViewController+Aspect.h
#import <UIKit/UIKit.h>
@interface UIViewController (Aspect)
@end
// UIViewController+Aspect.m
#import "UIViewController+Aspect.h"
#import "AspectUtility.h"
@implementation UIViewController (Aspect)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[AspectUtility hookClass:[self class]
originalSelector:@selector(viewDidLoad)
swizzledSelector:@selector(aspect_viewDidLoad)];
});
}
- (void)aspect_viewDidLoad {
// 插入前置逻辑
NSLog(@"Before viewDidLoad: %@", NSStringFromClass([self class]));
// 调用原始实现(实际已交换为 aspect_viewDidLoad)
[self aspect_viewDidLoad];
// 插入后置逻辑
NSLog(@"After viewDidLoad: %@", NSStringFromClass([self class]));
}
@end
三、代码解释
-
+load
方法- 类加载时自动调用,确保方法交换在程序启动时完成。
- 使用
dispatch_once
保证线程安全,避免重复交换。
-
动态添加方法
- 通过
class_addMethod
处理原方法未实现的情况(如父类方法未被子类实现)。
- 通过
-
方法交换流程
- 调用
aspect_viewDidLoad
时,实际执行的是原始viewDidLoad
的实现。 - 在自定义方法中插入日志代码后,通过
[self aspect_viewDidLoad]
调用原始实现。
- 调用
四、高级用法:Block 动态切片
通过关联对象存储 Block,实现更灵活的切片:
scss
#import <objc/runtime.h>
typedef void (^AspectBlock)(id target);
@implementation AspectUtility
+ (void)hookClass:(Class)targetClass
selector:(SEL)selector
preBlock:(AspectBlock)preBlock
postBlock:(AspectBlock)postBlock {
Method originalMethod = class_getInstanceMethod(targetClass, selector);
IMP originalIMP = method_getImplementation(originalMethod);
IMP newIMP = imp_implementationWithBlock(^(id self) {
if (preBlock) preBlock(self);
((void (*)(id, SEL))originalIMP)(self, selector);
if (postBlock) postBlock(self);
});
method_setImplementation(originalMethod, newIMP);
}
@end
// 调用示例
[AspectUtility hookClass:[UIViewController class]
selector:@selector(viewDidLoad)
preBlock:^(id self) {
NSLog(@"Before viewDidLoad");
}
postBlock:^(id self) {
NSLog(@"After viewDidLoad");
}];
五、注意事项
-
避免重复交换
- 使用
dispatch_once
确保每个方法只交换一次。
- 使用
-
命名冲突
- 为交换方法添加前缀(如
aspect_
),防止与系统方法冲突。
- 为交换方法添加前缀(如
-
子类未实现父类方法
- 优先使用
class_addMethod
确保原方法存在。
- 优先使用
-
性能影响
- 避免对高频调用的方法(如
dealloc
)进行 Hook。
- 避免对高频调用的方法(如
以下是 6 个深入用例及其技术实现细节,结合底层原理和代码示例,帮助你彻底掌握这一技术。
用例 1:监控所有按钮点击事件(埋点统计)
需求
- 无侵入式统计所有
UIButton
的点击事件,记录点击的类名和方法名。
实现方案
通过 Hook UIControl
的 sendAction:to:forEvent:
方法,插入埋点逻辑:
scss
// UIControl+AOP.m
#import <objc/runtime.h>
@implementation UIControl (AOP)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSendAction];
});
}
+ (void)swizzleSendAction {
Class cls = [UIControl class];
SEL originalSel = @selector(sendAction:to:forEvent:);
SEL swizzledSel = @selector(aop_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)aop_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// 插入埋点逻辑
NSString *className = NSStringFromClass([target class]);
NSString *methodName = NSStringFromSelector(action);
NSLog(@"埋点: %@ - %@", className, methodName);
// 调用原始实现
[self aop_sendAction:action to:target forEvent:event];
}
@end
核心原理
- Hook 的是
UIControl
的事件派发核心方法sendAction:to:forEvent:
。 - 所有按钮、开关等继承自
UIControl
的组件点击都会被捕获。
用例 2:全局页面生命周期监控
需求
- 监控所有
UIViewController
的viewDidAppear:
和viewDidDisappear:
方法。 - 统计页面停留时长。
实现代码
objectivec
// UIViewController+AOP.m
#import <objc/runtime.h>
@implementation UIViewController (AOP)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleMethod:@selector(viewDidAppear:)
withMethod:@selector(aop_viewDidAppear:)];
[self swizzleMethod:@selector(viewDidDisappear:)
withMethod:@selector(aop_viewDidDisappear:)];
});
}
+ (void)swizzleMethod:(SEL)originalSel withMethod:(SEL)swizzledSel {
Method originalMethod = class_getInstanceMethod(self, originalSel);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSel);
BOOL didAdd = class_addMethod(self,
originalSel,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAdd) {
class_replaceMethod(self,
swizzledSel,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)aop_viewDidAppear:(BOOL)animated {
[self aop_viewDidAppear:animated];
NSLog(@"进入页面: %@", NSStringFromClass([self class]));
self.enterTime = [NSDate date]; // 通过关联对象存储时间
}
- (void)aop_viewDidDisappear:(BOOL)animated {
[self aop_viewDidDisappear:animated];
NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.enterTime];
NSLog(@"离开页面: %@, 停留时长: %.2fs", NSStringFromClass([self class]), duration);
}
// 关联对象存储 enterTime
- (void)setEnterTime:(NSDate *)enterTime {
objc_setAssociatedObject(self, @selector(enterTime), enterTime, OBJC_ASSOCIATION_RETAIN);
}
- (NSDate *)enterTime {
return objc_getAssociatedObject(self, @selector(enterTime));
}
@end
关键技术点
- 关联对象(Associated Object) :用于存储页面进入时间。
- 精准时长统计 :在
viewDidAppear
记录时间点,在viewDidDisappear
计算差值。
用例 3:防止数组越界崩溃
需求
- Hook
NSArray
的objectAtIndex:
方法,在越界时返回nil
而非崩溃。
实现代码
objectivec
// NSArray+AOP.m
#import <objc/runtime.h>
@implementation NSArray (AOP)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"__NSArrayI"); // 不可变数组类簇
[self swizzleMethod:cls
originalSelector:@selector(objectAtIndex:)
swizzledSelector:@selector(aop_objectAtIndex:)];
});
}
+ (void)swizzleMethod:(Class)class originalSelector:(SEL)originalSel swizzledSelector:(SEL)swizzledSel {
Method originalMethod = class_getInstanceMethod(class, originalSel);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSel);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (id)aop_objectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self aop_objectAtIndex:index]; // 调用原始实现
} else {
NSLog(@"⚠️ 数组越界: index=%lu, count=%lu", (unsigned long)index, (unsigned long)self.count);
return nil;
}
}
@end
关键细节
- 类簇处理 :
NSArray
实际类为__NSArrayI
,需通过NSClassFromString
获取。 - 防御式编程:在调用原始方法前进行越界判断。
用例 4:动态替换方法实现(Block 高级用法)
需求
- 通过 Block 动态替换任意类的方法实现,支持前置(before)和后置(after)逻辑。
完整实现
scss
// AspectManager.h
typedef void (^AspectBlock)(id target, NSInvocation *invocation);
@interface AspectManager : NSObject
+ (void)hookInstanceMethod:(Class)targetClass
selector:(SEL)selector
beforeBlock:(AspectBlock)beforeBlock
afterBlock:(AspectBlock)afterBlock;
@end
// AspectManager.m
#import <objc/runtime.h>
#import <objc/message.h>
@implementation AspectManager
+ (void)hookInstanceMethod:(Class)targetClass
selector:(SEL)selector
beforeBlock:(AspectBlock)beforeBlock
afterBlock:(AspectBlock)afterBlock {
Method originalMethod = class_getInstanceMethod(targetClass, selector);
IMP originalIMP = method_getImplementation(originalMethod);
IMP newIMP = imp_implementationWithBlock(^(id self, ...) {
// 创建 NSInvocation
NSMethodSignature *signature = [targetClass instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:selector];
// 处理可变参数
va_list args;
va_start(args, self);
for (int i = 2; i < signature.numberOfArguments; i++) { // self 和 _cmd 是前两个参数
void *arg = va_arg(args, void *);
[invocation setArgument:arg atIndex:i];
}
va_end(args);
// 执行前置逻辑
if (beforeBlock) beforeBlock(self, invocation);
// 调用原始方法
((void (*)(id, SEL, ...))originalIMP)(self, selector, args);
// 执行后置逻辑
if (afterBlock) afterBlock(self, invocation);
};
method_setImplementation(originalMethod, newIMP);
}
@end
// 调用示例:Hook UIViewController 的 viewWillAppear:
[AspectManager hookInstanceMethod:[UIViewController class]
selector:@selector(viewWillAppear:)
beforeBlock:^(id target, NSInvocation *invocation) {
NSLog(@"Before viewWillAppear: %@", target);
}
afterBlock:^(id target, NSInvocation *invocation) {
NSLog(@"After viewWillAppear: %@", target);
}];
核心技术
NSInvocation
封装:处理可变参数和复杂方法签名。va_list
可变参数解析:兼容不同参数个数的方法。- IMP 与 Block 转换 :通过
imp_implementationWithBlock
动态创建方法实现。
用例 5:检测 Dealloc 是否执行(内存泄漏监控)
需求
- 监控指定对象的
dealloc
是否正常执行,用于排查内存泄漏。
实现代码
objectivec
// NSObject+DeallocMonitor.m
#import <objc/runtime.h>
@implementation NSObject (DeallocMonitor)
- (void)monitorDealloc {
@synchronized (self) {
static const char kDeallocMonitorKey;
if (objc_getAssociatedObject(self, &kDeallocMonitorKey)) return;
// 创建虚拟对象,在其 dealloc 时触发回调
__weak typeof(self) weakSelf = self;
id monitor = [[DeallocMonitor alloc] initWithBlock:^{
NSLog(@"✅ %@ 正常释放", NSStringFromClass([weakSelf class]));
}];
objc_setAssociatedObject(self, &kDeallocMonitorKey, monitor, OBJC_ASSOCIATION_RETAIN);
}
}
@end
// 辅助类 DeallocMonitor
@interface DeallocMonitor : NSObject
@property (nonatomic, copy) void (^deallocBlock)(void);
@end
@implementation DeallocMonitor
- (instancetype)initWithBlock:(void (^)(void))block {
if (self = [super init]) {
_deallocBlock = [block copy];
}
return self;
}
- (void)dealloc {
if (_deallocBlock) _deallocBlock();
}
@end
// 使用示例
UIViewController *vc = [[UIViewController alloc] init];
[vc monitorDealloc];
关键机制
- 关联对象生命周期绑定 :
monitor
对象与目标对象生命周期同步。 DeallocMonitor
辅助类 :在其dealloc
中触发回调,间接监控目标对象释放。
用例 6:方法替换的撤销(动态恢复原始方法)
需求
- 在某些条件下(如测试环境)撤销 Method Swizzling,恢复原始方法。
实现代码
scss
// AspectUtility+Rollback.m
@implementation AspectUtility (Rollback)
+ (void)rollbackHookForClass:(Class)targetClass
originalSelector:(SEL)originalSelector
swizzledSelector:(SEL)swizzledSelector {
Method originalMethod = class_getInstanceMethod(targetClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(targetClass, swizzledSelector);
if (!originalMethod || !swizzledMethod) return;
// 检查当前 IMP 是否已被交换
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
if (originalIMP == swizzledIMP) {
// 恢复原始 IMP
method_setImplementation(originalMethod, originalIMP);
}
}
@end
注意事项
- 需要记录原始 IMP 的指针,或通过其他方式确保能准确恢复。
- 在多线程环境下需加锁保证原子性。
深入原理:动态方法解析与消息转发
-
Method Swizzling 的本质
- 修改类的
method_list
,交换两个Method
结构体的IMP
指针。 - 通过
objc_msgSend
的消息查找机制,所有方法调用都会走到新的 IMP。
- 修改类的
-
为什么要在
+load
中执行?+load
方法在类被加载到 Runtime 时调用,早于main
函数执行。- 确保方法交换在程序启动时完成,避免多线程竞争。
-
类簇(Class Clusters)的特殊处理
- 如
NSArray
、NSString
等类属于类簇,实际类名是__NSArrayI
、__NSCFString
等。 - 需通过
NSClassFromString
或逆向工程获取真实类名。
- 如
总结:Runtime AOP 的最佳实践
场景 | 技术方案 | 注意事项 |
---|---|---|
事件埋点 | Hook UIControl 事件方法 |
注意类簇的真实类名 |
生命周期监控 | 交换 UIViewController 方法 |
使用关联对象存储额外数据 |
容器防崩溃 | 替换 NSArray /NSDictionary 方法 |
严格处理类簇 |
动态 Block 替换 | 使用 imp_implementationWithBlock |
处理可变参数和复杂方法签名 |
内存泄漏检测 | 关联对象 + 辅助监控类 | 避免循环引用 |
方法替换撤销 | 记录原始 IMP 并恢复 | 多线程环境下需加锁 |
黄金法则:
- 始终在
+load
中使用dispatch_once
。 - 优先使用
class_addMethod
避免覆盖父类实现。 - 为交换方法添加前缀(如
aop_
)防止命名冲突。 - 避免 Hook 高频方法(如
dealloc
),可能引发性能问题。
通过灵活组合这些技术,可以实现无侵入式的日志、监控、安全校验等全局功能,极大提升代码可维护性。