美文网首页runtime
Runtime基本用法

Runtime基本用法

作者: 猪猪行天下 | 来源:发表于2022-01-10 17:06 被阅读0次

Runtime的一些术语

SEL方法选择器

它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

typedef struct objc_selector *SEL;

我们可以看出它是个映射到方法的 C 字符串,你可以通过 Objc 编译器器命令@selector() 或者 Runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。

id类型

id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

typedef struct objc_object *id; 
struct objc_object { Class isa; };

以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。

isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。
KVO 的实现机理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型。

Method 方法

Method 代表类中某个方法的类型

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

objc_method 存储了方法名,方法类型和方法实现:

(1). 方法名类型为 SEL

(2). 方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型

(3). method_imp 指向了方法的实现,本质是一个函数指针

方法中的隐藏参数

  1. 接收消息的对象(也就是self指向的内容)

  2. 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。

IMP 方法的实现Implementation

IMP在objc.h中的定义是:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
//该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换,这里取反了就是不需要强转。
typedef void (*IMP)(void );
#else
typedef id (*IMP)(id, SEL , ...);
#endif

它就是一个函数指针,指向方法实现函数的起始地址,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。

如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。而一个确定的方法也只有唯一的一组 id 和 SEL 参数。

获取IMP的方式:

(1)方式一:

    IMP myImp = [self  methodForSelector:@selector(myFunc:)];//获取方法实现
    myImp();//执行方法

(2)方式二:

    Method method = class_getInstanceMethod([self class], @selector(myFunc:));
    IMP myImp = method_getImplementation(method);
    myImp();

(3)方式三:

    void (* myImp)(void);
    myImp = [self  methodForSelector:@selector(myFunc:)];
    myImp();//和方式一相同

(4)方式四:

    id (*myImp)(id, SEL, BOOL);
    myImp = (id (*)(id, SEL, BOOL))[self  methodForSelector:@selector(myFunc:)];
//这里会执行myFunc方法,如果没有该方法则转发!
    myImp(self,@selector(forwardInvocation:),YES);

Cache

Cache 定义如下:

typedef struct objc_cache *Cache 
struct objc_cache { 
  unsigned int mask OBJC2_UNAVAILABLE; 
  unsigned int occupied OBJC2_UNAVAILABLE; 
  Method buckets[1] OBJC2_UNAVAILABLE; 
};

Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

NSObject的一些方法

- (BOOL)respondsToSelector:(SEL)aSelector: 检查对象能否响应指定的消息;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol:检查对象是否实现了指定协议类的方法;
- (IMP)methodForSelector:(SEL)aSelector: 返回指定方法实现的地址。

使用runtime首先倒入#import <objc/runtime.h>

获取类的属性列表

-(void)getPropertyList{
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i < count; i++) {
        objc_property_t property = propertyList[i];
        const char *propertyNmae = property_getName(property);
        NSString *propertyString = [NSString stringWithUTF8String:propertyNmae];
      //attributes是objc_property_t的属性。相关符号可参考
        const char *attributes = property_getAttributes(property);
        NSLog(@"------%@\n att - %s",propertyString,attributes);
    }
   free(propertyList);
}

获取方法列表

-(void)getMethodList{
    unsigned int count;
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i=0;i < count; i++) {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        NSString *methodString = NSStringFromSelector(methodName);
        const char *methodType = (char*)method_getTypeEncoding(method);
        NSLog(@"---------%@\n type = %s",methodString,methodType);
    }
   free(methodList);
}

获取ivar列表,及获取带有下划线的全局变量列表

-(void)getIvarList{
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar ivar = ivarList[i];
       const char *type = ivar_getTypeEncoding(ivar);
        NSString *stringType =  [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        if ([stringType hasPrefix:@"@"]) {
            NSLog(@"对象类型");
        }else if ([stringType hasPrefix:@"{"]){
            NSLog(@"结构体类型");
        }
        else{
           NSLog(@"数据类型");
        }
        NSLog(@"type--------%@\n",stringType);
        const char *ivarName = ivar_getName(ivar);
        NSString *ivarString = [NSString stringWithUTF8String:ivarName];
        NSLog(@"--------%@\n",ivarString);//和属性列表一样,只是增加了下划线
    }
   
}

获取协议列表

-(void)protocolList{
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i < count; i++) {
        Protocol *protocol = protocolList[i];
        const char *protocolNmae = protocol_getName(protocol);
        NSString *protocolString = [NSString stringWithUTF8String:protocolNmae];
       
        NSLog(@"----%@",protocolString);
    }
}

实现方法交换

给需要交换方法的类添加一个分类

例如MyClass,设置分类MyClass+Extention,MyClass有方法myClassAmyClassB

-(void)myClassA{
        NSLog(@"我是myClassA");
    }

-(void)myClassB{
        NSLog(@"我是myClassB");
    }

然后在MyClass+Extention中添加load方法,在该方法中交换两个方法的实现:

+ (void)load{
  
    //交换实例方法
    Method myClassa = class_getInstanceMethod([self class], @selector(myClassA));
    Method myClassb = class_getInstanceMethod([self class], @selector(myClassB));
   
    method_exchangeImplementations(myClassa, myClassb);
}

这个时候MyClass实例调用myClassA方法时,则会打印"我是myClassB"

注意
若改成下面这样会是什么结果呢?会造成循环吗?

-(void)myClassA{
    [self myClassA];
        NSLog(@"我是myClassA");
    }

然而没有循环,确打印了两条信息:
"我是myClassA"
"我是myClassB"
这是因为[self myClassA]是指向 -(void)myClassB{NSLog(@"我是myClassB");}这个函数体的!

交换系统方法

给UIViewController添加一个分类,改变viewWillAppear:的实现。
在ViewController类中打印viewWillAppear:方法

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);  
}

在分类中添加load方法:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
       
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(my_viewWillAppear:);
       
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
       
        // 如果交换类方法, 采用如下的方式
        //交换实现
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

#pragma mark - Method Swizzling
- (void)my_viewWillAppear:(BOOL)animated {
    [self my_viewWillAppear:animated];
    NSLog(@"my_viewWillAppear: %@", self);
}

运行发现,先执行分类中的my_viewWillAppear方法,然后执行ViewController中的viewWillAppear方法。
也就是说这两个方法都会执行。
使用GCD的dispatch_once是为了保证不管有多少个线程,代码只会执行一次。测试发现在主线程中load方法只执行一次!

- (void)my_viewWillAppear:(BOOL)animated {
    [self my_viewWillAppear:animated];
    NSLog(@"my_viewWillAppear: %@", self);
}

看起来上面的代码可能导致无限循环,可奇怪的是,它并不会。在swizzling的过程中,my_viewWillAppear:已经被重新指向UIViewController 的原始实现-viewWillAppear:,但是如果我们在这个方法中调用viewWillAppear:则会循环crash。

交换和添加方法完整的实现

给需要交换方法的类添加一个分类。

// isClassMethod 是否是类方法
static void hk_exchangedMethod(SEL originalSelector, SEL swizzledSelector, Class class, BOOL isClassMethod) {
    
    Method originalMethod = isClassMethod ? class_getClassMethod(class, originalSelector) : class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = isClassMethod ? class_getClassMethod(class, swizzledSelector) : class_getInstanceMethod(class, swizzledSelector);
    
    //此处类方法要获取元类对象
    BOOL didAddMethod =
    class_addMethod(isClassMethod ? object_getClass(class):class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        hk_exchangedMethod(@selector(layoutSubviews), @selector(s_layoutSubviews), class,NO);
       
    });
}

- (void)s_layoutSubviews {
    [self s_layoutSubviews];
}

分类动态添加属性

1、属性为对象类型

-(void)setObjc:(NSObject *)objc{
objc_setAssociatedObject(self, @selector(objc), objc, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSObject*)objc{
return objc_getAssociatedObject(self, @selector(objc));
}

2、属性为BOOL类型

-(void)setIsFragment:(BOOL)isFragment{
objc_setAssociatedObject(self, @selector(isFragment), [NSNumber numberWithBool:isFragment], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(BOOL)isFragment{
return [objc_getAssociatedObject(self, @selector(isFragment)) boolValue];
}

3、属性为结构体类型

-(void)setOriginalPoint:(CGPoint)originalPoint{
objc_setAssociatedObject(self, @selector(originalPoint), NSStringFromCGPoint(originalPoint), OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(CGPoint)originalPoint{

return CGPointFromString(objc_getAssociatedObject(self, @selector(originalPoint)));
}

4、属性为block类型

static const void *kCompletionKey = @"kCompletionKey";
-(void)setCompletion:(void (^)(BOOL))completion{
objc_setAssociatedObject(self, kCompletionKey, completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(void (^)(BOOL))completion{
return objc_getAssociatedObject(self, kCompletionKey);
}

相关文章

  • runtime基本用法

    1 当我们动态加载某个类的时候,可以用到runtime,可以便捷的获取到动态加载类的所有信息 (属性,属性名)

  • runtime基本用法

    先导入 runtime 头文件。1、用 runtime 改变变量值; 2、使用 runtime 交换方法; 3、使...

  • Runtime基本用法

    Runtime的一些术语 SEL方法选择器 它是selector在 Objc 中的表示(Swift 中是 Sele...

  • Runtime快速上手(1)

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

  • iOS知识梳理8:万恶的Runtime

    本文中所使用的参考链接:ios开发-Runtime详解ios Runtime几种基本用法简记iOS运行时详解ios...

  • Runtime的几种基本用法

    本方看的 (cocoachina)上一位大老的 Demo消息机制在OOP术语中,消息传递是指一种在对象之间发送和接...

  • 再次用runtime的一次实践

    好久不用,再次使用runtime重写代码。就用高性能添加图片圆角来再一次实践一下runtime的基本用法。runt...

  • iOS-runtime的基本用法

    iOS runtime基本用法 本内容为作者原创, 未经允许, 不得用于商业用途 我的blog 一. 改变实例变量...

  • iOS Runtime 几种基本用法简记

    Runtime 介绍 这不是一遍介绍关于Runtime实现细节的文章,而是怎么利用Objective-C提供的Ru...

  • iOS面试点文章链接

    runtime基础方法、用法、消息转发、super: runtime 完整总结 runloop源码、runloop...

网友评论

    本文标题:Runtime基本用法

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