# 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 指向元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 中的指针定位到指定方法执行。如果一直没有找到,便会走完如下四个方法之后崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
如果调用的是不存在的实例方法则会在奔溃前进入该方法,防止崩溃可以在此处做处理
*/
+(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:动态添加方法
    因为我们调用了一个不存在的方法导致崩溃,那么我们在判断出不存在后就动态添加上一个方法吧 这样不就不会蹦了吗?我们先写一个方法用来给我们做出提示

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

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

1
2
3
4
5
6
7
8
9
+(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;
}

主要用到

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

然后运行下:

1
2
3
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,如图

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+ (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 的代码的效果,实现以上要求我们需要在交换之前执行这个方法。

1
class_addMethod(systemClass, sel_Custom, imp_System, method_getTypeEncoding(method_System))

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

1
2
3
4
5
6
7
8
9
10
11
.
.
之前的都一样就省略
.
.
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 方法。

1
2
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 方法的参数不能错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (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 中都是没有问题的。