Runtime简介

Runtim官方文档:
Objective-C Runtime Programming Guide
Objective-C Runtime
runtime开源代码

一、Runtime是什么

Runtime又叫运行时,是一套由C、C++、汇编语言编写的API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。

[receiver message];
// 底层运行时会被编译器转化为
objc_msgSend(receiver,selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector, arg1, arg2, ...)

二、为什么需要Runtime

  1. Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
  2. 因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。
  3. Runtime基本是用C和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。

三、Runtime的作用

  • 可以动态的创建、添加类,修改这个类的属性和方法。
  • 遍历一个类中的所有成员变量,属性,和方法。
  • 用于消息的传递和转发等。

OC在三种层面上与Runtime系统进行交互:

1. 通过Objective-C源代码

只需要编写OC代码,Runtime系统自动在幕后搞定一切,调用方法,编译器会将OC代码转换成运行时代码,在运行时确定数据结构和函数。

2. 通过Foundation框架的NSObject类定义的方法

Cocoa程序中绝大部分类都是NSObject类的子类,所以都继承了NSObject的行为。(NSProxy类是个例外,他是个抽象超类)
一些情况下,NSObject类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 description 方法,该方法返回类内容得字符串表示,该方法主要用来调试程序。NSObject类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject的子类可以重新实现。
还有一些NSObject的方法可以从Runtime系统中获取信息,允许对象进行自我检查。例如:
class方法返回对象的类;
isKindOfClass:isMemberOfClass:方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
respondsToSelector:检查对象是否能响应指定的消息;
conformsToProtocol:检查对象是否实现了指定协议类的方法;
methodForSelector:返回指定方法实现的地址。

3. 通过对Runtime库函数的直接调用

Runtime系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入<objc/Runtime.h>头文件即可。
许多函数可以让你使用纯C代码来实现Objc中同样的功能。除非写一些Objc与其他语言的桥接或是底层的debug工作,你在写Objc代码时一般不会用到这些C语言函数。

四、Runtime的相关术语

1. SEL

它是selector在Objc中的表示(Swift中是Selector类)。selector是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意OC在相同类中不会有命名相同的两个方法。selector对方法名进行包装,以便找到对应的方法实现。他的数据结构是:typedef struct objc_selector *SEL;
我们可以看出它是个映射到方法的C字符串,你可以通过Objc编译器命令@selector() 或者Runtime系统的sel_registerName 函数来获取一个SEL类型的方法选择器。
注意:不同类中相同名字的方法对应的selector是相同的,由于变量的类型不同,所以不会导致他们调用方法实现混乱。

2. id

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

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

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

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

3. Class

typedef struct objc_calss *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 *ivar             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;
    struct objc_cache *NSCache              OBJC2_UNAVAILABLE;
    struct objc_property_list *protocols    OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

从objc_class可以看到,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。
其中objc_ivar_list 和 objc_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;
}

由此可见,我们可以动态修改*methodLists 的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法的信息则由 objc_method 结构体存储。

值得注意的是,objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。

我们所熟悉的类方法,就源自于 Meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

当你发出一个类似 [NSObject alloc] (类方法) 的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(Root Meta Class)的实例。所有元类的 isa 指针最终都指向根元类。

所以当 [NSObject alloc] 这条消息发送给类对象的时候,运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。

最后 objc_class 中还有一个 objc_cache ,缓存,它的作用很重要,后面会提到。

4. 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: 指向了方法的实现,本质是一个函数指针

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

6. IMP

IMP在objc.h中的定义是:

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

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

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

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

7. 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 一样。

8. 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)

注意:
返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针

#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);
    }

打印结果如下:

test[2321:451525] 3
test[2321:451525] name--------T@"NSString",&,N,V_name
test[2321:451525] age--------Ti,N,V_age
test[2321:451525] weight--------Td,N,V_weight

property_getName 用来查找属性的名称,返回 c 字符串。
property_getAttributes 函数挖掘属性的真实名称和 @encode 类型,返回 c 字符串。

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

class_getProperty 和 protocol_getProperty 通过给出属性名在类和协议中获得属性的引用。

五、Runtime消息转发

OC 中的方法调用,编译时候都会转换为 objc_msgSend 函数的调用:

[obj methodName] => objc_msgSend(obj, @selector(methodName))
// 消息接收者:obj
// 消息名称: @selector(methodName)

objc_msgSend 的执行流程可以分为 3 大阶段

  • 消息发送
  • 找不到消息发送方法,就会进入动态方法解析,允许开发者动态创建新方法;
  • 如果动态方法解析没有做任何操作,这时候就开始进入消息转发。

如果这三个阶段都没有搞定,也就是说 objc_msgSend 没找到合适的方法调用,就会报一个很经典的错误:
unrecognized selector sent to instance

关于消息机制的这块的源码,主要是在objc-msg-arm64.sobjc-runtime-new.mm 以及 Core Foundation 的 forwarding 中(这一块不开源)。

1. 消息发送

下面是消息发送的流程:

请添加图片描述

2. 动态方法解析

下面是动态方法解析的流程:

请添加图片描述

我们通过代码去分析一波:

Person.h

@interface Person : NSObject

- (void)test;

@end

Person.m

#import <objc/runtime.h>
@implementation Person

- (void)other {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        // 获取 other 方法信息
        Method method = class_getInstanceMethod(self, @selector(other));
        // 动态添加 test 方法的实现 
        class_addMethod(self, sel, 
            method_getImplementation(method), 
            method_getTypeEncoding(method));
        // 返回 YES 代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

在 main 文件运行:

Person *person = [[Person alloc] init];
[person test];

我们通过打印能看到:

[Person other]

我们通过 resolveInstanceMethod 去动态配置 test,当我们运行 test 实际上调用的是 other 方法。上面分析了实例方法,类方法操作也是一样的,只是使用的方法不一样。这里需要注意的是类方法的动态解析中 class_addMethod 第一个参数传的不是 self 而是 object_getClass(self)

动态解析过后,会重新走“消息发送”的流程,从 receiverClass 的 cache 中查找方法这一步开始执行。

3. 消息转发

下面是动态方法解析的流程:
请添加图片描述

还是拿上述的 Person 类举例子:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        // objc_msgSend([[Student alloc] init], aSelector);
        return [[Student alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这时候在 main 调用 Person 的对象方法 test,实际执行的是 Student 的对象方法 test;
如果 forwardingTargetForSelector 返回值是空的;那么就会继续走 methodSignatureForSelector 方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return nil;
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// NSInvocation 封装了一个方法调用,包括:方法调用者、方法名、方法参数
// 方法调用者:anInvocation.target
// 方法名:anInvocation.selector
// 方法参数:[anInvocation getArgument:NULL atIndex:0];
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 等同与 [anInvocation invokeWithTarget:[[Student alloc] init]];
    anInvocation.target = [[Student alloc] init];
    [anInvocation invoke];
    
}

开发者可以在 forwardInvocation: 方法中自定义任何逻辑,以上方法都有对象方法、类方法。

4. super

super 调用,底层会转换为 objc_msgSendSuper2 函数的调用,接收2个参数:struct objc_super2、SEL。他直接调用获取父类方法。
这里消息接收者还是子类,只是说从父类开始查找方法实现。

struct objc_super2 {
  id receiver; // receiver 是消息接收者
  Class current_class; // current_class 是 receiver 的 Class 对象
} 

六、Runtime 常见应用场景

  • 查看私有成员变量
  • 字典转模型
  • 替换方法实现
  • 给分类增加属性

1. 查看私有成员变量

#import <Foundation/Foundation.h>

@interface Father : NSObject
@property (nonatomic, assign) int age;
@end

#import "Father.h"

@interface Father ()
{
  NSString *_name;
}
- (void)sayHello;
@end

@implementation Father
- (id)init
{
    if (self = [super init]) {
        _name = @"wengzilin";
        [_name copy];
        self.age = 27;
    }
    return self;
}
- (void)dealloc
{
    [_name release];
    _name = nil;
    [super dealloc];
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"name:%@, age:%d", _name, self.age];
}
- (void)sayHello
{
    NSLog(@"%@ says hello to you!", _name);
}
- (void)sayGoodbay
{
    NSLog(@"%@ says goodbya to you!", _name);
}

控制变量

- (void)tryMember
{
    Father *father = [[Father alloc] init];
    NSLog(@"before runtime:%@", [father description]);
    
    unsigned int count = 0;
    Ivar *members = class_copyIvarList([Father class], &count);
    for (int i = 0 ; i < count; i++) {
        Ivar var = members[i];
        const char *memberName = ivar_getName(var);
        const char *memberType = ivar_getTypeEncoding(var);
        NSLog(@"%s----%s", memberName, memberType);
    }
}

结果

 before runtime:name:wengzilin, age:27
 _name----@"NSString"
_age----int

2. 字典转模型

字典转模型重要的两个点:

  • 利用 Runtime 遍历所有的属性或者成员变量;
  • 利用 KVC 设值。

下面简单实现了一个字典转模型的代码,通过 Runtime 遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC 进行赋值操作。调用方式和 MJExtension、YYModel 类似,直接通过模型类调用类方法即可。

- (instancetype)initWithDict:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        unsigned int count = 0;
        objc_property_t *propertys = class_copyPropertyList([self class], &count);
        for (int i = 0; i < count; i++) {
            objc_property_t property = propertys[i];
            //通过 property_getName 函数获得属性的名称
            const char *name = property_getName(property);
            NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
            id value = [dict objectForKey:nameStr];
            [self setValue:value forKey:nameStr];
        }
        free(propertys);
    }
    return self;
}

3. 替换方法实现

替换方法实现常用的两个方法:

// 方法替换
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 方法交换
void method_exchangeImplementations(Method m1, Method m2) 

例子:

// class_replaceMethod 替换成 imp_implementationWithBlock 中的内容
Person *person = [[Person alloc] init];
class_replaceMethod([Person class], @selector(test), imp_implementationWithBlock(^{
    NSlog(@"11");
}), "v");
[person test];
// run 和 test 相互替换
Method runMethod = class_getInstanceMethod([Person class], @selector(run));
Method testMethod = class_getInstanceMethod([Person class], @selector(test));
method_exchangeImplementations(runMethod, testMethod);

4. 给分类增加属性

  1. 在分类的.h文件中声明想要定义的属性
  2. 在分类的.m文件中实现getter和setter方法
  3. 引入runtime头文件,然后在setter方法中用objc_setAssociatedObject关联对象
#import <Foundation/Foundation.h>
@interface NSObject (TempClass)
@property (nonatomic,copy)NSString *name;
@end

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

static void *kName = &kName;
@implementation NSObject (TempClass)

-(void)setName:(NSString *)name
{
    // object:给哪个对象添加属性
    // key:属性名,根据key去获取关联的对象 ,void * == id
    // value:关联的值
    // policy:策略
    objc_setAssociatedObject(self, kName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
    return objc_getAssociatedObject(self, kName);
}
@end

5. 对象自动归档解档

通过 Runtime 可以获取到对象的 Method List、Property List 等,不只可以用来做字典模型转换,还可以做很多工作。
例如:通过 Runtime 实现自动归档和解档,归档和解档通俗来讲就是将数据写入文件和从文件中读取数据,这一块操作在 iOS 中是需要遵循相对应的协议的。
用 Runtime 提供的函数遍历 Model 自身所有属性,并对属性进行 encode 和 decode 操作。

- (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常用API

1. Runtime 关于类的 API

//动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
// 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 
// 销毁一个类
void objc_disposeClassPair(Class cls)
// 获取isa指向的Class
Class object_getClass(id obj)
// 设置isa指向的Class
Class object_setClass(id obj, Class cls)
// 判断一个OC对象是否为Class
BOOL object_isClass(id obj)
// 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
// 获取父类
Class class_getSuperclass(Class cls)

2. Runtime 关于成员变量的 API

// 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
// 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
// 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
// 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
// 获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

3. Runtime 关于属性的 API

// 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
// 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
// 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)
// 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)
// 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

4. Runtime 关于方法的 API

// 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
// 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 
// 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
// 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
// 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
// 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
// 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
// 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

参考文档
iOS Runtime详解
iOS 底层原理|Runtime 详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值