美文网首页RuntimeiOS开发知识汇总小知识点
Runtime窥探 (一)| 基本介绍

Runtime窥探 (一)| 基本介绍

作者: Dely | 来源:发表于2017-09-25 15:26 被阅读441次

前言

我无意间删除了一个微信好友,发现我的王者荣耀段位排名上升了一位,仿佛找到了新大陆,然后我就一直删阿删,终于到最后我的段位排在了第一名,心里尤为高兴啊。后来我打开微信一看,好友里就我自己了。

毛主席说过:说不好段子的程序员不是好产品。上面我是我自制段子,仅供娱乐。

好长时间不写博客了,手都生疏了,但是我玩王者荣耀的手没有生疏是咋回事啊?,我入农药坑半年多了,发现真是一个农药啊,当你星星满了将要升段的时候,是不是发现特别难,感觉对面都是代打嘛,结果肯定是输的,然后你就会连跪的节奏,然后你再打满星星,升段局又输,依次循环。。。你们有没有跟我一样的遭遇?你才会发现世界就是一个轮回,我TM不玩了。但是作为一个资深MOBA玩家的装逼犯的我怎么可能认输呢?我最后还是打上去了!果断弃坑。。还是学习最有意思啊

楼主段位镇楼

那就跟我一起来上车装逼吧。要发车了,下面代码较多,请自备晕车药。

装逼犯

正文

Runtime:Objective-C是基于C加入了面向对象特性和消息转发机制的动态语言。这意味着不仅需要编译器,还需要一个运行系统来执行编译后的代码。而这个运行系统就是runtime,与其说是运行时机制,不如称它为中间调度系统,来控制消息发送、消息转发、查看对象信息等等,更加灵活。

由于rutime知识点比较多,应用比较灵活,我会写一个系列供大家阅读。
本系列所有runtime源码均来自 objc4-709
在使用runtime的api时一定要导入头文件#import <objc/message.h>#import <objc/message.h>

本文主要是讲解runtime的基本知识点,目录如下:

  • 部分常用API调用举例
  • 部分常用API汇总
  • isa和Class是什么?
  • Ivar是什么?
  • SEL是什么?
  • Method是什么?
  • IMP是什么?
  • Cache是什么?
  • Type Encodings是什么?
  • Declared Properties以及objc_property_attribute_t是什么?

1.部分常用API调用举例

这个例子不是只是介绍api该如何调用,实际应用肯定是组合使用才能出效果。

#pragma mark - -----类相关的方法-----
- (void)runtime_ClassMethod{
    
    Person *p = [[Person alloc] init];

    //获取对象的类
    Class class = object_getClass([Person class]);
    
    // 获取类的父类
    Class superClass = class_getSuperclass([NSObject class]);
    
    // 判断给定的Class是否是一个元类
    BOOL isMetaClass = class_isMetaClass(class);
    
    // 获取实例大小
    size_t size = class_getInstanceSize([p class]);
    
    //设置对象的类(将实例替换成另一个类)
    Class otherClass = object_setClass(p, [NSString class]);
    object_setClass(p, [Person class]);
    
    //获取对象的类名
    const char *c = object_getClassName(p);
    NSString *className = [[NSString alloc] initWithUTF8String:c];
    
    NSLog(@"class = %@ , className = %@",p,className);
    
    // 创建一个新类
    Class cls = objc_allocateClassPair([NSObject class], "newPerson",0);
    //动态添加属性、方法等
    //......
    //注册这个新类 给创建的新类添加属性 方法 协议 成员变量等都需要在注册之前完成
    objc_registerClassPair(cls);
}

#pragma mark - -----成员变量Ivar相关的方法-----
- (void)runtime_IvarMethod{
    
    Person *p = [Person new];
    p.name = @"Dely";
    p.age = 20;
    
    unsigned int count = 0;
    //获取成员变量列表 获取类对应的实例变量的Ivar指针以及个数
    Ivar *ivars = class_copyIvarList([p class], &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        
        //获取成员变量名
        const void *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        
        //获取Ivar的类型编码,
        const char *type = ivar_getTypeEncoding(ivar);
        NSString *typeStr = [NSString stringWithUTF8String:type];
        
        //获取对应对象类和实例变量名的Ivar
        Ivar instanceIvar = class_getInstanceVariable([p class], name);
        
        //获取对应类和实例变量名的Ivar
        Ivar classIvar = class_getClassVariable([Person class], name);
        
        //object_getIvar这个方法中,当遇到非objective-c对象时,并直接crash
        //官方解释:The value of the instance variable specified by ivar, or nil if object is nil
        //非objective-c的时候,需要跳过执行。
        
        NSLog(@"成员变量名=%@  类型编码=%@",key,typeStr);
        
        if (![typeStr hasPrefix:@"@"]) {
            //@开头的是对象
            continue;
        }
        
        //获取实例对象中Ivar的值
        id obj = object_getIvar(p, ivar);
        
        //设置实例对象中Ivar的值
        object_setIvar(p, ivar, @"rename——Dely");
        
        // object_setInstanceVariable不能在ARC的模式下使用
        // object_getInstanceVariable不能在ARC的模式下使用
    }
    free(ivars);
    
    //添加成员变量(添加成员变量只能在运行时创建的类,且不能为元类,定义类是静态创建的类则无法添加)
    //第4个参数是对齐方式_Alignof或者log2sizeof
    //第5个参数是编码类型使用@encode()可获取,并不能完全获取有特例,下面有一个对应表
    BOOL add = class_addIvar([p class], "jiayao", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
    
    NSLog(@"p.name = %@     add = %@",p.name,add?@"成功":@"失败");
}

#pragma mark - -----属性Property相关的方法-----
- (void)runtime_PropertyMethod{
    
    Person *p = [Person new];
    //获取属性
    [self class_copyPropertyList:[p class]];
    
    //特性值(objc_property_attribute_t有value和name)
    objc_property_attribute_t type = { "T", "@\"NSString\"" };
    objc_property_attribute_t ownership = { "&", "" }; // C = copy
    objc_property_attribute_t backingivar  = { "V", "" };
    objc_property_attribute_t attrs[] = { type, ownership ,backingivar};
    

    //添加一个属性
    BOOL add = class_addProperty([p class], "newProperty", attrs, 3);
    
    //替换属性
    class_replaceProperty([p class],"tmpProperty", attrs, 3);
    
//    [self class_copyPropertyList:[p class]];
    
}

- (void)class_copyPropertyList:(Class)class {
    unsigned int count;
    //获取属性列表
    objc_property_t *propertyList = class_copyPropertyList(class,&count);
    
    for (int i = 0; i < count; i++) {
        
        objc_property_t property = propertyList[i];
        // 获取属性名
        NSString *name = [NSString stringWithUTF8String:property_getName(property)];
        
        //获取属性的特性
        NSString *attributes = [NSString stringWithUTF8String:property_getAttributes(property)];
        NSLog(@"name = %@  attributes = %@",name,attributes);
        
        //获取属性特性数组
        unsigned int attCount = 0;
        objc_property_attribute_t *attList = property_copyAttributeList(property,&attCount);
        for (int j = 0; j < attCount; j++) {
            
            objc_property_attribute_t att = attList[j];
            
            NSLog(@"name = %s   value =%s", att.name, att.value);
            
            //获取属性中指定的特性value
            char *a = property_copyAttributeValue(property, att.name);
        }
        free(attList);

    }
    
    free(propertyList);
}

#pragma mark - -----选择器SEL相关的方法-----
- (void)runtime_SelMethod{
    Person *p = [Person new];
    
    //类实例能否响应这个SEL
    BOOL responds = class_respondsToSelector([p class], @selector(eat));
    
    
    SEL sel1 = @selector(eat);
    
    SEL sel2 = @selector(drink);
    
    //SEL转c字符串
    const char *cName = sel_getName(sel1);
    //根据str返回一个SEL变量
    SEL selName = sel_getUid(cName);
    //根据str注册一个SEL变量
    SEL registSel = sel_registerName(cName);
    //判断两个SEL变量是否相等
    BOOL equal = sel_isEqual(sel1, sel2);
    
    Method method = NULL;
    //Method转SEL变量
    SEL sel = method_getName(method);

    
}

#pragma mark - -----Method相关的方法-----
- (void)runtime_MethodMethod{
    
    Person *p = [Person new];
    
    //获取对象方法
    Method instanceMethod1 = class_getInstanceMethod([p class], @selector(eat));
    Method instanceMethod2 = class_getInstanceMethod([p class], @selector(drink));
    
    //获取类方法
    Method classMethod= class_getClassMethod([p class], @selector(eat));
    
    //交换方法
    method_exchangeImplementations(instanceMethod1, instanceMethod2);
    
    //其实在喝水
    [p eat];
    
    //给指定类添加新方法
    // class_addMethod(Class cls, SEL name, IMP imp,const char *types)
    [self addMethod];
    
    // 取代一个方法的实现,返回值是原方法的imp
    //IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)
    [self replaceMethod];
    
    
    //获取方法
    [self getAllMethods];

}

- (void)addMethod{
    /* 必须用performSelector:调用。为甚么呢???
     因为performSelector是运行时系统负责去找方法的,在编译时候不做任何校验;如果直接调用方法编译时就会报错*/
    Person *p = [Person new];
    
    class_addMethod([p class], @selector(testAddMethod), class_getMethodImplementation([self class], @selector(aMethod)), "v@:");
    
    [p performSelector:@selector(testAddMethod)];
    
}

- (void)aMethod{
    NSLog(@"给person动态添加了方法");
}



- (void)replaceMethod{
    Person *p = [Person new];
    class_replaceMethod([p class], @selector(run), class_getMethodImplementation([self class], @selector(rMethod)), "v@:");
    [p eat];
}


- (void)rMethod{
    NSLog(@"替换了person的run方法");
}

- (NSArray *)getAllMethods{
    
    unsigned int methodCount =0;
    Method* methodList = class_copyMethodList([Person class],&methodCount);
    NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount];
    
    for(int i=0;i<methodCount;i++){
        Method temp = methodList[i];
        IMP imp = method_getImplementation(temp);
        SEL name_f = method_getName(temp);
        const char* name_s =sel_getName(method_getName(temp));
        //参数个数
        int arguments = method_getNumberOfArguments(temp);
        
        //类型编码
        const char* encoding =method_getTypeEncoding(temp);
        NSLog(@"方法名:%@,参数个数:%d,编码方式:%@",[NSString stringWithUTF8String:name_s],
              arguments,
              [NSString stringWithUTF8String:encoding]);
        [methodsArray addObject:[NSString stringWithUTF8String:name_s]];
    }
    free(methodList);
    return methodsArray;
}


#pragma mark - -----IMP相关的方法-----
- (void)runtime_IMPMethod{
    Person *p = [Person new];
    
    Method instanceMethod1 = class_getInstanceMethod([p class], @selector(eat));
    Method instanceMethod2 = class_getInstanceMethod([p class], @selector(drink));
    
    SEL sel = method_getName(instanceMethod1);
    
    //Method转IMP指针
    IMP imp = method_getImplementation(instanceMethod1);
    
    
    //设置一个Method的IMP指针 返回之前的IMP指针
    IMP imp1 = method_setImplementation(instanceMethod2,imp);
    imp1(p,sel);

}

#pragma mark - -----关联值相关的方法-----
- (void)runtime_AssociatedMethod{
    
    Person *p = [Person new];
    //给一个对象关联一个指定的key和关联方式
    static NSString *key = @"associatekey";
    objc_setAssociatedObject(p, &key, @"name", OBJC_ASSOCIATION_COPY_NONATOMIC);
    //获取关联值
    id obj = objc_getAssociatedObject(p, &key);
    //移除关联值
    objc_removeAssociatedObjects(p);
}
#pragma mark - -----block相关的方法-----
- (void)runtime_BlockMethod{
    
    //block转IMP
    IMP blockImp = imp_implementationWithBlock( ^(id instance, id param) {
        NSLog(@"instance = %@",instance);
    });
    //IMP转block
    id block = imp_getBlock(blockImp);
    //移出IMP中的block
    BOOL remove = imp_removeBlock(blockImp);
}

#pragma mark - -----Protocol相关的方法-----
- (void)runtime_ProtocolMethod{
    
    unsigned int count = 0;
    //获取协议列表
    Protocol * __unsafe_unretained* protocolList =  objc_copyProtocolList(&count);
    
    
    for (int i = 0; i < count; i++) {
        Protocol *protocol = protocolList[i];
        
        const char * protocolName = protocol_getName(protocol);
        NSLog(@"协议名=%s",protocolName);
        
        //返回指定的协议
        Protocol *tmpProtocol = objc_getProtocol(protocolName);
        
    }
    
    free(protocolList);
    
    // 创建新的协议实例
    Protocol *newProtocol = objc_allocateProtocol("Dely_newProtocol");
    
    // 在运行时中注册新创建的协议
    objc_registerProtocol(newProtocol);
    
    // 为协议添加方法
    protocol_addMethodDescription (newProtocol, @selector(newProtocolMethod), "v@:", YES, YES);
    }

- (void)newProtocolMethod{
    NSLog(@"为协议添加新方法");
}

2.部分常用API汇总

---------------------- 类Class ---------------------- 
//获取对象的类
object_getClass(id _Nullable obj) 

//获取类的父类
class_getSuperclass(Class _Nullable cls) 

//Class是否是一个元类
class_isMetaClass(Class _Nullable cls) 

// 获取实例大小(class也是对象)
class_getInstanceSize(Class _Nullable cls) 

//设置对象的类
object_setClass(id _Nullable obj, Class _Nonnull cls) 

//获取对象的类名
object_getClassName(id _Nullable obj)

//创建一个新类
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) 

//注册这个新类 给创建的新类添加属性 方法 协议 成员变量等都需要在注册之前完成
objc_registerClassPair(Class _Nonnull cls)   


---------------------- 成员变量Ivar ----------------------
//获取成员变量列表 获取类对应的实例变量的Ivar指针以及个数
class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount) 
 
//获取成员变量名
ivar_getName(Ivar _Nonnull v)

//获取Ivar的类型编码
ivar_getTypeEncoding(Ivar _Nonnull v) 

//获取对应对象类和实例变量名的Ivar
class_getInstanceVariable(Class _Nullable cls, const char * _Nonnull name)

//获取对应类和实例变量名的Ivar
class_getClassVariable(Class _Nullable cls, const char * _Nonnull name)

//获取实例对象中Ivar的值
object_getIvar(id _Nullable obj, Ivar _Nonnull ivar)

//设置实例对象中Ivar的值
object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value)

//添加成员变量(添加成员变量只能在运行时创建的类,且不能为元类,定义类是静态创建的类则无法添加)
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,uint8_t alignment, const char * _Nullable types)  


---------------------- 属性Property ----------------------  
//获取属性列表
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)

//获取属性名
property_getName(objc_property_t _Nonnull property)

//获取属性的特性
property_getAttributes(objc_property_t _Nonnull property) 

//获取属性的特性列表
property_copyAttributeList(objc_property_t _Nonnull property,unsigned int * _Nullable outCount)

//获取属性中指定的特性value
property_copyAttributeValue(objc_property_t _Nonnull property, const char * _Nonnull attributeName)


---------------------- 选择器SEL ----------------------  
//类实例能否响应这个SEL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel)

//SEL转c字符串
sel_getName(SEL _Nonnull sel)

//根据str返回一个SEL变量
OBJC_EXPORT SEL _Nonnull sel_getUid(const char * _Nonnull str)

//根据str注册一个SEL变量
sel_registerName(const char * _Nonnull str)

//判断两个SEL变量是否相等
sel_isEqual(SEL _Nonnull lhs, SEL _Nonnull rhs) 

//Method转SEL变量
method_getName(Method _Nonnull m) 


---------------------- Method方法 ----------------------  
//获取对象方法
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

//获取类方法
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

//交换方法
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

//方法列表
class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount) 

//给指定类添加新方法
class_addMethod(Class cls, SEL name, IMP imp,const char *types)

//取代一个方法的实现
class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)


---------------------- IMP指针 ---------------------- 
//Method转IMP指针 
method_getImplementation(Method _Nonnull m)

//设置一个Method的IMP指针 返回之前的IMP指针
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 


---------------------- 关联值 ----------------------  
//给一个对象关联一个指定的key和关联方式
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)    

//获取关联值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

//移除关联值
objc_removeAssociatedObjects(id _Nonnull object)


---------------------- block ----------------------
//block转IMP
imp_implementationWithBlock(id _Nonnull block)

//IMP转block
imp_getBlock(IMP _Nonnull anImp)

//移出IMP中的block
imp_removeBlock(IMP _Nonnull anImp)

---------------------- Protocol ---------------------- 
//获取协议列表
objc_copyProtocolList(unsigned int * _Nullable outCount)

//获取协议名
protocol_getName(Protocol * _Nonnull proto)

//返回指定的协议
objc_getProtocol(const char * _Nonnull name)

//创建新的协议实例
objc_allocateProtocol(const char * _Nonnull name) 
    
//在运行时中注册新创建的协议
objc_registerProtocol(Protocol * _Nonnull proto)
    
//为协议添加方法
protocol_addMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull name,
                              const char * _Nullable types,
                              BOOL isRequiredMethod, BOOL isInstanceMethod)   
                  

注意事项:

  • 凡是调用带有copy的runtime方法,如class_copyIvarList/class_copyPropertyList等等,必须要free释放掉。不然会有内存泄漏
  • objc_registerClassPair注册这个新类 给创建的新类添加属性 方法 协议 成员变量等都需要在注册之前完成,不然是添加不成功的
  • class_addIvar添加成员变量(添加成员变量只能在运行时创建的类,且不能为元类,定义类是静态创建的类则无法添加)

上面一些举例只是让大家跟runtime的api混个脸熟,知道如何填入参数和简单调用,当你下次遇到了你会说:小伙又见到了啊。当然还有很多api没有列出来,你需要你自己可以去头文件中查看,在上面的了解中可能会有一些新的变量(比如:Ivar Method imp)或者参数的Type encodings不是很明白。下面会对这些新东西来讲解一下。

3.isa和class到底是什么鬼?

对象是怎么定义的?

我们oc中的基本上所有的类都是继承于NSOject,也就是说NSObject是根类,他没有父类了,他就是祖先。
我们来看下定义

//NSObject.h定义
@interface NSObject <NSObject> {
   Class isa  OBJC_ISA_AVAILABILITY;
}

//runtime.h定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//runtime源码中定义
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    .......
}


由此可见:

  • objc_object:对象本质上就是结构体对象
  • 所有的对象都包含一个Class类型的isa指针
  • isa实际上是isa_t类型的

Class是怎么定义的呢?

//runtime.h中
typedef struct objc_class *Class;

//runtime源码中
struct objc_class : objc_object {
    Class superclass;
    const char *name;
    uint32_t version;
    uint32_t info;
    uint32_t instance_size;
    struct old_ivar_list *ivars;
    struct old_method_list **methodLists;
    Cache cache;
    struct old_protocol_list *protocols;
    .......
}

由此可见:

  • Class是一个objc_class结构类型的指针
  • objc_class继承objc_object, 也就是说objc_class也是一个对象。即Class本身也是一个对象,也会有一个isa指针

平时用的id类型到底是什么?

//id定义
typedef struct objc_object *id;

由此可见:

  • id是一个objc_object结构类型的指针,也是一个对象,也会有一个isa指针。所有他能够指向任何对象。

objc_object、objc_class、isa、isa_t之间的关系:

objc关系图

我们再来看下runtime.h头文件中objc_class结构体定义

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
  • isa:对象结构体的第一个成员变量是Class类型的变量,该变量定义了对象所属的类,通常称为isa指针。每个实例对象都有个isa指针,他指向对象的类,而Class(也是对象)里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。

  • meteClass(元类):也是一个对象,所以也会有一个isa指针,元类的isa指针指向根元类(root meteClass),根元类的isa指针指向本身,这样形成了一个封闭的内循环。

  • super_class:父类,如果该类已经是最顶层的根类(NSObject),那么这个值为nil

  • name:名称,该类的名字

  • version:类的版本信息,默认为0

  • info:供运行期使用的一些位标识

  • instance_size:该类的实例变量大小

  • ivars:该类的成员变量数组

  • methodLists: 方法列表

  • cache:方法的缓存列表,对象接到一个消息会根据isa指针查找消息对象,这时会在methodLists中遍历,如果cache了,直接调用,就不会再methodLists查找了,从常用的方法调用时就能够提高调用的效率。

  • protocols : 协议链表

isa指针、对象、类、元类、根元类之间的关系:

类的关系图
  • 实现代表继承 虚线代表isa指针指向

  • 每一个对象本质上就是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向类

  • 每一个类本质上也是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。

  • 每一个元类本质上也是一个对象,元类其实是根元类的(root meteClass)的实例。元类通过元类的isa指针指向根元类

  • 根元类isa指针指向本身,形成一个封闭的内循环

  • 根元类的superclass指向根类(NSObject),形成一个封闭的内循环

  • 根类(root class)其实就是NSObject,NSObject是没有超类的,所以根类的superclass指向nil。

  • 每一个Class都一个isa指针指向唯一的meteClass

object_getClass、class、class_getSuperclass:

//runtime源码
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

+ (id)class
{
    return self;
}

Class class_getSuperclass(Class cls)
{
    if (!cls) return nil;
    return cls->superclass;
}

由此可见:

  • [obj class] 是通过isa指针获取到对象的所属类(类对象)
  • object_getClass(obj)获取到对象的isa,可能指向类对象(当obj是类的实例)/元类(当obj是类)/根元类(当obj是元类),
  • class_getSuperclass是获取当前类的父类

举例说明:

  1. 新建一个类Father,继承于NSObject,对象方法-(void)objMethodF、类方法+(void)classMethodF

  2. 新建一个类Son,继承于Father,对象方法-(void)objMethodS、类方法+(void)classMethodS

  3. 实例化一个对象:Son *son = [Son new];

调用过程分析:

  • 调用[son class] : son就会通过isa指针去找到Son的class。
  • 调用[son superclass] : son通过isa指针找到Son的class,在通过super_class,找到Father的class
  • 调用[son objMethodS] : son通过isa找到Son的class,在class的方法列表里面找到objMethodS;
  • 调用[son objMethodF] : son通过isa找到Son的class,发现class里面并没有这个方法,通过class里面的super_class找到Father的class,在里面的方法列表找到了objMethodF;
  • 调用[son classMethodS] : Son(类)通过isa找到metaclass,在metaclass的方法列表里面找到了classMethodS;
  • 调用[son classMethodF] : Son(类)通过isa找到metaclass,在metaclass的方法列表里面找classMethodF,发现metaclass里面并没有这个方法,通过metaclass里面的super_class找到Father的metaclass,在里面的方法列表找到了classMethodF;

4.Ivar是什么?

Ivar:代表类中的成员变量

//runtime.h中
typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}   

由此可见:

  • Ivar其实就是一个指向objc_ivar结构体指针,
  • objc_ivar是一个结构体,它包含了变量名(ivar_name)、变量类型(ivar_type)、基地址偏移字节(ivar_offset)等信息
  • ivar指针地址是根据class结构体的地址加上基地址偏移字节得到的.

疑问:为什么要这样添加一个ivar_offset变量呢?为了健壮的实例变量

原因:当一个类被编译时,实例变量的内存布局就形成了,它表明访问类的实例变量的位置。实例变量依次根据自己所占空间而产生位移,那如果我们类的成员变量跟NSObject里的变量相同有冲突怎么办?如果不处理就会有问题,怎么处理呢?就是添加一个ivar_offset, runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移。

5.SEL是什么?

SEL 也就是我们平时所说的方法选择器,也称作为方法索引或者方法编号。经常通过@selector()或者NSSelectorFromString()返回

//runtime源码中
typedef struct objc_selector *SEL;
  • 每个类的方法都存储在类对象或元类对象中
  • 每个方法都有一个与之对应的SEL类型的对象
  • 根据SEl就可以找到对应的方法地址,进而调用

工作原理简单介绍:

  • 新建Person类,继承于NSObject
  • 新建方法-(void)drink;-(void)eat;
    调用:[p drink];将会编译成objc_msgSend(p, @selecotr(drink));来执行
    一个小区是一个Person类:方法想象成一个门牌号。那方法列表就是[M.Q.N];
    @selecotr(drink)比如是19501
    @selecotr(eat)比如是19502
    那我们[p drink]就会变成objc_msgSend(p, 19501);意思就是你调用19501编号的方法就可以了,开始查找19501编号的方法,怎么查找到呢?通过p的isa指针找到对应的Person类对象。类对象中包含了MethodList,然后查看编号19501的方法有没有,如果没有找到,那么将使用指向父类的指 针找到父类空间结构进行查找编号19501的方法.如果仍然没有找到,就继续往父类的父类一 直找,直到找到为止, 如果到了根类 NSObject 中仍然找不到,将会抛出异常.

说到底SEL就是帮助我们找到方法的中间产物---方法的映射

6.Method是什么?

Method表示类中的某个方法

//runtime源码
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}  

  • 其实Method就是一个指向objc_method结构体指针
  • objc_method存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP(下面会说)

调用过程:
调用方法[p drink];会根据sel找到从methodlist查找到这个方法,而着这个方法就是Method类型,就会变成objc_msgSend(p,drinkMethod);因为drinkMethod包含了name/type/imp信息。进而根据这个信息继续执行。

说到底Method就是帮助我们找到方法的中间产物---方法的信息

7.IMP是什么?

IMP:方法的实现,其实本质上就是一个函数指针

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

上面说到objc_msgSend(p,drinkMethod); drinkMethod 包含了imp信息,也就是包含了函数的入口地址,所有会继续imp指向的函数

  • imp本质就是一个函数指针
  • imp指向的才是我们真正要执行的函数

8.Cache是什么?

//runtime源码
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
  • Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。
    当对象收到消息时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

9.Type Encodings是什么?

在上面的很多Api中会有一个入参数:const char *types
比如:

//给指定类添加新方法
class_addMethod(Class cls, SEL name, IMP imp,const char *types)
   
// 取代一个方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)

Type Encodings: 为了帮助运行时系统,编译器对每个方法的返回和参数类型进行编码成一个字符串,并将字符串与方法选择器相关联的编码方案。在其他情况下也是有用的,通过@encode(type)的编译指令来获取它。
当给定一个类型时,@encode返回这个类型的字符串编码。比如int、指针、结构体等等,事实上只要能够用sizeof()操作参数的类型都可以用于@encode()来获取类型编码。

苹果开发文档中:Type Encodings专门对此作了介绍,也列出了所有的类型编码的映射表。

Objective-C type encodings

type encodings
NSLog(@"int        : %s", @encode(int));      --> i
NSLog(@"float      : %s", @encode(float));  --> f
NSLog(@"float *    : %s", @encode(float*));  --> ^f
NSLog(@"char       : %s", @encode(char));  --> c
NSLog(@"char *     : %s", @encode(char *));  -->  *
NSLog(@"BOOL       : %s", @encode(BOOL));  --> b
NSLog(@"void       : %s", @encode(void));  --> v
NSLog(@"void *     : %s", @encode(void *));  --> ^v

NSLog(@"NSObject * : %s", @encode(NSObject *));  --> @
NSLog(@"NSObject   : %s", @encode(NSObject));  --> #
NSLog(@"[NSObject] : %s", @encode(typeof([NSObject class])));  --> {NSObject=#}
NSLog(@"NSError ** : %s", @encode(typeof(NSError **)));  --> ^@

int intArray[5] = {1, 2, 3, 4, 5};
NSLog(@"int[]      : %s", @encode(typeof(intArray)));  --> [5i]

float floatArray[3] = {0.1f, 0.2f, 0.3f};
NSLog(@"float[]    : %s", @encode(typeof(floatArray)));  --> [3f]

typedef struct _struct {
    short a;
    long long b;
    unsigned long long c;
} Struct;
NSLog(@"struct     : %s", @encode(typeof(Struct)));  --> {_struct=sqQ}

Objective-C method encodings

编译器内部有些关键字无法用 @encode() 返回,这些关键字也有自己的编码

method encoding
  • 已知数据类型:直接通过编译指令@encode()获取类型编码
  • 实例变量的类型编码:Runtime通过ivar_getTypeEncoding 来获取Class每个实例变量的类型编码。
  • 方法的类型编码:为支持消息的转发和动态调用,OC的 的 Type 信息也被以 “返回值 Type + 参数 Types” 的形式组合编码,其中有两个隐含参数self(位于第2位,对象类型@)和_cmd(位于第3位,selector类型:)
- (void)drink; //无参数无返回值===>"v@:"
- (NSString *)getName:(int)index table:(NSArray *):array;//有参数有返回值====>"@@:i@"

当我们使用const char * _Nullable method_getTypeEncoding(Method _Nonnull m);来获取一个方法的type,例如下面:

- (void)viewWillAppear:(BOOL)animated;  -> v20@0:8B16
- (NSArray *)friendsArray; -> "@16@0:8"

怎么理解这些:

  • v 第一个表示方法的返回类型
  • 20 整个方法参数占位的总长度(字节)
  • @ 代表隐含参数self,对象类型-> @
  • : 代表隐含参数_cmd 对象类型-> :
  • @0中的0,代表存储self变量的偏移地址为0字节的地方,对象指针变量占8字节 ,64位机器上对象指针占位8个字节
  • :8中的8,代表存储_cmd变量的偏移地址为8字节的地方,_cmd变量占8字节
  • B16 代表参数是BOOL类型的变量。他的存储位置在偏移16字节的地方,占4个字节(20-16=4)

当然我们可以从method encoding中获取每个参数的type encoding:

char * _Nullable method_copyArgumentType(Method _Nonnull m, unsigned int index);

了解这个Type Encodings对于runtime的很多api也会有进一步的认识,同时NSMethodSignature方法签名中也会有应用到Type Encodings

10.Declared Properties以及objc_property_attribute_t是什么?

Declared Properties

当编译器碰到property声明时,它会生成描述性的带有关于class, category或者protocol的metadata元数据。
你能通过函数来访问这这些元数据,通过class,protocol上的name来查找property。可以通过@encode来获取property的encode string。可以copy property的attributes list(C strings)

class和protocol都有一个properties list的值

Property Type and Functions

Property的定义如下:

typedef struct objc_property *Property;

可以通过class_copyPropertyListprotocol_copyPropertyList来遍历class(包括loaded categories)和protocol的property list

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

Property type string

Property type string

我们可以使用方法property_copyAttributeList来获取属性的特性列表,里面包含了很多objc_property_attribute_t,这就是属性的特性,其实是一个结构体,里面包含了一个name和value:

typedef struct {
    const char * _Nonnull name;           /**< The name of the attribute */
    const char * _Nonnull value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
  • 属性类型 name值:T value:变化
  • 编码类型 name值:C(copy) &(strong) W(weak)空(assign) D(dynamic) W(__weak) value:无
  • 非/原子性 name值:空(atomic) N(Nonatomic) value:无
  • 变量名称 name值:V value:变化

使用property_getAttributes获得的描述是property_copyAttributeList能获取到的所有的name和value的总体描述,如 T@"NSDictionary",C,N,V_tmpDict

苹果官方文档中说了,这个是有顺序的

The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable. Between these, the attributes are specified by the following descriptors, separated by commas:

  • 第一个是属性类型
  • 最后一个是变量名称
  • 中间是其他objc_property_attribute_t的键值对
  • 当我们动态添加一个property以及objc_property_attribute_t时候也要注意这个顺序

我们可以看下苹果官网文档中Property type string中的例子对应特性列表值:

苹果官方文档Declared Properties以及atts例子介绍

结尾

看完这些是不是对开头的方法或者对象有新的认知,因为这是一个系列,所有后面还会有更精彩的内容,runtime对了解oc语言的调用机制有很大帮助、可见很巧妙的解决很多问题。很多优秀的开源库基本上都会用到runtime相关知识点。

有人会问学runtime有啥用?我的答案是:

学runtime不为了装逼,那跟咸鱼有什么区别?

喜欢的可以点个赞^_^

喜欢点个赞啊

相关文章

  • Runtime窥探 (一)| 基本介绍

    前言 我无意间删除了一个微信好友,发现我的王者荣耀段位排名上升了一位,仿佛找到了新大陆,然后我就一直删阿删,终于到...

  • Runtime学习日程

    1、Runtime全方位装逼指南 2、Runtime窥探 (一)| 基本介绍 3、iOS-runtime通篇详解-...

  • Runtime基本介绍

    Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的...

  • 窥探runtime

    一:什么是Runtime? 1.Oc是一门动态性比较强的编程语言,和C/C++等语言有着很大的不同,OC的动态性是...

  • Runtime快速上手(1)

    依照惯例跳过Runtime的介绍,这里只介绍Runtime的基本用法和简单应用例子。 一、获取属性列表 输出结果:...

  • 【iOS重学】Category的底层原理

    写在前面 本文博主将从Category的基本使用和底层原理来窥探一下Runtime下的Category 是如何实现...

  • 自己实现OC的KVO

    Runtime系列文章在这:Runtime介绍---术语介绍Runtime应用--动态添加方法Runtime应用-...

  • RunTime系统学习《一》

    一,RunTime介绍 Runtime基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。 在Runtim...

  • Runtime的应用

    Runtime的基本属性和消息转发机制已经介绍过了,下面来了解一些Runtime提供的api在实际项目中的应用。 ...

  • Runtime梳理(一)消息机制及应用

    Runtime的介绍 Runtime消息的传递和转发 Runtime的应用 1.Runtime的介绍 Object...

网友评论

  • 小小鱼来了:纠正下class_addIvar只能在objc_allocateClassPair之后 objc_registerClassPair之前调用才能返回true
  • AppleIdGX:这个系列写的挺好的,可能是现在涉及runtime的资料太多了,所以才没什么人看吧。
    顶个赞
    Dely:@我帮你打水 谢谢:smile:,对自己学习总结的一个记录,如果别人看了有点收获那就可以了
  • 0668c9156f1e:看来是憋了很久啊
    Dely:@MustangYM 还有5篇待发表:joy:

本文标题:Runtime窥探 (一)| 基本介绍

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