runtime的理解

runtime简称运行时,就是系统在运行的时候的一些机制(主要是消息机制)。对于C语言,函数在编辑的时候已经决定调用哪个函数,编辑完成后直接顺序执行。OC的函数调用为消息发送,只有在真正运行的时候才会根据函数的名称查找对应的函数进行调用。

概念

我们写的代码在程序运行过程中都会被转化成runtime的C代码执行。例如:[target doSomething];会被转化成obj_msgSend(large,@selector(doSomething));

OC中一切都被设计成了对象,当一个类被初始化成一个实例后,这个实例就是一个对象;实际上一个类在OC中也是一个对象,在runtime中用结构体表示。相关的定义:

///描述类中的一个方法
typedef struct objc_method *Method;
///实例变量
typedef struct objc_ivar *Ivar;
///类别Category
typedef struct objc_category *Category;
///类中声明的属性
typedef struct objc_property *objc_property_t;

类在runtime中的表现
//类在runtime中的表示
struct objc_class {
	Class isa;//指针,顾名思义,表示是一个什么,
	//实例的isa指向类对象,类对象的isa指向元类

	#if !__OBJC2__
		Class super_class;  //指向父类
		const char *name;  //类名
		long version;
		long info;
		long instance_size
		struct objc_ivar_list *ivars //成员变量列表
		struct objc_method_list **methodLists; //方法列表
		struct objc_cache *cache;//缓存
		//一种优化,调用过的方法存入缓存列表,下次调用先找缓存
		struct objc_protocol_list *protocols //协议列表
	#endif
} OBJC2_UNAVAILABLE;

C方法

之前有一个分享问runtime是什么?他说是指c的方法可以获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。先看代码:

#import <objc/runtime.h>

	unsigned int count;
	//获取属性列表
	objc_property_t *propertyList = class_copyPropertyList([self class], &count);
	for (int i=0; i<count; i++) {
		const char *propertyName = property_getName(propertyList[i]);
		NSLog(@"property -->>%@",[NSString stringWithUTF8String:propertyName]);
	}
    
	//获取方法列表
	Method *methodList = class_copyMethodList([self class], &count);
	for (int i=0; i<count; i++) {
		Method method = methodList[i];
		NSLog(@"method-->>%@",NSStringFromSelector(method_getName(method)));
	}
    
	//获取成员变量列表
	Ivar *ivarList = class_copyIvarList([self class], &count);
	for (int i=0; i<count; i++) {
		Ivar myIvar = ivarList[i];
		const char *ivarName = ivar_getName(myIvar);
		NSLog(@"Ivar---->>%@",[NSString stringWithUTF8String:ivarName]);
	}
    
	///获取需要实现协议列表
	__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
	for (int i=0; i<count; i++) {
		Protocol *myProtocol = protocolList[i];
		const char *protocolName = protocol_getName(myProtocol);
		NSLog(@"protocol--->>%@",[NSString stringWithUTF8String:protocolName]);
	}

更多

实际除了可以通过C的函数获取一些类的信息外(包括方法交换),runtime不仅是这样,还有更多的功能,如果拦截调用,动态添加方法,添加属性。实现这个几方法可以看出,当oc在运行的时候,我们可以动态的为类或对象干更多的事情。 先了解一下在运行过程中方法调用的过程。

  • 1.当实例对象调用实例方法;会到实例a的isa指针指向的对象(也就是类对象)中操作。如果直接调用类方法,则会到类对象的isa指针指向的对象(也就是元类对象)中操作。
  • 2.先在类中的缓存方法列表中找相应的方法,如果找到则转向相应实现并执行方法。
  • 3.没有在缓存方法列表中找到,则在方法列表中找,如果找到则转向相应实现并执行方法。并把此方法加放到缓存,方便下次调用 。
  • 4.如果还没有找到,去父类指针指向的对象中执行2,3;以此类推,一直到根类还没有找到,转向上文提到的拦截调用。
  • 5.如果没有重写拦截调用的方法,程序报错。
  • 关于重写父类的方法,只是在当前类对象中找到了这个方法后,就不会在父类中查找了。如果想调用父类的方法,需要使用super这个编译器标识,它会在运行时跳过当前的类对象直接去父类中查找。

    考题

    下面这个题和上面的解释有点绕:

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

    输入结果是两个Son;原理请看http://www.cocoachina.com/ios/20141224/10740.html

    实际还有一个有意思的题:

     1. NSMutableArray *ary = [[NSString alloc] initWithString:@"test"];
    2. [ary addObject:@"error"];
    					

  • 第一行代码,编译有警告,运行可以通过,第二行代码编译没问题,运行报错。
  • 这也算是runtime中的一种,ary对象在运行的时候指向了一个NSString的地址。但是在运行的在NSString中无法找到addObject方法,会报-[__NSCFConstantString addObject:]: unrecognized selector sent to instance 。字符串没有找到相关addObject的方法。
  • 考点:OC只有在真正运行的时候才会根据函数的名称查找对应的函数进行调用。
  • 拦截调用 :

    上面的说到了,如果ary没有找到addObject的方法,就会转向拦截调用,拦截调用是指在找不到调用的方法程序崩溃之前,我们可以重写NSObject的四个方法来处理:
    + (BOOL)resolveClassMethod:(SEL)sel;
    + (BOOL)resloveInstanceMethod:(SEL)sel;
    ///后两个方法需要转发到其他的类处理
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;

  • 1.方法一当你调用一个不存在的类方法时,会调用这个方法,默认返回NO;你可以加上自己的处理然后返回YES;
  • 2.方法二同上,只不是处理的实例方法。
  • 3.方法三是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target;
  • 4.方法四是将你调用的不存在的方法打包成NSInvocation传给你,做完你自己的处理后,调用invaokeWithTarget:方法让某个target触发这个方法。
  • 动态添加方法:

    这个是基于上面的拦截调用来实现的,当前对象调用的方法无法在方法列表中找到时,就会调用 + (BOOL)resloveInstanceMethod:(SEL)sel; 而这时我们可以在里面直接加一个方法

    void dynamicMethodIMP(id self, SEL _cmd) {  //定义一个新方法
    	NSLog(@" >> dynamicMethodIMP");
    }
    
    ///当前对象的方法没有被找到时,会调用 resolveInstanceMethod,我们里面把没有找到的方法替换为了dynamicMethodIMP。
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
    	class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
    	return [super resolveClassMethod:sel];
    }

    添加属性:

    这个我们直接 在NSObject里面的扩展加了一个方法

    //添加关联对象
    - (void)addAssociatedObject:(id)object{
    	objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    //获取关联对象
    - (id)getAssociatedObject{
    	return objc_getAssociatedObject(self, _cmd);
    }

    注意:这里不用声明一个全局变量(用它的地址作为关联对象的key),而是用getAssociatedObject的方法地址作为唯一的key,_cmd代码当前调用方法的地址 在runtime中,我们还有方法交换的姿势没有说。

    合理使用+(void)load{} load方法会在类第一次加载的时候被调用,调用的时间比较靠前