关于objective-c:ObjectiveC之runtime漫谈

58次阅读

共计 10222 个字符,预计需要花费 26 分钟才能阅读完成。

runtime 简介

因为 objective- c 是一门动静语言,也就是说只有编译器是不够的,还须要一个运行时零碎(runtime system)来执行编译后的代码。这是整个 objective- c 运行框架的一块基石。runtime 简称运行时。其中最次要的就是音讯机制。对于编译期语言,会在编译的时候决定调用哪个函数。对于 OC 的函数,是动静调用的,在编译的时候并不能决定真正调用哪个函数,只有在运行时才会依据函数的名称找到对应的函数来调用。

runtime 的作用

Objc 在三种层面上与 Runtime 零碎进行交互:1.  通过 Objective-C 源代码
   2.  通过 Foundation 框架的 NSObject 类定义的办法
   3.  通过对 Runtime 库函数的间接调用

runtime 源码

苹果和 GNU 各自保护一个开源的 runtime 版本,这两个版本之间都在致力的保持一致。

都是运行时的头文件,其中次要应用的函数定义在 message.h 和 runtime.h 这两个文件中。

通过 Foundation 框架的 NSObject 类定义的办法

Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类是个例外,它是个形象超类)
  • -class办法返回对象的类;
  • -isKindOfClass:-isMemberOfClass: 办法查看对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者以后类的成员变量);
  • -respondsToSelector: 查看对象是否响应指定的音讯;
  • -conformsToProtocol:查看对象是否实现了指定协定类的办法;
  • -methodForSelector: 返回指定办法实现的地址。

通过对 Runtime 库函数的间接调用

Runtime 零碎是具备公共接口的动静共享库。头文件寄存于 /usr/include/objc 目录下,这意味着咱们应用时只须要引入 objc/Runtime.h 头文件即可。

许多函数能够让你应用纯 C 代码来实现 Objc 中同样的性能。除非是写一些 Objc 与其余语言的桥接或是底层的 debug 工作,你在写 Objc 代码时个别不会用到这些 C 语言函数。对于公共接口都有哪些,前面会讲到。我将会参考苹果官网的 API 文档。

Runtime 的术语的数据结构

SEL

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

typedef struct objc_selector *SEL;

咱们能够看出它是个映射到办法的 C 字符串,你能够通过 Objc 编译器器命令@selector() 或者 Runtime 零碎的 sel_registerName 函数来获取一个 SEL 类型的办法选择器。

id

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

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

以上定义,看到 objc_object 构造体蕴含一个 isa 指针,依据 isa 指针就能够找到对象所属的类。

Class

typedef struct objc_class *Class;

Class 其实是指向 objc_class 构造体的指针。objc_class 的数据结构如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

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

} OBJC2_UNAVAILABLE;

objc_class 能够看到,一个运行时类中关联了它的父类指针、类名、成员变量、办法、缓存以及从属的协定。

其中 objc_ivar_listobjc_method_list 别离是成员变量列表和办法列表:

// 成员变量列表
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            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

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 存储了办法名,办法类型和办法实现:

  • 办法名类型为 SEL
  • 办法类型 method_types 是个 char 指针,存储办法的参数类型和返回值类型
  • method_imp 指向了办法的实现,实质是一个函数指针

Ivar

Ivar 是示意成员变量的类型。

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_offset 是基地址偏移字节

IMP

IMP 在 objc.h 中的定义是:

typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发动一个 ObjC 音讯之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个办法的实现。

如果失去了执行某个实例某个办法的入口,咱们就能够绕开消息传递阶段,间接执行办法,这在前面 Cache 中会提到。

你会发现 IMP 指向的办法与 objc_msgSend 函数类型雷同,参数都蕴含 idSEL 类型。每个办法名都对应一个 SEL 类型的办法选择器,而每个实例对象中的 SEL 对应的办法实现必定是惟一的,通过一组 idSEL 参数就能确定惟一的办法实现地址。

而一个确定的办法也只有惟一的一组 idSEL 参数。

Cache

Cache 定义如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

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

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

Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;// 这个更罕用

能够通过class_copyPropertyListprotocol_copyPropertyList 办法获取类和协定中的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;

/** age */
@property (assign, nonatomic) int age;

/** weight */
@property (assign, nonatomic) double weight;

@end

以上是一个 Person 类,有 3 个属性。让咱们用上述办法获取类的运行时属性。

    unsigned int outCount = 0;

    objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

    NSLog(@"%d", outCount);

    for (NSInteger i = 0; i < outCount; i++) {NSString *name = @(property_getName(properties[i]));
        NSString *attributes = @(property_getAttributes(properties[i]));
        NSLog(@"%@--------%@", name, attributes);
    }

音讯

一些 Runtime 术语讲完了,接下来就要说到音讯了。领会苹果官网文档中的 messages aren’t bound to method implementations until Runtime。音讯直到运行时才会与办法实现进行绑定。

这里要分明一点,objc_msgSend 办法看清来如同返回了数据,其实objc_msgSend 从不返回数据,而是你的办法在运行时实现被调用后才会返回数据。上面具体叙述音讯发送的步骤(如下图):

深刻代码了解 instance、class object、metaclass

通过上图能够看出,一个实例对象 `struct objc_object` 的 isa 指针指向它的 `struct objc_class` 类对象,类对象的 isa 指针指向它的元类;`super_class` 指针指向了父类的 ` 类对象 `,而 ` 元类 ` 的 `super_class` 指针指向了父类的 ` 元类 `。

runtime 的利用

发送音讯

办法调用的实质,就是让对象发送音讯。objc\_msgSend, 只有对象能力发送音讯,因而以 objc 结尾。应用音讯机制前提,必须导入 #import <objc/message.h>
音讯机制简略应用:
// 创立 person 对象
    Person *p = [[Person alloc] init];

    // 调用对象办法
    [p eat];

    // 实质:让对象发送音讯
    objc_msgSend(p, @selector(eat));

    // 调用类办法的形式:两种
    // 第一种通过类名调用
    [Person eat];
    // 第二种通过类对象调用
    [[Person class] eat];

    // 用类名调用类办法,底层会主动把类名转换成类对象调用
    // 实质:让类对象发送音讯
    objc_msgSend([Person class], @selector(eat));
咱们能够通过 clang 来查看代码生成的 CPP 代码。例如:clang 将 oc main.m 文件转成 c ++ main\_cpp 文件代码:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_cpp.cpp

替换办法

替换两个办法的实现个别写在类的 load 办法外面,因为 load 办法会在程序运行前加载一次,而 initialize 办法会在类或者子类在 第一次应用的时候调用,当有分类的时候会调用屡次。
@implementation ViewController

- (void)viewDidLoad {[super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // 需要:给 imageNamed 办法提供性能,每次加载图片就判断下图片是否加载胜利。// 步骤一:先搞个分类,定义一个能加载图片并且能打印的办法 + (instancetype)imageWithName:(NSString *)name;
    // 步骤二:替换 imageNamed 和 imageWithName 的实现,就能调用 imageWithName,间接调用 imageWithName 的实现。UIImage *image = [UIImage imageNamed:@"123"];
}

@end

@implementation UIImage (Image)
// 加载分类到内存的时候调用
+ (void)load
{
    // 替换办法

    // 获取 imageWithName 办法地址
    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));

    // 获取 imageWithName 办法地址
    Method imageName = class_getClassMethod(self, @selector(imageNamed:));

    // 替换办法地址,相当于替换实现形式
    method_exchangeImplementations(imageWithName, imageName);
}

// 不能在分类中重写零碎办法 imageNamed,因为会把零碎的性能给笼罩掉,而且分类中不能调用 super.

// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
    // 这里调用 imageWithName,相当于调用 imageName
    UIImage *image = [self imageWithName:name];

    if (image == nil) {NSLog(@"加载空的图片");
    }

    return image;
}

@end

类 / 对象的关联对象

应用形式一:给分类增加属性
@implementation ViewController

- (void)viewDidLoad {[super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 给零碎 NSObject 类动静增加属性 name
    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"小码哥";
    NSLog(@"%@",objc.name);
}

@end


// 定义关联的 key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 依据关联的 key,获取关联的值。return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
    // 第一个参数:给哪个对象增加关联
    // 第二个参数:关联的 key,通过这个 key 获取
    // 第三个参数:关联的 value
    // 第四个参数: 关联的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
应用形式二:给对象增加关联对象。
/**
 *  删除点击
 *  @param recId        购物车 ID
 */
- (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
{UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@""message:@" 确认要删除这个宝贝 "delegate:self cancelButtonTitle:@" 勾销 "otherButtonTitles:@" 确定 ", nil];
    
    // 传递多参数
    objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    alert.tag = [recId intValue];
    [alert show];
}

/**
 *  确定删除操作
 */
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {if (buttonIndex == 1) {NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id");
        NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id");
        NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag];
    }
}

 动静增加办法

简略应用:
@implementation ViewController

- (void)viewDidLoad {[super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    Person *p = [[Person alloc] init];

    // 默认 person,没有实现 eat 办法,能够通过 performSelector 调用,然而会报错。// 动静增加办法就不会报错
    [p performSelector:@selector(eat)];
}

@end


@implementation Person
// void(*)()
// 默认办法都有两个隐式参数,void eat(id self,SEL sel)
{NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的办法,会调用这个办法解决, 并且会把对应的办法列表传过来.
// 刚好能够用来判断,未实现的办法是不是咱们想要动静增加的办法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{if (sel == @selector(eat)) {
        // 动静增加 eat 办法

        // 第一个参数:给哪个类增加办法
        // 第二个参数:增加办法的办法编号
        // 第三个参数:增加办法的函数实现(函数地址)// 第四个参数:函数的类型,(返回值 + 参数类型) v:void @: 对象 ->self : 示意 SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}
@end

字典转模型 KVC 实现

// Ivar: 成员变量 以下划线结尾
// Property: 属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{id objc = [[self alloc] init];
    
    // runtime: 依据模型中属性, 去字典中取出对应的 value 给模型属性赋值
    // 1. 获取模型中所有成员变量 key
    // 获取哪个类的成员变量
    // count: 成员变量个数
    unsigned int count = 0;
    // 获取成员变量数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 获取成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 获取 key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 去字典中查找对应 value
        // key:user  value:NSDictionary
        
        id value = dict[key];
        
        // 二级转换: 判断下 value 是否是字典, 如果是, 字典转换层对应的模型
        // 并且是自定义对象才须要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            // 字典转换成模型 userDict => User 模型
            // 转换成哪个模型

            // 获取类
            Class modelClass = NSClassFromString(ivarType);
            
            value = [modelClass modelWithDict:value];
        }
        
        // 给模型中属性赋值
        if (value) {;}
    }
        
    return objc;
}

+ load 和 + initialize 原理解说

+load 总结

  • load 办法调用在 main 之前,并且不须要咱们初始化,程序启动就会把所有文件加载
  • 主类的调用优先于分类,分类的调动优先于以后类优先于分类
  • 主类和分类的调用程序跟编译程序无关
  • 分类之间加载,也就是平级之前加载取决于编译程序,谁先编译就先加载谁

注意事项

1. 咱们发现。load 的加载比 main 还要早,所以如果咱们再 load 办法外面做了耗时的操作,那么肯定会影响程序的启动工夫,所以在 load 外面肯定不要写耗时的代码。
2. 不要在 load 外面取加载对象,因为咱们再 load 调用的时候基本就不确定咱们的对象是否曾经初始化了,所以不要去做对象的初始化

调用程序延长(category)

分类中的同名办法,源码中是依照逆序加载的,也就是说后编译的分类办法会笼罩后面所有的同名的办法,分类还有一个个性就是,不论把申明写在主类还是分类,只有分类中实现了就能够找到。

+ initialize

+initialize 实质为 objc/_msgSend,如果子类没有实现 initialize 则会去父类查找,如果分类中实现,那么会笼罩主类,和 runtime 音讯转发逻辑一样

initialize 总结

1.initialize 会在类第一次接管到音讯的时候调用
2. 先调用父类的 initialize,而后调用子类。
3.initialize 是通过 objc_msgSend 调用的
4. 如果子类没有实现 initialize,会调用父类的 initialize(父类可能被调用屡次)
5. 如果分类实现了 initialize,会笼罩本类的 initialize 办法

正文完
 0