IOS架构:组件化方案与AOP面向切面编程

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、模块可以被任意地方调用
  • 二、当前app中的模块可以无侵入的受app约束
  • 三、模块之间互不干扰
  • 四、面向切面编程思想
  • 五、AOP:事务处理之版本控制
  • 六、AOP:事务处理之安全可变容器
  • 七、AOP:日志记录

各种编程思想的发展
  • 面向过程:早期学习C时就按面向过程编程,为解决问题,按步骤一步一步进行。
  • 面向对象(OOP):将解决问题的过程抽象为多个对象协同作战的过程。面向对象具有封装、继承、多态三大特性。
  • 面向服务:为实现一个系统或程序,将一个功能由一个服务提供,多个服务组合而成了系统或程序。也就是我们组件化的思想。
  • 面向切面(AOP):面向切面是由面向对象后发展而来的,将通用需求功能从不相关类之中分离出来,成为一个行为。
面向对象和面向切面的区别

OOP的特点在于它可以很好的将系统横向分为很多个模块(比如文章展示模块,通知模块,问答模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。使用 AOP 可以解决 OOP由于切面需求导致单一职责被破坏的问题。通过 AOP 可以不侵入 OOP 开发,非常方便地插入切面需求功能。

举个例子:所有页面都需要上报曝光的打点,如果在每个页面上都执行打点的行为,就需要到每个页面去增加代码,这无形中对原因业务进行了侵入,代码分散在各个角落,后期维护起来也比较麻烦。但是我们要是通过AOP的思想,就可以不侵入业务代码,在每个页面展示的时机作为一个切点,插入我们的打点代码。所以AOP并不是独立于OOP的另一种编程思想,而是一种补充。就像上图展示的逻辑,从各个模块中抽象出相同的行为,找到合适的切点。

在iOS中怎么实现AOP

在iOS中通常是用Method Swizzling(俗称iOS黑魔法)来实现AOPMethod Swizzling其实就是在运行时把一个方法的实现与另一个方法的实现互相替换。这个黑魔法,相信大家在开发中已经遇到过或经常使用,大家又恨又爱,恨的是很容易出现问题、有风险;爱的是它的确可以解决很多复杂繁琐的问题。

直接使用Runtime进行方法交换非常简单,代码如下。class_addMethod()函数返回成功表示被交换的方法没实现,然后会通过class_addMethod()函数先实现;返回失败则表示被交换方法已存在,可以直接进行IMP 指针交换。

# import "SMHook.h"
# import 

@implementation SMHook

+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
    Class class = classObject;
    
    // 得到被交换类的实例方法
    Method fromMethod = class_getInstanceMethod(class, fromSelector);
    
    // 得到交换类的实例方法
    Method toMethod = class_getInstanceMethod(class, toSelector);
    
    if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        // 进行方法的交换
        class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
    } else {
        // 交换 IMP 指针
        method_exchangeImplementations(fromMethod, toMethod);
    }
}

@end

但是又会存在一些风险,如:

第一个风险是,需要在+load方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。

第二个风险是,被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。

第三个风险是,交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd

第四个风险是,方法交换命名冲突。如果出现冲突,可能会导致方法交换失败。

这几种风险具体在什么情况下可能产生,进一步的理解可以参考这篇文章:iOS界的毒瘤-MethodSwizzling

那有没有更安全的方法呢,可以利用Aspects来实现AOP解决我们业务中的一些问题。


一个组件化的app可能具备以下的几点
  • 模块可以被任意地方调用。模块可以是UI,也可以是service,可以被native调用,也可以被webview调用
  • 当前app中的模块可以无侵入的受app约束。例如:图片浏览模块,在appA中可以横屏,在appB中不支持横屏
  • 模块之间互不干扰。例如:A模块的导航栏是透明的,B模块的导航栏是隐藏的,C模块导航栏是普通的,3个模块之间调用的时候,导航栏样式要做到互不影响

一、模块可以被任意地方调用

1、基本的组件化

一个完整的业务最好独立成一个模块,这样可以做到调用入口统一,也方便在调用之前做一些初始化操作。这里看起来很好理解,外部调用登录页面,不再直接引入LoginVC,而是引入AccountComponent,通过它进行帐号相关的操作。

@interface AccountComponent : NSObject

+ (instancetype)shareInstance;
- (void)pushLoginVCWithAccount:(NSString *)account pwd:(NSString *)pwd complete:(void(^)(BOOL res))complete;

@end
@interface LoginViewController : UIViewController

- (void)initWithAccount:(NSString *)account pwd:(NSString *)pwd complete:(void(^)(BOOL res))complete;

@end

AccountComponent 内部的实现大概是这样:

+ (instancetype)shareInstance {
    // 创建单例对象
    AccountComponent *component = [[AccountComponent alloc] init];
    // 初始化模块需要的基本数据,例如网络返回的配置信息,数据库表初始化等
    return component;
}

- (void)pushLoginVCWithAccount:(NSString *)account pwd:(NSString *)pwd complete:(void(^)(BOOL res))complete {
}

外部调用起来是这样子:

#import "AccountComponent"

[[AccountComponent shareInstance] pushLoginVCWithAccount:@"" pwd:@"" complete:^(BOOL res) {
   // 回调
}];

没有开玩笑,这样已经完成了基本的组件化。组件化最重要的是思想,只要具有组件化思想,最基本的代码就可以实现组件化,这样已经可以解决很多问题了。当然了,既然说这样能解决很多问题,那就还是有一些问题没有解决的:

  • 在所有需要account 模块的地方,都需要引入 AccountComponent。能不能尽量少的进行文件引用?
  • webview中的h5想打开某个native 模块,应该怎么处理?
  • h5调用native 模块,在调用处能不能和native 调用某个 native 模块的行为是一致的?
  • 一个控件需要动态控制打开某一个模块怎么做?例如:同一个button,不同角色的登录用户,点击打开的是不同的模块

2、引入 Router

Router 的使用方式:在模块一开始加载时候注册,一般选在模块的+load()方法内部调用。

[Router registerName:@"router://loginVC" callback:^(NSDictionary * _Nonnull data, void (^ _Nonnull responseCallBack)(NSDictionary * _Nonnull)) {
    
    NSLog(@"收到了调用方的参数: %@", data);
    [[AccountComponent shareInstance] pushLoginVCWithAccount:data.name pwd:data.pwd complete:^(BOOL res) {
        // 模块处理结束后的回调
        responseCallBack(res);
    }];
}];

在任意想打开loginVC的地方都可以调用:

[Router callHandlerName:@"router://loginVC?name=xiejiapei&age=18" params:@{@"params": @"xiejiapei"} callback:^(NSDictionary * _Nonnull data) {
    NSLog(@"调用完成后的回调:%@", data);
}];

这样就可以解决了前文提到的问题:

  • 使用者不需要引入模块头文件,但仍然可以进行正常的传参与回调,参数可以拼接在query里,也可以单独传递。
  • h5 可以直接用window.location.href="router://loginVC?name=xiejiapei&age=18"来发起调用,在webView的代理方法拦截router://开头的url,然后使用Router方式打开。(这种方式只是单向的唤起页面,无法回调给h5。如果h5需要native模块的回调,那么还请使用jsbridge的方式)
  • 在使用层面,h5和native 调用模块的方式是一模一样的。
  • 接口返回当前用户可以打开的模块的router地址。例如接口给A用户返回:router://loginVC,接口给B用户返回router://homeVC,控件根本不需要知道打开哪个模块,直接用[Router callHandlerName:@"url"调用就完了。

3、Router的实现原理

router的核心思想非常简单,主要是路由表的概念,这部分思路参考了jsbridge的实现方案。由于Router是在统一语言环境,所以比jsbridge的实现要简单太多,但是核心思想是一致的。主要分为如下步骤:

  1. 建立一个NSDictionary,用来存储模块url---回调block的映射
/// 路由表
@property (nonatomic, strong) NSMutableDictionary *routers;
  1. 调用[Router registerName: callback:]时,将 namecallback 存储到表中
/// 注册方法
- (void)registerName:(NSString *)name callback:(void (^)(NSDictionary * _Nonnull, void (^ _Nonnull)(NSDictionary * _Nonnull)))callback {
    [self.routers setObject:callback forKey:name];
}
  1. 调用[Router callHandlerName: data: responseCallback:]时,根据name去查找一开始注册的方法,拿到存储的回调block,调用callback(data, responseCallback)
/// 调用方法
- (void)callHandlerName:(NSString *)name params:(NSDictionary *)params callback:(void (^)(NSDictionary * _Nonnull))callback {
    // 过滤query里的参数
    NSURLComponents *urlComponents = [NSURLComponents componentsWithString:name];
    
    NSMutableString *routerName = [NSMutableString string];
    if (urlComponents.scheme) {
        [routerName appendFormat:@"%@://", urlComponents.scheme];
    }
    if (urlComponents.host) {
        [routerName appendString:urlComponents.host];
    }
    if (urlComponents.path) {
        [routerName appendString:urlComponents.path];
    }
    
    NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
    for (NSURLQueryItem *item in urlComponents.queryItems) {
        [parameters setObject:item.value forKey:item.name];
    }
    [parameters addEntriesFromDictionary:params];
    
    void (^responseHandler)(NSDictionary * _Nonnull, void (^ _Nonnull)(NSDictionary * _Nonnull)) = [self.routers objectForKey:routerName];
    if (responseHandler) {
        responseHandler(parameters, callback);
    }
}

就这么简单。当然,如果你愿意的话,可以把模块封装成一个私有的pod仓库。这样给其它app使用时,就可以通过pod install引入,完全组件化调用,在多个app间共享某些功能。为什么我把建私有库放到最后来提?因为这个真的不影响组件化,这些都是锦上添花的东西,如果具有了组件化的思想,没有pod 一样可以做一个完整的组件化方案。千万不要局限在工具,一提到做组件化就要建pod 仓库的思路是不对滴。


二、当前app中的模块可以无侵入的受app约束

上文我们完成了模块间的跳转,通过Router可以很方便的在任意地方用统一的姿势来打开某一模块,非常解偶。但是需求是无尽的,例如,一个通用模块中的ViewController默认是支持屏幕旋转,加入我们app后,怎么能统一的禁止掉默认的屏幕旋转?

注意:这里不考虑苹果不推荐的做法,例如:在info.plist中设置app只支持一个方向。如果这样的话,假如app中有视频播放页面需要横屏怎么处理?所以需要通过页面来处理屏幕旋转。

当有这种统一性需求的时候,很容易想到的方案就是AOP。苹果的runtime是做系统方法AOP的利器,非常方便我们拦截系统的方法,做统一的处理。思路是:

  1. UIViewContrller 添加一个category,在分类的+load方法中做方法替换,替换系统的- shouldAutorotate()方法为- custom_shouldAutorotate();
  2. custom_shouldAutorotate()方法中默认返回NO

这样,在原本应该调用 [viewcontroller shouldAutorotate]的地方都调用了我们自己的custom_shouldAutorotate(),自然全部都不支持旋转了。

如果某一个页面想要横屏怎么办?简单,假如这个页面叫:TestVC,其继承自 UIViewController。在TestVC中实现系统的shouldAutorotate方法,返回yes就好了。因为如果TestVC实现了shouldAutorotate方法,那么就不会调用父类的方法,进而就不会调用我们前文替换的方法了,所以一切OK。

/// 方法替换的核心
/// @param class 需要替换方法的类
/// @param originalSEL 原始方法签名
/// @param swizzleSEL 新的方法签名
+ (void)methodSwizzleWithClass:(Class)class originalSEL:(SEL)originalSEL swizzleSEL:(SEL)swizzleSEL {
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL);
    ......
}

如果添加成功,证明class 原来没有originalSEL方法,上文获取的 originalMethod 是父类的方法。如果添加失败,则证明 class 原本有 originalSEL 方法。

BOOL addOriginalMethod = class_addMethod(class, originalSEL, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));

成功则替换swizzleSEL的实现为originalMethod。此处originalMethod其实是class父类的实现。失败则证明原方法存在,所以只需要交换两个方法的实现就好了

if (addOriginalMethod) {
    class_replaceMethod(class, swizzleSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
    method_exchangeImplementations(originalMethod, swizzleMethod);
}

三、模块之间互不干扰

iOS的app有一个特点,统一导航控制器推出的页面,共用一个导航栏。这样的好处是,使用自定义的专场后,导航栏内容切换动画会很流畅。缺点也很明显,两个模块之间相互影响。例如我在A模块的导航栏是隐藏的,打开B模块,而B模块是展示导航栏的,这时候导航栏样式会比较难处理,更不用说那种导航栏随着页面滚动来改变透明度的需求了。那么有没有办法让每个模块有自己的导航栏?甚至每一个UIViewController 都有自己单独的导航栏?这样就可以自己管好自己,想怎么处理就怎么处理。当然有办法。

普通的页面切换:这是最普通的导航,推出的页面共用一个导航栏

UINavigationController ----> UIViewController ---> UIViewController

理想中的页面切换:理想中的导航切换,每个UIViewController外面都包一个UINavigationController推出,就各自有各自的导航栏了。显然,没那么简单。这样写会crash,苹果表示,不支持 UINavigationController推出一个UINavigationController

UINavigationController ----> UINavigationController( UIViewController) ---> UINavigationController(UIViewController)

最终的实现:最外层有一个UINavigationController,它隐藏了导航栏。将要被推出的控制器UIViewController内部包裹了一个UINavigationController,这个内部的UINavigationController才真正包裹了目标控制器UIViewController

UINavigationController ----> (UIViewController(UINavigationController(UIViewController)))

可能有点绕,那我们换个表述。把最外层导航控制器称为OutterNavController,容器称为ContainerVC,内部导航控制器称为:InnerNavController,真正被展示的控制器为:VC,那么推出方式应该为:

OutterNavController --> ContainerVC(InnerNavController(VC)) --> ContainerVC(InnerNavController(VC))

理论说明白了,代码就不那么重要了,无非就是一些细节的补充:

【OutterNavigationController】

第一次添加rootVC时候,要包裹一层ContainerVC,而ContainerVC的初始化,内部其实已经包了一个InnerNavController的。

- (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
    if (self = [super initWithRootViewController:rootViewController]) {
        ULInnerViewController *innerViewController = [ULInnerViewController innerViewControllerWithRootViewController:rootViewController];
        rootViewController.ul_InnerViewContorller = innerViewController;
        self.viewControllers = @[innerViewController];
    }
    return self;
}
【ContainerVC】
+ (instancetype)innerViewControllerWithRootViewController:(UIViewController *)rootViewController {
    return  [[[self class] alloc] initWithInnerViewController:rootViewController];
}
【InnerNavigationController】

初始化的时候,就包裹了一个InnerNavController

- (instancetype)initWithInnerViewController:(UIViewController *)viewController {
    if (self = [super init]) {
        ULInnerNavigationController *innerNavigationController = [[ULInnerNavigationController alloc]  initWithViewController:viewController];
        self.innerNavigationController = innerNavigationController;
        
        [self addChildViewController:innerNavigationController];
        [innerNavigationController didMoveToParentViewController:self];
        
        self.holdViewController = viewController;
    }
    return self;
}

每次在VC中调用[self.navigationController pushViewController:]的时候,都会走这个方法。其实调用的外层导航控制器进行push。这样就可以实现了各个页面控制自己的导航栏,做到了UI的互不干扰。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [(ULLNavgationController *)self.navigationController pushViewController:[self p_constructVisiableViewController:viewController] animated:animated];
}
- (ULInnerViewController *)p_constructVisiableViewController:(UIViewController *)viewController {
    ULInnerViewController *innerVC =  [ULInnerViewController innerViewControllerWithRootViewController:viewController];
    return innerVC;
}

补充一点:为了能更方便的在任意地方都可以推出新的页面,所以我们需要能在任意地方都拿到当前界面最顶层的控制器。核心思想就是通过keyWindow.rootViewController作入口,递归拿到最顶层的控制器。我们可以实现一个UIViewController的分类,代码如下:

获取当前app最顶层控制器:

+ (UIViewController *)topViewController {
    UIViewController* rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
    return [[self class] currentViewControllerWithFromViewController:rootViewController];
}

获取某一控制器的最顶层控制器:

+ (UIViewController *)topViewControllerWithFromViewController:(UIViewController *)fromViewController {
    if ([fromViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController *nav = (UINavigationController *)fromViewController;
        return [[self class] currentViewControllerWithFromViewController:nav.viewControllers.lastObject];
    } else if([fromViewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController *tabBarController = (UITabBarController *)fromViewController;
        return [[self class] currentViewControllerWithFromViewController:tabBarController.selectedViewController];
    } else if (fromViewController.presentedViewController) {
        return [[self class] currentViewControllerWithFromViewController:fromViewController.presentedViewController];
    } else if ([fromViewController isKindOfClass:[ULInnerViewController class]]) {
        return [(ULInnerViewController *)fromViewController holdViewController];
    }else {
        return fromViewController;
    }
}

四、面向切面编程思想

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

AOPOOP一样,并不是一种技术,而是一种编程思想,所有实现了AOP编程思想的都算是AOP的实现。在iOS中我们通常使用Method Swizzling(俗称iOS黑魔法)来实现AOPMethod Swizzling其实就是一种在Runtime的时候把一个方法的实现与另一个方法的实现互相替换。

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

+ (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

五、AOP:事务处理之版本控制

假如一个APP有很多的版本比如Pro版本、Lite版本。顾名思义Lite其实就是关闭了很多功能的一个版本,很多对应的事件在Lite不会被触发,所以为了阻止事件的触发,每个用户事件中都会出现一段如下的宏判断。

- (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的宏,然后在做对应的事件处理。Lite的版本不是一个主版本,我们的业务逻辑里主要还是需要触发用户对应的事件,所以这个时候我们就可以用到AOP的思想。

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

#import "AppLiteDelegate+LiteEvent.h"

#import "Aspects.h"

typedef void (^AspectHandlerBlock)(id aspectInfo);

@implementation AppLiteDelegate (LiteEvent)

- (void)setLiteEvent {
#ifdef LITE_VERSION

...

#endif
}
NSDictionary *configs = @{
    @"AOPTopViewController": @{
    UserTrackedEvents: @[
                         @{
    UserEventName: @"detailBtn",
    UserEventSelectorName: @"detailTopBtnEvent:",
    UserEventHandlerBlock: ^(id aspectInfo) {
        NSLog(@"Top detailBtn clicked, this is lite version");
    },
    },
                         ],
    },
    
    @"AOPBottomViewController": @{
    UserTrackedEvents: @[
                         @{
    UserEventName: @"detailBtn",
    UserEventSelectorName: @"detailBottomBtnEvent:",
    UserEventHandlerBlock: ^(id aspectInfo) {
        NSLog(@"Bottom detailBtn clicked this is lite version");
    },
    },
                         ],
    },
    
    @"AOPLeftViewController": @{
    UserTrackedEvents: @[
                         @{
    UserEventName: @"detailBtn",
    UserEventSelectorName: @"detailLeftBtnEvent:",
    UserEventHandlerBlock: ^(id aspectInfo) {
        NSLog(@"Left detailBtn clicked this is lite version");
    },
    },
                         ],
    },
    
    @"AOPRightViewController": @{
    UserTrackedEvents: @[
                         @{
    UserEventName: @"detailBtn",
    UserEventSelectorName: @"detailRightBtnEvent:",
    UserEventHandlerBlock: ^(id 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) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    block(aspectInfo);
                });
            } error:NULL];
            
        }
    }
}

六、AOP:事务处理之安全可变容器

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

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

#import "NSMutableArray+SafeArray.h"
#import 

@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]);
}
- (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:日志记录

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

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

        @"DetailViewController": @{
              GLLoggingPageImpression: @"page imp - detail page",
        }
    };
    
    [GLLogging setupWithConfiguration:config];
}
typedef void (^AspectHandlerBlock)(id aspectInfo);

+ (void)setupWithConfiguration:(NSDictionary *)configs
{
    // Hook Page Impression
    [UIViewController aspect_hookSelector:@selector(viewDidAppear:)
                              withOptions:AspectPositionAfter
                               usingBlock:^(id 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) {
                                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                                        block(aspectInfo);
                                    });
                                } error:NULL];
                
            }
        }
    }
}

你可能感兴趣的:(IOS架构:组件化方案与AOP面向切面编程)