iOS Runtime实用详解(一)

基本概念

  • 了解 C/C++编译
    • C/C++编译就是将C/C++的代码映射到相应的机器码,编译过程包括几个部分分别是编译,汇编和链接(具体可以去看下汇编语言基础知识)。在函数的编译中,C++和C语言的编译方式是不同的,C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现)。 在C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。这个重命名的过程是通过一个特殊的算法来实现的,称为名字编码(Name Mangling)。但函数的调用都是在编译的时候会决定调用哪个函数。
  • Object-C是根据C语言所衍生出来的语言,继承了C语言的特性,是扩充C的面向对象编程语言,它与C有必然的联系,但是在编译的时候又有本质的区别,对于oc方法(函数),属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数调用,这就是我们所了解的运行时系统 (runtime system),这里有几个概念必须需要掌握:
    • 动态特性
      Objective-C 具有相当多的动态特性,表现在三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时才会做一些事情。
      • 动态类型:及运行时再决定对象的类型。这类动态类型在日常应用中非常常见。简单说就是id类型。实际上静态类型因为其固定性和可预知性而使用的非常广泛,静态类型是强类型,而动态类型属于弱类型。运行时决定接受者。
      • 动态绑定 :基于动态类型,在某个实例对象被确定后,其类型就被确定了。该对象的属性和响应的消息也被完全确定,这就是动态绑定。
      • 动态加载 根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,让程序在运行时添加代码块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
    • 消息机制
      • 消息发送是 OC底层的底层操作(引入 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。删除掉一些强制转换语句,可以看到调用方法本质就是发消息,[[NSObject alloc]init]语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。简单点说函数(方法)的调用就发送消息,但是Xcode5.0开始,苹果就不建议开发者直接使用消息机制(5.0后苹果这时引用runtime,runtime对消息机制作了封装,内部就是runtime的底层实现)。但是我们一定要自己实现消息机制发送,怎么发呢?我们在编译设置Build Setting的搜索输入框,输入msg,把Yes设置成No即可发送


        Snip20170728_16.png
     //Person *p  = [[Person alloc] init]; 底层发送是这样
    Person *p  = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    p = objc_msgSend(p, sel_registerName("init"));
    //把p替换下,
    Person *p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
    // p对象发送方法
   objc_msgSend(p, sel_registerName("eat"));

接下来我们去验证下oc底层发送,如何验证请看下面


Snip20170728_18.png

关掉工程 command + S,找到消息验证目录,输入clang -rewrite-objc main.m 生成main.cpp


Snip20170728_19.png

最后打开main.cpp 把main.cpp拖到底部,如图
Snip20170728_21.png

看到的Person跟我们发送的完全一样,其实从这里我们可以发现objc_msgSend的函数调用过程:

第一步:检测这个selector是不是要忽略的;
第二步:检测这个target是不是nil对象。nil对象发送任何一个消息都会被忽略掉
第三步:
    调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,
  如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,
如果仍找不到则继续通过super_class向上查找知道metaclass;
    调用类方法时,首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,
如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体
第四步:如果前三步都找不到方法则进入动态方法解析
消息动态解析具体流程
第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;
第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三步;
第三步:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;
第四步:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。
  • isa指针


    来自网络获取图片.png
  • 消息动态解析


    动态解析流程图(图片来自网络).png
  • runtime
    • runtime 也就是所谓的“运行时”,是一个机制,oc的底层实现,runtime是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数),需要引入< <objc/runtime.h>;
  • runtime的运用
    • 拦截系统自带的方法调用(Method Swizzling黑魔法),利用runtime 将系统的方法实现和我们自定义的方法实现进行交换
    • runtime动态添加方法
    • 动态创建一个类((比如KVO的底层实现))
    • 实现字典的模型和自动转换
    • 实现NSCoding的自动归档和接档
    • ...
    //获取类方法
    class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
     //获取实例方法
    class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
   案例实现:
#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()

@end

@implementation ViewController

+ (void)load {
   
    Method CustomString = class_getClassMethod([NSURL class],@selector(TestURLWithString:));
    Method URLString = class_getClassMethod([NSURL class], @selector(URLWithString:));
    method_exchangeImplementations(URLString, CustomString);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/中文"];
}
@end

#import "NSURL+url.h"
#import <objc/runtime.h>

@implementation NSURL (url)
#pragma mark - 这里运用了runtime
+ (instancetype)TestURLWithString:(NSString *)str
{
    NSURL *url = [NSURL TestURLWithString:str];
    if (!url) {
        NSLog(@"url为空");
    }
    return url;
}
@end
  • runtime动态添加方法
#import "Person.h"
#import <objc/runtime.h>

@implementation Person

//当这个类被调用类一个没有实现的类方法!就会来这里
//+ (BOOL)resolveClassMethod:(SEL)sel
//{
//    
//}

//当这个类被调用类一个没有实现的对象方法!就会来这里
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

    NSLog(@"%@",NSStringFromSelector(sel));
    //动态的添加方法
    if (sel == sel_registerName("eat")) {
        
        /**
         class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, const char *types)
         1.Class 类类型
         2.SEL name 方法编号
         3.IMP implementation的简称,方法的实现,就是一个函数指针,指向一个实现
         4.type 返回类型 command + shift + 0 快速打开文档,搜索class_addMethod里面Parameters types 可以看到一张表,这里v表示无返回值(void)。
         */
        class_addMethod([Person class], sel, (IMP)eat, "v");
    }
    return [super resolveInstanceMethod:sel];
}
//记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数
void eat(id self, SEL _cmd) {
    NSLog(@"哥们调用了%@的%@方法",self,NSStringFromSelector(_cmd));
    NSLog(@"哥们吃了");
}

////记住任何一个函数都有两个参数,一个self,一个cmd,两个隐试参数,带了个参数
//void eat(id self, SEL _cmd,id objc) {
//    
//    NSLog(@"哥们调用了%@的%@方法%@",self,NSStringFromSelector(_cmd),objc);
//
//    
//}

@end

#import "ViewController.h"
#import <objc/runtime.h>
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p  = [[Person alloc] init];
    
    [p performSelector:@selector(eat)];
    
    //带参数的测试 懒加载方法
    //[p performSelector:@selector(eat) withObject:@"测试"];
}
@end
  • runtime动态创建一个类((KVO的底层实现))
// KVO的底层实现原理
// KVO监听的是setter方法
#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"

@interface ViewController ()

@property (nonatomic, strong)Person *p;

@end

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   Person *p = [[Person alloc] init];
   
   //KVO的基本写法
   [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    _p = p;
   
   //自定义 kvo底层
   [p test_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
   NSLog(@"观察到了%@的%@属性的变化了%@",object,keyPath,change);

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
   // KVO监听的是setter方法,
   //一个类的类型只能通过isa指针来看
   /*如果name是成员变量的话,KVO就不能监听了*/
   _p.name = @"test";//简单的说底部实现,就是利用runtime创建了个子类对象 
}
@end

#import "NSObject+KVO.h"
#import <objc/message.h>

@implementation NSObject (KVO)

- (void)test_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
   /**
    1.自定义NSTestKVO子类
    2.重写set方法,在内部恢复父类的做法,通知观察者
    3.修改self的isa指针!!恢复指向自定义的NSTestKVO子类!
    */
   
   //动态生成一个类
   NSString *oldClassName = NSStringFromClass([self class]);
   NSString *newClassName = [@"NSTestKVO" stringByAppendingString:oldClassName];
   const char *newName = [newClassName UTF8String];
   
   /**定义一个类
    
    1.继承那个类
    2.类的名称
    */
   
   Class MyClass = objc_allocateClassPair([self class], newName, 0);
   
   //重写setName方法,这里写死了的
   class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
   
   //注册该类
   objc_registerClassPair(MyClass);
   
   //修改self的isa指针,指向自定义的子类MyClass
   object_setClass(self, MyClass);
   
   //将观察者保存到当前对象
   objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


void setName(id self, SEL _cmd,NSString *newName){
   
   //保存当前类型
   Class class = [self class];

   //改变isa指针
   object_setClass(self, class_getSuperclass(class));
   
   //调用父类的set方法
   objc_msgSend(self, @selector(setName:),newName);
   
   
   //拿出观察者
   id objc = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
   
   //通知观察者, 发送消息,注意设置msg
   objc_msgSend(objc,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,nil,nil);
   
   
   //改回子类类型,否则没法继续监听
   object_setClass(self, class);
}
@end
  • runtime 实现字典的模型和自动转换
    • 案例
{
  "age" : "<null>",
  "money" : "1000",
  "cat" : {
    "name" : "Persia",
    "price" : "500",
    "fish" : {
      "name" : "鱼",
      "weight" : 50
    }
  },

 "students" : [
                  {
                  "name1" : "苦逼的码农",
                  "price1" : 20.8,
                  "publisher1" : "清华大学出版社"
                  }
                 ],
    
  "books" : [
    {
      "name" : "C语言程序设计",
      "price" : 20.8,
      "publisher" : "清华大学出版社",
      "bookUsers" : [
                     {
                   "name1" : "苦逼的码农",
                   "price1" : 20.8,
                   "publisher1" : "清华大学出版社"
                     }
                  ]
 
    },
    {
      "name" : "乔布斯传",
      "price" : 50.2,
      "publisher" : "苹果出版社",
      "bookUsers" : [
                     {
                     "name1" : "乔布斯传",
                     "price1" : 50.2,
                     "publisher1" : "苹果出版社"
                     }
                    ]
    }
  ],
  "name" : "Tom",
  "height" : "181"

}

#import <Foundation/Foundation.h>

@interface NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict;
+ (instancetype )objectWithDict:(NSDictionary *)dict;
// 告诉数组中都是什么类型的模型对象
-(NSString *)arrayObjectClassWithKey:(NSString *)keyword;

@end

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

@implementation NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];
            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
            if (value == nil || [value isEqual:[NSNull class]]) continue;
            
            // 获得成员变量的类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            // 如果属性是对象类型
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // 那么截取对象的名字(比如@"Dog",截取为Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                

                
                // 排除系统的对象类型
                if (![type hasPrefix:@"NS"]) {
                    // 将对象名转换为对象的类型,将新的对象字典转模型(递归)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                    
                }else if ([type isEqualToString:@"NSArray"]) {
                    
                    // 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
                    NSArray *array = (NSArray *)value;
                    
                    NSLog(@"key====:%@",key);
                    
                    NSMutableArray *mArray = [NSMutableArray array];
                    
                    // 获取到每个模型的类型
                    id class ;
                    if ([self respondsToSelector:@selector(arrayObjectClassWithKey:)]) {
                        
                        NSString *classStr = [self arrayObjectClassWithKey:key];
                        class = NSClassFromString(classStr);
                    }
                   
                    else {
                        
                        NSLog(@"数组内模型是未知类型");
                        return;
                    }
                    // 将数组中的所有模型进行字典转模型
                    for (int i = 0; i < array.count; i++) {
                        
                        
                        [mArray addObject:[class objectWithDict:value[i]]];
                    }
                    
                    value = mArray;
                }
            }
            
            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
+ (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}
@end

#import "ViewController.h"
#import "Person.h"
#import "User.h"
#import "NSObject+JSONExtension.h"
#import "Book.h"
#import "BookUsers.h"
#import "Student.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self json];
}

/// 字典转模型demo
- (void)json {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"Model2.json" ofType:nil];
    NSData *jsonData = [NSData dataWithContentsOfFile:path];
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:NULL];
    
    User *user = [User objectWithDict:json];
    Book *book= user.books[0];
    Student *englistBook = user.students[0];

    //NSLog(@"%@",book.name);
}

model 

#import <Foundation/Foundation.h>
#import "Cat.h"

@interface User : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double height;
@property (nonatomic,assign) int age;

// 属性是一个对象
@property (nonatomic,strong) Cat*cat;
// 属性是一个数组
@property (nonatomic,strong) NSArray *books;

// 属性是一个数组
@property (nonatomic,strong) NSArray *students;

@end
#import "User.h"



@implementation User

 //返回数组中都是什么类型的模型对象
//- (NSString *)arrayObjectClass {
//   return @"Book";
//}

- (NSString *)arrayObjectClassWithKey:(NSString *)key {
    if ([key isEqualToString:@"books"]) {
        return @"Book";
    }else
    {
        return @"Student";
    }
}
@end

#cat model
#import <Foundation/Foundation.h>
#import "Fish.h"

@interface Cat : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double price;
// 属性是一个对象
@property (nonatomic,strong) Fish *fish;

@end

#import "Cat.h"

@implementation Cat

@end

#Book model
#import <Foundation/Foundation.h>
#import "Book.h"

@interface Book : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) double price;
@property (nonatomic,copy) NSString *publisher;
@property (nonatomic,strong)NSArray *bookUsers;
@end
#import "Book.h"

@implementation Book
//返回数组中都是什么类型的模型对象
- (NSString *)arrayObjectClass {
    return @"BookUsers";
}
@end

#BookUsers model
#import <Foundation/Foundation.h>

@interface BookUsers : NSObject


@property (nonatomic,copy) NSString *name1;
@property (nonatomic,assign) double price1;
@property (nonatomic,copy) NSString *publisher1;
@end

#import "BookUsers.h"

@implementation BookUsers

@end

本文参考文献

(Runtime基本原理及Demo)http://www.jianshu.com/p/e28f6b279f5f#
([iOS] runtime 的使用场景--实战篇)http://www.jianshu.com/p/07b6c4a40a90

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

推荐阅读更多精彩内容

  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,525评论 33 466
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,125评论 0 9
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,925评论 1 3
  • 几米说:有的路,你必须一个人走,这不是孤独,而是选择。 1 前段时间,好朋友园子失恋了,她大半夜打电话给我,泣不成...
    孙小山阅读 863评论 4 10
  • 常闻稻花香 也想住洋房 奔波劳碌命 弄得还挺忙 与人性本善 当你傻子骗 应作如是观 不必说再见
    郅公子阅读 43评论 0 0