AOP在iOS中的具体应用

原创文章转载请注明出处,谢谢

AOP(Aspect Oriented Programming)面向切面编程

相比传统的OOP来说,OOP的特点在于它可以很好的将系统横向分为很多个模块(比如通讯录模块,聊天模块,发现模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。

AOP主要是被使用在日志记录,性能统计,安全控制,事务处理,异常处理几个方面。由于本人并不是位大神(正在成长中),所以目前只在项目里用到了日志记录和事物处理这两个方面,另外几个方面会在以后陆陆续续更新。

注意:AOP和OOP一样,并不是一种技术,而是一种编程思想,所有实现了AOP编程思想的都算是AOP的实现。

在iOS中我们通常使用Method Swizzling(俗称iOS黑魔法)来实现AOP,Method Swizzling其实就是一种在Runtime的时候把一个方法的实现与另一个方法的实现互相替换。具体详见Method Swizzling

Aspects 一个基于Objective-C的AOP开发框架,封装了 Runtime ,是我们平时比较常用到实现Method Swizzling的一个类库,它提供了如下两个API:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.
    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

AOP iOS中的事务处理之版本控制

我之前维护过一个项目Toast系列(Mac下一个比较有名的刻盘软件),它有很多的版本比如Pro版本,LT(lite)版本。顾名思义LT其实就是关闭了很多功能的一个版本,很多对应的事件在LT不会被触发,所以为了阻止事件的触发,每个用户事件中都会出现一段如下的宏判断

- (void)detailBottomBtnEvent:(id)sender {
//if we not use AOP, we must write this code in project
#ifdef LITE_VERSION
    //do nothing
#else
   //do all thing
#endif
}

这显然不是我们想要看到的结果,每个用户事件里都会去判断LT_VERSION的宏,然后在做对应的事件处理。LT的版本不是一个主版本,我们的业务逻辑里主要还是需要触发用户对应的事件,所以这个时候我们就可以用到AOP的思想

//
//  AppDelegate+LiteEvent.m
//  AOPTransactionIntactDemo
//
//  Created by wuyike on 16/5/19.
//  Copyright © 2016年 bongmi. All rights reserved.
//

#import "AppLiteDelegate+LiteEvent.h"

#import "Aspects.h"

typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);

@implementation AppLiteDelegate (LiteEvent)

- (void)setLiteEvent {
#ifdef LITE_VERSION
    
    NSDictionary *configs = @{
                             @"AOPTopViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailTopBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Top detailBtn clicked, this is lite version");
                                                 },
                                                 },
                                             ],
                                     },
                             
                             @"AOPBottomViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailBottomBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Bottom detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },
                             
                             @"AOPLeftViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailLeftBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Left detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },
                             
                             @"AOPRightViewController": @{
                                     UserTrackedEvents: @[
                                             @{
                                                 UserEventName: @"detailBtn",
                                                 UserEventSelectorName: @"detailRightBtnEvent:",
                                                 UserEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                                                     NSLog(@"Right detailBtn clicked this is lite version");
                                                 },
                                                 },
                                             ],
                                     },
                             };
    
    for (NSString *className in configs) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configs[className];
        
        if (config[UserTrackedEvents]) {
            for (NSDictionary *event in config[UserTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[UserEventSelectorName]);
                AspectHandlerBlock block = event[UserEventHandlerBlock];
                
                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionInstead
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                  } error:NULL];
                
            }
        }
    }
#endif
}

我们有Top,Buttom,Left,Right四个ViewController,每个ViewController中都有一个对应的用户触发事件,我们只需要在LT版本下替换对应的事件就可以,每个模块的业务逻辑不需要任何改动。

Demo 下载

AOP iOS中的事务处理之安全可变容器

OC中任何以NSMutable开头的类都是可变容器,它们一般都具有(insert 、remove、replace)等操作,所以我们经常需要判断容器是否为空,以及指针越界等问题。为了避免我们在每次操作这些容器的时候都去判断,一般有以下几种解决方法:

  1. 派生类
  2. Category
  3. Method Swizzling

使用派生类肯定不是好的方法,Category可以解决我们的问题,但是导致项目中所有用到容器操作的地方都需要显示的调用我们新加的方法,所以也不是很优雅。所以这个时候用Method Swizzling就是一个不错的选择。

#import "NSMutableArray+SafeArray.h"
#import <objc/runtime.h>

@implementation NSMutableArray (SafeArray)

+ (void)load {
    [[self class] swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)];
    [[self class] swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(safeObjectAtIndex:)];
    [[self class] swizzleMethod:@selector(insertObject:atIndex:) withMethod:@selector(safeInsertObject:atIndex:)];
    [[self class] swizzleMethod:@selector(removeObjectAtIndex:) withMethod:@selector(safeRemoveObjectAtIndex:)];
    [[self class] swizzleMethod:@selector(replaceObjectAtIndex:withObject:) withMethod:@selector(safeReplaceObjectAtIndex:withObject:)];
    NSLog(@"%@ %@", @"SafeArray", [self class]);
}

#pragma mark - magic

- (void)safeAddObject:(id)anObject {
    //do safe operate
    if (anObject) {
        [self safeAddObject:anObject];
    } else {
        NSLog(@"safeAddObject: anObject is nil");
    }
}

- (id)safeObjectAtIndex:(NSInteger)index {
    //do safe operate
    if (index >= 0 && index <= self.count) {
        return [self safeObjectAtIndex:index];
    }
    NSLog(@"safeObjectAtIndex: index is invalid");
    return nil;
}

- (void)safeInsertObject:(id)anObject
                 atIndex:(NSUInteger)index {
   //do safe operate
    if (anObject && index >= 0 && index <= self.count) {
        [self safeInsertObject:anObject atIndex:index];
    } else {
        NSLog(@"safeInsertObject:atIndex: anObject or index is invalid");
    }
}

- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
  //do safe operate
    if (index >= 0 && index <= self.count) {
        [self safeRemoveObjectAtIndex:index];
    } else {
        NSLog(@"safeRemoveObjectAtIndex: index is invalid");
    }
}

- (void)safeReplaceObjectAtIndex:(NSUInteger)index
                      withObject:(id)anObject {
   //do safe operate
    if (anObject && index >= 0 && index <= self.count) {
        [self safeReplaceObjectAtIndex:index withObject:anObject];
    } else {
        NSLog(@"safeReplaceObjectAtIndex:withObject: anObject or index is invalid");
    }
}

- (void)swizzleMethod:(SEL)origSelector
           withMethod:(SEL)newSelector {
    Class class = [self class];
    
    Method originalMethod = class_getInstanceMethod(class, origSelector);
    Method swizzledMethod = class_getInstanceMethod(class, newSelector);
    
    BOOL didAddMethod = class_addMethod(class,
                                        origSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(class,
                            newSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

以上就是我用AOP思想在事件处理方面的两个具体的应用。

AOP iOS中的日志记录

通常我们会在项目中收集用户的日志,以及用户行为,以用来分析Bug,以及提升产品质量。项目往往包含很多的模块,以及下面会有更多的子模块,所以如果把这些操作具体加载每个事件中,显然这种做法是不可取的,第一所有收集用户行为的操作不属于业务逻辑范畴,我们不需要分散到各个业务中。第二这种方式的添加不利于后期维护,而且改动量是巨大的。所以这里使用上面提到的版本控制事件处理相同的方式,这里抄袭一个Demo

- (void)setupLogging
{
    NSDictionary *config = @{
        @"MainViewController": @{
              GLLoggingPageImpression: @"page imp - main page",
              GLLoggingTrackedEvents: @[
                      @{
                          GLLoggingEventName: @"button one clicked",
                          GLLoggingEventSelectorName: @"buttonOneClicked:",
                          GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                              NSLog(@"button one clicked");
                          },
                        },
                      @{
                          GLLoggingEventName: @"button two clicked",
                          GLLoggingEventSelectorName: @"buttonTwoClicked:",
                          GLLoggingEventHandlerBlock: ^(id<AspectInfo> aspectInfo) {
                              NSLog(@"button two clicked");
                          },
                        },
                      ],
        },

        @"DetailViewController": @{
              GLLoggingPageImpression: @"page imp - detail page",
        }
    };
    
    [GLLogging setupWithConfiguration:config];
}

typedef void (^AspectHandlerBlock)(id<AspectInfo> aspectInfo);

+ (void)setupWithConfiguration:(NSDictionary *)configs
{
    // Hook Page Impression
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id<AspectInfo> aspectInfo) {
                                   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                       NSString *className = NSStringFromClass([[aspectInfo instance] class]);
                                       NSString *pageImp = configs[className][GLLoggingPageImpression];
                                       if (pageImp) {
                                           NSLog(@"%@", pageImp);
                                       }
                                   });
                               } error:NULL];

    // Hook Events
    for (NSString *className in configs) {
        Class clazz = NSClassFromString(className);
        NSDictionary *config = configs[className];
        
        if (config[GLLoggingTrackedEvents]) {
            for (NSDictionary *event in config[GLLoggingTrackedEvents]) {
                SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);
                AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];
                
                [clazz aspect_hookSelector:selekor
                               withOptions:AspectPositionAfter
                                usingBlock:^(id<AspectInfo> aspectInfo) {
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                } error:NULL];
                
            }
        }
    }
}

我们通过上述的操作可以在每个用户事件触发后都追加一个用户的行为记录,同时又不需要修改业务逻辑。

总结

这里反馈一个关于Aspect的问题,貌似Aspect不支持不同类中含有相同名称的方法时,会出现不能正确替换方法的情况,详细可以参见https://github.com/steipete/Aspects/issues/48 https://github.com/steipete/Aspects/issues/24

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容