为了账号安全,请及时绑定邮箱和手机立即绑定

iOS Runtime探索之旅

标签:
iOS

对于iOS开发小伙伴来说,不管平常开发还是面试总能遇到些Runtime相关内容。Runtime从以前的神秘到现在大家的习以为常,各大博客也写了很多Runtime内容,但是我相信大部分人对于Runtime的了解还不是很全面,网上的资料虽然很多,但是很零碎,甚至还有些知识点已经过时了,所以我在此由浅到深给大家系统地写点Runtime知识,让大家对iOS Runtime有个整体直观的理解,反响不错的话,会在后续文章中,补充很多知识点。

首先我们得理解什么是Runtime,这里我给了张示意图:

图很直观,苹果公司基于Runtime,才把C语言封装成了我们所认识的OC语言。完整来说苹果当初是这样考虑的,在现有C语言基础上加上个编译器运行时环境就构成了OC语言最基本框架,编译器用于把OC语言转换成C语言,而运行时环境就是所说的Runtime了,用于处理转换之后的C语言,可以理解为微型的操作系统,也正是因为有这样一个环境,让OC语言成为了一门动态语言。这里可能会有两个疑问:

  • 疑问1:上述所说的编译器具体是啥?
    当然是Clang编译器,它可以把OC语言转换成C++语言,为什么不是C语言,这是因为需要C++的一些功能,而且Runtime准确来说是一套由C、C++、汇编实现的API。Clang编译器是啥?借用百度百科的一句话,Clang是一个C语言、C++、Objective-C语言的轻量级编译器。不同于Windows、Linux系统下的常用C语言编译器—GCC,Mac OS下的默认C语言编译器是Clang,它是苹果赞助开发的,更好的支持苹果自家产品。可能有人会问GCC是啥?这个嘛,简单点说就是把我们人类能看懂的高级语言转换成机器能识别的机器语言。
  • 疑问2:什么是动态语言?是不还有静态语言。
    对的,还有静态语言,它一经编译之后,就不能添加新的类、新的方法,或者其它结构上的一些变化,比如C、C++。那么动态语言刚好与静态语言相反,比如我们在OC中可以动态添加一个新的类、交换方法、消息转发等,这些动态性都是利用Runtime实现的。

想要研究Runtime底层代码,首先必须要拿到苹果的Runtime源码, 苹果开源网站上可以下载到很多开源项目,当前最新macOS系统为10.14,为什么不选择iOS呢?因为苹果只开源mac OS下的Runtime啊。

点击进去可以看到一个objc4开头的文件夹,没错它就是我们要找的Runtime源码,后面序号为其版本号:
图片描述
但是我们拿到它是不能直接在Xcode编译成功的,因为少些头文件,这些头文件需要我们自己去苹果的其它开源库中去寻找,这里涉及的头文件比较多就不一一介绍了,感兴趣的话,我后续文章可以手把手教大家编译它。
拿到编译好的Runtime源码后,如何使用呢。在Runtime源码中有两个非常重要的头文件<objc/runtime.h><objc/message.h>,第一个文件里面实现了很多类相关的操作,第二个文件主要处理OC调用方法时所发送的消息。当我们要调用Runtime的API时,一般要导入这两个头文件。

首先OC语言与C语言相比,很明显具有两大特性,一是面向对象,二是运行时。面向对象则需要考虑到对象的一些基本结构体,还有对象模型。运行时里面主要核心内容就是消息流程了,因为我们知道OC中的方法调用最终都会变成发送消息。

  1. 关于面向对象需要考虑哪些内容呢?
  • 首先苹果设计了这些结构体来帮助实现类的相关功能:
  • 其次和其他语言一样,OC语言也有子类、父类、元类、类、实例、实例方法、类方法等概念:
  1. 运行时具体体现在哪些方面呢?
  • 实际开发中很多看起来炫酷的功能都来自它的运行时特性,这里我总结了一些比较重要的功能:
  • 只要涉及到OC中方法的操作都可以看做消息流程这块的内容,这块内容比较多,也是重点,我挑重点的重点说些:
    (1) 首先要从我们OC方法调用说起,它经常被转换为发送消息,样子看起来这个样子:
    这是一个概括性的说话,具体是这样滴,我们的Clang编译器在编译时会把OC方法调用转换成C函数的调用,这里C函数主要是指objc_msgSend,当然还有objc_msgSendSuperobjc_msgSend_stretobjc_msgSendSuper_stret等方法,都是用于发送消息的,在message.h文件中。有两个参数需要注意,id selfSEL op,第一个是消息接受者,第二个是要执行的方法@selector,所有的OC方法调用在中转换成objc_msgSend时,都会添加好这两个参数。
    (2) 新的问题又出现,调用objc_msgSend到底会发生什么?
    在最新Runtime源码中可以看到它会直接调用一块汇编代码,这块汇编代码当然也属于Runtime的内容,在objc-msg-x86_64.s汇编文件中可以看到它的实现:

    主要处理就是获取isa,然后从缓存Cache中查询IMP,找到了IMP直接调用它,没有找到则到当前isa的方法列表中去查询IMP,这里最终会调用的一个C函数_class_lookupMethodAndLoadCache3,这个C函数是提供给汇编代码的一个回调:

    发现没有,这里有个非常、非常、非常重要的C函数lookUpImpOrForward,具体的方法列表中查找指定IMP过程就这里面啦。
    说到这里先缓缓,解决两个最基本概念:
    一是isa,它是OC类中的一个核心内容,用于维护OC类之间的一些关系,比如上面所说的实例、类、元类三者之间的关系枢纽就是isa啦,当然我们的实例方法、类方法也离不开它。三个重要送给自己吧。
    二是IMP,暂时就简单点理解吧,它其实就是个函数的地址,函数的地址在C语言中可以认为就是一个函数名,比如:

    可以把getName当作IMP,那么OC调用方法的实际过程可以理解为,从对应类的方法列表中查找对应的IMP,然后调用IMP,此时可以传些参数,和平常调用C函数一模一样。举个栗子:
    [person eatingWithFood:@"apple" inPlace:@"kitchen"];
    等于
    SEL eatSel = @selector(eatingWithFood:inPlace:);
    IMP eatIMP = [person methodForSelector:eatSel];
    eatIMP(person,eatSel,@"apple",@"kitchen");
    回归正传,在lookUpImpOrForward函数中到底干了些啥?平常所说的消息查找流程和消息转发都在这里面,先看消息超找流程,这里有个简单示意图:

    整个过程的目的就是需要查找到对应的IMP,先从缓存中查,没有,再到当前类查,再没有,到父类中查,直到把所有父类都查完,还是没有,那么代表这个方法在当前类和其父类中都没有实现,此时先尝试动态解析,解析失败就进入到了消息转发流程这块了,也给个示意图:

    当然还有很多细节,暂时这样吧,希望能帮助大家对消息流程这块有个比较直观的理解。

除了上述Runtime的两大核心内容,OC这门语言还有一些特色内容,理解好这些内容使得日常开发游刃有余,而且还能帮助我们增加面试通过率。

  • Runtime加载过程
    先抛个问题给大家,上面所说到的类的方法列表,里面内容是什么时候添加进去的?

    我们知道执行项目代码的入口函数是main函数,那么这个main函数执行之前是否还有其它操作呢。实际上当APP启动之后系统处理好一些服务之后会把处理权交个动态链接器dyld,从名字上就可以知道它和动态库有关系,dyld此时会把App类用到的所有动态库给加载起来,其中有个核心动态库libSystem,每个App都需要它,我们的Runtime就在里面,那么当加载到此动态库时,Runtime就会向dyld注册几个回调函数:
_dyld_objc_notify_register(&map_images, load_images, unmap_image);

当dyld每次往内存中添加新的二进制文件(此时称为image)之后,都会执行这些回调函数,比较重要的回调函数是map_imagesload_images,map_images方法里面就会往类的方法列表添加这个类的所有方法(方法是一个结构体,包含了方法名SEL,还有方法实现IMP),除此之外还有很多类的相关操作都在这里面,分类中的方法、协议、属性也是在这个时候添加到对应的类里去的;而load_images方法里主要是调用了一个load方法,所以我们可以发现OC类中load方法的调用时机比main函数都早,意味着我们是不是可以在这个方法里搞点事情做做呢?

这里给个大概示意图:

  • Category实现原理
    刚刚说到了把分类中的属性、协议、方法添加到对应的类中去,我们在仔细看看map_images的方法实现,再分析分析Category实现原理。
    在map_images中最终会调用_read_images这个方法,这里要敲敲黑板了—重点,里面有个remethodizeClass函数:

    在这个函数里面,会有具体三个操作,对应的把方法、协议、属性添加到对应的类中去:

    细心的小伙伴会发现,一个类中可能对应的有多个分类啊,怎么操作的?
    我从_read_images函数里选些核心代码:
//遍历分类列表
for (i = 0; i < count; i++) {
	category_t *cat = catlist[i];
	if (cat->instanceMethods ||  cat->protocols  ||  cat->instanceProperties) 
	{
		//---将分类添加分类表中去
		addUnattachedCategoryForClass(cat, cls, hi);
        //---将分类的method、protocol、property添加到class中去
		remethodizeClass(cls);
	}
	if (cat->classMethods  ||  cat->protocols  ||  (hasClassProperties && cat->_classProperties)) 
	{
		//将分类添加到元类的分类数组中去
		addUnattachedCategoryForClass(cat, cls->ISA(), hi);
		remethodizeClass(cls->ISA());
	}
}

这里面有个addUnattachedCategoryForClass函数,它主要是向一个全局哈希表catory_map里面添加类对应的分类,一个类的所有分类都在其list中,如图所示:

可以发现当给一个类添加方法、属性、协议时,会首先从全局哈希表中拿到其对应的分类列表list,然后遍历list,依次把每个分类中的方法、属性、协议都添加到主类中去。

  • Category Associate实现原理
    说到Category,不得不说Category Associate,因为我们知道在catory中添加的属性是不能直接调用的,想要直接调用得通过Category Associate来实现属性的Setter、Getter方法。那么Category Associate是如何实现的呢?
    想要分析这块原理,得需要看两个函数objc_setAssociatedObjectobjc_getAssociatedObject的实现,这两个方法又各自调用了函数_object_set_associative_reference和函数_object_get_associative_reference,具体操作都在里面。
    先看我们平常是如何调用objc_setAssociatedObject的, 这里给了个例子,给age属性实现setter方法:
static const char ageKey;
- (void)setAge:(NSString *)age
{
	  objc_setAssociatedObject(self,&ageKey,age,OBJC_ASSOCIATION_COPY_NONATOMIC);
}

这里也有个全局哈希表AssociationsHashMap(为什么又是哈希表,你有更好的建议?),这个表里面维护了所有对象的关联属性,并且对象的地址作为查询的key,但是我们知道每个对象下面可能有多个关联属性,所有此时又有个哈希表,这个表里面的key是关联属性所对应的key,实际中我们调用objc_setAssociatedObject函数时,传的key,就用在这里的,那么对应的value就是我们所关联的属性了吗,NO(哭了),此时的value是一个结构体,这个结构体里面有个成员变量new_value才是我们关联的具体属性。给个示意图帮助大家理解:

分析完这块,是不也可以推测出objc_getAssociatedObject的实现原理了,首先根据对象地址从AssociationsHashMap拿到ObjectAssociationMap,然后根据我们自己设置的key从ObjectAssociationMap拿到ObjcAssociation,然后获取成员变量new_value,就是我们要找的目标属性了。
现在又要抛个疑问了,这里关联的属性如何释放的?

我们知道当一个类的引用计数为0时,需要释放这个类,而当这个类在别的类中使用了关联属性把它关联了起来,比如说把我们上述的属性age,换成一个person对象,此时需要销毁person,那么如何处理person被关联的这块。
销毁对象这块最终需要调用到NSObject类中的dealloc方法:

//释放对象
- (void)dealloc {
    //---
    _objc_rootDealloc(self);
}

沿着调用函数流程dealloc—>_objc_rootDealloc—>rootDealloc object_dispose—>objc_destructInstance走最终会发现:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //---释放实例变量
        if (cxx) object_cxxDestruct(obj);
        //---移除关联属性
        if (assoc) _object_remove_assocations(obj);
        //---将弱引用置为nil
        obj->clearDeallocating();
    }
    return obj;
}

_object_remove_assocations中会移除掉关联属性,具体怎么移除,简单来说就是从上面说的两个哈希表中去移除对应的key和value就行了。

  • Weak实现原理
    嘿嘿,又要搞事情了,看到没有上面那块代码中的clearDeallocating函数了没有,它就是用来处理弱引用的。
    给个例子:
__weak Person *weakPerson = person;

想想我们的person对象已经被释放掉了,那么是不得告诉对象weakPerson一声,防止野指针,导致程序崩溃,而clearDeallocating函数所做的事情就是把很多类似weakPerson这样的弱引用全部都置为nil。具体weak是如何实现的,这里给了一个表:

从图中知道有个全局表叫SideTables,它里面维护了所有对象的引用计数,还有弱引用对象,当RefcountMap表中某个对象的引用计数为0时,系统会调用此类的dealloc方法,再调用其父类的dealloc,沿着继承链一直调用到NSObject的dealloc方法,最终走到objc_destructInstance函数里,主要操作四个:释放实例变量移除关联属性把弱引用置为nil释放自己self。在weak_table里有个数组weak_entries,数组里元素都是结构体weak_entry_t,这结构体里面有被引用的对象referent,还有引用数组referrersinline_referrers,这需要注意的是,不是所有的对象都具有结构体weak_entry_t的,只有当某个对象具有弱引用时,才会给这对象创建一个weak_entry_t,并把它添加到数组weak_entries中去,同理当一个对象变得没有弱引用时,会从数组weak_entries中删去它对应的weak_entry_t。我们知道一个对象可能有多个若引用,比如:

__weak Person *weakPerson1 = person;
__weak Person *weakPerson2 = person;

此时weakPerson1和weakPerson2都会放到weak_entry_t中的referrers数组或者inline_referrers数组中去,二者区别主要是看数组长度大小超过4了没有,超过4,则放到referrers中,否则放到inline_referrers中,此时对应的referent是person。
这里说个非常重要的头文件objc-weak.h,它专门处理OC中对象的弱引用问题,里面有几个核心方法:
weak_register_no_lock:给指定对象添加一个弱引用,当我们执行__weak ClassA *objB= objA类似代码时,会触发NSObject的objc_initWeak方法,最终会调用到weak_register_no_lock方法。
weak_unregister_no_lock:移除指定对象的一个弱引用。
weak_is_registered_no_lock:判断指定对象是否存在弱引用。
weak_clear_no_lock:清除指定对象所有弱引用,上述clearDeallocating里最终调用的就是此方法。


内容暂时就这么多吧,大家估计也看累了,休息休息。

点击查看更多内容
7人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
101
获赞与收藏
22

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消