Method Swizzling、AOP 面向切片编程

对 App 的用户行为进行追踪和分析。简单说,就是当用户看到某个 View 或者点击某个 Button 的时候,就把这个事件记下来。

手动添加

最直接粗暴的方式就是在每个 viewDidAppear 里添加记录事件的代码。

1
2
3
4
5
6
7
8
9
10
11
@implementation MyViewController ()
- (void)viewDidAppear:(BOOL)animated {
    [ super  viewDidAppear:animated];     // Custom code 
    // Logging
    [Logging logWithEventName:@“my view did appear”];
}

- (void)myButtonClicked:(id)sender {     // Custom code 
    // Logging
    [Logging logWithEventName:@“my button clicked”];
}

这种方式的缺点也很明显:它破坏了代码的干净整洁。因为 Logging 的代码本身并不属于ViewController 里的主要逻辑。随着项目扩大、代码量增加,你的 ViewController 里会到处散布着 Logging 的代码。这时,要找到一段事件记录的代码会变得困难,也很容易忘记添加事件记录的代码。

你可能会想到用继承或者类别,在重写的方法里添加事件记录的代码。比如用类别的代码大概长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation UIViewController (Logging)
- (void)viewDidAppear:(BOOL)animated {
    [ super  viewDidAppear:animated];     // Custom code 
    // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}

- (void)myButtonClicked:(id)sender {     // Custom code 
    // Logging
    NSString *name = [NSString stringWithFormat:@“my button  in  %@ is clicked”, NSStringFromClass([self class])];
    [Logging logWithEventName:name];
}

Logging 的代码都很相似,通过继承或类别重写相关方法是可以把它从主要逻辑中剥离出来。但同时也带来新的问题:

1.你需要继承 UIViewController, UITableViewController, UICollectionViewController 所有这些 ViewController ,或者给他们添加类别;

2.每个 ViewController 里的 ButtonClick 方法命名不可能都一样;

3.你不能控制别人如何去实例化你的子类;

4.对于类别,你没办法调用到原来的方法实现。大多时候,我们重写一个方法只是为了添加一些代码,而不是完全取代它。

5.如果有两个类别都实现了相同的方法,运行时没法保证哪一个类别的方法会给调用。


Method Swizzling

Method Swizzling 利用 Runtime 特性把一个方法的实现与另一个方法的实现进行替换。

上一篇文章有讲到每个类里都有一个 Dispatch Table ,将方法的名字(SEL)跟方法的实现(IMP,指向 C 函数的指针)一一对应。Swizzle 一个方法其实就是在程序运行时在 Dispatch Table 里做点改动,让这个方法的名字(SEL)对应到另个 IMP 。

首先定义一个类别,添加将要 Swizzled 的方法:

1
2
3
4
5
@implementation UIViewController (Logging)
- (void)swizzled_viewDidAppear:(BOOL)animated {     // call original implementation
    [self swizzled_viewDidAppear:animated];     // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}

代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。调用 viewDidAppear: 会调用你实现的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 里调用 swizzled_viewDidAppear: 实际上调用的是原来的 viewDidAppear: 。

接下来实现 swizzle 的方法 :

1
2
3
4
5
6
7
8
9
10
11
@implementation UIViewController (Logging)
-(void)swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)  {     // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);     // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));     // the method doesn’t exist and we just added one
    if  (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
   
    else  {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

这里唯一可能需要解释的是 class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

最后,我们只需要确保在程序启动的时候调用 swizzleMethod 方法。比如,我们可以在之前 UIViewController 的 Logging 类别里添加 +load: 方法,然后在 +load: 里把 viewDidAppear 给替换掉:

1
2
3
4
@implementation UIViewController (Logging)
+ (void)load {
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load: 是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个 +load: 消息。


完整实现:



@implementation UIViewController (Logging)
+ (void)load {
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
-(void)swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)  {     // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);     // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));     // the method doesn’t exist and we just added one
    if  (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
   
    else  {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
- (void)swizzled_viewDidAppear:(BOOL)animated {     // call original implementation
    [self swizzled_viewDidAppear:animated];     // Logging
    [Logging logWithEventName:NSStringFromClass([self class])];
}



通过 Method Swizzling ,我们成功把逻辑代码跟处理事件记录的代码解耦。当然除了 Logging ,还有很多类似的事务,如 Authentication 和 Caching。这些事务琐碎,跟主要业务逻辑无关,在很多地方都有,又很难抽象出来单独的模块。这种程序设计问题,业界也给了他们一个名字 - Cross Cutting Concerns

而像上面例子用 Method Swizzling 动态给指定的方法添加代码,以解决 Cross Cutting Concerns 的编程方式叫:Aspect Oriented Programming

Aspect Oriented Programming (面向切面编程)

利用 objective-C Runtime 特性和 Aspect Oriented Programming ,我们可以把琐碎事务的逻辑从主逻辑中分离出来,作为单独的模块。它是对面向对象编程模式的一个补充。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值