美文网首页
ReactNative 中 ObjC Runtime 编程的运用

ReactNative 中 ObjC Runtime 编程的运用

作者: 一半晴天 | 来源:发表于2018-06-16 16:24 被阅读11次

此笔记是我在 2016/03/17 写的,所以是比较老的了

类检测

RCTBridge 类中的 initialize 方法中对 应用所有加载的类进行的检测,
来检查是否有实现了 RCTBridgeMdoule 类却没有 公开的模块.
这里用到了3个 Runtime 的函数.

  1. 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) 来将数组释放.

  2. BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
    此函数作用: 判断某一个类是否实现了某一个协议. 如果实现则返回 true,否则 返回 false
    注意:其文档说在普通的编程中,应该优先使用 NSObjectconformsToProtocol: 方法而不是这个 .

  3. 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

  1. Method *class_copyMethodList(Class cls, unsigned int *outCount)
    此函数返回: 一个以 NULL 结尾的 Method 指针数组,(使用完需要使用 free 方法释放)
    值得说明的是:此方法返回的是此类实现的实例方法.父类实例方法并不能通过此方法返回.
    要返回所以的方法,可以使用 class_getInstanceMethod,或 class_getClassMethod
    要获得类的类方法,应该使用类的元类作为参数即: class_copyMethodList(object_getClass(cls),&count)

  2. Class object_getClass(id obj) 返回一个实例对应的类,
    这里可以简单的理解下: 类在内存中的表示其实是其元类的一个实例. 那么类方法就是元类的实例的方法.

RN 的代码中就需要的是返回其类的类方法:

    unsigned int methodCount;
  Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount);
  1. 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
  1. 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 进行方法调用

  1. 先根据 SEL 获得相应的 NSMethodSignature 对象
    + (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
  NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
  1. 使用 NSMethodSignature 对象得到 NSInvocation 对象,并设置 selector 及 通过 setArgument:atIndex: 设置参数.
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  invocation.selector = _selector;
  1. 设置参数
    这整个过程相当复杂,这里就不详细说明了.
 NSUInteger numberOfArguments = methodSignature.numberOfArguments;
   for (NSUInteger i = 2; i < numberOfArguments; i++) {
            const char *objcType = [methodSignature getArgumentTypeAtIndex:i];
        // ...
  }
  1. 然后调用:
 [_invocation invokeWithTarget:module];

完了之后释放掉所有参数值指针引用 .

这里对 NSMethodSignatureNSInvocation 进行进一步的说明.

NSMethodSignature

NSMethodSignature 用于记录方法的参数类型,及返回值相关信息.
它用一转发一个接收对象不响应的消息. 特别是分布式对象.
一般通过 [NSObject methodSignatureForSelector:] 来创建它的实例.
然后用于创建 NSInvocation 对象, 然后通过 forwardInvocation: 转发给那些能处理此消息的对象.

默认情况下 NSObject 对于无法响应的消息直接调用 doesNotRecognizeSelector: 然后抛出异常.

  1. getArgumentTypeAtIndex: 方法提供参数的类型信息. 值得注意的是 索引0,和 1 分别被 self_cmd 这两个方法的隐式参数给占用了.
    _cmd 大约是 Current MethoD 的意思吧,也是一个 Selector 对象. 可以用来得到当前方法的SEL 对象.

  2. numberOfArguments 返回参数总数.

  3. frameLength 所有参数共占用的 stack frame 的长度(平台相关)

  4. methodReturnLength 返回值的类型长度.

  5. methodReturnType 返回值的类型.

  6. isOneway 是否是异步的.

NSInvocation

NSInvocation 对象是 ObjC 消息的静态表示,是操作的对象表示.NSInvocation 用来在不同对象或甚至不同应用之间转发消息. 主要是通过 NSTimer 对象,及分布式对象系统.

NSInvocation 包含了一个 ObjC 消息的所有元素.
一个 target,一个 selector,参数, 返回值.
所以这些值都可以直接设置,当 NSInvocation 对象派发时返回值将会自动设置.

必须通过调用 invocationWithMethodSignature: 来得到 NSInvocation 对象而不是 alloc init

NSInvocation 对象可重用,其中的参数可以随便修改. 方便发送大量变种的消息.

  1. setArgument:atIndex: 设置对应的参数, index 01 分别 对应 self_cmd 隐式参数,不过应该使用 targetselector 来设置相应的值.
  1. getArgument:AtIndex:set 方法对应的 get

类扩展

AssociatedObject

通过使用 objc_getAssociatedObjectobjc_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)

  1. 创建代码类结构
 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;
  }
  1. 为类添加方法
    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)

  1. 替换掉代理类的 initialize 方法,以免其执行时重复调用父类(moduleClass的initialize方法)
class_replaceMethod(object_getClass(proxyClass), @selector(initialize), imp_implementationWithBlock(^{}), "v@:");

imp_implementationWithBlock 函数是将一个 Block 转成一个 IMP 指针.

  1. 替换代理类(及代理类元类)的 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));
}
  1. 将构造的类在 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 中也有多个对方法实现的替换操作的包装函数.

相关文章

网友评论

      本文标题:ReactNative 中 ObjC Runtime 编程的运用

      本文链接:https://www.haomeiwen.com/subject/lfybeftx.html