iOS开发,runtime实现切片编程原理以及实战用例

在 iOS 开发中,利用 Objective-C Runtime 实现切片编程(AOP,Aspect-Oriented Programming)的核心原理是 Method Swizzling。通过动态交换方法的实现(IMP),可以在不修改原始代码的情况下插入自定义逻辑。以下是详细原理和代码示例:


一、核心原理

  1. Method Swizzling

    • 通过 class_getInstanceMethodmethod_exchangeImplementations 交换两个方法的实现。
    • 将原始方法替换为自定义方法,在自定义方法中插入切片逻辑后调用原始实现。
  2. 动态消息转发

    • 使用 class_addMethod 动态添加方法实现,避免因原方法未实现导致的 Crash。
  3. 关联对象(可选)

    • 通过 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 UIViewControllerviewDidLoad 方法为例:

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

三、代码解释

  1. +load 方法

    • 类加载时自动调用,确保方法交换在程序启动时完成。
    • 使用 dispatch_once 保证线程安全,避免重复交换。
  2. 动态添加方法

    • 通过 class_addMethod 处理原方法未实现的情况(如父类方法未被子类实现)。
  3. 方法交换流程

    • 调用 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");
              }];

五、注意事项

  1. 避免重复交换

    • 使用 dispatch_once 确保每个方法只交换一次。
  2. 命名冲突

    • 为交换方法添加前缀(如 aspect_),防止与系统方法冲突。
  3. 子类未实现父类方法

    • 优先使用 class_addMethod 确保原方法存在。
  4. 性能影响

    • 避免对高频调用的方法(如 dealloc)进行 Hook。

以下是 6 个深入用例及其技术实现细节,结合底层原理和代码示例,帮助你彻底掌握这一技术。


用例 1:监控所有按钮点击事件(埋点统计)

需求

  • 无侵入式统计所有 UIButton 的点击事件,记录点击的类名和方法名。

实现方案

通过 Hook UIControlsendAction: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:全局页面生命周期监控

需求

  • 监控所有 UIViewControllerviewDidAppear: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

关键技术点

  1. 关联对象(Associated Object) :用于存储页面进入时间。
  2. 精准时长统计 :在 viewDidAppear 记录时间点,在 viewDidDisappear 计算差值。

用例 3:防止数组越界崩溃

需求

  • Hook NSArrayobjectAtIndex: 方法,在越界时返回 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

关键细节

  1. 类簇处理NSArray 实际类为 __NSArrayI,需通过 NSClassFromString 获取。
  2. 防御式编程:在调用原始方法前进行越界判断。

用例 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);
                        }];

核心技术

  1. NSInvocation 封装:处理可变参数和复杂方法签名。
  2. va_list 可变参数解析:兼容不同参数个数的方法。
  3. 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];

关键机制

  1. 关联对象生命周期绑定monitor 对象与目标对象生命周期同步。
  2. 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 的指针,或通过其他方式确保能准确恢复。
  • 在多线程环境下需加锁保证原子性。

深入原理:动态方法解析与消息转发

  1. Method Swizzling 的本质

    • 修改类的 method_list,交换两个 Method 结构体的 IMP 指针。
    • 通过 objc_msgSend 的消息查找机制,所有方法调用都会走到新的 IMP。
  2. 为什么要在 +load 中执行?

    • +load 方法在类被加载到 Runtime 时调用,早于 main 函数执行。
    • 确保方法交换在程序启动时完成,避免多线程竞争。
  3. 类簇(Class Clusters)的特殊处理

    • NSArrayNSString 等类属于类簇,实际类名是 __NSArrayI__NSCFString 等。
    • 需通过 NSClassFromString 或逆向工程获取真实类名。

总结:Runtime AOP 的最佳实践

场景 技术方案 注意事项
事件埋点 Hook UIControl 事件方法 注意类簇的真实类名
生命周期监控 交换 UIViewController 方法 使用关联对象存储额外数据
容器防崩溃 替换 NSArray/NSDictionary 方法 严格处理类簇
动态 Block 替换 使用 imp_implementationWithBlock 处理可变参数和复杂方法签名
内存泄漏检测 关联对象 + 辅助监控类 避免循环引用
方法替换撤销 记录原始 IMP 并恢复 多线程环境下需加锁

黄金法则

  • 始终在 +load 中使用 dispatch_once
  • 优先使用 class_addMethod 避免覆盖父类实现。
  • 为交换方法添加前缀(如 aop_)防止命名冲突。
  • 避免 Hook 高频方法(如 dealloc),可能引发性能问题。

通过灵活组合这些技术,可以实现无侵入式的日志、监控、安全校验等全局功能,极大提升代码可维护性。

相关推荐
uhakadotcom4 分钟前
Mars与PyODPS DataFrame:功能、区别和使用场景
后端·面试·github
uhakadotcom8 分钟前
PyTorch 分布式训练入门指南
算法·面试·github
uhakadotcom12 分钟前
PyTorch 与 Amazon SageMaker 配合使用:基础知识与实践
算法·面试·github
Moment22 分钟前
多人协同编辑算法 —— CRDT 算法 🐂🐂🐂
前端·javascript·面试
uhakadotcom29 分钟前
在Google Cloud上使用PyTorch:如何在Vertex AI上训练和调优PyTorch模型
算法·面试·github
鸿蒙布道师35 分钟前
鸿蒙NEXT开发数值工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
程序员清风2 小时前
Redis Pipeline 和 MGET,如果报错了,他们的异常机制是什么样的?
java·后端·面试
今天也想MK代码3 小时前
ReFormX:现代化的 React 表单解决方案 - 深度解析与最佳实践
前端·react.js·性能优化
雷渊4 小时前
深入分析学习 Arthas 在项目中的应用
java·后端·面试
佩奇的技术笔记4 小时前
高级:性能优化面试题深度剖析
性能优化