在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 在Objective-C中也是可以实现AOP的.

这两天阅读了两个简单的第三方框架, MJRefresh和DZNEmptyDataSet. 这两个框架中都有用到AOP相关的方法.

1. 简单谈谈Runtime

在Objective-C中要实现面向切面, 必须借助runtime中的方法和原理.

在runtime中,Objective-C中的一个方法使用C语言结构体表示,叫Method, struct objc_method的定义如下所示:

struct objc_method
     SEL method_name         OBJC2_UNAVAILABLE;
     char *method_types      OBJC2_UNAVAILABLE;
     IMP method_imp          OBJC2_UNAVAILABLE;
}
  1. method_name : 是一个方法的方法选择器.
  2. *method_types : 是一个c字符串, 编码了方法的参数和返回值.
  3. method_imp : 是一个方法的指针, 指向这个方法在内存中的地址.

通过以下两个方法可以获取一个Method类型的对象:

Method class_getClassMethod(Class aClass, SEL aSelector);
Method class_getInstanceMethod(Class aClass, SEL aSelector);

如果我们获取了一个类的一个Method, 并更改了它的method_imp指针的指向, 这样就能更改方法的实现, 这样调用系统方法, 却会调用到我们自己的方法, 我们可以在自己定义的方法中调用系统的方法, 这才是正确的面向切面编程.

2. MJRefresh中交换两个方法的实现的实现

MJRefresh直接使用runtime提供的方法, 简单粗暴, 直接交换了两个方法的实现.

代码如下所示:

// 交换两个实例方法的实现
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{//method_exchangeImplementations作用是交换两个方法的实现, 参数是两个Method类型的结构体指针. Method是objc_method类型的结构体指针
    method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}

// load是一个初始化方法, 在类刚刚开始初始化时候调用, 在类刚开始初始化时候调用这个方法, 交换两个方法的实现
+ (void)load
{
    [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
}

// 这里的逻辑是这样的, 我们刷新tableView数据时候调用reloadData, 实际上会调用到mj_reloadData方法, 这个方法的实现调用mj_reloadData实际上会调用到系统的reloadData方法, 然后执行 UIScrollView的executeReloadDataBlock方法, 来执行一个block代码块
- (void)mj_reloadData
{
    [self mj_reloadData];
    
    [self executeReloadDataBlock];
}

MJRefresh是直接在+ (void)load方法中实现两个方法交换的, 就是说, 在APP启动将代码装载进内存的时候先交换了方法的实现. exchangeInstanceMethod1:Method2:方法使用runtime提供的方法交换了method1和method2的实现. 交换之后, 当调用reloadData方法, 实际上会调用到mj_reloadData方法, 我们可以在mj_reloadData方法中根据需要在执行系统方法之前或者之后做自己需要的操作.

MJRefresh中这种实现的一个缺点是: 不管tableView中是否使用了上下拉刷新, 只要调用tableView的reloadData方法都会调用到分类中的mj_reloadData方法. 而DZNEmptyDataSet中的方法就不会出现这种情况.

3. DZNEmptyDataSet中交换两个方法的实现的实现

DZNEmptyDataSet中, 并不像MJRefresh中那样简单的交换, 可以理解成是交换两个方法具体的过程, 这部分主要由三个方法构成. 如下所示.

DZNEmptyDataSet中Method Swizzling的实现过程, 在设置代理方法时候, 调用swizzleIfPossible:方法, 改变方法的实现, 再将本实例对象指定方法的类名,方法名,方法指针存放到一个字典中去,再将字典存放在_impLookupTable这个字典中, 比如tableView, 如果调用了reloadData方法, 实际上会调用到一个自定义的方法dzn_original_implementation(), 在这个方法中执行了一些操作, 比如, 重新更新提示信息, 最后, 通过((void(*)(id,SEL))impPointer)(self,_cmd);执行一个原来的方法.

以下是DZNEmptyDataSet中的代码:

#pragma mark - Method Swizzling

static NSMutableDictionary *_impLookupTable;
static NSString *const DZNSwizzleInfoPointerKey = @"pointer";
static NSString *const DZNSwizzleInfoOwnerKey = @"owner";
static NSString *const DZNSwizzleInfoSelectorKey = @"selector";

// Based on Bryce Buchanan's swizzling technique http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/

void dzn_original_implementation(id self, SEL _cmd)
{
    // Fetch original implementation from lookup table  通过查表获取方法原始的实现
    //获取key
    NSString *key = dzn_implementationKey(self, _cmd);
    
    NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key]; //通过key值, 从字典中取值
    NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey]; //通过指针获取信息
    
    IMP impPointer = [impValue pointerValue]; //获取IMP
    
    // We then inject the additional implementation for reloading the empty dataset
    // Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time.
    // 我们先注入附加的方法用来加载无数据时候的提示信息,  在执行原始方法之前执行自己附加的方法
    [self dzn_reloadEmptyDataSet];
    
    // If found, call original implementation
    // 如果找到了这个指针, 则去执行这个方法去
    if (impPointer) {
        ((void(*)(id,SEL))impPointer)(self,_cmd);
    }
}

/**
 *  获取当前对象应该使用的key值
 *
 *  @param target   目标
 *  @param selector 方法
 *
 *  @return 返回由目标类名字和方法名字组成的key值
 */
NSString *dzn_implementationKey(id target, SEL selector)
{
    if (!target || !selector) {
        return nil;
    }
    
    Class baseClass;
    if ([target isKindOfClass:[UITableView class]]) baseClass = [UITableView class];
    else if ([target isKindOfClass:[UICollectionView class]]) baseClass = [UICollectionView class];
    else if ([target isKindOfClass:[UIScrollView class]]) baseClass = [UIScrollView class];
    else return nil;
    
    NSString *className = NSStringFromClass([baseClass class]);
    
    NSString *selectorName = NSStringFromSelector(selector);
    return [NSString stringWithFormat:@"%@_%@",className,selectorName];
}

/**
 *  使用 _impLookupTable 字典变量记录本类中需要被替换的方法相应的结构体
 *
 *  @param selector 需要被替换的方法
 */
- (void)swizzleIfPossible:(SEL)selector
{
    // Check if the target responds to selector
    if (![self respondsToSelector:selector]) {
        return;
    }
    
    // Create the lookup table
    if (!_impLookupTable) {
        _impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:2];
    }
    
    // We make sure that setImplementation is called once per class kind, UITableView or UICollectionView.
    // 为了确保每种类型只调用一次
    for (NSDictionary *info in [_impLookupTable allValues]) {
        Class class = [info objectForKey:DZNSwizzleInfoOwnerKey];
        NSString *selectorName = [info objectForKey:DZNSwizzleInfoSelectorKey];
        
        if ([selectorName isEqualToString:NSStringFromSelector(selector)]) {
            if ([self isKindOfClass:class]) {
                return;
            }
        }
    }
    
    NSString *key = dzn_implementationKey(self, selector);
    NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
    
    // If the implementation for this class already exist, skip!!
    // 如果这种实现已经存在, 则跳过
    if (impValue || !key) {
        return;
    }
    
    // Swizzle by injecting additional implementation
    Method method = class_getInstanceMethod([self class], selector);
    IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
    
    // Store the new implementation in the lookup table
    NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: [self class],
                                   DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
                                   DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
    
    [_impLookupTable setObject:swizzledInfo forKey:key];
}

4. 写在后面的

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


参考: https://blog.newrelic.com/2014/04/16/right-way-to-swizzle/