【IOS开发高级系列 整理】AOP和Method Swizzling专题

主要内容参考链接:Method Swizzling和AOP实践

http://tech.glowing.com/cn/method-swizzling-aop/


1 Method Swizzling

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

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

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

@implementation UIViewController (Logging)


- (void)swizzled_viewDidAppear:(BOOL)animated

{

        // call original implementation

        [selfswizzled_viewDidAppear:animated];


        // Logging

       [Logging logWithEventName:NSStringFromClass([selfclass])];

}

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

接下来实现 swizzle 的方法 :

@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 给替换掉:

@implementation UIViewController (Logging)


+ (void)load

{

    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));

}

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

        其实,这里还可以更简化点:直接用新的 IMP 取代原 IMP ,而不是替换。只需要有全局的函数指针指向原 IMP 就可以。

void (gOriginalViewDidAppear)(id, SEL, BOOL);


void newViewDidAppear(UIViewController *self, SEL _cmd, BOOLanimated) 

{

    // call original implementation

    gOriginalViewDidAppear(self, _cmd, animated);


     // Logging

    [Logging logWithEventName:NSStringFromClass([selfclass])];

}


+ (void)load

{

    Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));

    gOriginalViewDidAppear = (void*)method_getImplementation(originalMethod);


    if(!class_addMethod(self, @selector(viewDidAppear:),(IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {

       method_setImplementation(originalMethod, (IMP) newViewDidAppear);

    }

}

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

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

2 load方法

一.学会使用load方法

          在开发过程当中,我们可能可能遇到老板经常修改需求。可能为了提高逼格,老板说要我们可以将我们的app分享出去。诺!现在不是要接入统计吗?我们又需要在我们的AppDelegate中接入一段代码;一段时间后,老板又想要接入统计功能,我代码结构这么好,是不是想死的心都有了。

          需求是无穷无尽的,我需要bug统计(fir hud)、提醒用户评分系统(iRate)、推送(Jpush)。当初你一心想把所有的代码封装好,现在是不是被老板的需求给完全打败了。

          别担心,现在我来教你你一些小技巧。

          也许您还没有用过IQKeyBoardManager和iRate这种智能库。大牛在readme中写了这么一段话

  1.CodeLess , zero line of Code不需要写任何代码

  2.Works Automatically自动工作

  3.No more scrollView  不需要scrollView

  4.No more subClass不需要继承父类

  5.No more manual Work不需要配置

  6.No more #import不需要导入

        其实也不是什么很神奇的东西,只是大牛用到了+ (load)方法

        学习OC的人都应该了解到,load方法是在一个类被加载到运行库中时会被自动调用。这不就实现了自动调用。接下来我直接上代码:

#import <Foundation/Foundation.h>


@interface ThirdPartService : NSObject


@end


 #import "ThirdPartService.h"

 #import "UMSocial.h"

 #import "UMSocialWechatHandler.h"

 #import "UMSocialQQHandler.h"

 #import <MobClick.h>

 #import <FIR/FIR.h>


@implementation ThirdPartService

 + (void)load {

    static dispatch_once_tonceToken;

    dispatch_once(&onceToken, ^{

       //    TODO  这里是我自己测试的  fir hud

       [FIR handleCrashWithKey:@"XX"];

       //    友盟

       [UMSocialData setAppKey:@"XX"];

       //     隐藏未安装的平台

       [UMSocialConfig hiddenNotInstallPlatforms: @[UMShareToQQ, UMShareToQzone, UMShareToWechatSession, UMShareToWechatTimeline]];

       //    注册微信

       [UMSocialWechatHandler setWXAppId: @"XX" appSecret: @"XX" url: ""];

       //    注册QQ

       //    TODO   QQ的不是真的

       [UMSocialQQHandler setQQWithAppId: @"XX" appKey: @"XX" url: @""];


       //    TODO    UM统计

       [MobClick startWithAppkey: @""];

       [MobClick setCrashReportEnabled:NO];

       NSLog(@"第三方服务注册完毕");

  });

}

@end


        看上面我们就可以把我们要配置的一些代码都写在load方法里面,这样我们就可以将服务和模块完全分离了。

        但是有的服务,如APNS需要在launchOptions里面写,那就只能在appDelegate中写了,不过这样的话我们同样已经摘除了许多冗余的代码,只剩下几个固定的。到时候我们在修改一下里面的代码就行了。


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

3.1 AOP简介

        Wikipedia 里对 AOP 是这么介绍的:

An aspect can alter the behavior of the base code by applying advice(additional behavior) at various join points (points in a program) specified ina quantification or query called a pointcut (that detects whether a given joinpoint matches).

        在 Objective-C 的世界里,这句话意思就是利用 Runtime 特性给指定的方法添加自定义代码。有很多方式可以实现 AOP ,Method Swizzling 就是其中之一。而且幸运的是,目前已经有一些第三方库可以让你不需要了解 Runtime ,就能直接开始使用 AOP 。

3.2 AOP第三方库Aspects使用

        Aspects就是一个不错的 AOP 库,封装了 Runtime , Method Swizzling 这些黑色技巧,只提供两个简单的API:

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

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

        使用 Aspects 提供的 API,我们之前的例子会进化成这个样子:

@implementation UIViewController (Logging)


+ (void)load

{

    [UIViewController aspect_hookSelector: @selector(viewDidAppear:) withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) {

       NSString *className = NSStringFromClass([[aspectInfo instance] class]);

       [Logging logWithEventName: className];

    } error: NULL];

}

        你可以用同样的方式在任何你感兴趣的方法里添加自定义代码,比如 IBAction 的方法里。更好的方式,你提供一个 Logging 的配置文件作为唯一处理事件记录的地方:

@implementation AppDelegate (Logging)


+ (void) setupLogging

{

    NSDictionary*config = @{

       @"MainViewController": @{

           GLLoggingPageImpression:@"page imp - main page",

           GLLoggingTrackedEvents: @[

                @{

                    GLLoggingEventName: @"button one clicked",

                    GLLoggingEventSelectorName: @"buttonOneClicked:",

                    GLLoggingEventHandlerBlock: ^(id aspectInfo) {

                        [Logging logWithEventName: @"button one clicked"];

                    },

                },

                @{

                    GLLoggingEventName: @"button two clicked",

                    GLLoggingEventSelectorName: @"buttonTwoClicked:",

                    GLLoggingEventHandlerBlock: ^(id aspectInfo) {

                        [Logging logWithEventName: @"button two clicked"];

                    },

                },

          ],

       },


       @"DetailViewController": @{

           GLLoggingPageImpression: @"page imp - detail page",

       }

   };


   [AppDelegate setupWithConfiguration: config];

}


+ (void)setupWithConfiguration: (NSDictionary*) configs

{

    // Hook Page Impression

    [UIViewController aspect_hookSelector: @selector(viewDidAppear:) withOptions: AspectPositionAfter usingBlock: ^(idaspectInfo) {

         NSString *className = NSStringFromClass([[aspectInfo instance] class]);

         [Logging logWithEventName: className];

    } error:NULL];


    // Hook Events

    for (NSString *className inconfigs) {

       Class clazz = NSClassFromString(className);

       NSDictionary *config = configs[className];


       if (config[GLLoggingTrackedEvents]) {

           for (NSDictionary *event inconfig[GLLoggingTrackedEvents]) {

                SEL selekor = NSSelectorFromString(event[GLLoggingEventSelectorName]);

                AspectHandlerBlock block = event[GLLoggingEventHandlerBlock];


                [clazz aspect_hookSelector:selekor withOptions:AspectPositionAfter  usingBlock: ^(id aspectInfo) {

                      block(aspectInfo);

                   } error: NULL];

           }

       }

    }

}

    然后在 -application: didFinishLaunchingWithOptions: 里调用 setupLogging:

- (BOOL) application: (UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary*) launchOptions {

    // Override point for customization after application launch.

    [self setupLogging];

    return YES;

}

3.3 最后的话

        利用 objective-C Runtime 特性和 Aspect Oriented Programming ,我们可以把琐碎事务的逻辑从主逻辑中分离出来,作为单独的模块。它是对面向对象编程模式的一个补充。Logging 是个经典的应用,这里做个抛砖引玉,发挥想象力,可以做出其他有趣的应用。

        使用 Aspects 完整的例子可以从这里获得:AspectsDemo


4 参考链接

• method-swizzling

• method replacement for fun and profit 

• Aspects

Method Swizzling和AOP实践

http://tech.glowing.com/cn/method-swizzling-aop/

如何判断method是否被swizzling

https://segmentfault.com/a/1190000003950284

如何判断method是否被swizzled(续)

https://segmentfault.com/a/1190000004542608

UIViewController-Swizzled源码分析

http://www.tuicool.com/articles/FFjaqi

iOS编程重要知识之load method swizzled

http://blog.csdn.net/u014484863/article/details/48210105

iOS应用使用Aspects框架实现类方法的拦截

http://www.jianshu.com/p/ba73744b8484

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

推荐阅读更多精彩内容