您的当前位置:首页初探Objective-C的runtime机制

初探Objective-C的runtime机制

2024-12-13 来源:哗拓教育

Objective-C的runtime

在既有类中使用关联对象存放自定义数据

相关介绍

有时候需要在对象中存放相关信息。这个时候我们会从对象所属类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能时又某种机制创建出来,而开发中无法令这种机制创建出自己写的子类实例。因此Objective-C提供了一项强大的特性解决此问题,即“关联对象”(Associated Object)

通过关联对象可以给已有类关联许多其他的对象,这些对象通过“键”来区分,在存储关联对象值的时候一并指明“存储策略”(storage policy),用以维护相应的“内存管理语义”

对象关联类型

关联类型 等效的@property属性
OBJC_ASSOCIATION_COPY copy
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic copy
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic retain

我们可以利用下列方法管理关联对象

  • 用给定的键和存储策略为某个对象设置关联对象

    void objc_setAssociatedObject(id object, void *key, id value, objc_AssociatedPolicy ploicy)

  • 更具给定的键从某对象中获取关联的对象值

    id objc_getAssociatedObject(id object, void *key)

  • 移除指定对象的全部关联对象

    void objc_removeAssociatedObjects(id object)

应用举例

开发过程中经常用到UIAlertView类,在用户点击按钮关闭视图的时候,需要用到委托协议 来处理动作。但是当一个视图中有多个UIAlerView视图的时候,这个时候我们就需要在协议方法里面判断UIAlertView的传入参数来确定到底是哪一个UIAlertView了,并且按钮回调的方法的代码和创建UIAlertView的代码并不在一起,这样就造成阅读起来也很不方便。我们像如果能给已知的UIAlertView类添加一个按钮点击回调的block,那么这个问题不就好解决了吗?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"warmth warning" message:@"we are learning object associated of runtime" delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"default", nil];
    void (^associatedBlock)(NSInteger clickedBtn) = ^(NSInteger clickedBtn){
      
        if (clickedBtn == 0)
        {
            NSLog(@"you clicked cancel button");
        }
        else
        {
            NSLog(@"you clicked default button");
        }
    };
    objc_setAssociatedObject(alertView, associatedBlockPorpertyKey, associatedBlock, OBJC_ASSOCIATION_COPY);//绑定属性
    [alertView show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    void (^associatedBlock)(NSInteger clickedBtn) = objc_getAssociatedObject(alertView, associatedBlockPorpertyKey);
    associatedBlock(buttonIndex);
}

上面代码的实质就是为UIAlertView动态的添加了一个associatedBlock属性,在按钮点击的时候,我们先获取这个属性,然后执行associatedBlock()。这样即使有多个UIAlertView,而每个UIAlertView都带有属于自己的associatedBlock属性,因此在协议方法中我们就不用判断UIAlertView的参数来确定到底是那一个UIAlertView,而是直接去除associatedBlock属性执行即可。并且每个UIAlertView的代码和处理按钮的逻辑代码都写在一起,阅读起来也非常的方便。

理解动态绑定和静态绑定

由于Objective-C是C的超级,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”(static binding),也就是说在编译器就能决定运行时所应调用的函数。

void prinHelloWorld()
{
    printf("Hello World");
}

void printGoodBye()
{
    printf("Good Bye");
}

void doSomething(int type)
{
    if (type == 0)
    {
        prinHelloWorld();
    }
    else
    {
        printGoodBye();
    }
}

如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序有printHelloWorld和printGoogBye这两个函数了,于是会直接生成调用这些函数的指令,而函数地址实际上是硬编码在指令中的,如是将上面的代码改写成如下会怎么样呢?

void prinHelloWorld()
{
    printf("Hello World");
}

void printGoodBye()
{
    printf("Good Bye");
}

void doSomething(int type)
{
    void (*func)();
    if (type == 0)
    {
        func = prinHelloWorld;
    }
    else
    {
        func = printGoodBye;
    }
    func();
}

这时就使用到“动态绑定”( dynamic binding)了,因为所有调用函数在运行期才能确定,编译器在这种情况下生成的指令与修改之前不一样。在修改前,if与else语句里面都有函数点用指令,而修改以后只有一个函数调用指令,不过待调用函数地址无法硬编码在指令中,而是在运行期读取出来。

在Objective-C中,如果像某个对象发送消息,那就使用动态绑定机制来决定那个方法。在底层,所有方法都试C语言的普通函数,然而对象收到消息之后究竟调用哪个方法完全取决于运行期环境。甚至可以在程序运行时改变这些特性使得Objective-C称为一门真正的动态语言。

方法调配技术和调试黑盒方法

大家都知道,如果使用nil去初始化一个数组的时候,程序必定会抛出异常

    NSString *str = nil;
    NSArray *arr = @[@"123", str];

但是,上面提到Objective-C对象在接受到消息之后,究竟会调用何种方法需要在运行期才能解析出来。因此我们可以利用runtime机制来交换两个方法的实现,从而达到上述代码不在抛出异常。这里我们需要给NSObject添加一个类目

#import <Foundation/Foundation.h>

@interface NSObject (SafeNS)

@end

@interface NSArray (SafeNS)

@end

#import "NSObject+SafeNS.h"
#import <objc/runtime.h>

@implementation NSObject (SafeNS)

+ (void)swapInstanceSel:(SEL)originalSel withSel:(SEL)swapSel
{
    Method originalMethod = class_getInstanceMethod(self, originalSel);
    Method swapMehod = class_getInstanceMethod(self, swapSel);
    if (originalMethod && swapMehod)
    {
        method_exchangeImplementations(originalMethod, swapMehod);
    }
}

+ (void)swapClassSel:(SEL)originalSel with:(SEL)swapSel
{
    Method oriMethod = class_getClassMethod(self, originalSel);
    Method swapMethod = class_getClassMethod(self, swapSel);
    if (oriMethod && swapMethod)
    {
        method_exchangeImplementations(oriMethod, swapMethod);
    }
}

@end


@implementation NSArray (SafeNS)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        //swap instance methods
        [self swapInstanceSel:@selector(initWithObjects:count:) withSel:@selector(swapInitWithObjects:count:)];
        //swap class method
        [self swapClassSel:@selector(arrayWithObjects:count:) with:@selector(swapArrayWithObjects:count:)];
        [self swapClassSel:@selector(arrayWithObject:) with:@selector(swapArrayWithObject:)];
    });
}

- (instancetype)swapInitWithObjects:(const id [])objects count:(NSUInteger)count
{
    id objTmp[count];
    for (int i = 0; i < count; i++)
    {
        if (!objects[i])//将nil对象转换成NSNull的对象,避免程序跑抛出异常
        {
            objTmp[i] = [NSNull null];
        }
        else
        {
            objTmp[i] = objects[i];
        }
    }
    return [self swapInitWithObjects:objTmp count:count];//看上去程序好想是递归进入是循环,实质是调用了系统的方法
}

+ (instancetype)swapArrayWithObjects:(const id [])objects count:(NSUInteger)count
{
    id objTmp[count];
    NSLog(@"111");
    for (int i = 0; i < count; i++)
    {
        if (!objects[i])
        {
            objTmp[i] = [NSNull null];
        }
        else
        {
            objTmp[i] = objects[i];
        }
    }
    return [[self class] swapArrayWithObjects:objTmp count:count];
}

+ (instancetype)swapArrayWithObject:(id)anObject
{
    if (!anObject)
    {
        anObject = [NSNull null];
    }
    return [[self class] swapArrayWithObject:anObject];
}

@end
显示全文