Runtime Method Swizzling技术

运行时确实是个好东西,俗话说学会了Runtime还不够,要懂得如何运用它来为项目带来便利,那才叫做真正的懂它。其实,很多方面我们需要用到它,只是很多时候我们不知道它的存在或者根本不会去了解和深入学习它而已。譬如,不同iOS版本的API兼容问题或是替换原有系统的IMP,又或是通过反射来获取系统的私有API,还有在APP安全防护和攻击时也用到它。
说到Runtime,不得不说说它的swizzling技术。这个名词好像在14年那会挺吸眼球的,在逆向工程中偶尔会见到它的身影。其实,说的通俗点就是用自己写的方法偷换系统的IMP。
最常见的就是通过一个例子说明来谈谈这个技术。

+ (void)load { 
      static dispatch_once_t onceToken; 
      dispatch_once(&onceToken, ^{ 
           Class aClass = [self class]; 
           SEL originalSelector = @selector(viewWillAppear:); 
           SEL swizzledSelector = @selector(xxx_viewWillAppear:); 

          // 通过实例方法来获取Method
           Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
           Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
    
           // 这个是获取类名,既是获取metaClass.
           // Class aClass = object_getClass((id)self);
           // 通过类方法来获取Method
           // Method originalMethod = class_getClassMethod(aClass, originalSelector);
           // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

          // 为类添加新方法
           BOOL didAddMethod = 
           class_addMethod(aClass, 
                        originalSelector, 
                        method_getImplementation(swizzledMethod), 
                        method_getTypeEncoding(swizzledMethod)); 

           if (didAddMethod) { 
                 class_replaceMethod(aClass, 
                 swizzledSelector, 
                 method_getImplementation(originalMethod), 
                 method_getTypeEncoding(originalMethod)); 
           } else { 
                 method_exchangeImplementations(originalMethod, swizzledMethod); 
           } 
      }); 
} 

#pragma mark - Method Swizzling 
- (void)xxx_viewWillAppear:(BOOL)animated { 
      [self xxx_viewWillAppear:animated]; 
      NSLog(@"viewWillAppear: %@", self); 
} 

备注:class_addMethod是为该类动态添加方法Method,具体就是通过原有的selector来扩展新的IMP,
通过它来判断该类是否已经动态为其添加过该方法,
如果添加过了,通过class_replaceMethod来替换原有的selector实现,从而达到对换这两个selector来交换其实现的IMP。否则,通过method_exchangeImplementations来交换两者的IMP实现。
这里强调一下+(void)load,这个方法是系统第一次装载程序到内存时调用,而且只调用一次,在程序开启时,程序主要把所有.m文件都加载到内存中。还有与之对应的方法是+ (void)initialize. 官方介绍如下:

+(void)initialize
The runtime sends initialize to each class in a program exactly one time just before the class,     
or any class that inherits from it, is sent its first message from within the program. (Thus the    
method may never be invoked if the class is not used.) The runtime sends the initialize 
message to classes in a thread-safe manner. Superclasses receive this message before their 
subclasses.

+(void)load
The load message is sent to classes and categories that are both dynamically loaded and  
statically linked, but only if the newly loaded class or category implements a method that can  
respond.

The order of initialization is as follows:

All initializers in any framework you link to.
All +load methods in your image.
All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
All initializers in frameworks that link to you.
In addition:

A class’s +load method is called after all of its superclasses’ +load methods.
A category +load method is called after the class’s own +load method.
In a custom implementation of load you can therefore safely message other unrelated classes    
from the same image, but any load methods implemented by those classes may not have run yet.

Apple的文档很清楚地说明了initialize和load的区别在于:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。

它们的相同点在于:方法只会被调用一次。(其实这是相对runtime来说的,后边会做进一步解释)。

文档也明确阐述了方法调用的顺序:父类(Superclass)的方法优先于子类(Subclass)的方法,类中的方法优先于类别(Category)中的方法。

似乎有点扯远了。。。。。。

这个技术真的是很常见,同时可以为我们省去很多麻烦和琐碎的细节。譬如,如果一个控件的API在iOS7上没有这个方法,而iOS8及以上有这个方法,可以通过写这个控件的分类来判断不同版本下同时调用这个方法,但是在该分类中通过版本判断,在iOS7及以下用自己实现类似系统API的方法来替换系统的该方法,从而达到不用修改原有的代码。

还有就是在对NSArray,NSMutableArray,NSDictionary,NSMutableDictionary中,通过字面量访问方式或者通过objectAtIndex等方法进行访问时,如果服务器那边不小心传入nil来插入数组或者字典或访问越界数据,都会导致应用崩溃。所以,通过实现NSArray等数据结构的分类,来对nil,越界等进行判断处理,防止程序崩溃。但是使用这个技术,如果数据出现错误的情况,很难通过该方法来查找bug,所以要谨慎使用之。

由此,我们可以根据上面所学,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,但是,你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是为什么?这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。NSArray的真身是__NSArrayI,而NSMutableArray的真身是__NSArrayM, NSDictionary的真身是__NSDictionaryI,NSMutableDictionary的真身是__NSDictionaryM。这几个东东是不是很熟悉啊,没错,还记得当崩溃时控制台输出的信息中就有这几个关键字的出现吗?好吧,这种事情你们自己去发掘好了。
代码如下:

#import "NSArray+Extension.h"
#import <objc/runtime.h>
@implementation NSArray (Extension)
+ (void)load {
       Method originMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
       Method swizzleMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(wt_objectAtIndex:));
       method_exchangeImplementations(originMethod, swizzleMethod);
}

// 分类记得要加前缀,否则会和别人写的分类冲突,导致只有一个方法映射到IMP中。
- (id)wt_objectAtIndex:(NSUInteger)index {
       if (self.count-1 < index) {
           @try {
                return [self lxz_objectAtIndex:index];
       }
       @catch (NSException *exception) {
           // 打印崩溃信息,方便调试
           NSLog(@"---------- %s Crash Method %s  ----------\n", class_getName(self.class), __func__);
           NSLog(@"%@", [exception callStackSymbols]);
           return nil;
       }
       @finally {}
        } 
        else {
             return [self wt_objectAtIndex:index];
        }
 }
@end

总之,合适的地方恰当的运用该技术,会达到事半功倍的效果,而且这也给自己的技术功底提升了不少呢。

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

推荐阅读更多精彩内容