美文网首页
2020 阿里、字节iOS面试题之Runtime相关问题1(附答

2020 阿里、字节iOS面试题之Runtime相关问题1(附答

作者: 小船2022 | 来源:发表于2022-10-19 08:51 被阅读0次

    目录

    2020 阿里、字节iOS面试题之Runtime相关问题1

    2020 阿里、字节iOS面试题之Runtime相关问题2

    2020 阿里、字节iOS面试题之Runtime相关问题3

    面试题的结构分类和细化

    runtime相关问题

    runtime结构模型

    内存管理

    关联属性或者hook相关的Method Swizzle

    NSNotification相关

    参考GNUStep源码

    NSNotification实现原理 相关

    Runloop & KVO

    runloop

    KVO

    Block

    Block实现原理和注意事项相关

    多线程

    GCD相关和一些多线程概念

    视图&图像相关

    视图UI布局方案

    视图渲染相关

    性能优化

    开发证书

    架构设计

    各种设计模式

    自己的设计

    其他问题

    方法调用和切面编程等

    系统基础知识

    数据结构与算法

    runtime相关问题

    objc-runtime源码地址

    objc4官方源码地址

    结构模型

    介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

    对象

    OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。

    /// Represents an instance of a class.structobjc_object{Class _Nonnull isa  OBJC_ISA_AVAILABILITY;};/// A pointer to an instance of a class.typedefstructobjc_object*id;

    这个objc_object 的实现比较长 在这里查看

    在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class;

    对应的结构体如下:

    structobjc_class{Class _Nonnull isa  OBJC_ISA_AVAILABILITY;#if!__OBJC2__Class _Nullable super_class                              OBJC2_UNAVAILABLE;constchar*_Nonnull name                              OBJC2_UNAVAILABLE;longversion                                            OBJC2_UNAVAILABLE;longinfo                                                OBJC2_UNAVAILABLE;longinstance_size                                      OBJC2_UNAVAILABLE;structobjc_ivar_list*_Nullable ivars                  OBJC2_UNAVAILABLE;structobjc_method_list*_Nullable*_Nullable methodLists                    OBJC2_UNAVAILABLE;structobjc_cache*_Nonnull cache                      OBJC2_UNAVAILABLE;structobjc_protocol_list*_Nullable protocols          OBJC2_UNAVAILABLE;#endif}

    class和 object 小结

    从结构体中定义的变量可知,OC的Class类型包括如下

    数据(即:元数据metadata):super_class(父类类对象);

    name(类对象的名称);

    version、info(版本和相关信息);

    instance_size(实例内存大小);

    ivars(实例变量列表);

    methodLists(方法列表);

    cache(缓存);

    protocols(实现的协议列表);

    当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象, 这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。

    image

    从图中可知,最终的基类NSObject的元类对象isa指向的是自己本身,从而形成一个闭环。

    元类(Meta Class):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。

    我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体

    objc_ivar_list:存储了类的成员变量,

    可以通过object_getIvar或class_copyIvarList获取;

    另外这两个方法是用来获取类的属性列表的class_getProperty和class_copyPropertyList,属性和成员变量是有区别的。

    structobjc_ivar{char*_Nullable ivar_name                              OBJC2_UNAVAILABLE;char*_Nullable ivar_type                              OBJC2_UNAVAILABLE;intivar_offset                                          OBJC2_UNAVAILABLE;#ifdef__LP64__intspace                                                OBJC2_UNAVAILABLE;#endif}OBJC2_UNAVAILABLE;structobjc_ivar_list{intivar_count                                          OBJC2_UNAVAILABLE;#ifdef__LP64__intspace                                                OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_ivarivar_list[1]OBJC2_UNAVAILABLE;}

    objc_method_list:存储了类的方法列表,可以通过class_copyMethodList获取。

    结构体如下:

    structobjc_method{SEL _Nonnull method_name                                OBJC2_UNAVAILABLE;char*_Nullable method_types                            OBJC2_UNAVAILABLE;IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;}OBJC2_UNAVAILABLE;structobjc_method_list{structobjc_method_list*_Nullable obsolete            OBJC2_UNAVAILABLE;intmethod_count                                        OBJC2_UNAVAILABLE;#ifdef__LP64__intspace                                                OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_methodmethod_list[1]OBJC2_UNAVAILABLE;}

    objc_protocol_list:储存了类的协议列表,可以通过class_copyProtocolList获取。

    结构体如下:

    structobjc_protocol_list{structobjc_protocol_list*_Nullable next;longcount;__unsafe_unretained Protocol*_Nullable list[1];};

    此问题参考介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

    为什么要设计metaclass?

    先说结论: 为了更好的复用传递消息.metaclass只是需要实现复用消息传递为目的工具.而Objective-C所有的类默认都是同一个MetaClass(通过isa指针最终指向metaclass). 因为Objective-C的特性基本上是照搬的Smalltalk,Smalltalk中的MetaClass的设计是Smalltalk-80加入的.所以Objective-C也就有了metaclass的设计.

    本质上因为Smalltalk的面向对象的亮点是它的消息发送机制.

    回答这个问题之前我们先回看一下上边的Objective-C的对象原型继承链

    image

    通过上图我们明白如下 重点内容:

    实例的实例方法函数存在类结构体中

    类方法函数存在metaclass结构体中

    而Objective-C的方法调用(消息)就会根据对象去找isa指针指向的Class对象中的方法列表找到对应的方法。 > isa 指向的类就是我们创建实例的类型.

    通过为什么Objective-C中有MetaClass这个设计?文章我们了解到一个十分重要的概念,python和Objective-C不太一样的是,并不是每一个类都有一个MetaClass,而是Objective-C所有的类默认都是同一个MetaClass.

    Smalltalk中的metaclass

    Smalltalk,被公认为历史上第二个面向对象的语言,其亮点是它的消息发送机制

    Smalltalk中的MetaClass的设计是Smalltalk-80加入的。而之前的Smalltalk-76,并不是每个类有一个MetaClass,而是所有类的isa指针都指向一个特殊的类,叫做Class(这种设计之后也被Java借鉴了)。

    而每个类都有自己MetaClass的设计,加入的原因是,因为Smalltalk里面,类是对象,而对象就可以响应消息,那么类的消息的响应的方法就应该由类的类去存储,而每个MetaClass就持有每个类的类方法。

    每个MetaClass的isa指针指向什么?

    如果MetaClass再有MetaClass,那么这个关系将无穷无尽。Smalltalk里的解决方案是,指向同一个叫MetaClass的类。

    MetaClass的isa指针指向什么?

    指向他的实例,也就是实例的isa指向MetaClass,同时MetaClassisa指向实例,相互指着。

    那么Smalltalk的继承关系,其实和Objective-C的很像了(后面有class的是前者的MetaClass)。

    image

    这时候产生了一个重要的问题,假如去掉MetaClass,把类方法放到也类里面是否可行?

    这个问题,我思索许久,发现其实是一个对面向对象的哲学思想问题,要对这个问题下结论,不得不重新讲讲面向对象

    从Smalltalk重新认识面向对象

    以前谈到面向对象,总会提到,面向对象三特征:封装、继承、多态。但其实,面向对象中也分流派,如C++这种来自Simula的设计思想的,更注重的是类的划分,因为方法调用是静态的。而如Objective-C这种借鉴Smalltalk的,更注重的是消息传递,是动态响应消息。

    而面向对象三种特征,更基于的是类的划分而提出的。

    这两种思想最大的不同,我认为是自上而下和自下而上的思考方式。

    类的划分,要求类的设计者是以一个很高的层次去设计这个类,提取出类的特性和本质,进行类的构建。知道类型才可以去发送消息给对象。

    消息传递,要求的是类的设计者以消息为起点去构建类,也就是对外界的变化进行响应,而不关心自身的类型,设计接口。尝试理解消息,无法处理则进行特殊处理。 在此不讨论两种方式的优劣之分,而着重讲讲Smalltalk这种设计。

    消息传递对于面向对象的设计,其实在于给出一种对消息的解决方案。而面向对象优点之一的复用,在这种设计里,更多在于复用解决方案,而不是单纯的类本身。这种思想就如设计组件一般,关心接口,关心组合而非类本身。其实之所以有MetaClass这种设计,我的理解并不是先有MetaClass,而是在万物都是对象的Smalltalk里,向对象发送消息的基本解决方案是统一的,希望复用的。而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。

    为什么要设计metaclass小结

    回到一开始那个问题,为什么要设计MetaClass,去掉把类方法放到类里面行不行?

    我的理解是,可以,但不Smalltalk。这样的设计是C++那种自上而下的设计方式,类方法也是类的一种特征描述。而Smalltalk的精髓正在于消息传递,复用消息传递才是根本目的,而MetaClass只不过是因此需要的一个工具罢了。

    参考为什么Objective-C中有MetaClass这个设计?

    class_copyIvarList() & class_copyPropertyList()区别

    先说结论:

    class_copyIvarList() 能获取到所有的成员变量,包括 花括号内的变量(.h和.m都包括).

    class_copyPropertyList() 只能获取到 以@property关键字 声明的中属性(.h和.m都包括)

    区别:

    class_copyIvarList()获取默认是带下划线的变量

    class_copyPropertyList()获取默认是不带下划线的变量名称.

    但是以上两个方法都只能获取到当前类的属性和变量(也就是说获取不到父类的属性和变量)

    举例说明:

    我们声明一个ClassA 通过 调试代码实现

    #import#import@interfaceClassA:NSObject{int_a;int_b;int_c;CGFloat d;//不推荐这样写}@property(nonatomic,strong)NSArray*arrayA;@property(nonatomic,copy)NSString*stringA;@property(nonatomic,assign)dispatch_queue_t testQueue;@end@implementationClassA@end

    如果是通过class_copyIvarList()函数获取则打印如下结果.

    ---class_copyIvarList ↓↓↓---_a_b_cd_arrayA_stringA_testQueue--------------END----------------

    如果是通过class_copyPropertyList()函数获取则打印如下结果.

    ---class_copyPropertyList ↓↓↓---arrayAstringAtestQueue--------------END----------------

    debug代码如下:

    -(void)printIvarOrProperty{NSLog(@"--- class_copyPropertyList ↓↓↓---");ClassA*classA=[[ClassA alloc]init];unsignedintpropertyCount;objc_property_t*result=class_copyPropertyList(object_getClass(classA),&propertyCount);for(unsignedinti=0;i<propertyCount;i++){objc_property_t objc_property_name=result[i];NSLog(@"%@",[NSString stringWithFormat:@"%s",property_getName(objc_property_name)]);}free(result);NSLog(@"--------------END----------------");NSLog(@"--- class_copyIvarList ↓↓↓---");Ivar*iv=class_copyIvarList(object_getClass(classA),&propertyCount);for(unsignedinti=0;i<propertyCount;i++){Ivar ivar=iv[i];NSLog(@"%@",[NSString stringWithFormat:@"%s",ivar_getName(ivar)]);}free(iv);NSLog(@"--------------END----------------");}

    以上demo点击这里下载

    下面我们看下objc的源码

    以下代码位于objc-runtime-new.mm中

    /***********************************************************************

    * class_copyPropertyList. Returns a heap block containing the

    * properties declared in the class, or nil if the class

    * declares no properties. Caller must free the block.

    * Does not copy any superclass's properties.

    * Locking: read-locks runtimeLock

    **********************************************************************/objc_property_t*class_copyPropertyList(Classcls,unsigned int*outCount){if(!cls){if(outCount)*outCount=0;returnnil;}mutex_locker_tlock(runtimeLock);checkIsKnownClass(cls);ASSERT(cls->isRealized());auto rw=cls->data();property_t**result=nil;unsigned int count=rw->properties.count();if(count>0){result=(property_t**)malloc((count+1)*sizeof(property_t*));count=0;for(auto&prop:rw->properties){result[count++]=&prop;}result[count]=nil;}if(outCount)*outCount=count;return(objc_property_t*)result;}

    通过源码我们可以看到

    auto rw=cls->data();rw->properties;//通过rw直接拿到properties

    通过rw直接拿到properties,然后便利拿出想要的 以@property关键字 声明变量名称.

    properties详细内容 还请异步运行时源码看下这里篇幅限制就不啰嗦了.

    /***********************************************************************

    * class_copyIvarList

    * fixme

    * Locking: read-locks runtimeLock

    **********************************************************************/Ivar*class_copyIvarList(Classcls,unsigned int*outCount){constivar_list_t*ivars;Ivar*result=nil;unsigned int count=0;if(!cls){if(outCount)*outCount=0;returnnil;}mutex_locker_tlock(runtimeLock);ASSERT(cls->isRealized());if((ivars=cls->data()->ro->ivars)&&ivars->count){result=(Ivar*)malloc((ivars->count+1)*sizeof(Ivar));for(auto&ivar:*ivars){if(!ivar.offset)continue;// anonymous bitfieldresult[count++]=&ivar;}result[count]=nil;}if(outCount)*outCount=count;returnresult;}

    这里就一个关键点

    ivars=cls->data()->ro->ivars

    拿到ivars.

    由于这两者拿到的成员不一样所以两个API就会有区别.

    class_rw_t 和 class_ro_t 的区别

    先说结论:

    两个结构体都存放着当前类的属性、实例变量、方法、协议等.

    class_ro_t存放的是编译期间就确定的.

    而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容.

    下面我来深入了解两者具体是什么

    首先我们需要了解它俩的由来,在objc_class我们知道有一个成员变量叫isa,我们这里要介绍的是objc_class的另一成员变量bits.

    objc_class的结构如下:

    image

    bits 用来存储类的属性,方法,协议等信息。它是一个class_data_bits_t类型

    class_data_bits_t 如下:

    structclass_data_bits_t{uintptr_t bits;// method here}

    这个结构体只有一个64bit的成员变量bits,先来看看这64bit分别存放的什么信息:

    image

    is_swift : 第一个bit,判断类是否是Swift类

    has_default_rr :第二个bit,判断当前类或者父类含有默认的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法

    require_raw_isa :第三个bit, 判断当前类的实例是否需要raw_isa

    data : 第4-48位,存放一个指向class_rw_t结构体的指针,该结构体包含了该类的属性,方法,协议等信息。至于为何只用44bit来存放地址

    class_rw_t 和class_ro_t

    先来看看两个结构体的内部成员变量

    structclass_rw_t{uint32_tflags;uint32_tversion;constclass_ro_t*ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;Class firstSubclass;Class nextSiblingClass;};

    structclass_ro_t{uint32_tflags;uint32_tinstanceStart;uint32_tinstanceSize;uint32_treserved;constuint8_t*ivarLayout;constchar*name;method_list_t*baseMethodList;protocol_list_t*baseProtocols;constivar_list_t*ivars;constuint8_t*weakIvarLayout;property_list_t*baseProperties;};

    class_rw_t结构体内有一个指向class_ro_t结构体的指针.

    每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

    用两张图来说明这个过程:

    类的realizeClass运行之前:

    image

    类的realizeClass运行之后:

    image

    细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

    属性(property)存放在class_rw_t中,实例变量(ivar)存放在class_ro_t中。

    详细内容请 参考资料Objective-C runtime - 属性与方法

    category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

    结论:

    category 是 这样 realizeClass -> methodizeClass() -> attachCategories() 一步步被加载的.

    主类与分类的加载顺序是:主类优先于分类加载,无关编译顺序.

    分类间的加载顺序取决于编译的顺序:编译在前则先加载,编译在后则后加载.

    category如何被加载的

    我在运行时的源码 objc-runtime-new.mm中找到如下:

    staticClassrealizeClassWithoutSwift(Class cls,Class previously){...// Attach categories  被加载methodizeClass(cls,previously);returncls;}

    realizeClass -> methodizeClass() -> attachCategories()

    核心是在methodizeClass()函数中实现的.

    staticvoidmethodizeClass(Class cls){runtimeLock.assertLocked();boolisMeta=cls->isMetaClass();autorw=cls->data();autoro=rw->ro;...property_list_t*proplist=ro->baseProperties;if(proplist){rw->properties.attachLists(&proplist,1);}...// Attach categories.category_list*cats=unattachedCategoriesForClass(cls,true/*realizing*/);attachCategories(cls,cats,false/*don't flush caches*/);...if(cats)free(cats);}

    通过上述代码我们发现ro->baseProperties; , baseProperties 在前,category 在后,

    property_list_t*proplist=ro->baseProperties;if(proplist){rw->properties.attachLists(&proplist,1);}

    但决定顺序的是 rw->properties.attachLists ()这个方法.

    /// category 被附加进去voidattachLists(List*const*addedLists,uint32_taddedCount){if(addedCount==0)return;if(hasArray()){// many lists -> many listsuint32_toldCount=array()->count;uint32_tnewCount=oldCount+addedCount;setArray((array_t*)realloc(array(),array_t::byteSize(newCount)));array()->count=newCount;// 将旧内容移动偏移量 addedCount 然后将 addedLists copy 到起始位置/*

        struct array_t {

            uint32_t count;

            List* lists[0];

            };

    */memmove(array()->lists+addedCount,array()->lists,oldCount*sizeof(array()->lists[0]));memcpy(array()->lists,addedLists,addedCount*sizeof(array()->lists[0]));}elseif(!list&&addedCount==1){// 0 lists -> 1 listlist=addedLists[0];}else{// 1 list -> many listsList*oldList=list;uint32_toldCount=oldList?1:0;uint32_tnewCount=oldCount+addedCount;setArray((array_t*)malloc(array_t::byteSize(newCount)));array()->count=newCount;if(oldList)array()->lists[addedCount]=oldList;memcpy(array()->lists,addedLists,addedCount*sizeof(array()->lists[0]));}}

    所以 category 的属性总是在前面的,baseClass的属性被往后偏移了。

    两个category的load方法的加载顺序

    Aclass’s+load methodiscalled after all of its superclasses’+load methods.一个类的+load方法在其父类的+load方法后调用A category+load methodiscalled after theclass’s own+load method.一个Category的+load方法在被其扩展的类的自有+load方法后调用

    结论: 主类与分类的加载顺序是:主类优先于分类加载,无关编译顺序.

    两个category的同名方法的加载顺序

    应用程序 image 镜像加载到内存中时, Category 解析的过程,注意下面的 while(i--) 循环 这里倒序将 category 中的协议 方法 属性添加到了rw = cls->data()中的 methods/properties/protocols中。

    staticvoidattachCategories(Class cls,category_list*cats,boolflush_caches){if(!cats)return;if(PrintReplacedMethods)printReplacements(cls,cats);boolisMeta=cls->isMetaClass();// fixme rearrange to remove these intermediate allocationsmethod_list_t**mlists=(method_list_t**)malloc(cats->count*sizeof(*mlists));property_list_t**proplists=(property_list_t**)malloc(cats->count*sizeof(*proplists));protocol_list_t**protolists=(protocol_list_t**)malloc(cats->count*sizeof(*protolists));// Count backwards through cats to get newest categories firstintmcount=0;intpropcount=0;intprotocount=0;inti=cats->count;boolfromBundle=NO;while(i--){auto&entry=cats->list[i];method_list_t*mlist=entry.cat->methodsForMeta(isMeta);if(mlist){mlists[mcount++]=mlist;fromBundle|=entry.hi->isBundle();}property_list_t*proplist=entry.cat->propertiesForMeta(isMeta,entry.hi);if(proplist){proplists[propcount++]=proplist;}protocol_list_t*protolist=entry.cat->protocols;if(protolist){protolists[protocount++]=protolist;}}autorw=cls->data();// 注意下面的代码,上面采用倒叙遍历方式,所以后编译的 category 会先add到数组的前部prepareMethodLists(cls,mlists,mcount,NO,fromBundle);rw->methods.attachLists(mlists,mcount);free(mlists);if(flush_caches&&mcount>0)flushCaches(cls);rw->properties.attachLists(proplists,propcount);free(proplists);rw->protocols.attachLists(protolists,protocount);free(protolists);}

    所以结论是:分类间的加载顺序取决于编译的顺序:编译在前则先加载,编译在后则后加载

    这个问题网上有很多例子 就不多在这举例了.

    category & extension区别,能给NSObject添加Extension吗,结果如何

    category

    运行时添加分类属性/协议/方法

    分类添加的方法会“覆盖”原类方法,因为方法查找的话是从头至尾,一旦查找到了就停止了

    同名分类方法谁生效取决于编译顺序,image 读取的信息是倒叙的,所以编译越靠后的越先读入

    名字相同的分类会引起编译报错;

    extension

    编译时决议

    只以声明的形式存在,多数情况下就存在于 .m 文件中;

    不能为系统类添加扩展

    可以给类添加成员变量,但是是私有的 可以給类添加方法,但是是私有的 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。 伴随着类的产生而产生,也随着类的消失而消失

    必须有类的源码才可以给类添加extension!!!

    category & extension区别

    Category的小括号中有名字,而Extension没有;

    Category只能扩充方法,不能扩充成员变量和属性;

    如果Category声明了声明了一个属性,那么Category只会生成这个属性的set,get方法的声明,也就不是会实现.所以对于系统一些类,如nsstring,就无法添加类扩展 不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension

    能给NSObject添加Extension吗,结果如何?

    不能 因为没有NSObject的.m源码文件.

    如果能的话那应该不叫Extension.或者我们自己通过运行时的api自己造一套ExtensionDIY.结果就是你用的根本不能称为Extension,而是api调用而已.

    消息转发机制,消息转发机制和其他语言的消息机制优劣对比

    前言: 了解消息转发之前我们有必要了解一些Objectivce-C中的消息传递机制

    消息传递机制

    在Objectivce-C中,我们通过实例变量(对象)或者类方法名调用一个方法,那么我们实际上是在发送一条消息

    id returnValue=[someObject messageName:parameter];//实例调用方式id returnValue=[ClassA messageName:parameter];//类调用方式

    上述someObject和ClassA是接受者(receiver),messageName:是选择器(selector),选择器和参数合起来称为消息(message)。编译器看到此消息后,将其转换为一条标准的c语言函数调用,所调用的函数乃是消息传递机制中的核心函数:objc_msgSend()。

    voidobjc_msgSend(idself,SEL cmd,...)

    第一个参数代表接受者,第二个参数代表选择子,后续参数就是消息中的那些参数 编译器会把刚才的那个例子中的消息转换为如下函数:

    id returnValue=objc_msgSend(someObject,@selector(messageName:),parameter);id returnValue=objc_msgSend(ClassA,@selector(messageName:),parameter);

    objc_msgSend()函数会依据接受者与选择器的类型来调用适当的方法.为来完成此操作,该方法需要在接受者所属的类中搜寻其“方法列表”(也就是上文我们说的class_ro_t中的method_list)。找到则跳到现实代码,否则,就沿着继承体系继续向上查找,如果还没有则执行消息转发操作。对于其他的“边界情况”,则需要交由Objective-c运行环境的另一些函数来处理:

    objc_msgSend_stret//待发送的消息返回结构体时objc_msgSend_fpret//消息返回的是浮点型objc_msgSendSuper//如果要给超类发送消息

    消息转发机制

    结合上边的消息传递机制,在Objective-C中如果给一个对象发送一条它无法处理的消息,就会进入下图描述的消息转发(Message Forwarding)流程

    image

    在objc中消息转发需要经历3个阶段 resolveInstanceMethod -> forwardingTargetForSelectoer -> forwardInvocation ->消息未能处理。

    第一阶段:动态方法解析(Dynamic Method Resolution)也就是在所属的类中先征询接受者,看其是否能动态加方法,来处理当前这个未知选择器

    第二阶段:替换消息接收者快速转发

    第三阶段:完全消息转发机制

    第一阶段:动态方法解析(Dynamic Method Resolution)

    对象在受到无法解读的消息后,首先将调用其所属类的下列类方法:

    +(BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5,2.0,9.0,1.0,2.0);+(BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5,2.0,9.0,1.0,2.0);

    这俩方法在NSObject.h中

    返回一个Boolean类型,表示这个类是否能新增一个实例方法以处理选择器.

    在 消息转发过程中,我们可以使用resolveInstanceMethod:动态的将一个方法添加到一个类中.

    例下面示例代码:

    @implementationMyClass+(BOOL)resolveInstanceMethod:(SEL)aSEL{if(aSEL==@selector(resolveThisMethodDynamically)){class_addMethod([selfclass],aSEL,(IMP)dynamicMethodIMP,"v@:");returnYES;}return[superresolveInstanceMethod:aSEL];}@end

    这里我们用到一个运行时函数class_addMethod().

    {if(!cls)returnNO;mutex_locker_tlock(runtimeLock);return!addMethod(cls,name,imp,types?:"",NO);}

    class_addMethod()最后一个参数叫做types,是一个描述方法的参数类型的字符串.

    v代表void

    @代表对象或者说id类型

    :(这个冒号)代表方法选择器SEL

    具体代表什么不是我们瞎写的,得按照苹果的这个标准 Objective-C Runtime Programming Guide->Type Encodings

    上面的dynamicMethodIMP,返回值是void,两个入参分别是id和SEL,所以描述这个方法的参数类型的字符串就是v@:

    这个阶段的意义是为一个类动态提供方法实现,严格来说,还没进入消息转发流程。

    resolveInstanceMethod: 控制这下面两个方法是否会被调用

    respondsToSelector:

    instancesRespondToSelector:

    也就是说,如果resolveInstanceMethod:返回了YES,那么respondsToSelector:和instancesRespondToSelector:都会返回YES.

    第二阶段:替换消息接收者(快速转发)

    如果第一阶段中resolveInstanceMethod:返回NO,就会调用forwardingTargetForSelector:询问是否把消息转发给另一个对象.消息的接收者就改变了。

    -(id)forwardingTargetForSelector:(SEL)aSelector{returnsomeOtherObject;}

    第三阶段:完全消息转发机制

    如果第二阶段的forwardingTargetForSelector:返回了nil,这就进入了所谓完全消息转发的机制。

    首先调用methodSignatureForSelector:为要转发的消息返回正确的签名:

    -(void)forwardInvocation:(NSInvocation*)anInvocation{NSLog(@"forwardInvocation");SomeOtherObject*someOtherObject=[SomeOtherObjectnew];if([someOtherObject respondsToSelector:[anInvocation selector]]){[anInvocation invokeWithTarget:someOtherObject];}else{[super forwardInvocation:anInvocation];}}

    上面代码是将消息转发给其他对象,其实这与第二阶段中示例代码做的事情是一样的。区别就在于这个阶段会有一个NSInvocation对象。NSInvocation是一个用来存储和转发消息的对象。它包含了一个Objective-C消息的所有元素:一个target,一个selector,参数和返回值。每个元素都可以被直接设置。

    NSInvocation可以简单理解为一个对象把我们用到 selector方法和对象都存储了一下,然后哪个是指向我们需要调用的指针对象.

    所以不同与第二阶段,在这个阶段你可以:

    把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。

    修改消息的target,selector,参数等

    多次转发这个消息,转发给多个对象

    显然在这个阶段,你可以对一个OC消息做更多的事情

    消息转发机制和其他语言的消息机制优劣对比

    这个目前没有深入其它编程语言的运行时层面,比如C的底层或者C++的底层或者Java的底层消息传递

    在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

    Objective-C 实例对象执行方法步骤

    获取 receiver 对应的类 Class

    在 Class 缓存列表中(就是objc_class里的cache_t到class_ro_t的方法list)根据选择子selector查找IMP

    若缓存中没有找到,则在方法列表中继续查找.

    若方法列表没有,则从父类查找,重复以上步骤.

    若最终没有找到,则进行消息转发操作.

    方法查询之前 要知道 receiver和 selector.主要是要明确我们是哪个实例调用了哪个方法.

    动态解析解析之前要 在所属的类中先征询接受者,看其是否能动态加方法,来处理当前这个未知选择器.

    消息转发 之前 要询问是否把消息转发给另一个对象.

    如果更深入的而理解 那应该是 objc_msgSend() 为啥是汇编实现的,上面的那些方法 调用之前 汇编的哪些指令被执行

    这里找到两篇文章可以参考一下

    深入了解Objective-C消息发送与转发过程

    汇编语言编写的,其中具体过程细节

    IMP、SEL、Method的区别和使用场景

    IMP : 是方法的具体实现(指针)

    SEL :方法名称

    Method:是objc_method类型指针,它是一个结构体 ,如下:

    struct objc_method{SEL_Nonnull method_nameOBJC2_UNAVAILABLE;char*_Nullable method_typesOBJC2_UNAVAILABLE;IMP_Nonnull method_impOBJC2_UNAVAILABLE;}```

        使用场景

        * 例如 Button添加Target和Selector的时候.或者 实现类的`swizzle`的时候会用到,通过`class_getInstanceMethod(class,SEL)`来获取类的方法`Method`,其中用到了SEL作为方法名

        * 例如 给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:

        ```objcstaticvoidfuncName(id receiver,SELcmd,方法参数...){// 方法具体的实现 }

    SEL相当于 方法的类型 关键字.

    load、initialize方法的区别什么?在继承关系中他们有什么区别

    在Objective-C的类被加载和初始化的时候, 类 是 可以收到 方法回调的.

    -(void)load;-(void)initialize;

    +load

    + load方法是在这个文件(就是你复写的子类化的class)被程序装载时调用,只要是在Xcode Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此+load方法总是在main()函数之前调用.

    调用时机比较早,运行环境有不确定因素。具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理。

    补充上面一点,对于有依赖关系的两个库中,被依赖的类的+load会优先调用。但在一个库之内,父、子类、类别之间调用有顺序,不同类之间调用顺序是不确定的。

    关于继承:对于一个类而言,没有+load方法实现就不会调用,不会考虑对NSObject的继承,就是不会沿用父类的+load。

    父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明[super load],父类就会收到调用。

    本类和Category的调用:本类的方法优先于类别(Category)中的方法。Category的+load也会收到调用,但顺序上在本类的+load调用之后。

    不会直接触发initialize的调用。

    +initialize

    +initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用,并且只会调用一次。initialize方法实际上是一种惰性(lazy load)调用,也就是说如果一个类一直没被用到,那它的initialize方法也不会被调用,这一点有利于节约资源.

    runtime 使用了发送消息 objc_msgSend 的方式对 +initialize 方法进行调用。也就是说 +initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize方法,那么就会对这个类中的实现造成覆盖(override)。

    initialize的自然调用是在第一次主动使用当前类的时候。

    在initialize方法收到调用时,运行环境基本健全。

    关于继承:和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍,就是会沿用父类的+initialize。(沿用父类的方法中,self还是指子类)

    父类和本类的调用:子类的+initialize将要调用时会激发父类调用的+initialize方法,所以也不需要在子类写明[super initialize]。(本着除主动调用外,只会调用一次的原则,如果父类的+initialize方法调用过了,则不会再调用)

    本类和Category的调用:Category中的+initialize方法会覆盖本类的方法,只执行一个Category的+initialize方法。

    下面是我整理的一个表格希望对解释这俩方法有帮助:

    + load+ initialize

    调用方式直接使用函数内存地址objc_msgSend()方式

    调用时机被程序装载时调用main()函数之前,就是被添加到runtime时在本类或它的子类收到第一条消息之前被调用

    是否被系统单次调用(除主动调用外)是是

    运行时环境是否稳定不确定稳定

    线程是否安全默认是安全的(已加锁)安全(已加锁 )

    特性由于非objc_msgSend()方式调用就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用+initialize 方法的调用与普通方法的调用是一样的,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖

    参考类方法load和initialize的区别

    在继承关系中他们有什么区别

    super的方法会成功调用,但是这是多余的,因为runtime会自动对父类的+load方法进行调用,而+initialize则会随子类自动激发父类的方法(如Apple文档中所言)不需要显示调用。另一方面,如果父类中的方法用到的self(像示例中的方法),其指代的依然是类自身,而不是父类

    说说消息转发机制的优劣

    优点:

    利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

    使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。

    缺点:

    Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

    总结

    本篇讲述的面试题中的runtime相关问题结构模型部分。

    推荐

    更多:iOS面试题大全

    更多:《BAT面试答案文集.PDF》,获取可加iOS技术交流圈:937194184

    收录:原文地址

    作者:iOS猿_员

    链接:https://www.jianshu.com/p/7f94db2e5928

    来源:简书

    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

          本文标题:2020 阿里、字节iOS面试题之Runtime相关问题1(附答

          本文链接:https://www.haomeiwen.com/subject/wkwtartx.html