(三)iOS--Runtime原理、应用浅谈

今天来总结运行时(Runtime)的相关知识。有的人总是说runtime我没用过啊,那为什么面试的时候经常要问。其实在项目里你肯定用到了,只是你没注意。看了好多博客文章,都是先说的原理,但是原理好多人一看一大堆直接就不想看了,今天我们先来看看runtime的有什么用。它对我们的开发又有哪些帮助呢。怎么用它

  • 关联对象(Objective-C Associated Objects)给分类增加属性
  • Method Swizzling方法添加和替换 KVO实现
  • 消息转发(热更新)解决Bug
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模形的自动转换
一、Runtime的使用
关联对象添加属性
//关联对象
//objc 被关联的对象  
//key 关联的key 要求唯一 
//value关联的对象  
//policy 内存管理的策略
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)

上面说了方法和参数,下面来使用

#import <UIKit/UIKit.h>
#import <objc/runtime.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIView (DefaultColor)

@property (nonatomic, strong) UIColor *defaultColor;

@end

NS_ASSUME_NONNULL_END

@implementation UIView (DefaultColor)
//@dynamic defaultColor;

static char kDefaultColorKey;

-(void)setDefaultColor:(UIColor *)defaultColor{
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIColor *)defaultColor{
    return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end

在set方法中关联属性,get方法中获取关联的对象。为了验证是否添加成功,下来调用一下

UIView *firstView=[UIView new];
    firstView.defaultColor=[UIColor redColor];
    NSLog(@"默认颜色是:%@",firstView.defaultColor);

RuntimeDemo[33582:12690582] 默认颜色是:UIExtendedSRGBColorSpace 1 0 0 1

成功的在分类上添加了一个属性,通过关联对象实现的内存管理是由ARC管理的,所以只需要内定合适的内存策略,就不用担心对象的释放。

二 动态方法交换
//cls 获取方法的类
//name 方法的名称SEL
//获取类方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//获取实例对象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交换两个方法的实现
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
1.动态方法交换

下面来看看具体的实现过程

    [self testA];
    [self testB];
}

-(void)testA{
    NSLog(@"测试A");
}
-(void)testB{
    NSLog(@"测试B");
}

-(IBAction)btnclick:(UIButton *)sender{
    
    Method test1=class_getInstanceMethod([self class], @selector(testA));
    Method test2=class_getInstanceMethod([self class], @selector(testB));
    method_exchangeImplementations(test1, test2);
    [self testA];
    [self testB];


2021-03-15 15:01:31.348613+0800 RuntimeDemo[33614:12699375] 测试A
2021-03-15 15:01:31.348648+0800 RuntimeDemo[33614:12699375] 测试B
2021-03-15 15:01:46.393346+0800 RuntimeDemo[33614:12699375] 测试B
2021-03-15 15:01:46.393598+0800 RuntimeDemo[33614:12699375] 测试A    

在最初调用的顺序来看,是测试A--测试B,之后点击按钮完成方法的交换,最后打印出测试B--测试A,交换成功。
上面是一个简单的方法交换,那么对于系统的方法又是怎么去替换的呢?

拦截并替换系统方法
#import "UIFont+Test.h"
#import <objc/runtime.h>

@implementation UIFont (Test)


+(UIFont *)test_systemFontOfSize:(CGFloat)fontSize{
    
    //获取设备屏幕宽度,并计算出比例scale
        CGFloat width = [[UIScreen mainScreen] bounds].size.width;
        CGFloat scale  = width/375.0;
        //注意:由于方法交换,系统的方法名已变成了自定义的方法名,所以这里使用了
        //自定义的方法名来获取UIFont
        return [UIFont test_systemFontOfSize:fontSize * scale];
}

+(void)load{
    
    Method method1=class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
    Method method2=class_getClassMethod([UIFont class], @selector(test_systemFontOfSize:));
    method_exchangeImplementations(method1, method2);
    
}

创建了UIFont的一个分类,并且拦截替换了系统的systemFontOfSize方法。当在调用systemFontOfSize时,可以看到效果是test_systemFontOfSize。
当然也可以拦截替换viewDidLoad。

kvo的实现我们放到后面仔细的说一说原理
实现NSCoding的自动归档和解档

归档过程中model中有超级多的属性是时一个一个处理起来很麻烦,这个时候就可以使用Runtim来改进他们

//归档
-(void)encodeWithCoder:(NSCoder *)coder{
    
    unsigned int count=0;
    Ivar *varlist=class_copyIvarList([self class], &count);
    for (NSInteger i=0; i<count; i++) {
        Ivar ivar=varlist[i];
        NSString *key=[NSString stringWithUTF8String:ivar_getName(ivar)];
        id value=[self valueForKey:key];
        [coder encodeObject:value forKey:key];
    }
    free(varlist);
}

-(instancetype)initWithCoder:(NSCoder *)coder{
    
    self=[super init];
    if (self) {
        unsigned int count=0;
        Ivar *varlist=class_copyIvarList([self class], &count);
        for (NSInteger i=0; i<count; i++) {
            Ivar ivar=varlist[i];
            const char *name = ivar_getName(ivar);
            NSString *key=[NSString stringWithUTF8String:name];
            id value = [coder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(varlist);
    }
    return self;
    
}

通过Runtime我们拿到类的属性列表,遍历列表来归档和解档。

 StudetModel *stModel=[[StudetModel alloc]init];
    stModel.name=@"小李子";
    stModel.age=@"18";
    stModel.number=@"9527";
    stModel.score=@"598";
    NSString *temp=NSTemporaryDirectory();
    NSString *file=[temp stringByAppendingString:@"student.data"];
    [NSKeyedArchiver archiveRootObject:stModel toFile:file];
    
    StudetModel *model=[NSKeyedUnarchiver unarchiveObjectWithFile:file];
    NSLog(@"person-name:%@,person-age:%@",model.name,model.age);

打印出:2021-03-15 16:43:38.446580+0800 RuntimeDemo[33700:12732235] person-name:小李子,person-age:18
实现字典和模型的自动转换(MJExtension)
- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        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];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);

        //(2)根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;

}

以上就是runtime的一些简单应用场景

Runtime消息传递原理

先来看看

 PeopleOBJC *people=[[PeopleOBJC alloc]init];
    [people testRuntime:@"888"];

上面这段代码它的实现原理是什么呢?
个人把它分为了3个阶段

定位方法

在这之前先要明白2个概念
1.Class
class被定义为指向objc_class的指针

struct objc_class {
    Class _Nonnull isa  //指向元类

#if !__OBJC2__
    Class _Nullable super_class                              //父类
    const char * _Nonnull name                               //类名
    long version                                            //类的版本信息
    long info                                                //类信息
    long instance_size                                       //该类的大小
    struct objc_ivar_list * _Nullable ivars                  //类的成员变量链表
    struct objc_method_list * _Nullable * _Nullable methodLists                    //方法定义链表
    struct objc_cache * _Nonnull cache                     //方法缓存
    struct objc_protocol_list * _Nullable protocols         //协议链表
#endif
} OBJC2_UNAVAILABLE;

isa指向元类,后面在详细的说。可以看到,一个勒种保存了所有的成员变量(ivar)、所有的方法(method_list)、所有实现的协议(protocol_list).cache 我们后面在说。下面来看对象的定义

struct objc_object {
    Class isa;
};

typedef struct objc_object *id;

这里id被定义指向了objc_object的指针,说明objc_object就是我们常用对象的定义。一个对象唯一保存的信息是它的class的地址。当我们调用一个对象的方法时,它会通过对象的isa找到对应的objc_class。
2.元类
在上面我们看了objc_class,我们把成员变量 方法列表统统去掉,发现没,是不是和objc_object。这说明objc_class不仅是个类,它同时也是一个对象
那么问题来了,它指向那个类了?
这时候就引出了Meta Class(元类)。
每一个类都有对应的元类,而在元类的methodLists 中,保存了类的方法列表。isa指针指向对应的元类。
那么方法的定位就可以变成
1.对象的isa找到对应的类
2.然后通过类的isa找到了对应的元类
3.在元类的methodLists 中,找到对应的方法。

元类也是objc_class,那它应该也对应一个对象的啊,按这么弄下去,无限循环没个头啊,所以元类的isa指向的是基类的元类。而基类的元类的 isa 指向自己。这样就形成了一个完美的闭环。
然后下面这个比较常见的图片也就能理解了

截屏2021-03-16 下午5.33.59.png

方法的实现
既然找到了方法,我们就来看看方法是怎么实现的。先来看看几个定义

typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}            

Method定义了一个objc_method指针,而在objc_method中定义了SEL和imp。那么问题来了,这两又是什么玩意?
其实在我们平常的项目中绝对见过SEL

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);          // 输出:viewDidLoad

打印出来的是viewDidLoad,这说明SEL它只是保存了一个方法名的字符串。
这是不是也就解释了为什么在同一个类中,不能取相同的名字。即使它们的参数类型不同,也不能行。

2.imp

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

可以看到它是一个函数指针,它就是函数的地址。imp中有两个参数,第一个参数id就是当前对象的地址。第二个参数SEL 就是方法名

这么一看,Method的机构就很明了了。它建立了SEL和IMP的关联,当对一个对象发送消息是,会通过给出的SEL去找到IMP,然后执行

上面调用方法的过程是不是也就清晰了。
1.当想一个对象发送消息时,通过对象isa找到它锁对应的类
2.类的isa指向的元类,在缓存列表中查找方法。
3.若在缓存中找到,调用方法,若没找到,则去MethodList列表中查找。
4.如果在当前类的方法列表中还没找到,则需要到他的父类去查找。
5找到方法以后,通过SEL找到IMP,然后调用方法。
拦截调用
既然是消息的传递肯定会有找不到方法的时候,那找不到方法时,又是怎么处理的呢。先来看看这张图

截屏2021-03-17 上午10.49.14.png

从上图看到,运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,可以提供一个函数实现。如果添加了函数,并且返回yes,运行时系统就会重新启动一次消息发送过程

 [self performSelector:@selector(testbtnclick:)];
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel==@selector(testbtnclick:)) {
        class_addMethod([self class], sel, (IMP)testbtnclickMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void testbtnclickMethod(id obj,SEL _cmd){
    
    NSLog(@"测试动态解析");
}

输出: RuntimeDemo[35397:13336395] 测试动态解析

平常没有实现testbtnclick方法的时候,是不是直接就奔溃了,但是现在我们通过class_addMethod动态的添加testbtnclickMethod函数,并执行这个函数的IMP,打印出结果。
那我不想在这个方法里设置,还有没有其他的方式呢?当然是有的
如果resolve方法返回 YES ,运行时就会移到下一步:forwardingTargetForSelector。
如果目标对象实现了forwardingTargetForSelector:,runtime就会调用这个方法,就有把这个消息转发给其他对象的机会。上代码

#import "PeopleOBJC.h"

@implementation PeopleOBJC
-(void)testRuntime{
    
    NSLog(@"测试runtime数据");
    
}
@end


    [self performSelector:@selector(testRuntime)];
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    return YES;
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector==@selector(testRuntime)) {
        return [PeopleOBJC new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

打印出:RuntimeDemo[35441:13341243] 测试runtime数据

把当前的testRuntime方法转到了PeopleOBJC实现。

零零总总的几天,总结Runtime,方便自己系统化的理解。要是真的一行一行的去看runtime的源码,我肯定看不下来。只能看别人的博客,加上自己的理解,写出来。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容