iOS Runtime

作者: Dylan_J | 来源:发表于2019-11-07 18:49 被阅读0次

Objective-C是动态语言让编译链接时期要做的放在运行时进行,具有灵活性,不仅需要编译器还需要运行时系统执行编译代码,来创建类和对象、进行消息的传递和转发。该语言是C语言的扩展,加入了面向对象的特性和Smalltalk的消息传递机制。

Runtime是基于C和汇编写的,高级编程语言需要先编译或解释为汇编语言再汇编成机器语言才能生成可执行文件。

Runtime的工作:

1.封装:对象用结构体表示,方法用c函数实现,结构体和函数被封装后可以在运行时创建、检查、修改对象和他们的方法。
2.找出方法的最终执行代码。

Runtime消息传递

假设一个对象调用自身的一个方法[obj func],编译器会将方法的调用转化成消息发送objc_msg(obj, func)。

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

具体步骤为:

  • 通过obj对象的isa指针找到obj的类;
  • 在类class结构体的方法列表method list中查找func方法;
  • 如果找到了。执行方法的IMP指针存储的实现;
  • 如果当前class内有找到func,则继续寻找该类的super class的方法列表;
  • 最终没有找到方法则抛出方法找不到的异常。

这种实现效率低。为了避免重复遍历方法列表,就是用objc_cache:
在找到func方法后把func的method_name作为key,method_imp作为value存起来,当再次受到func的消息后,就可以直接从cache中获取。

Class类和对象

Object-C的类用Class表示,实质是一个指向objc_class结构体的指针。

类的实质

typedef struct objc_class *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;
//方法列表
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;
} OBJC2_UNAVAILABLE;

由结构体可见,类结构体的第一个成员变量也是isa指针,所以Class本身也是一个对象,称之为类对象,类对象在编译期产生用于创建实例对象,是单例。结构体存放的数据成为元数据(metadata)。

对象

对象的本质

typedef struct objc_object *id;

对象的结构体

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

创建实例的相关信息存储在类对象中的元数据中,而类对象和类方法是从isa指针指向的结构体创建,类对象的isa指针指向元类(metaclass),元类中保存了创建类对象及类方法的所需全部信息。


isa指向.png

通过上图可以了解到:
整个体系构成一个自闭环,实例对象的isa指针指向类对象,类对象的isa指针指向元类,子类元类的isa指向根元类,根元类对象的isa指针指向自己。

方法

方法的实质

typedef struct objc_method *Method;
//方法
struct objc_method {
    SEL method_name                             OBJC2_UNAVAILABLE; // 方法名
    char *method_types                          OBJC2_UNAVAILABLE; // 方法类型
    IMP method_imp                              OBJC2_UNAVAILABLE; // 方法实现
}

从结构体看,说明SEL和IMP其实是Method的属性。

SEL实质

typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的标示类型。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是:

@property SEL selector;

selector是SEL的一个实例。是一个映射到方法的C字符串,可以用Objective-C的@selector()或者Runtime的rel_registerName函数来获取一个SEL类型的方法选择器。

selector命名规则:
同一个类,selector不能重复
不同的类,selector可以重复

此规则限制了函数重载,selector只记了method的name,没有参数,所以没法区分不同的method。

IMP

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

指向最终实现程序的内存地址的指针。

类缓存(objc_cache)

当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

Category(objc_category)

Category是表示一个指向分类的结构体指针:

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};
  • name:是指class_name而不是category_name。
  • cls:要扩展的类对象,编译期间是不会定义的,而是Runtime界面通过name对应到对应的类对象。
  • instanceMethods:category中所有给类添加的实例方法的列表。
  • classMethods:category中所有添加的类方法的列表。
  • protocols:category实现的所有协议的列表。
  • instanceProperties:表示category里面所有的properties,这就是我们通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
    从上面的category_t的结构体可以看出,category中可以添加实例方法、类方法,甚至可以实现协议、添加属性,不可以添加成员变量。

Runtime应用

  • 关联对象(Objective-C Associated Objects)给分类添加属性
  • 方法实现替换(Method Swizzling)方法添加、替换和KVO实现
  • 消息转发解决Bug(JSPatch)
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模型的自动转换

关联对象

分类Category中是不能自定义属性和成员变量的,但是通过关联属性可以实现相应的功能。

成员变量

Ivar作为一个对象中实际存储信息的变量,它实际上是一个指向ivar_t结构体的指针

typedef struct ivar_t *Ivar;
struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t size;
    ...
}

常用方法:

// 为某元类添加成员变量
BOOL class_addIvar(Class cls, const char *name, size_t size, unit8_t alignment, const char *types)
// 获取类的所有成员变量
Ivar * class_copyIvarList(Class cls, unsigned int *outCount);
// 通过变量名称获取类中的成员变量
Ivar class_getInstanceVariable(Class cls, const char *name);
// 获取Ivar的ivar_name
const char *ivar_getName(Ivar v);
// 获取Ivar的ivar_type
const char *ivar_getTypeEncoding(Ivar v);
// 获取Ivar的ivar_offset
ptrdiff_t ivar_getOffset(Ivar v);
// 设置实例对象中Ivar值
void object_setIvar(id obj, Ivar ivar, id value);
// 获取实例对象中Ivar的值
id object_getIvar(id obj, Ivar ivar);
// 关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 获取关联对象
id objc_getAssociatedObject(id object, const void *key)
// 移除关联对象
void objc_removeAssociatedObjects(id object)
  • object:被关联的对象
  • key:关联的key,要求唯一
  • value:关联的对象
  • policy:内存管理的策略

内存管理的策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
}

实现Category添加自定义属性

#import "objc/runtime.h"

@interface Object (Title)

@property (nonatomic, copy) NSString *title;

@end

@implementation Object (Title)

static char kTitleKey;

- (void)setTitle:(NSString *)title {
    objc_setAssociatedObject(self, &kTitleKey, title, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)title {
    return objc_getAssociatedObject(self, &kTitleKey);
}

@end

调用

@interface ViewController ()

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Object *obj = [Object new];
    obj.title = @"Title";
    NSLog(@"%@", obj.title);
}

@end

这样就成功添加了一个属性,实现了它的Setter、Getter方法,通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了。

内存策略 属性修饰 描述
OBJC_ASSOCIATION_ASSIGN @property (assign)或@property (unsafe_unretained) 指定一个关联对象的弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) @progerty (nonatomic, strong)指定一个关联对象的强引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一个关联对象的强引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, strong) 指定一个关联对象的copy引用,能被原子化使用。

方法添加与替换

方法添加

class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

使用

class_addMethod([self class], func, (IMP)funcMethod, "v@:");
  • cls 被添加方法的类
  • name 添加的方法的名称的SEL
  • imp 方法的实现,该函数必须至少要有两个参数,self,_cmd
  • 类型编码

方法替换

实现替换ViewController的viewDidLoad方法


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(mViewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 判断方法名 方法是否已经存在
        BOOL didAddMethod = class_addMethod(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)mViewDidLoad {
    NSLog(@"替换的方法");
    
    [self mViewDidLoad];
}

- (void)viewDidLoad {
    NSLog(@"自带的方法");
    
    [super viewDidLoad];
}

swizzling应该只在+load中完成。在Objective-C的运行时中,每个类有两个方法都会自动调用。+load在在一个类被初始装在时调用,+initialize是在应用第一次调用该类的类方法或者实例方法前调用的。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。
swizzling应该只在dispatch_once中完成,由于swizzling改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是不同的线程中也能确保代码只执行一次。Grand Central Dispatch的dispatch_once满足了所需要的需求,并且应该被当做使用swizzling的初始化单例方法的标准。

消息转发

JSPatch是一个iOS动态更新框架,只需要在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复bug。

消息转发机制分为三个级别,可以在每级实现替换功能,实现消息转发,从而不会造成崩溃。JSPatch不仅能够实现消息转发,还可以实现方法添加、替换等一系列功能。

实现NSCoding的自动归档和自动解档

原理:用Runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
核心方法:在Model的基类中重写方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:Key];
    }
}

实现字典和模型的自动转换

原理:用runtime提供的函数遍历model自身所有属性,如果属性在json中有对象的值,则将其赋值。

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        NSMutableArray *keys = [NSMutableArray array];
        NSMutableArray *attributes = [NSMutableArray array];
        
        unsigned int outCount;
        objc_property_t *properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            
            NSString *propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        free(properties);
        
        for (NSString *key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}

参考:
https://www.jianshu.com/p/6ebda3cd8052

相关文章

网友评论

    本文标题:iOS Runtime

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