iOS运行时机制之Runtime

简介

Runtime简称运行时机制。Objective-C就是运行时机制。也就是在代码编译后运行时的一些机制,其中最重要的就是Objective-C的消息机制

对比C语言和Objective-C

  • 对于C语言来说,函数的调用在编译时就决定了调用哪个函数。
  • 对于Objective-C语言来说,函数的调用属于动态调用的过程,就是编译的时候并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数调用

实际测试证明:

  • 在编译阶段,Objective-C可以调用任何函数,即使这个函数并没有实现,只要有声明就可以。
  • 在编译阶段,C语言调用未具体实现的函数就会报错

Runtime的本质

(一)发送消息

通过命令clang -write-objc file.m可以查看Objective-C文件对应的编译后的C++文件源码

  • 比如:通过命令clang -write-objc main.m编译文件main.m
  • main.m中的源码,编译前
    #import 
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
        
            NSObject *objc = [NSObject alloc];
            objc = [objc init];
        }
        return 0;
    }
    
  • main.cpp中的源码:编译后
    int main(int argc, const char * argv[]) {
          /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
              NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"),   sel_registerName("alloc"));
              objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc, sel_registerName("init"));
          }
          return 0;
      }
    
  • 在Objective-C中,方法调用的本质就是发送消息,只有对象才能发送消息,也因此方法以objc开头
  • 在Objective-C中使用Runtime需要先导入#import
    • 也可以导入#import ,但是建议导入#import ,因为#import 包含了#import

Tips
当我们在XCode的编译器上第一次使用Runtime的时候,输入objc_msgSend(),没有提示输入参数,即括号中没有任何参数,当我们点进该方法,会看到如下方法:

objc_msgSend(void /* id self, SEL op, ... */ )

该方法有参数,只是参数被注释了,根据注释的参数,我们可以知道该方法需要两个参数

  1. 发送消息的对象,即谁发送
  2. 发送的消息,即发送什么消息

根据通过clang -write-objc main.m命令编译的得到的源码

NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"),   >sel_registerName("alloc"));
objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc, sel_registerName("init"));

我们发现在objc_msgSend前有一个类型强转((NSObject *(*)(id, SEL))(void *),把函数objc_msgSend强转为((NSObject *(*)(id, SEL))(void *)类型。((NSObject *(*)(id, SEL))(void *)类型中的(id, SEL)即函数objc_msgSend的两个参数:

  1. 发送消息的对象
  2. 对象发送的消息

为什么出现没有提示参数的原因,是因为Apple在XCode6之后有意做了限制不希望开发者使用更加底层的Runtime,我们可以打开限制通过以下设置。


iOS运行时机制之Runtime_第1张图片
修改XCode设置,以便使用Runtime

再次输入objc_msgSend,可以得到有提示的Runtime发送消息的方法

objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)

通过Runtime实现创建objc对象

// Runtime和OC发送消息混合写法
NSObject *objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));

完全等同于

、
NSObject *objc = [NSObject alloc];
objc = [objc init];

纯Runtime写法

NSObject *objc = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
objc = objc_msgSend(objc, sel_registerName("init"));

Tips:
我们知道在Objective-C中只有对象才可以发送消息

NSObject *objc = objc_msgSend([NSObject class], @selector(alloc));
objc = objc_msgSend(objc, @selector(init));

以上这段代码中发送消息的是[NSObject class],其实在Objective-C中类都有一个类对象,通过class类方法获取。
类方法保存在元类(meta class)

(二)发送消息的流程

  1. 方法调用的本质是让对象发消息
  2. 对象方法保存在对应的类中,类中有一个保存全部对象方法的方法列表。类方法保存在元类中
  3. 发送消息的时候,对象根据对应的isa指针(isa = (class)Person)去对应的类的方法列表中根据方法编号(sel_registerName("run:dis:")中的run:dis:就是方法编号)找对应的方法名run:dis:
    • Tips:方法编号是一串数字,因为系统操作数字比操作字符串更快,所以需要通过方法编号找到方法名,而不是直接找方法名
  4. 根据方法名就可以在内存中“程序代码区”找到对应的函数实现
    • Tips:所有的OC方法都会转换成函数实现。所以方法调用的流程最后一步是通过方法名在程序代码去区找到对应的函数实现

内存分为5大区:

  1. 栈区
  2. 堆区:
    • 堆是向高地址扩展的数据结构,不连续的内存区域。

    • 系统用链表储存空闲地址的。链表遍历由低向高。

    • 堆大小直接受设备有效虚拟内存影响。

      1. 首先应该知道操作系统有一个记录空闲内存地址的链表。
      2. 当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
      3. 由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中
    • 使用:存放实体对象的。由程序员分配和释放(arc自动插入分配和释放代码),例如alloc 申请的会放入堆中。

  3. 全局\静态区(static)
    • 静态变量和全局变量是存储在一起的。
    • 初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域
    • 程序结束后有系统释放。
int a;//未初始化的静态区
int b = 10;//初始化的静态区
  1. 文字常量区
    • 存放常量字符串,程序结束系统释放
  2. 程序代码区
    • 存放函数实现的二进制代码

Runtime的使用场景

(一)封装框架
(二)调用私有方法 (通过Objc调用私有方法会报错,通过Runtime调用私有方法不会报错)
(三)交换方法
通过Runtime用自己交换系统方法的实现和自己实现的方法的实现
比如:我们想要给系统类UIImage的类方法imageNamed新增一个功能在加载图片的时候打印“加载成功”或“加载失败”。

我们先在分类中实现带有加载图片提示的加载图片方法

+ (UIImage *)sf_imageNamed:(NSString *)name {
    
    UIImage *image = [UIImage sf_imageNamed:name];
    
    if (image) {
        NSLog(@"加载成功");
    } else {
        NSLog(@"加载失败");
    }
    
    return image;
}

接下来需要交换系统类方法imageNamed和自己定义的类方法sf_imageNamed的实现
考虑到我们需要在类加载进内存前让系统类方法的实现就替换为自己实现的类方法的实现,我们只能在load方法中实现交换方法(load方法在类加载进内存前调用)

+ (void)load {
    
    // 获取系统类方法
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    
    // 获取自己实现的类方法
    Method sf_imageNamedMethod = class_getClassMethod(self, @selector(sf_imageNamed:));
    
    // 交换方法
    method_exchangeImplementations(imageNamedMethod, sf_imageNamedMethod);
}

Tips

  • 自己实现的类方法sf_imageNamed中需要调用sf_imageNamedsf_imageNamed的实现在交换方法后已经是系统类方法imageNamed在交换之前的实现,这样才不会发生循环调用。

(三)动态添加方法
为什么需要动态添加方法,在Objc中方法都是懒加载的,如果在类中实现方法,当类加载进内存时,就会把方法加载进内存中,这时内存中就会有一个方法列表。

但是某些时候某些方法可能很少用,甚至可能不用。比如:某些App有会员机制,会员机制相关的方法如果不是会员永远都不会加载。

所以通过Runtime可以动态添加一些方法,只要不用到这个方法,该方法永远都不会加载进内存中。

通过实现resolveInstanceMethod类方法就可以实现动态加载方法
resolveInstanceMethod专门用来处理未实现的方法,在调用了一个未实现的方法时会调用resolveInstanceMethod方法

[p performSelector:@selector(aaaa)];
[p performSelector:@selector(bbbb:) withObject:@10];
/*
 * 处理未实现的对象方法
 * 调用了一个未实现的对象方法时就会执行resolveInstanceMethod
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    /*
     * 动态添加play方法
     */
    if (sel == NSSelectorFromString(@"aaaa")) {
        /*
         * 参数一:给哪个类添加方法
         * 参数二:添加的方法
         * 参数三:添加方法的实现(函数的入口:函数名)
         * 参数四:
            > "v": 返回值(void)
            > "@": 第一个参数类型(An object (whether statically typed or typed id))
            > ":": 第二个参数类型(A method selector (SEL))
         */
        class_addMethod(self, sel, aaaa, "v@:");
    } else if (sel == NSSelectorFromString(@"bbbb:")) {
        /*
         "v@:@"
            > v: 方法的返回值为void
            > @: 第一个参数的类型为一个对象self
            > :: 第二个参数的类型为一个方法选择器
            > @: 第三个参数的类型为一个对象NSNumber *metre
         */
        class_addMethod(self, @selector(bbbb:), bbbb, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}
/*
 * self:方法调用者
 * _cmd:当前方法编号
 * 任何一个方法都能调用self,_cmd,任何一个方法也都有这两个参数,隐士参数
 */
void aaaa(id self, SEL _cmd) {
    NSLog(@"打篮球!!!");
}
void bbbb(id self, SEL _cmd, NSNumber *metre) {
    NSLog(@"打篮球,进了%@球", metre);
}

(四)动态添加属性
在iOS开发中,我们可能会遇到需要给系统类添加一个属性的问题,一提到给一个类添加属性,我们就会想到OC中的分类(在Swift中扩展目前无法扩展一个类的属性),原则上分类可以添加属性,但是分类中添加的属性就真的只有属性,不会自动生成属性对应的成员变量、set和get方法。当然你说说我可以自己添加成员变量、set和get方法嘛,但是不好意思,OC语法不允许在分类中添加成员变量。所有即使你自己实现了属性对应的set、get方法,在set方法中你也无法拿到属性对应的加"_"的成员变量。

#import 

@interface NSObject (SFProperty)

@property (nonatomic, strong) NSString *name;

@end

#import "NSObject+SFProperty.h"

@implementation NSObject (SFProperty)

- (void)setName:(NSString *)name {
    self.name = ??(没有_name成员变量)
}

- (NSString *)name {
    return ??(没有_name成员变量)
}

@end

当然我们可以通过添加一个“中介”静态变量来来实现

#import 

@interface NSObject (SFProperty)

// 作为“中介”的静态变量
@property (nonatomic, strong) NSString *name;

@end

#import "NSObject+SFProperty.h"

static NSString *testName;

@implementation NSObject (SFProperty)

- (void)setName:(NSString *)name {
    self.name = testName;
}

- (NSString *)name {
    return testName;
}

@end

但是我们知道正常给一个类添加的属性,当类从内存中“销毁”时,类对应的属性占用的内存也会被释放,但是静态静态需要等到程序运行结束后才会被释放。

说了这么多,就是要引出接下来的主角Runtime,Runtime让我们对一个类,哪怕是系统类完美添加一个属性也是可以实现的。
本来在分类中完美添加一个属性的障碍是无法在分类中添加一个成员变量,导致我们无法实现set、get方法。我们有了Runtime,就不再需要成员变量,直接在分类中通过Runtime实现分类中添加的属性的set、get方法中。

#import 

@interface NSObject (SFProperty)

// 原则上分类中不能添加(OC语法)

// 注意:如果category新增property,category中的property只会生成属性name的set,get方法的声明,并不会生成set,get方法的实现和下划线成员属性(_name)
@property (nonatomic, strong) NSString *name;

@end
#import "NSObject+SFProperty.h"

#import 

@implementation NSObject (SFProperty)

- (void)setName:(NSString *)name {
    
    /*
     第一个参数:需要添加属性的对象
     第二个参数:添加的属性
     第三个参数:添加的属性保存的值
     第四个参数:保存属性的策略(weak、strong、retain、assign...)
     */
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    /*
     第一个参数:取出属性的对象
     第二个参数:需要取出的属性的名称
     */
    return objc_getAssociatedObject(self, @"name");
}

@end
NSString *name = [NSString stringWithFormat:@"hello world"];
NSObject *objc = [[NSObject alloc] init];
objc.name = name;
NSLog(@"objc.name = %@", objc.name);

(五)Runtime实现字典转模型
在iOS开发中,任何一个App不可避免都要从网络请求数据,请求回来的数据大部分都是json的格式,我们会在本地创建一个model,在model中定义一次网络请求中返回的数据和key同名的属性。
我们会json数据进行操作,把json数据转成App能认识的数据类型(Dictionary/NSDictionary、Array/NSArray、NSData/Data)等等。
我们还需要对这些数据进行进一步的操作,把数据存储在我们定义的model中,这个时候系统提供给我们的存储方式就是通过KVC的键值存储:setValuesForKeysWithDictionary实现。
这种方式有两个缺陷:
(1)如果只需要用到返回数据中的一部分数据,那么我们需要重写- (void)setValue:(id)value forUndefinedKey:(NSString *)key;方法。
(2)每个模型都要重复写大量数据转换的代码。作为一个“懒”程序员,不能忍!!!

换种思路
我们知道KVC的本质是根据字典中的key到模型中找到和key同名的set方法
如果找到,直接调用set方法给属性赋值,如果找不到会再去找模型中和字典中的key同名的加“”的成员变量
如果找到,直接给加“
”的成员变量赋值,如果找不到,会再去找模型中和字典中的key同名的属性
如果找到,直接给属性赋值,如果找不到,会直接调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key方法报错

根据KVC的本质的启发,因为在大多数情况下,我们只会用到返回数据中的一部分,我们只需要定义一部分属性就够用。所以KVC根据字典中的全部KVC到模型中找到对应的属性,如果找不到就会调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key这部分找不到的属性所做的工作就属于多余的工作。
所以我们的思路可以是根据模型中的属性去返回的数据中找和属性同名的key存储的value值,如果找到就把值赋值给属性。这样就可以避免上述多余的工作。

在开发中所有的模型都会继承自NSObject,所以我们只需要在NSObject中封装模型转换的方法,实现以上的思路。就可让所有的模型都具备自动通过模型属性名到返回数据的字典中查找属性同名的key保存的value值

#import 

@interface NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end
#import "NSObject+Model.h"

#import 

/*
 MJExtension的实现思路:
    > KVC实现字典转模型的思路是根据字典中的key到model中找对应的属性,并把key值对应的value赋值给对应的属性,如果找不到就调用setValue: forUndefinedKey:报错。
    > 在实际开发中,我们一般只需要用到字典中的一部分属性
    > 所以我们可以逆向思路,根据模型中的属性去字典中找对应的key值保存的value给属性赋值
 
 获取类中的所有方法:通过这种方式找到一个类中(一般是系统类)的私有方法,研究系统底层
    class_copyMethodList(<#Class  _Nullable __unsafe_unretained cls#>, <#unsigned int * _Nullable outCount#>)
 
 获取一个类中的所有属性:
    class_copyPropertyList(<#Class  _Nullable __unsafe_unretained cls#>, <#unsigned int * _Nullable outCount#>)
 
 获取类中的所有成员变量
    class_copyIvarList(<#Class  _Nullable __unsafe_unretained cls#>, <#unsigned int * _Nullable outCount#>)
 
 为什么获取模型中所有key时使用class_copyIvarList,而不使用class_copyPropertyList?
    class_copyPropertyList:获取属性
    class_copyIvarList:获取成员变量
    > 用@property修饰的属性都会生成下划线开头的成员变量
    > 但是定义了成员变量,不一定定义了属性
    > 所有获取模型中所有key时,获取成员变量可以保证不漏掉任何一个key
 
 通过Runtime实现字典转模型的步骤
 1. 获取模型中所有的成员变量
 2. 根据模型中成员变量的key,去字典中找对应key保存的value
 3. 根据key和value给模型对象设置值
 */

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict {
    
    
    id objc = [[self alloc] init];
    
    /*
     第一个参数:获取那个类的成员变量
     第二个参数:记录成员变量个数的变量(count)
     返回值:保存全部成员变量的数组
     */
    unsigned int count;  // 定义一个count,传入count的地址&count,count会在方法执行后记录通过Runtime获取到的成员变量的数量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍历全部成员变量
    for (int i = 0; i < count; i++) {
        // 获取数组中的成员变量
        Ivar ivar = ivarList[i];
        // 获取成员变量的名字,并转成OC对象
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 获取模型的类名
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        // 由于获取到的成员变量前有一个下划线,去掉“_”
        NSString *key = [ivarName substringFromIndex:1];
        // 获取字典中key对应的value
        id value = dict[key];
        
        // 如果value是字典,需要进行二级转换
        // 如果模型中的key为NSDictionary,理解成不需要转换(如果是模型类才转换),做进一步判断
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            // 根据类名字典串转成类对象
            Class modelClass = NSClassFromString(ivarType);
        
            // 字典转模型: 把二级字典转换为二级模型
            value = [modelClass modelWithDict:value];
        }
        
        // 通过获取到的key和value给模型设置值
        [objc setValue:value forKey:key];
    }
    
    return objc;
}

@end

接下来所有的模型都可以通过调用modelWithDict实现字典转模型

SFPerson *person = [SFPerson modelWithDict:dict];

你可能感兴趣的:(iOS运行时机制之Runtime)