iOS Runtime简单介绍,以及不同类的Method Swizzling

Runtime介绍:

runtime顾名思义就是运行时,其实我们的App从你按下command+R开始一直到App运行起来经历了大致两个阶段,1:编译时,2:运行时。还记得一道很经典的面试题


这里给大家解释下:首先, * testObject 是告诉编译器,testObject是一个指向某个Objective-C对象的指针。因为不管指向的是什么类型的对象,一个指针所占的内存空间都是固定的,所以这里声明成任何类型的对象,最终生成的可执行代码都是没有区别的。这里限定了NSString只不过是告诉编译器,请把testObject当做一个NSString来检查,如果后面调用了非NSString的方法,会产生警告。接着,你创建了一个NSData对象,然后把这个对象所在的内存地址保存在testObject里。那么运行时(从这段代码执行开始,到程序结束),testObject指向的内存空间就是一个NSData对象。你可以把testObject当做一个NSData对象来用。 所以编译时是NSString,运行时是NSData。
runtime是什么:
在runtime中,所有的类在OC中都会被定义成一个结构体,像这样
类在runtime中的表示
struct objc_class {
Class isa;//指针,顾名思义,表示是一个什么, //实例的isa指向类对象,类对象的isa指向元类

#if !__OBJC2__
        Class super_class;  //指向父类
        const char *name;  //类名
        long version;     //类的版本信息,默认初始化为 0。我们可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。
        long info;   /*供运行期使用的一些位标识。有如下一些位掩码:
                        CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
                      CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
                      CLS_INITIALIZED (0x4L) 表示该类已经被运行期初始化了,这个标识位只被 objc_addClass 所设置;
                      CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在ObjC 2.0中被废弃了);
                      CLS_MAPPED (0x10L) 为ObjC运行期所使用
                      CLS_FLUSH_CACHE (0x20L) 为ObjC运行期所使用
                      CLS_GROW_CACHE (0x40L) 为ObjC运行期所使用
                      CLS_NEED_BIND (0x80L) 为ObjC运行期所使用
                      CLS_METHOD_ARRAY (0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是一个包含 objc_method_list 指针的数组;*/
        long instance_size  //该类的实例变量大小(包括从父类继承下来的实例变量);
        struct objc_ivar_list *ivars //成员变量列表
        struct objc_method_list **methodLists; //方法列表
        struct objc_cache *cache;//缓存   一种优化,调用过的方法存入缓存列表,下次调用先找缓存
        struct objc_protocol_list *protocols //协议列表
        #endif
} OBJC2_UNAVAILABLE;

相关的定义
/// 描述类中的一个方法
typedef struct objc_method *Method;

/// 实例变量
typedef struct objc_ivar *Ivar;

/// 类别Category
typedef struct objc_category *Category;

/// 类中声明的属性
typedef struct objc_property *objc_property_t;

ObjC 为每个类的定义生成两个 objc_class ,一个即普通的 class,另一个即 metaclass。我们可以在运行期创建这两个 objc_class 数据结构,然后使用 objc_addClass 动态地创建新的类定义。

runtime能干什么:
  • :1:获取一个类中的列表比如方法列表、属性列表、协议列表、成员变量列表像如下这样 其中获取到的属性、方法都是可以获取public和private的。
unsigned int count;
    Class clas = [WKWebViewController class]; //是我自己的类,之所以不用系统的类是因为系统的类方法属性太多了
    
    objc_property_t * propertyList = class_copyPropertyList(clas, &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"  %@  属性(包括私有) -------->>>>>    %@",clas,[NSString stringWithUTF8String:propertyName]);
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
    
    Method * methodList = class_copyMethodList(clas, &count);
    for (int i = 0; i < count; i++) {
        Method methodName = methodList[i];
        NSLog(@"  %@ 方法(包括私有)  -------->>>>>    %@",clas,NSStringFromSelector(method_getName(methodName)));
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
    
    
    Ivar *ivarList = class_copyIvarList(clas, &count);
    for (int i = 0; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"%@ 成员变量(包括私有) -------->>>>> %@",clas, [NSString stringWithUTF8String:ivarName]);
    }
    NSLog(@"-------------------------------------------------------------------------------------------------------------- ");
    
    
    //获取协议列表
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (int i = 0; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"%@ 协议 -------->>>>> %@",clas, [NSString stringWithUTF8String:protocolName]);
    }

输出后的结果是


image.png

其中也包括了私有方法。

  • 2:拦截方法调用
    有的时候我们用一个类或者一个实例变量去调用一个方法,由于操作失误或者是其他原因,导致这个所被调用的方法并不存在,报出这样的错误,然后闪退!


    image.png

这个时候如果我们想避免这些崩溃,我们就需要在运行时对其做一些手脚。iOS中方法调用的流程:其实调用方法就是发送消息,所有调用方法的代码例如 [obj aaa] 在运行时runtime会将这段代码转换为objc_msgSend(obj, [@selector]);(本质就是发送消息)然后obj会通过其中isa指针去该类的缓存中(cache)查找对应函数的Method, 如果没有找到,再去该类的方法列表(methodList)中查找,如果没有找到再去该类的父类找,如果找到了,就先将方法添加到缓存中,以便下次查找,然后通过method中的指针定位到指定方法执行。如果一直没有找到,便会走完如下四个方法之后崩溃。

/**
    如果调用的是不存在的实例方法则会在奔溃前进入该方法,防止崩溃可以在此处做处理
 */
+(BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

/**
 如果调用的是不存在的类方法则会在奔溃前进入该方法,防止崩溃可以在此处做处理
 */
+(BOOL)resolveClassMethod:(SEL)sel {
    return YES;
}

/**
 这个方法会把你所调用的不存在的方法重定向到一个声明了该方法的类中,只需要你返回一个有该方法的
 类就可以,如果你重定向的这个类仍然不具有该方法那么会继续崩溃
 */
-(id)forwardingTargetForSelector:(SEL)aSelector {
    
}

/**
 将你不存在的方法打包成NSInvocation对象,做完你自己的处理之后
 调用invokeWithTarget让某个target来处理该方法
 */
-(void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:self];
}

3:动态添加方法
因为我们调用了一个不存在的方法导致崩溃,那么我们在判断出不存在后就动态添加上一个方法吧 这样不就不会蹦了吗?我们先写一个方法用来给我们做出提示

- (void) errorMethod {
    NSLog(@"no method!!!!!!!");
}

如果调用了没有的方法,那么就把这个方法添加进去,然后把被调用的方法的指针指向这个error1:,那么一旦调用了没有的方法就会走这个。我们来看代码

+(BOOL)resolveInstanceMethod:(SEL)sel {
    Method errorMethod =  class_getInstanceMethod([self class], @selector(errorMethod));
    if ([NSStringFromSelector(sel) isEqualToString:@"testMethod"]) {
        BOOL isAdd =  class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));
        NSLog(@"tinajia  = %d",isAdd);
    }
    //Do something
    return YES;
}

主要用到

/**
    添加方法
     @param class] 在哪个类里添加
     @param sel 添加的方法的名字
     @param errorMethod 添加的方法的实现IMP指
     @param types 方法的标示符
     @return 是否添加成功
         */
BOOL isAdd =  class_addMethod([self class], sel, method_getImplementation(errorMethod), method_getTypeEncoding(errorMethod));

然后运行下:

WKWebViewController * vc= [[WKWebViewController alloc] init];
[vc performSelector:@selector(testMethod)];

我调用了并不存在的testMethod方法并没有崩溃并且方法成功添加了

image.png
  • 4:动态交换方法(也叫iOS黑魔法,慎用)

    没什么好例子,用一个网上说的例子(引用别人的东西,懒得复制了,就接了图)
    其实本质即使SEL和IMP的交换,原理是这样的:在iOS中每一个类中都有一个叫dispatch table的东西,里面存放在SEL 和他所对应的IMP指针,之前也说过方法调用就是通过sel找IMP指针然后指针定位调用方法。方法交换就是对这个dispatch table进行操作。让A的SEL去对应B的IMP,B的SEL对应A的IMP,如图

    这样就达到方法交换的目的,下面看代码:

+ (void)changeMethod {
    //  如果是类方法 要使用 !
    //  如果是系统的集合类的属性要用元类 比如 __NSSetM = NSMutableSet
    //  Class  class = NSClassFromString(@"__NSSetM");
    //  Class metaClass = objc_getMetaClass([NSStringFromClass(class) UTF8String]);
    Class systemClass = NSClassFromString(__NSSetM);
    
    SEL sel_System = NSSelectorFromString(addObject:);
    SEL sel_Custom = @selector(swizzle_addObject:);
    
    Method method_System = class_getInstanceMethod(systemClass, sel_System);
    Method method_Custom = class_getInstanceMethod([self class], sel_Custom);
    
    IMP imp_System = method_getImplementation(method_System);
    IMP imp_Custom = method_getImplementation(method_Custom);
    
    method_exchangeImplementations(method_System, method_Custom);
}

- (void)swizzle_addObject:(id) obj {
    if (obj) {
        [self swizzle_addObject:obj];
    }
}

主要代码 method_exchangeImplementations(method1, method2); 这两个参数很简单,就是两个需要交换的方法。
最后我调用了m1但是实际上走了m2。

动态交换方法的原理以及交换过程中指针的变化

在通常的方法交换中我们通常有两种情景,一种是我会针对被交换的类建一个category,然后hook的方法会写在category中。另一种是自己创建一个Tool类里面放些常用的工具方法其中包含了方法交换。可能大家普遍选择第一种方法,但是如果你需要hook的类非常多的(我实际项目中就遇到这样的问题)那你就需要针对不同的类创建category,就会导致文件过多,且每一个文件中只有一个hook方法,这样一来左侧一堆文件,所以我用了第二种方法,但是在使用过程中出现一个问题,先看下我的代码机构


image.png

我要hook的是ViewController中的viewDidLoad方法,我建立了两个类一个是ViewController的category,另一个是Tool类,为了一会区别演示不同类hook的不同(两个类中hook的代码完全一样)

  • ViewController中将要被替换的系统方法


    被替换的方法(系统方法)
  • Category中将要用来替换的自定义方法


    用来替换的方法(自定义方法)
  • 然后在ViewController中的load中做方法替换


    进行方法替换

    运行一下的输出结果想必大家已经猜到了先执行custom再执行system,这是通常情况下大家的做法。


    结果

    下面再来看下如果我将替换方法写在不同类中会怎样,调用Tool中的交换方法
    执行Tool中的交换方法

    然后直接看结果了,因为代码都是一模一样的我直接复制过去的


    结果

    发生了crash,原因是ViewController中没有swizzel_viewDidLoad_custom这个方法,为什么不同类的交换会出现这种问题,我们用个图来说明下
    image.png

    解决的办法是我们在交换方法之前要先像其中添加方法,也就是说把customMethod添加到SystemClass中,但是注意要把customMethod的实现指向syetemMethod的实现。这样一来就可以达到SystemClass调用customMethod却执行systemMethod的代码的效果,实现以上要求我们需要在交换之前执行这个方法。
class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))

其中第一个参数是需要往哪个类添加;第二个参数是要添加的方法的方法名;第三个参数是所添加的方法的方法实现,第四个是方法的标识符。经过就该之后我们的代码是这样

.
.
之前的都一样就省略
.
.
if (class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))) {
        class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));
    } else {
        method_exchangeImplementations(method_System, method_Custom);
    }

我们来看下执行完add操作之后此时的方法和类的对应关系(红色的为add的修改)

关系

因为SystemClass中本身不包含customMethod所以add一定是成功的,也就是说会进入判断执行replace方法。

class_replaceMethod(systemClass, sel_System, imp_Custom, method_getTypeEncoding(method_System));

第一个参数:需要修改的方法的所在的类;第二个参数:需要替换其实现的方法名;第三个参数:需要把哪个实现替换给他;第四个参数:方法标识符。此时看下我们做完replace之后的类与方法名以及他们实现的关系(红色的为replace的修改)。


关系

此时大家已经看出来了,虽然没有执行exchange方法,但是我已经达到了方法交换的目的。系统执行systemMethod时候会走customMethod的实现但是因为在customMethod方法中我会递归执行[self customMethod],所以又会走到systemMethod的实现,因为之前进行了方法添加,所以此时A类中有了customMethod方法,不会再发生之前的crash。达到一个不同类进行Method Swizzling的目的。

综上来看一个完整严谨的MethodSwizzling应该在交换前先add,并且add方法的参数不能错
+ (void)changeMethod {

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,682评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,544评论 33 466
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,176评论 0 7
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 913评论 0 6
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 748评论 0 1