Objective-C 语言特性之RunTime

一:什么是Runtime?平时项目中有用过吗?

        OC 是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。OC的动态性就是由Runtime来支撑和实现的,Runtime 是一套C语言的API,封装了很多动态性相关的函数。平时写的OC代码,底层都是转换成了RuntimeAPI进行调用。Runtime的底层实现一方面使我们的消息发送的效率更高,为我们提供了对象的接口方便我们在方法发送的过程中处理方法找不到的问题。另一方面提供了一整套动态方法,让我们可以在运行时动态的添加类,添加方法,操作属性等。

        具体来说:OC 是一门使用消息结构的语言,由Smalltalk语言演化而来,Smalltalk 是消息型语言的鼻祖。而java,c++之类的语言则是函数调用类型的语言。消息型语言与函数调用性语言的关键区别是:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定,而使用函数调用的语言,则由编译器决定。采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接受消息的对象问题也要在运行时处理,其过程叫做‘动态绑定’。Objective-C 的重要工作都由“运行期组件”(runtime component)而非编译器来完成。使用Objective-C 的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部的内存管理方法。运行期组件本质上就是一种与开发者所编代码相链接的“动态库”,其代码能把开发所编写的所有程序粘合起来,这样的话,只需更换运行期组件,即可提升应用程序性能。而那种需要哼多工作都需要在“编译期”完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。

        RunTime 包含类,变量,属性,协议的动态管理接口,通过这些接口,我们可以实现以下的应用。

具体应用:

(1)利用关联对象给分类添加成员变量。(具体如给手势ui 关联消息类型方法,给uilaert 添加block,方便代码查看,给uibutton添加 关联变量 实现,button按钮的设置。给cell关联动作描述,处理对应操作。)

(2)遍历类的所有成员变量(修改textfield 的占位文字颜色,字典转模型,自动归档解档)

(3)交换方法实现(实际应用是交换系统UI的方法实现,添加自己的处理,如替换按钮点击)

(4)利用消息转发机制解决找不到方法的异常问题。


1. 动态的添加对象的成员变量和方法,修改属性值和方法

2. 动态交换两个方法的实现

3. 实现分类也可以添加属性

4. 实现NSCoding的自动归档和解档

5. 实现字典转模型的自动转换

6. 动态创建一个类(比如KVO的底层实现)


二:Objective的消息机制

OC中的方法调用其实都是转换成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)

objc_msgSernd 底层有3大阶段

 (1)消息发送(当前类,父类中查找)

(2)动态方法解析,

(3)消息转发

消息发送过程


            首先从消息接受者的 类的缓存中去查找,如果有,就调用方法,结束查找。如果没有从消息接受者的类结构中的class_rw_t的方法列表中查找,如果找到,就调用方法,并将该方法保存到缓存中。如果还未找到就去接受者的类的父类的缓存中查找,如果找到就调用,并缓存。如果父类缓存中没有,就去父类的方法列表中查找,如果有,就调用,并缓存到自己的缓存中。如果还没有,则继续找父类的父类的缓存,以此类推。

动态解析阶段

        当消费发送阶段完全结束,并且仍未找到对应的方法时,会进入动态方法解析阶段,在该阶段,我们可以通过在类方法中添加resolveInstanceMethod方法来解决实例对象找不到方法的问题,也可以通过添加resolveClassMethod方法来解决类方法无法解决的问题。    添加过动态方法解析后,消息发送会重新从消息发送的初始阶段开始,再找一遍方法。

消息转发

在该阶段有两种措施可以解决未找到方法的问题。

一种是在类中添加。forwardingTargetForSelector方法,在该方法中,可以将方法转发到其他的类中进行解决。

另外一种是通过添加methodSignatureForselector方法和doesNotRecognizeSelector方法,在前者的方法中编写函数签名以实现转发,在后者方法中进行方法的调用。

三:Runtime 常用方法总结

(1)类相关函数

动态创建一个类(参数:父类,类名,额外的内存空间)

Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

注册一个类(要在类注册之前添加成员变量)

void objc_registerClassPair(Classcls) 

销毁一个类

void objc_disposeClassPair(Class cls)

获取isa指向的Class

Class object_getClass(id obj)

设置isa指向的Class

Class object_setClass(id obj, Class cls)

判断一个OC对象是否为Class

BOOL object_isClass(id obj)

判断一个Class是否为元类

BOOL class_isMetaClass(Class cls)

获取父类

Class class_getSuperclass(Class cls)

(2)成员变量相关

获取一个实例变量信息

Ivar class_getInstanceVariable(Class cls, const char *name)

拷贝实例变量列表(最后需要调用free释放)

Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

设置和获取成员变量的值

void object_setIvar(id obj, Ivar ivar, id value)

id object_getIvar(id obj, Ivar ivar)

动态添加成员变量(已经注册的类是不能动态添加成员变量的)

BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

获取成员变量的相关信息

const char *ivar_getName(Ivar v)

const char *ivar_getTypeEncoding(Ivar v)

(3)属性相关

获取一个属性

objc_property_t class_getProperty(Class cls, const char *name)

拷贝属性列表(最后需要调用free释放)

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

动态添加属性

BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,

                  unsigned int attributeCount)

动态替换属性

void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,

                      unsigned int attributeCount)

获取属性的一些信息

const char *property_getName(objc_property_t property)

const char *property_getAttributes(objc_property_t property)

(4)方法相关

获得一个实例方法、类方法

Method class_getInstanceMethod(Class cls, SEL name)

Method class_getClassMethod(Class cls, SEL name)

方法实现相关操作

IMP class_getMethodImplementation(Class cls, SELname) 

IMP method_setImplementation(Method m, IMP imp)

void method_exchangeImplementations(Method m1, Methodm2) 

拷贝方法列表(最后需要调用free释放)

Method *class_copyMethodList(Class cls, unsigned int *outCount)

动态添加方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

动态替换方法

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

获取方法的相关信息(带有copy的需要调用free去释放)

SEL method_getName(Method m)

IMP method_getImplementation(Method m)

const char *method_getTypeEncoding(Method m)

unsigned int method_getNumberOfArguments(Method m)

char *method_copyReturnType(Method m)

char *method_copyArgumentType(Method m, unsigned int index)

选择器相关

const char *sel_getName(SEL sel)

SEL sel_registerName(const char *str)

用block作为方法实现

IMP imp_implementationWithBlock(id block)

id imp_getBlock(IMP anImp)

BOOL imp_removeBlock(IMP anImp)

五:Runtime加载过程

自己总结:

        app启动后将控制权交给dyld,dyld 加载所有动态库,其中有个动态库libSystem,在这个动态库中加载RunTime。

Runtime 通过向dyld中注册回调函数(主要是map_images和load_images,map_images这几个函数)_dyld_objc_notify_register(&map_images, load_images, unmap_image),

每次加载image后,都会执行回调函数,其中map_images和load_images,map_images这几个重要的回调函数会依次往类的结构中添加他的所有方法。而load_images方法里主要是调用了一个load方法,所以我们可以发现OC类中load方法的调用时机比main函数都早。

APP启动之后系统处理好一些服务之后会把处理权交个动态链接器dyld,从名字上就可以知道它和动态库有关系,dyld此时会把App类用到的所有动态库给加载起来,其中有个核心动态库libSystem,每个App都需要它,我们的Runtime就在里面,那么当加载到此动态库时,Runtime就会向dyld注册几个回调函数:

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

当dyld每次往内存中添加新的二进制文件(此时称为image)之后,都会执行这些回调函数,比较重要的回调函数是map_images和load_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_setAssociatedObject和objc_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,还有引用数组referrers和inline_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里最终调用的就是此方法。

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

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

推荐阅读更多精彩内容