Jun 17

oc中的反射

Lrdcq , 2016/06/17 15:34 , 程序 , 閱讀(4102) , Via 本站原創
反射是在初学java的过程中反复提及的概念与操作方式。浅显的理解反射的话,它是一组api,让你可以在程序运行时了解并获得程序内部结构,就像镜子一样,在镜子中看到程序自己本身。然后,动态的对程序本身进行修改与操作。当然,这种看起来另辟蹊径的程序运行方式一般来说并不推荐,适用场景往往出现在需要动态注入代码,调用非可见属性方法等需要高灵活性的使用场景中。那么在oc中,是否也有类似的功能呢?有过之而无不及,oc的runtime库就可以完全做到这些东西。

回顾一下oc的runtime,运行时环境库。大家都知道oc是一门基于c的动态语言,就像java运行在jvm中一样,oc运行在runtime中的,而与java不同的是,oc是作为c层面的语言,和runtime在同一个环境下运行的,而通过runtime库,我们可以直接接触到运行时环境,当然也就可以操作运行时环境中的所有oc代码运行了——而类似与反射的功能实现,当然可以轻而易举的做到,大炮轰蚊子咯。

1.动态获取类与方法

在面向对象的语言中,类即Class是最重要的,所以一般要动态处理一段代码或者数据,首先要获取到类本身。在oc中获取到类很方便:
//通过实例获取类
Class clz_1 = [obj class];
//通过类名获取类
Class clz_2 = NSClassFromString(@"LRDTestObject");
//用类来实例化对象
LRDTestObject *new_obj = [[clz_1 alloc] init];
//以上代码等效的内容:
LRDTestObject *new_obj = [[LRDTestObject alloc] init];

一脸蒙逼的这样写主要的作用是什么呢?主要是通过类名获取类,首先,我们可以判断这个类是否存在,特别是在涉及到面向接口设计的程序的时候,一个动态的实现类名是很常见的,用动态构建类的方式,可以动态的编译入不同类的同时动态使用不同类。同理,我们能获得class的话我们也可以遍历这个类的属性和方法了,像这样:
//遍历属性
unsigned int propertyCount = 0;
objc_property_t * properties = class_copyPropertyList([self class], &propertyCount);
    
for (unsigned int i = 0; i < propertyCount; ++i) {
    objc_property_t property = properties[i];
    const char * name = property_getName(property);
}
free(properties);
//遍历方法
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(clz, &methodCount);
    
for (unsigned int i = 0; i < methodCount; i++) {
    Method method = methods[i];
    printf("\t'%s' has method named '%s' of encoding '%s'\n",
        class_getName(clz),
        sel_getName(method_getName(method)),
        method_getTypeEncoding(method));
}
free(methods);

通过针对class的函数,我们很容易就可以获得出这个类所有属性和方法的列表。其中属性是objc_property_t类型的结构体,对应方法是Method类型的结构体。另外通过这个函数获取出来的属性和方法并不区分共有和私有,其实是oc本身并不去分共有和私有属性,只是引入的h头文件可以不写上不暴露的方法,那使用者也就不知道了。总之,通过这种方法,我们可以找到所有的属性和方法——然后当然就是使用了:
//调用属性用kvc就可以了,主要是调用方法,使用如下函数
id output = method_invoke (new_obj, method);
//当然,从method中取出selector也是可以的
[obj performSelector:method_getName(method) withObject:nil];
//要注意的是SEL类型本质就是方法名,也是一个字符串,因此可以直接这样使用

2.动态添加类,属性,方法

能动态使用是一方面,动态添加着更为强大,可以为程序添加入特定的代码与功能,在hack一些自带和第三方库时有奇效,用于热更新也是非常有创意的选择(JSPatch原理)。不过我们首先回顾一下,oc中的类是分了接口和实现的,那接口和实现在runtime中分别对应的什么呢?
首先,接口中的属性,是共有属性property,但它其实只有一个名字,实际数据处理是在实现中的;然后是实现中写的私有属性,被称为实例变量ivar,每一个共有属性会自动为我们添加一个实例变量,还有我们自己加的实例变量,类的实例所有的属性事实上都是储存在这里的。如果考虑到其实c系的语言,头文件的声明并没有什么卵用,可以说,属性只是为实例变量提供的一些外界访问的入口(setter,getter)而已。
然后就是方法了,头文件的方法声明并没有什么卵用,直接看到实现中的方法声明。虽然oc中的方法就是一个方法了,但事实上它分为方法声明SEL,和实现IMP,其中方法声明即常说的selecter其实本质是方法的名字类似于application:didReceiveRemoteNotification:的字符串,然后方法的实现即方法体,本质上是一个函数且引用是指向这个函数的函数指针。
那么回到主题,首先,动态创建一个类,虽然作用并不是很大,但是作为面向对象程序的基础,还是必须知道,要比如这样:
NSString *name = @"testString";  
Class newClass = objc_allocateClassPair([NSString class], [name UTF8String], 0);  
objc_registerClassPair(newClass);

我们动态的创建了一个类叫testString,它继承于NSString并且后面带了长度为0的属性区域(对,第三个参数通常为0就可以了)。然后,我们把这个类注册在了运行环境中,我们就可以通过这个newClass或者调用NSClassFromString来使用它了。当然,使用前,我们可以动态添加上实例变量:
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types) 

由于实际使用中的类型事实上是一个静态的结构体,所以调用这个方法必须在构建类型前也就是objc_registerClassPair之前添加上,它会按照设定的大小和对齐方式拼接在class体后边,每一个class实例的数据结构除了一开始的两位isa指针,后面全是从子类到父类罗列的ivar数据体了。同样,如果不是自己新建的类而已已经存在的类的话,更常见的,我们也可以用另一个函数动态添加类的属性,写出来是这样的:
objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t ownership = { "C", "" }; // C = copy
objc_property_attribute_t backingivar  = { "V", "_privateName" };
objc_property_attribute_t attrs[] = { type, ownership, backingivar };
class_addProperty(newClass, "name", attrs, 3);

其中属性里很多必要的内容是通过attrs的key-value值来加入的,上面加入的属性几乎都是必要的。T是值得属性的类型,这里写了是NSString,C是值得使用内存管理的copy来赋值,V是指的指向的实例变量即上面所添加的Ivar,是用名字来指定的,最后告诉了函数我们用了3个属性。简单清晰,也就是说,对于一个新增的类添加属性,我们需要先添加Ivar即属性的实例变量,即属性的实现,然后在添加Property即外界访问属性的入口,否则只相当于是一个私有属性而已。然后添加方法:
id myCustomFunction(id self, SEL _cmd, [other params...]) {
  //要添加的方法的实现函数
}
class_addMethod(newClass, @"selecterName:data:", (IMP)myCustomFunction, "encodeTypeString");

其中有几个要注意的地方:1.实现一个方法的函数的头两个参数必须是id,表示self和SEL来表示调用这个函数的方法。另外一个,就是参数对应,的encodeTypeString的了。这个encodeTypeString到底是什么呢?我也不知道,通过获取别的方法的encodeTypeString比如通过@encode宏来获取或者通过method_getTypeEncoding把别的数据和方法的encodeTypeString打出来看看,然后对照一下官方文档:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html,大概能拼出个七七八八,写普通方法的encodeTypeString是没问题了。

除了添加方法,更加常用的功能是替换方法,多是用于热修复和为内建库和第三方库添加功能和钩子的地方,替换方法的函数是:
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

和class_addMethod几乎一样,唯一不通的是它会把被替换下来的方法实现(函数指针给返回回来)。结合其他runtime函数,我们能做一些很有意义的事情:

    1.动态添加属性:由于category只能为类直接添加方法,要添加属性的话,只有通过runtime来实现。

    2.为一个方法调用添加hook:思路是钩子函数的入参是钩子运行的内容的block,钩子运行时先运行block内容,再运行原本方法内容;把钩子方法替换到原本的方法上,然后为原本的方法设置一个别名,比如old_前缀添加到类中,那么就可以实现在钩子里再运行原本方法内容了。

    3.为一个对象添加方法(属性):思路是获取当前对象的类作为父类添加一个新的类,将方法加入这个新类中并把那个对象的类型指向这个新类即可。

    4.实现多继承:通过一段辅助代码,把多个类的方法一个一个添加进去就可以了,属性则通过gettersetter进行转发即可。

最后,推荐把ocruntime功能发挥到极致的吊炸天库,拯救世界的利器:https://github.com/bang590/JSPatch(作者解析文:https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3)
logo