iOS底层实现

源于一个面试题。


我们知道在Runtime中的swizzling:通过selector来找IMP,可以利用Runtime来实现交换原方法和目标方法的IMP,以完全代替原方法的实现,或为原实现前后相当于加一段额外的代码。

就是在分类的load方法中,通过class_getClassMethodclass_getInstanceMethod获取类方法和实例方法,然后method_exchangeImplementations(),交换方法实现,或者是其他class_addMethods、class_addIvar、class_addProtocol、class_addProperty来动态的添加方法或者成员变量。还有class_copyIvarList、class_copyMethodList获得某个类所有的成员变量和所有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import<objc/runtime.h>
@interface ClassA: NSObject
- (void)methodA;
+ (void)methodB;
@end

...

@implementation ClassA (Swizzle)

+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(methodA));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_methodA));
method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)swizzled_methodA {
...
[self swizzled_methodA];
...
}

@end

AOP的库Aspects 支持多次hook同一个方法,支持从hook返回的id对象删除对应的hook,IMP即函数指针。
Aspects 的大致原理:替换原方法的IMP为 消息转发函数指针 _objc_msgForward或_objc_msgForward_stret,把原方法IMP添加并对应到SEL aspects_originalSelector,将forwardInvocation:的IMP替换为参数对齐的C函数ASPECTS_ARE_BEING_CALLED(NSObject self, SEL selector, NSInvocation invocation)的指针。在ASPECTS_ARE_BEING_CALLED函数中,替换invocation的selector为aspects_originalSelector,相当于要发送调用原始方法实现的消息。对于插入位置在前面,替换,后面的多个block,构建新的blockInvocation,从invocation中提取参数,最后通过invokeWithTarget:block来完成依次调用。

libffi 简介

    libffi 可以认为是实现了C语言上的runtime,简单来说,libffi 可根据 参数类型(ffi_type),参数个数 生成一个 模板(ffi_cif);可以输入 模板、函数指针 和 参数地址 来直接完成 函数调用(ffi_call); 模板 也可以生成一个所谓的 闭包(ffi_closure),并得到指针,当执行到这个地址时,会执行到自定义的void function(ffi_cif cif, void ret, void args, void userdata)函数,在这里,我们可以获得所有参数的地址(包括返回值),以及自定义数据userdata。当然,在这个函数里我们可以做一些额外的操作。
    
    
如何hook ObjC方法和实现AOP,思路:我们可以将ffi_closure关联的指针替换原方法的IMP,当对象收到该方法的消息时objc_msgSend(id self, SEL sel, …),将最终执行自定义函数void ffi_function(ffi_cif
cif, void *ret, void
args, void *userdata)。而实现这一切的主要工作是:设计可行的结构,存储类的多个hook信息;根据包含不同参数的方法和切面block,生成包含匹配ffi_type的cif;替换类某个方法的实现为ffi_closure关联的imp,记录hook;在ffi_function里,根据获得的参数,动态调用原始imp和block。

动态调用C函数

使用libffi提供接口动态调用流程如下:

  1. 准备好参数数据及其对应ffi_type数组、返回值内存指针、函数指针
  2. 创建与函数特征相匹配的函数原型:ffi_cif对象
  3. 使用“ffi_call”来完成函数调用
    使用ffi,只要有函数原型cif对象,函数实现指针,返回值内存指针和函数参数数组,我们就可以实现在运行时动态调用任意C函数。

所以如果想实现其他语言(譬如JS),执行过程中动态调用C函数,只需在调用过程中加一层转换,将参数及返回值类型转换成libffi对应类型,并封装成函数原型cif对象,准备好参数数据,找到对应函数指针,然后调用即可。

动态定义C函数

libffi还有一个特别强大的函数,通过它我们可以将任意参数和返回值类型的函数指针,绑定到一个函数实体上。那么这样我们就可以很方便的实现动态定义一个C函数了!同时这个函数在编写解释器或提供任意函数的包装器(通用block)时非常有用,此函数是:

1
2
3
4
5
ffi_status ffi_prep_closure_loc (ffi_closure *closure,  //闭包,一个ffi_closure对象
ffi_cif *cif, //函数原型
void (*fun) (ffi_cif *cif, void *ret, void **args, void*user_data), //函数实体
void *user_data, //函数上下文,函数实体实参
void *codeloc) //函数指针,指向函数实体

通过一个简单的例子,看下如何将一个函数指针绑定到一个函数实体上:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <ffi.h>

/* Acts like puts with the file given at time of enclosure. */
// 函数实体
void puts_binding(ffi_cif *cif, unsigned int *ret, void* args[],
FILE *stream)
{
*ret = fputs(*(char **)args[0], stream);
}

int main()
{
ffi_cif cif;
ffi_type *args[1];
ffi_closure *closure;

int (*bound_puts)(char *); //声明一个函数指针
int rc;

/* Allocate closure and bound_puts */ //创建closure
closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);

if (closure)
{
/* Initialize the argument info vectors */
args[0] = &ffi_type_pointer;

/* Initialize the cif */ //生成函数原型
if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1,
&ffi_type_uint, args) == FFI_OK)
{
/* Initialize the closure, setting stream to stdout */
// 通过 ffi_closure 把 函数原型_cifPtr / 函数实体JPBlockInterpreter / 上下文对象self / 函数指针blockImp 关联起来
if (ffi_prep_closure_loc(closure, &cif, puts_binding,
stdout, bound_puts) == FFI_OK)
{
rc = bound_puts("Hello World!");
/* rc now holds the result of the call to fputs */
}
}
}

/* Deallocate both closure, and bound_puts */
ffi_closure_free(closure); //释放闭包

return 0;
}

上述步骤大致分为:

  1. 准备一个函数实体
  2. 声明一个函数指针
  3. 根据函数参数个数/参数及返回值类型生成一个函数原型
  4. 创建一个ffi_closure对象,并用其将函数原型、函数实体、函数上下文、函数指针关联起来
  5. 释放closure

通过以上这5步,我们就可以在执行过程中将一个函数指针,绑定到一个函数实体上,从而轻而易举的实现动态定义一个C函数。

参考资料

Hook方法的新姿势–(使用libffi实现AOP )

如何动态调用 C 函数

如何动态创建 block – JPBlock 扩展原理详解

文章目录
  1. 1. libffi 简介
    1. 1.1. 动态调用C函数
    2. 1.2. 动态定义C函数
  2. 2. 参考资料
本站总访问量 本站访客数人次 ,