欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

IOS Object-C 中Runtime详解及实例代码

程序员文章站 2024-02-14 16:43:28
ios object-c 中runtime详解 最近了解了一下oc的runtime,真的是oc中很强大的一个机制,看起来比较底层,但其实可以有很多活用的方式。 什么是r...

ios object-c 中runtime详解

最近了解了一下oc的runtime,真的是oc中很强大的一个机制,看起来比较底层,但其实可以有很多活用的方式。

什么是runtime

我们虽然是用objective-c写的代码,其实在运行过程中都会被转化成c代码去执行。比如说oc的方法调用都会转成c函数 id objc_msgsend ( id self, sel op, … ); 而oc中的对象其实在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;
/* use `class` instead of `struct objc_class *` */

整个runtime机制其实可以挖的点很多,这里只是简单的介绍一些常见的用法,如果将其细细解析,相信一定会对oc的理解加深几个层面。

获取属性/方法/协议列表

最直接的一种用法,就是获取我们的结构体中存储的对象的属性、方法、协议等列表,从而获取其所有这些信息。

要获取也比较简单,但是自己尝试之前需要注意几点:

一定要自己给类加几个属性、方法,遵循一些协议,否则当然是看不到输出信息的。

要使用这些获取的方法,需要导入头文件 #import

#import <objc/runtime.h>

// 输出类的一些信息
- (void)loginfo {
 unsigned int count;// 用于记录列表内的数量,进行循环输出

 // 获取属性列表
 objc_property_t *propertylist = class_copypropertylist([self class], &count);
 for (unsigned 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 (unsigned int i; i < count; i++) {
 method method = methodlist[i];
 nslog(@"method --> %@", nsstringfromselector(method_getname(method)));
 }

 // 获取成员变量列表
 ivar *ivarlist = class_copyivarlist([self class], &count);
 for (unsigned int i; 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 (unsigned int i; i < count; i++) {
 protocol *myprotocal = protocollist[i];
 const char *protocolname = protocol_getname(myprotocal);
 nslog(@"protocol --> %@", [nsstring stringwithutf8string:protocolname]);
 }
}

方法调用的过程

调用方法分为调用实例方法和调用类方法,在结构体我们可以看到第一个就是一个 isa 指针,会指向类对象或者元类,什么叫元类呢?

每个实例对象有个isa的指针,他指向对象的类,而类里也有个isa的指针, 指向meteclass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteclass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteclass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。

IOS Object-C 中Runtime详解及实例代码

通过isa,就可以不断往上方去回溯自己的父类等,而方法的调用也利用了这个过程:

  1. 首先,当然在对象自己缓存的方法列表中去找要调用的方法,找到了就直接执行其实现。
  2. 缓存里没找到,就去上面说的它的方法列表里找,找到了就执行其实现。
  3. 还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
  4. 如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,我们可以自己在拦截调用方法里面做一些处理。
  5. 如果没有在拦截调用里做处理,那么就会报错崩溃。

以上就是方法调用的过程。我们可以看到的是,所谓重写父类方法,并不是抹除了父类方法,父类的方法还是存在的,只是我们在子类里面找到了就不会再去父类里找了,是这个层面的“覆盖”。而我们熟悉的 super 调用父类的方法实现,就是告知要去调用父类实现的标识。

拦截调用与动态添加

上面说到了拦截调用,就是在所有地方都找不到方法的实现的话,会出发拦截调用,可以自己去做一些处理避免程序崩溃。那在什么地方做处理呢?nsobject有四个方法可以用来做处理:

// 调用不存在的类方法时触发,默认返回no,可以加上自己的处理后返回yes
+ (bool)resolveclassmethod:(sel)sel;

// 调用不存在的实例方法时触发,默认返回no,可以加上自己的处理后返回yes
+ (bool)resolveinstancemethod:(sel)sel;

// 将调用的不存在的方法重定向到一个其他声明了这个方法的类里去,返回那个类的target
- (id)forwardingtargetforselector:(sel)aselector;

// 将调用的不存在的方法打包成 nsinvocation 给你,自己处理后调用 invokewithtarget: 方法让某个类来触发
- (void)forwardinvocation:(nsinvocation *)aninvocation;

假设我们成功拦截下来了,那我们可以做什么处理呢?这个其实就是根据具体情况看你要怎么处理了。但这里有一个久闻其名的东西就可以派上用场了:动态添加方法。

我们知道oc是动态的,也就是说很多东西它不是在编译时去判断,而是在运行时去处理的,这其实带来了很大的灵活性,比如这里我们对于一些不存在的方法的调用,就可以动态去添加上一个方法来代替我们要调用的方法。

比如我们添加一个button,点击事件我们关联到一个没有具体实现的实例方法,这种情况下如果不做任何处理那么点击button后一定会崩溃,但是如果我们拦截并动态添加一个方法来报警,就不会崩溃了:

@interface viewcontroller ()
@property (nonatomic, strong) uibutton *logbtn;

- (void)nothas;// 要调用的实例方法,没有具体实现
@end

- (void)viewdidload {
 [super viewdidload];

 // 添加按钮
 self.logbtn = [[uibutton alloc] initwithframe:cgrectmake(100, 200, 100, 20)];
 [self.logbtn settitle:@"测 试" forstate:uicontrolstatenormal];
 [self.logbtn settitlecolor:[uicolor lightgraycolor] forstate:uicontrolstatenormal];
 // 添加没有实现的点击事件
 [self.logbtn addtarget:self action:@selector(nothas) forcontrolevents:uicontroleventtouchupinside];
 [self.view addsubview:self.logbtn];

}

// 拦截对不存在的方法的调用
+ (bool)resolveinstancemethod:(sel)sel {
 nslog(@"notfind!");
 // 给本类动态添加一个方法
 if ([nsstringfromselector(sel) isequaltostring:@"nothas"]) {
 class_addmethod(self, sel, (imp)runaddmethod, "v@:*");
 }
 // 注意要返回yes
 return yes;
}

// 要动态添加的方法,这是一个c方法
 void runaddmethod(id self, sel _cmd, nsstring *string) {
 nslog(@"动态添加一个方法来提示");
}

按照上面的处理,点击按钮后就不会崩溃,而是转到了对 runaddmethod 方法的调用。其实更明确地说,应该是重现了对要调用的方法的实现,将原本要调用的方法的实现,改为了一个新的实现。class_addmethod 方法的第二个参数是要重写的方法,这里用的就是传进来的参数sel,第三个参数就是重写后的实现。第四个参数是方法的签名。

关联对象

什么叫关联对象?说通俗一点,我们都知道用category类别可以给一些已经存在的,比如系统的类添加方法,但是不能添加新属性,那怎么添加属性呢?一种直接的方法是继承,但如果只是为了添加一个属性就去做一次继承,还是有点重,这时候,就可以用关联对象的方法。

有两个相关的方法:

objc_setassociatedobject 方法用来给类关联一个属性;
objc_getassociatedobject 方法用来获取之前关联的属性。

比如说给自己这个类关联一个字符串:

 // 关联对象
 static char associatedobjectkey;
 objc_setassociatedobject(self, &associatedobjectkey, @"我就是要关联的字符串对象内容", objc_association_retain_nonatomic);
 nsstring *thestring = objc_getassociatedobject(self, &associatedobjectkey);
 nslog(@"关联对象:%@", thestring);

我们先给self关联了一个字符串内容,然后通过get方法获取了关联的字符串内容,并输出。

从代码中其实也可以猜到各个参数的意思,self的参数就是要处理的类;associatedobjectkey 的参数其实就类似于key,用来标识区分你要关联的这个对象;第三个参数是要关联的对象;第四个参数是关联的策略,用命名就可以看出来全是在添加@property属性时用到的一些修饰符,有五种策略:

enum {
 objc_association_assign = 0,
 objc_association_retain_nonatomic = 1, 
 objc_association_copy_nonatomic = 3,
 objc_association_retain = 01401,
 objc_association_copy = 01403 
};

熟悉@property属性修饰符的应该能直接明白了,不熟悉的可以看这篇文章:传送门:ios中assign、retain、copy、weak、strong的区别以及nonatomic的含义

当然,你也可以和类别一起用,创建两个方法用来关联和获取对象,比如下面这样:

//添加关联对象
- (void)addassociatedobject:(id)object{
 objc_setassociatedobject(self, @selector(getassociatedobject), object, objc_association_retain_nonatomic);
}

//获取关联对象
- (id)getassociatedobject{
 return objc_getassociatedobject(self, _cmd);
}

这样就既能通过category类别来添加方法,用一起顺便提供了对属性的添加了。

以上是对runtime的一点浅薄的理解和使用,runtime的天地应该是很广阔的,也能挖出很多高级的使用方法来,对于理解oc的运行机制是很有帮助的。

源码下载:http://xiazai.jb51.net/201703/yuanma/runtimedemo-master(jb51.net).rar

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!