iOS之RunTime探索与实践

Runtime 概念

Runtime 相关概念

Runtime 实践


Runtime概念

Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
对于C语言,函数的调用在编译的时候会决定调用哪个函数。
对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。
当你用Objective-C创建对象时

[[NSObject alloc]init]; 

底层runtime创建对象的核心代码如下:

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

需要注意的是,使用objc_msgSend() sel_registerName()方法需要导入头文件<objc/message.h>

Runtime相关概念

1.Self

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

两次打印self 因为super为编译器标示符,向super发送的消息被编译成objc_msgSendSuper,但仍以self作为reveiver

2.Selector

在 Objective-C 中 selector 只是一个 C 的数据结构,用于表示一个你想在一个对象上执行的 Objective-C 方法。

typedef struct objc_selector *SEL;
SEL sel = @selector(doSomething) 

Method Message 发送消息

[obj doSomething:prama];

方括号里面就是消息,obj是发送消息目标对象,doSomething是目标对象执行的方法,prama是发送的参数。

Objective-C的消息和C语言的函数调用是不同的。发送了消息之后,obj对象会检查消息发送者,后决定执行或转发消息到另一个目标对象。

下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];

编译运行正常,两行代码都执行-foo。 [NSObject foo]方法查找路线为 NSObject meta class –super-> NSObject class,和第二题知识点很相似。

3.Class

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

Class类的结构体和一个对象的结构体。objc_object只有一个isa pointer指针,当向对象发送消息时,

下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end

编译运行正常,输出ViewController中的self对象。 编译运行正常,调用了-speak方法,由于

id cls = [Sark class];
void *obj = &cls;
obj已经满足了构成一个objc对象的全部要求(首地址指向ClassObject),遂能够正常走消息机制;
由于这个人造的对象在栈上,而取self.name的操作本质上是self指针在内存向高位地址偏移(32位下一个指针是4字节),按viewDidLoad执行时各个变量入栈顺序从高到底为(self, _cmd, self.class, self, obj)(前两个是方法隐含入参,随后两个为super调用的两个压栈参数),遂栈低地址的obj+4取到了self。

objc_msgSend()
Objective-C对象执行方法时,编译器会把

[target doMethodWith:var1];

转换为

objc_msgSend(target,@selector(doMethodWith:),var);

关于objc_msgSend(),

4.IMP

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

IMP是指向方法实现的函数指针,由编译器生成。

5.Class类

#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_*protocols          OBJC2_UNAVAILABLE;// 遵循的协议
#endif  

6.动态语言 与 静态语言

Objective-C 是面向运行时的语言,它尽可能的把编译和链接时要执行的逻辑延迟到运行时。
这个特性给了我们很大的灵活性,可以按需要把消息重定向给合适的对象,实现交换方法等

7.相关函数

objc_msgSend : 给对象发送消息
class_copyMethodList : 遍历某个类所有的方法
class_copyIvarList : 遍历某个类所有的成员变量
...

Runtime 实践

一.Method Swizzle 应用场景

1.方法交换
// 获取两个类的类方法
Method m1 = class_getClassMethod([Person class], @selector(doSomething1));
Method m2 = class_getClassMethod([Person class], @selector(doSomething2));
// 开始交换方法实现
method_exchangeImplementations(m1, m2);
2.拦截系统方法

创建一个Category类目,在分类中实现自定义方法,在load方法中实现方法交换,可以给NSObject添加分类,统计创建了多少个对象,给UIViewController添加类目,统计一共创建了多少个控制器。

二.关联对象 类目增加属性的功能 应用场景

类目中无法设置属性的,如果在类目申明中写@property 只能为其生成get 和set 方法的申明,但无法生成成员变量,虽然可以调用,但程序执行后会crash

/** step 1 */
#import <objc/runtime.h>
@property(nonatomic,copy)NSString *name;

/** step 2 重写set get 方法 */
char nameKey;

- (void)setName:(NSString *)name {
// 将某个值跟某个对象关联起来,将某个值存储到某个对象中
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}

三.动态添加方法 应用场景

+ (void)createClass
{
    //  动态创建类
    Class cls = objc_allocateClassPair([NSObject class], "cls", 0);
    
    //  动态添加属性
    //  3.添加一个NSString的变量,参数4.对齐方式,参数5.参数类型
    if (class_addIvar(cls, "name", sizeof(NSString *), 0, "@")) {
        NSLog(@"add ivar success");
    }
    
//    NSUInteger size = 4;
//    NSGetSizeAndAlignment("*", &size
//                          , &size);
//    
//    class_addIvar(cls, "name", size, size, "*");
    // "*" 代表字符(),iOS字符为4位,并采用4位对齐cls
    
    //  动态添加方法
    // 1对象 2.已实现的方法 3.已实现的函数 4."v@:"见参数类型链接
    class_addMethod(cls, @selector(method:), (IMP)method, "v@:");
    // 注册这个类到runtime系统中就可以使用了
    objc_registerClassPair(cls);
    // 用创建的类生成一个实例化对象
    id myobj = [[cls alloc]init];
    // 给添加的属性赋值
    NSString *str = @"参数";
    [myobj setValue:str forKey:@"name"];
    // 调用添加的method:方法,给myobject这个接受者发送method:消息
    [myobj method:10];
    
}

//  这个方法并没有被调用,但必须实现,否则不会调用下面的方法
- (void)method:(int)prama{
    NSLog(@" + int prama is %d",prama);

}

//  实际调用的方法
static void method(id self,SEL _cmd,int prama){

Ivar v = class_getInstanceVariable([self class], "name");
//  返回名为name的ivar 的变量的值
id o = object_getIvar(self, v);
//  成功打印出结果
NSLog(@"%@- %@ - int prama is %d",o,self,prama);

}

/* type
 *"v@:@",解释v-返回值void类型,@-self指针id类型,:-SEL指针SEL类型,@-函数第一个参数为id类型
   "@@:",解释@-返回值id类型,@-self指针id类型,:-SEL指针SEL类型,*/

四.获取属性列表 应用场景

1.解档归档
// 注意不要和系统方法重名重写
- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行归解档方法
    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)];
        
        // 如果有实现该方法再去调用
        if ([self respondsToSelector:@selector(ignoredNames)]) {
            if ([[self ignoredNames] containsObject:key]) continue;
        }
        
        id value = [aDecoder decodeObjectForKey:key];
        [self setValue:value forKey:key];
    }
    free(ivars);
    c = [c superclass];
    }
    
    }
    
    - (void)encode:(NSCoder *)aCoder {
        // 一层层父类往上查找,对父类的属性执行归解档方法
        Class c = self.class;
        while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        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)];
            
        // 如果有实现该方法再去调用
        if ([self respondsToSelector:@selector(ignoredNames)]) {
            if ([[self ignoredNames] containsObject:key]) continue;
        }
        
        id value = [self valueForKeyPath:key];
        [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        c = [c superclass];
        }
    }
2.字典转模型

以往使用KVC进行直接赋值,键值对对不上会crash,使用字典转模型,重写setValue:forUndefinedKey:方法防止crash ,但是瘦model文件过多没有什么意义,用runtime进行字典转模型。

字典和模型属性不匹配
1.键值多于模型属性数量(不用处理)
2.键值少于模型属性数量
模型中嵌套模型
数组中装着模型

- (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) 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;
                NSMutableArray *mArray = [NSMutableArray array];
                
                // 获取到每个模型的类型
                id class ;
                if ([self respondsToSelector:@selector(arrayObjectClass)]) {
                    
                    NSString *classStr = [self arrayObjectClass];
                    class = NSClassFromString(classStr);
                }
                // 将数组中的所有模型进行字典转模型
                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;
}

五.动态跳转页面

@implementation RuntimePush

- (void)pushByPrama:(NSDictionary *)pramas{
    // 动态创建类
    NSString *class = [NSString stringWithFormat:@"%@",pramas[@"class"]];
    const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
    Class newClass = objc_getClass(className);
    
    if (!newClass) {
        //  创建一个类
        Class superClass = [NSObject class];
        newClass = objc_allocateClassPair(superClass, className, 0);
        //  注册你创建的这个类
        objc_registerClassPair(newClass);
        newClass = objc_allocateClassPair(superClass, className, 0);
        //  注册你创建的这个类
        objc_registerClassPair(newClass);
    }
    //  创建对象
    id instance = [[newClass alloc]init];
    //  对该对象赋值属性
    NSDictionary *propertys = pramas[@"property"];
    
    [propertys enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            // 检测这个对象是否存在
        if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
            // 使用KVC赋值
            [instance setValue:obj forKey:key];
        }
    }];
    
    //  获取导航控制器
    UITabBarController *tabVC = (UITabBarController *)self.window.rootViewController;
    UINavigationController *pushClassStance = (UINavigationController *)tabVC.viewControllers[tabVC.selectedIndex];
    [pushClassStance pushViewController:instance animated:YES];
}


// 检测是否存在该属性
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
    unsigned int outCount,i;
    //  获取对象里的对象
    objc_property_t *properties = class_copyPropertyList([instance class], &outCount);

for (i = 0; i<outCount; i++) {
    objc_property_t property = properties[i];
    //  属性名改字符串
    NSString *propertyName = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
    //  判断属性是否存在
    if ([propertyName isEqualToString:verifyPropertyName]) {
        free(properties);
        return YES;
    }
}
free(properties);
return NO;
};

其他相关应用
插件化开发(利用runtime解耦)
Jspath 热更新

相关推荐
http://blog.sunnyxx.com/2014/11/06/runtime-nuts/
http://cocoasamurai.blogspot.jp/2010/01/understanding-objective-c-runtime.html

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 宝贝,昨天我们之间又是一次互相伤害的争论,其实就是一个事情拖沓。这个事情真的已经用各种道理,行动来分析和改变,为什...
    Cycm阅读 95评论 1 1
  • 今天看到一个次贷危机出现原因的新解--由政府强迫商业银行改变区别对待贷款申请人的标准造成。政府当时规定若是歧视弱势...
    瑜yuon阅读 197评论 0 1
  • 方法一:基于绝对定位的解决办法:它要求元素具有固定的宽度和高度 显然,这个方法最大的局限在于它要求元素的宽高是固定...
    overflow_hidden阅读 364评论 4 0
  • 狂奔过拥挤的人潮,抢一张回家的车票。在人潮里奋力奔跑。火车站人来人往那么多人,所有迷人的故事都在这里上演,悲伤困苦...
    初深恩阅读 754评论 0 3