iOS底层原理之runtime

文章目录

      • Runtime简介
      • isa指针详解
      • OC方法底层结构
      • 方法缓存
      • 消息(objc_msgSend)发送执行流程
        • 1.消息发送
        • 2.动态方法解析
        • 3.消息转发
      • super本质
      • runtime 一些常用的api
      • runtime常见的应用
      • runtime面试题
          • 1. `isMemberOfClass`和`isKindOfClass`区别,以及使用注意点。
          • 2.讲一下 OC 的消息机制
          • 3.什么是Runtime?平时项目中有用过么?

Runtime简介

  • Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同,Objective-C的动态性是由Runtime API来支撑的,Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写。
  • runtime原码下载:objc4-723

isa指针详解

  • 在前面OC对象本质中讲到OC对象本质是一个结构体,里面有一个isa指针,OC中的方法调用就是根据这个isa去给实例对象或者类对象发消息。
  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。
  • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
  • arm64架构下isa的底层结构如下:
    iOS底层原理之runtime_第1张图片
  • 共用体(union)结构:共用声明和共用一变量定义,以达到节省空间的目的。(可参考百度百科:union)
  • 位域:是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。(可参考百度百科:位域)
  • 上面的bits共用体就存储了下面结构体中所有的元素,并且制定了每个元素所占的内存的位数和位置,比如nonpointer是一个char类型要占一个字节也就是8位,但现在就指定其占1位,结构体中的前三个变量只占了三位还不到一个字节,这样就大大的节省了内存空间。最前面的元素位数最低。
  • 那么isa里面的结构体的每一元素的作用:
  • nonpointer
    0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
    1,代表优化过,使用位域存储更多的信息
  • has_assoc
    是否有设置过关联对象,如果没有,释放时会更快
  • has_cxx_dtor
    是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
  • shiftcls
    存储着Class、Meta-Class对象的内存地址信息(通过ias地址与ISA_MASK(0x0000000ffffffff8ULL)按位与就可以得到class对象的地址,可以看出对象的地址总是0或8,因为后面三位不是对象的指向对象的,所以参与运算以后都是0)
  • magic
    用于在调试时分辨对象是否未完成初始化
  • weakly_referenced
    是否有被弱引用指向过,如果没有,释放时会更快
  • deallocating
    对象是否正在释放
  • extra_rc
    里面存储的值是引用计数器减1
  • has_sidetable_rc
    引用计数器是否过大无法存储在isa中
    如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

OC方法底层结构

  • 先看一下类的底层结构
    iOS底层原理之runtime_第2张图片
  • 具体类信息class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。其方法列表具体结构示意图如下:
    iOS底层原理之runtime_第3张图片
  • class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。
  • 头文件中可以看出的 typedef struct objc_method *Method;Method方法的本质是objc_method结构体,runtime原文件可以找到objc_method的底层结构如下:
struct objc_method {
    SEL name; //函数名                               
    const char * types ;//编码(返回值类型,参数类型)
    IMP mp ;//指向函数的指针
}  ;

SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似。
可以通过@selector()sel_registerName()获得;
可以通过sel_getName()NSStringFromSelector()转成字符串;
不同类中相同名字的方法,所对应的方法选择器是相同的。
SEL底层结构:

typedef struct objc_selector *SEL;

types包含了函数返回值、参数编码的字符串。
IMP代表函数的具体实现。

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
  • Type Encoding
    iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码。
    iOS底层原理之runtime_第4张图片
    例如:
        NSLog(@"%s",@encode(int));
        NSLog(@"%s",@encode(char));
        NSLog(@"%s",@encode(NSObject));
        NSLog(@"%s",@encode(NSArray));

打印结果为:
@encode打印结果

方法缓存

  • Class内部结构中有个方法缓存(cache_t ),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。其底层结构如下:
struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    IMP imp(SEL selector)
    {
        mask_t begin = _mask & (long long)selector;//获得脚标,以便快速查找到对应的方法
        mask_t i = begin;
        do {
            if (_buckets[i]._key == 0  ||  _buckets[i]._key == (long long)selector) {
                return _buckets[i]._imp;
            }
        } while ((i = cache_next(i, _mask)) != begin);//如果没有找到对应的方法,就去查找比当前小的那一位有没有,依次遍历查找直到为0时就从最大的那一位再重新遍历,如果都没有就会报方法找不到。
        return NULL;
    }
};

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

iOS底层原理之runtime_第5张图片
buckets:是一个散列表(百度百科:哈希表),相当于一个数组里面存储很对字典,每个字典的key是SEL,即方法名_imp是指向行数的指针,用来调用函数。
_mask:山列表的长度-1。
_occupied:已经缓存的方法数量。

  • 方法缓存的流程:
    方法调用时会先看cache_t里面有没有,有就从缓存中去,没有就将方法加入到cache_t中。
    当每次将方法加入到cache_t时会先看散列表内存够不够,如果够就加进去,不够就重新分配内存(是之前内存的2倍),之前加入到散列表中的方法也就都没了。(比如第一次调用了方法a,第二次调用b的时候放学内存不够了,就重新分配内存,并且把b保存进去,这时候散列表并没有方法a了)。
  • 方法缓存的好处肯定是能够快速查找方法,通过_mask & (long long)selector(散列表长度减去1和selector按位与)计算获得脚标,通过这个脚标去散列表中快速定位方法。但是这样计算可能得到多个方法的脚标是一样的,这是就还要判断散列表里面的key是不是与当前方法名相同, 如果定查找到key和当前调用的方法名相同就去获取指向该方法的指针去调用该方法,如果没有找到就去比当前脚标小1位的去匹配,依次类推,如果到脚标到最小都没匹配到该方法,就从散列表最大的脚标从大到小重新遍历查找(但是大于最开始计算出来的脚标),找到了就调用,找不到就不再报错了。也就是最多遍历了一遍散列表,最少就一次就定位到了正确的方法。
    注意:方法保存到方法缓存散列表中时也是上面的机制,如计算脚标对应的元素为空就将该方法的方法名和指向方法指针插入进去,如果不为空就用计算脚标减一去查看还有没空的地方,如果都没有了就重新分配跟大的内存去保存该方法。散列表是空间换时间(浪费更多的内存来换取快去查找方法)来达到快速存储和调用方法信息的。

消息(objc_msgSend)发送执行流程

  • objc_msgSend执行流程 – 源码跟读

objc-msg-arm64.s
ENTRY _objc_msgSend
b.le LNilOrTagged
CacheLookup NORMAL
.macro CacheLookup
.macro CheckMiss
STATIC_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
__class_lookupMethodAndLoadCache3
STATIC_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
Core Foundation
forwarding(不开源)

objc-runtime-new.mm
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache

  • OC中的方法调用(消息机制:给方法调用者发送消息), 其实都是转换为objc_msgSend函数的调用,objc_msgSend具体实现是汇编语言这里不再详解,在里面可以看到其主要是调用了lookUpImpOrForward(objc-runtime-new.mm文件可以看起具体实现)。
  • objc_msgSend简化结构如下:
void objc_msgSend(id receiver, SEL selector)
{
    if (receiver == nil) return;
    //调用方法
}

objc_msgSend有两个参数receiver(消息接收者)selector(消息名称),也就是说每个方法都会有这两个参数,只不过是隐式参数不显示而已。

  • 实例:
 Person * person = [[Person alloc]init];
//对象方法
[person test];
//其编译后为:
objc_msgSend(person, sel_registerName("test"));
//等价于:
objc_msgSend(person,@selector(test));

//类方法
[Person initialize];
objc_msgSend((id)objc_getClass("Person"), sel_registerName("initialize"));
//等价于:
objc_msgSend([MJPerson class], @selector(initialize));

*objc_msgSend的执行流程可以分为3大阶段:消息发送 、动态方法解析、消息转发。

1.消息发送

iOS底层原理之runtime_第6张图片

  • 消息发送流程详述:
  1. 在调用方法时会先判断`receiver(消息接收者)是否为空,如果为空则返回,不再往下执行;
  2. 如果不为空就走继承的那一套机制,通过isa去方法缓存(cache_t)的散列表中去查找又没有改方法,有就调用,没有就去class_rw_t中的方法列表中去查找,有就调用。如果不为空就走继承的那一套机制,通过isa去方法缓存(cache_t)的散列表中去查找又没有改方法,有就调用,没有就去class_rw_t中的方法列表中去查找,有就调用。
  3. 如果本类的方法列表没有就通过superclass去父类的方法缓存和方法列表去查找,有就调用;
  4. 如果指定基类都找不到该方法就会开始动态的方法解析。
    注意:上面在class_rw_t中查找方法,已经排序的,二分查找;没有排序的,遍历查找。

2.动态方法解析

  • 动态方法解析原码(精简版部分代码)
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{  
   //isa,superclass机制都完成后还没找到方法的实现
   //将要动态解析方法,并且还没有动态解析该方法
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        //动态解析该方法
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        //标记已经解析了
        triedResolver = YES;
        //继续重复上面的isa和superclass机制
        goto retry;
    }
    }
    //动态解析方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{  
   //如果不是原类,就解析实例对象方法
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //否则就是解析类方法
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
//动态解析对象方法(本质还是objc_msgSend)
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

  • 动态方法解析结构图

iOS底层原理之runtime_第7张图片

  • 方法动态解析流程详述:
  1. 先判断是否是原类,如果不是就会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法解析实例对象方法,如果是就调用+ (BOOL)resolveClassMethod:(SEL)sel来动态解析类方法。
  2. 调用上面两个 方法后标记为已经动态解析了,然后将方法相关信息添加到方法列表中,然后再调用objc_msgSend即消息发送的那一套继承等机制。
  3. 如果动态解析后还没找到该方法实现就来到消息转发。
  • 示例:
#import 
@interface Person : NSObject
-(void)test;
+(int)classTest:(int)a;
@end

#import "Person.h"
#import 

@implementation Person


//运行时可以动态添加方法的实现,
- (void)other{
    NSLog(@"%s",__func__);
}
//c语言函数函数实现
void other_c(id self,SEL _cmd){
    NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
//动态解析实例对象方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    //如是方法名为test就动态添加方法的实现
    if (sel == @selector(test)) {
        //获取其他方法实现,两个参数分别表示是那个对象的那个对象方法
//        Method method = class_getInstanceMethod(self, @selector(other));
        //添加方法,4个参数表示:拥有该方法的类对象,方法名,指向方法实现的指针,方法的返回值和参数的enconde编码
//        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        
        
        //如果是c语言函数直接添加
        //c语言函数名就是指向c语言函数实现的指针
        /*enconde编码参数分表示:
        v返回值类型为void,
         16 所有参数一共所占内存大小,
         @ 第一个参数类型(self),
         0 第一个参数从0开始的,
         : 第二个参数方法名(SEL),
         8 第二个参数从8开始的
         */
        class_addMethod(self, sel, (IMP)other_c, "v16@0:8");
        return YES;// 返回YES代表有动态添加方法,写NO也是可以的,只做参数打印调用
    }
    
    return [super resolveInstanceMethod:sel];
}

//类方法
+ (int)classOther:(int)a{
    NSLog(@"%s",__func__);
    return a++;
}

int classOther_c(id self,SEL _cmd,int a){
    NSLog(@"%s:a=%d",__func__,a);
    return a++;
}

//动态解析类方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(classTest:)) {
        //oc方法实现
//        Method method = class_getClassMethod(self, @selector(classOther:));
//        class_addMethod(object_getClass(self), sel, method_getImplementation(method), method_getTypeEncoding(method));
        //c语言方法实现
//        class_addMethod(object_getClass(self), sel, (IMP)classOther_c, "i20@0:8i16");
        //types 可以只写类型不写内存大小
        class_addMethod(object_getClass(self), sel, (IMP)classOther_c, "i@:i");
        return NO;
    }
    return [super resolveClassMethod:sel];
}
@end

结果不再打印,-(void)test;+(int)classTest:(int)a;分别是一个对象方法和有一个类方法,都是指在Person.h文件中声明了,但是并没有实现;通过重写+(BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel动态的添加了对象方法和类方法的实现,具体实现既可以是oc的方法也可以是c语言的函数。

  • 注意:
    1. 也可以动态添加别的类的方法来实现,只要把cls缓存别的类的实例对象(示例中是self)或者类对象即可。
    2. 动态解析方法实现,添加的方法或者函数必须是结构和被解析的方法一致,即方法的的返回值和参数类型,个数,顺序一致。
    3. 动态解析方法可以用来交换方法的实现。

3.消息转发

  • 当动态方法解析还没有找到到方法,就会来到消息转发。会调用_objc_msgForward_impcache方法,具体实现是汇编,其内部调用方法顺序及逻辑结构图如下:
    iOS底层原理之runtime_第8张图片* 消息转发流程详述:
  1. 首先会调用(id)forwardingTargetForSelector:(SEL)aSelector方法(目的是将该方法转发给对应targget,就是调用对应targget对应的同名方法),这时候如果返回的是一个类或者实例对象,就会调用该类或者实例对象同名的方法(直接用该对象或类调用其对应的方法);
  2. 如果返回值为nil或者没有重写上面的forwardingTargetForSelector方法,就会调用(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法签名的方法,返回方法EncodeType(目的是指定方法的返回值和参数类型);
  3. 如果返回的是一个合理的方法签名(有值,并且返回值和参数类型一致),就调用(void)forwardInvocation:(NSInvocation *)anInvocation,NSInvocation封装了一个方法调用,包括方法调用者,方法名,方法返回值及参数,指定方法调用者,指定方法,然后调用; 这样就可以让某个类或对象调用自己的方法来达到消息转发的目的。如果返回值为nil或者不匹配就会调用doesNotRecognizeSelector:报方法找不到是错误。
    注意:上面三个方法重写时,如果是转发类方法都是+开头,如果是转发对象方法都是-开头。

示例:

//Person类声明
#import 
@interface Person : NSObject
-(void)test;
+(void)classTest;
@end

//Person类实现
#import "Person.h"
#import "Dog.h"
#import 
@implementation Person
//消息转发
//对象方法
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
//        return [[Dog alloc]init];//前提是方法名相同
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}

//当forwardingTargetForSelector为nil的时候就会调用(方法签名的方法),返回值类型,参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
//        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        //如果返回的是一个合理的方法签名(有值,并且返回值和参数类型一致),就调用forwardInvocation
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//转发方法调用,NSInvocation封装了一个方法调用,包括方法调用者,方法名,方法返回值及参数
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//    anInvocation.target = [[Dog alloc]init];//指定对象目标
//    [anInvocation invoke];//调用
    if (anInvocation.selector == @selector(test)) {
         anInvocation.selector = @selector(test1);//指定方法,如果不指定,默认是同名的方法(即不写就是调用Dog的test方法);
    }
    [anInvocation invokeWithTarget:[[Dog alloc]init]];
}

//类方法
+(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(classTest)) {
        //        return [Dog class];
        //        return object_getClass([[Dog alloc]init]);
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(classTest)){
        //return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return [[Dog class] methodSignatureForSelector:@selector(classTest1)];
    }
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    if (anInvocation.selector == @selector(classTest)){
        anInvocation.selector = @selector(classTest1);
    }
    [anInvocation invokeWithTarget:[Dog class]];
}
@end

//Dog类实现部分
#import "Dog.h"
@implementation Dog
- (void)test{
    NSLog(@"%s",__func__);
}
+(void)classTest{
    NSLog(@"%s",__func__);
}
- (void)test1{
    NSLog(@"%s",__func__);
}
+(void)classTest1{
    NSLog(@"%s",__func__);
}
@end

示例中并没有实现Person类的- (void)test+(void)classTest,当调用这两个方法是通过消息转发调用Dog的- (void)test1+(void)classTest1实现的。

  • (void)forwardInvocation:(NSInvocation *)anInvocation的其他用法;
    只要来到了forwardInvocation方法,就可以在里面随便写方法的实现(可以在forwardInvocation:方法中自定义任何逻辑),也就是这时候就算没有实现对应的方法,也不会报方法找不到的错误。
    例如在上面的forwardInvocation中只做打印操作也是可以的:
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
}

super本质

  • super调用,底层会转换为objc_msgSendSuper函数的调用,接收2个参数struct objc_super2和*SEL;objc_super2的结构如下:
    objc_super2
    其中receiver是消息接收者,current_classreceiverClass对象。
    所以可以类推super的底层结构如下:
//super底层结构
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};

[super message]的底层实现 ,1.消息接收者仍然是当前类对象,2.从父类开始查找方法的实现。

  • 特例分析
//Man类是继承Person类
#import "Man.h"
@implementation Man
-(instancetype)init{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]); // Man
        NSLog(@"[self superclass] = %@", [self superclass]); // Person
        NSLog(@"--------------------------------");
        // objc_msgSendSuper({self, [MJPerson class]}, @selector(class));
        NSLog(@"[super class] = %@", [super class]); // Man
        NSLog(@"[super superclass] = %@", [super superclass]); // Person
    }
    return self;
}
@end

打印结果:
iOS底层原理之runtime_第9张图片
结果分析:class和superclass都是基类NSObject的方法(其中superclass是只读属性),在self调用这两个方法时,消息接收者的Man实例对象,所以返回的分别是Man和person;当super调用者两个方法时,也是给消息接收者发消息(objc_msgSendSuper({self, [MJPerson class]}, @selector(class));),而此时消息接收者还是Man实例对象,所以结果和self调用一样;其中classsuperclass的结构如下(简化后的):

//Class结构
- (Class)class
{
    return object_getClass(self);
}
//superclass结构
- (Class)superclass
{
    return class_getSuperclass(object_getClass(self));
}

runtime 一些常用的api

动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 
销毁一个类
void objc_disposeClassPair(Class cls)
获取isa指向的Class
Class object_getClass(id obj)
设置isa指向的Class
Class object_setClass(id obj, Class cls)
判断一个OC对象是否为Class
BOOL object_isClass(id obj)
判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
获取父类
Class class_getSuperclass(Class cls)
  • 成员变量
获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
  • 属性
获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)
动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)
获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
  • 方法
获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 
拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

runtime常见的应用

  • 设置UITextField占位文字的颜色,文字大小,字体等
unsigned int count;
    Ivar *ivars = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[i];
        NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    }
    free(ivars);
    self.textField.placeholder = @"请输入用户名";
    [self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
  • 字典转模型
    利用Runtime遍历所有的属性或者成员变量,利用KVC设值。
@implementation NSObject (Json)

+ (instancetype)mj_objectWithJson:(NSDictionary *)json
{
    id obj = [[self alloc] init];
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[i];
        NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        [name deleteCharactersInRange:NSMakeRange(0, 1)];
        
        // 设值
        id value = json[name];
        if ([name isEqualToString:@"ID"]) {
            value = json[@"id"];
        }
        [obj setValue:value forKey:name];
    }
    free(ivars);
    
    return obj;
}
  • 替换方法实现
    class_replaceMethodmethod_exchangeImplementations两个函数都可以替换方法的实现。
void test(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        
        Method runMethod = class_getInstanceMethod([MJPerson class], @selector(run));
        Method testMethod = class_getInstanceMethod([MJPerson class], @selector(test));
        method_exchangeImplementations(runMethod, testMethod);

        [person run];
        test();
    }
    return 0;
}

void myrun()
{
    NSLog(@"---myrun");
}

void test(void)
{
    MJPerson *person = [[MJPerson alloc] init];
    //用myrun来替换run方法的实现
//    class_replaceMethod([MJPerson class], @selector(run), (IMP)myrun, "v");
    
    //用block里面的代码替换run方法的实现
    class_replaceMethod([MJPerson class], @selector(run), imp_implementationWithBlock(^{
        NSLog(@"123123");
    }), "v");
    [person run];
}

一般是hook系统的方法实现,要实现系统的方法同时添加进去自己的操作。

@implementation UIControl (Extension)
+ (void)load
{
    // hook:钩子函数
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}

- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    
    // 调用系统原来的实现
    [self mj_sendAction:action to:target forEvent:event];
    
//    [target performSelector:action];
    
//    if ([self isKindOfClass:[UIButton class]]) {
//        // 拦截了所有按钮的事件
//
//    }
}
@end

runtime面试题

1. isMemberOfClassisKindOfClass区别,以及使用注意点。

先来看一下他们的底层实现:

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

可以看出isMemberOfClass就是比较是不是当前类对象(-开头的)或者原类对象(+开头的);isKindOfClass比较是不是当前(原)类对象及父类的(原)类对象。

  • 典型示例,Person是继承NSObject的,分别输出的值是什么?
        NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
        NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
        NSLog(@"%d", [[Person class] isKindOfClass:[Person class]]);
        NSLog(@"%d", [[Person class] isMemberOfClass:[Person class]]);

可以打印一下,输出的都是1,0,0,0;上面调用的都是+好开头的类方法;第一个为YES,因为NSObject原类superclass指针指向的NSObject类(这是一个特例);第二个为NO,因为类调用+isMemberOfClass,是比较原类的,二后面传进来的参数是[NSObject class]是类(二应该是通原类的class方法比较),所以为NO;第三个和第四个都为NO原因同第二个。

  • 注意
    1. 如果是-号开头的,也就是对象调用上面两个方法,后面传入的参数应该为类对象(比较实例对象指向的class(类)与后面传进来的class);
    2. 如果+号开头的,也就是类调用上面两个方法,后面传入的应该为原类对象(比较类对象指向的class(原类)与后面传进来的原类class);
    3. 不管是实例对象还是类对象调用class方法返回的都是类对象;
    4. runtime Api object_getClass(id _Nullable obj)如果传进去的是实例对象则获得的是类对象,传进去的是类对象的获得的是原类对象。
  • 所以上面的后三个可以按下面修改就都返回YES了:
  NSLog(@"%d", [NSObject isMemberOfClass:object_getClass([NSObject class])]);
  NSLog(@"%d", [Person isKindOfClass:object_getClass([Person class])]);
  NSLog(@"%d", [Person isMemberOfClass:object_getClass([Person class])]);
2.讲一下 OC 的消息机制

OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名);objc_msgSend底层有3大阶段,消息发送(当前类、父类中查找)、动态方法解析、消息转发。

3.什么是Runtime?平时项目中有用过么?

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数,平时编写的OC代码,底层都是转换成了Runtime API进行调用;
具体应用:
利用关联对象(AssociatedObject)给分类添加属性,
遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档),
交换方法实现(交换系统的方法),
利用消息转发机制解决方法找不到的异常问题。

你可能感兴趣的:(iOS,OC)