此笔记是我在 2016/03/17 写的,所以是比较老的了
类检测
在 RCTBridge
类中的 initialize
方法中对 应用所有加载的类进行的检测,
来检查是否有实现了 RCTBridgeMdoule
类却没有 公开的模块.
这里用到了3个 Runtime 的函数.
-
Class *objc_copyClassList(unsigned int *outCount)
此函数 作用: 创建并返回所有已经注册的函数指针的数组,数组以nil
元素作为结束标志.
其中的outCount
int *
指针用来返回此应用运行时总共注册了多少个类.值得注意的是,此方法将返回所有的类,而并不只是本应用程序注册的类.
如下:
objc_copyClassList count一个简单的 Hello World OC 程序就有 1548 个已经注册的类.
可以通过如下方式,遍历输出所有的类名:
for(unsigned int i = 0 ; i < classCount; i++){ Class cls = classList[i]; printf("%s\n",class_getName(cls)); }
注意
classList
在使用完之后,应该使用free(classList)
来将数组释放. -
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
此函数作用: 判断某一个类是否实现了某一个协议. 如果实现则返回true
,否则 返回false
注意:其文档说在普通的编程中,应该优先使用NSObject
的conformsToProtocol:
方法而不是这个 . -
Class class_getSuperclass(Class cls)
此函数作用: 返回一个类的父类,如果没有返回nil
例如通过如下代码便可遍历整个类的继承树:
Class cls = [NSMutableAttributedString class]; printf("%s\n",class_getName(cls)); Class superclass = class_getSuperclass(cls); int current_level = 0; while (superclass) { current_level++; for (int level = 0; level < current_level;level++) { if(level + 1 == current_level){ printf("|----"); }else{ printf(" "); } } printf("%s\n",class_getName(superclass)); superclass = class_getSuperclass(superclass); }
不过太部分的 基础类库的类的继承树不深,基本上父类就是
NSObject
,
方法查找
在 RCTModuleData.m
类中有一个 - (NSArray<id<RCTBridgeMethod>> *)methods
方法是将此注册的模块类的中公开的方法都查找出来.
例如如下的代码:
@implementation MyMacroDecl
+ (NSArray<NSString *> *)__rct_export__itemAtIndex600 {
return @[@"itemAtIndex", @"itemAtIndexPath:(NSIndexPath*)indexPath"];
}
- (void)itemAtIndexPath:(NSIndexPath*)indexPath{
}
@end
就需要将其中的以 __rct_export__
为前缀的方法找出来,然后调用此方法得到对应的方法映射表元组.
主要使用了如下 Runtime API
-
Method *class_copyMethodList(Class cls, unsigned int *outCount)
此函数返回: 一个以NULL
结尾的 Method 指针数组,(使用完需要使用 free 方法释放)
值得说明的是:此方法返回的是此类实现的实例方法.父类实例方法并不能通过此方法返回.
要返回所以的方法,可以使用class_getInstanceMethod
,或class_getClassMethod
要获得类的类方法,应该使用类的元类作为参数即:class_copyMethodList(object_getClass(cls),&count)
-
Class object_getClass(id obj)
返回一个实例对应的类,
这里可以简单的理解下: 类在内存中的表示其实是其元类的一个实例. 那么类方法就是元类的实例的方法.
RN 的代码中就需要的是返回其类的类方法:
unsigned int methodCount;
Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount);
-
SEL method_getName(Method m)
此函数的作用: 返回方法对象的 方法签名.
返回这个方法对应的SEL
选择器对象.
可以通过const char *sel_getName(SEL sel)
方法返回SEL
的 C 字符串.
下面便是一段输出 NSObject
类的方法列表的代码.
Class cls = [NSObject class];
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
NSLog(@"Instance Method Of :%@,count:%d", NSStringFromClass(cls),methodCount);
for(unsigned int i = 0; i < methodCount; i++){
Method method = methods[i];
SEL methodSelector = method_getName(method);
char const * method_name = sel_getName(methodSelector);
printf("%s\n", method_name);
}
// Instance Method Of :NSObject,count:98
-
IMP method_getImplementation(Method m)
此函数作用: 返回此方法实例的 方法实现签名.
这个 IMP 的定义是一个函数签名.
typedef id (*IMP)(id, SEL, ...);
方法调用
使用 IMP
进行方法调用
结合上面的方法查找:
下面是使用 IMP 函数指针调用:
在 RN 中是这样使用的:
IMP imp = method_getImplementation(method);
NSArray<NSString *> *entries =
((NSArray<NSString *> *(*)(id, SEL))imp)(_moduleClass, selector);
上面代码中 NSArray<NSString *> *
是函数指针的返回值,一个NSString 的数组
(id,SEL)
表示函数指针的参数
使用 NSInvocation
进行方法调用
- 先根据
SEL
获得相应的NSMethodSignature
对象
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
- 使用
NSMethodSignature
对象得到NSInvocation
对象,并设置selector
及 通过setArgument:atIndex:
设置参数.
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.selector = _selector;
- 设置参数
这整个过程相当复杂,这里就不详细说明了.
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *objcType = [methodSignature getArgumentTypeAtIndex:i];
// ...
}
- 然后调用:
[_invocation invokeWithTarget:module];
完了之后释放掉所有参数值指针引用 .
这里对 NSMethodSignature
及 NSInvocation
进行进一步的说明.
NSMethodSignature
NSMethodSignature
用于记录方法的参数类型,及返回值相关信息.
它用一转发一个接收对象不响应的消息. 特别是分布式对象.
一般通过[NSObject methodSignatureForSelector:]
来创建它的实例.
然后用于创建NSInvocation
对象, 然后通过forwardInvocation:
转发给那些能处理此消息的对象.
默认情况下 NSObject
对于无法响应的消息直接调用 doesNotRecognizeSelector:
然后抛出异常.
-
getArgumentTypeAtIndex:
方法提供参数的类型信息. 值得注意的是 索引0,和 1 分别被self
及_cmd
这两个方法的隐式参数给占用了.
_cmd
大约是Current MethoD
的意思吧,也是一个Selector
对象. 可以用来得到当前方法的SEL
对象. -
numberOfArguments
返回参数总数. -
frameLength
所有参数共占用的 stack frame 的长度(平台相关) -
methodReturnLength
返回值的类型长度. -
methodReturnType
返回值的类型. -
isOneway
是否是异步的.
NSInvocation
NSInvocation
对象是 ObjC 消息的静态表示,是操作的对象表示.NSInvocation
用来在不同对象或甚至不同应用之间转发消息. 主要是通过NSTimer
对象,及分布式对象系统.
NSInvocation
包含了一个 ObjC 消息的所有元素.
一个target
,一个selector
,参数, 返回值.
所以这些值都可以直接设置,当NSInvocation
对象派发时返回值将会自动设置.
必须通过调用
invocationWithMethodSignature:
来得到NSInvocation
对象而不是alloc init
NSInvocation
对象可重用,其中的参数可以随便修改. 方便发送大量变种的消息.
-
setArgument:atIndex:
设置对应的参数, index0
和1
分别 对应self
及_cmd
隐式参数,不过应该使用target
和selector
来设置相应的值.
-
getArgument:AtIndex:
与set
方法对应的get
类扩展
AssociatedObject
通过使用 objc_getAssociatedObject
和 objc_setAssociatedObject
来为已经有的类增加属性,
以关联对象来实现.
- (NSNumber *)reactTag
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactTag:(NSNumber *)reactTag
{
objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
类及实例修改及动态构造
在 React 的 Profile 模块中,重度使用了 OC 运行时的类的修改,替换,动态添加方法等 Runtime 编程技术来实现
对每一个模块的方法进行 profile的功能
主要体现在 (React/Profile/RCTProfile.m# RCTProfileHookInstance)函数中.
代理类调用原始类方法,一开始是使用方法转发(forwardInvocation)来实现的,后来改为通过类似 obj_msgSend
的汇编来实现的: https://github.com/facebook/react-native/commit/89c1747c332c3629d1376d739334faa7842a339c
判断某一个实例已经被 HOOK 过了.
Class moduleClass = object_getClass(instance);
if([instance class] != moduleClass){
return ; // 说明已经被 HOOK 过了.
}
被 HOOK 过的 instance,-class
实例方法原始的类对象,但是 object_getClass
将返回代理类(即实际起了类对象作用的类)
为运行时动态添加新类
OBJC_EXPORT Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
- 创建代码类结构
Class proxyClass = objc_allocateClassPair(moduleClass, RCTProfileProxyClassName(moduleClass), 0);
上面的 RCTProfileProxyClassName
是创建基于moduleClass
类名加上前缀的类名.
例如moduleClass
的类名是 RCTAppState
那么新的类的类名将是:rct_profile_RCTAppState
新创建的proxyClass
是原始的 moduleClass
类的子类.
如果函数调用返回的是 nil
那么说明已经有同名的类注册过了.
所以 RN 中的处理中,对于已经注册过的代理类,其类的结构已经生成好了,然后直接
通过名称获得已经构造好的类对象,然后用来Hook
if (!proxyClass) {
proxyClass = objc_getClass(RCTProfileProxyClassName(moduleClass));
if (proxyClass) {
object_setClass(instance, proxyClass);
}
return;
}
- 为类添加方法
class_addMethod
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char *types)
RN 中是为代理类添加所有其代理类(父类)的所有方法,并且使用相同的签名,以达到相当于重写了父类方法的效果.
unsigned int methodCount;
Method *methods = class_copyMethodList(moduleClass,&methodCount);
for(unsigned int i = 0; i< methodCount;i++){
Method method = method[i];
SEL selector = method_getName(method);
if(/*此方法不应该被重写*/){ continue;}
const char *types = method_getTypeEncoding(method);
class_addMethod(proxyClass,selector,(IMP)RCTProfileTrampoline,types);
}
free(methods)
Note:
(1) class_copyMethodList
只会返回当前类实现了的方法
(2) method_getTypeEncoding
返回方法参数及返回值的类型字符串编码表示.
(3)
- 替换掉代理类的
initialize
方法,以免其执行时重复调用父类(moduleClass的initialize
方法)
class_replaceMethod(object_getClass(proxyClass), @selector(initialize), imp_implementationWithBlock(^{}), "v@:");
imp_implementationWithBlock
函数是将一个 Block 转成一个 IMP
指针.
- 替换代理类(及代理类元类)的
class
方法
for (Class cls in @[proxyClass, object_getClass(proxyClass)]) {
Method oldImp = class_getInstanceMethod(cls, @selector(class));
class_replaceMethod(cls, @selector(class), imp_implementationWithBlock(^{ return moduleClass; }), method_getTypeEncoding(oldImp));
}
- 将构造的类在 OC Runtime 中注册,同时将 instance 的类对象修改为新的代理类
objc_registerClassPair(proxyClass);
object_setClass(instance, proxyClass);
方法实现的替换
class_replaceMethod
可以实现将整个方法签名及实现都替换掉的功能.
如果只是要替换方法实现,还可以使用 method_setImplementation
方法.
RN 中使用如下:
Method createView = class_getInstanceMethod([RCTComponentData class], @selector(createViewWithTag:));
if (method_getImplementation(createView) != (IMP)RCTProfileCreateView) {
originalCreateView = (typeof(originalCreateView))method_getImplementation(createView);
method_setImplementation(createView, (IMP)RCTProfileCreateView);
}
替换 RCTComponentData
类中的 createViewWithTag:
方法.
然后替换为对每一个生成的 UIView 实例都进行HOOK过的实例的新函数
RCT_EXTERN UIView *RCTProfileCreateView(RCTComponentData *self, SEL _cmd, NSNumber *tag);
UIView *RCTProfileCreateView(RCTComponentData *self, SEL _cmd, NSNumber *tag)
{
UIView *view = originalCreateView(self, _cmd, tag);
RCTProfileHookInstance(view);
return view;
}
在 RCTUtils.m
中也有多个对方法实现的替换操作的包装函数.
网友评论