iOS Developer - RuntimecodingiOS Developer

iOS之RunTime探索与实践

2016-06-29  本文已影响317人  StarkShen

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

上一篇下一篇

猜你喜欢

热点阅读