Runtime相关面试题

344 阅读14分钟

结构模型

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

iOS14 以前:

img

iOS14以后:

img Clean Memory: 加载后不会发生更改的内存块,class_ro_t属于Clean Memory,因为它是只读的。
Dirty Memory: 运行时会进行更改的内存块,类一旦被加载,就会变成Dirty Memory,例如,我们可以在 Runtime 给类动态的添加方法

  • 在 iPhone 中,我们在系统测量了大约 30MB 的这些class_rw_t结构。应该如何优化这些内存呢?通过测量实际设备上的使用情况,我们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。因此,我们能可以把这部分动态的部分提取出来,我们称之为class_rw_ext_t

2.为什么要设计metaclass

  • OC面向对象能力的部分师承于Smalltalk
  • 从消息查找上分析,倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, ...)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的...就是各式各样的参数。
  • 通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。

3. class_copyIvarList & class_copyPropertyList区别

  • class_copyPropertyList:通过类名获得类的属性变量(由@property修饰过的变量)。
  • class_copyIvarList: 通过类名获得类的实例变量(class_copyPropertyList修饰的以及在m文件的中@implementation内定义的变量)。
- (void)testProperties {
    const char *str = [@"ZMPerson" UTF8String];
    id zmObject = objc_getClass(str);
    unsigned int count = 0;
    unsigned int iconut = 0;
    objc_property_t *pts = class_copyPropertyList(zmObject, &count);
    Ivar *ivars = class_copyIvarList([ZMPerson class], &iconut);
    for (int i = 0; i < count; i++) {
        objc_property_t p = pts[i];
        NSLog(@"property--%@", [[NSString alloc] initWithUTF8String:property_getName(p)]);
    }
    for (int i = 0; i < iconut; i++) {
        NSString *p = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
        NSLog(@"ivar---%@", p);
    }
}

4.class_rw_t 和 class_ro_t 的区别

改版后 class_ro_t属于Clean Memory,因为它是只读的。 同问题1.

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

  • category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。
  • category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。

image.png

  • category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。

扩展 +initialize

  • +initialize 方法会在类第一次接收到消息的时候调用,一般是alloc
  • 调用顺序
    • 先父类,后子类
    • 先初始化父类在初始化子类,每个类只会初始化一次
  • +initialize+load区别是 他是通过objc_msgSend进行调用的,所以有如下特点
    • 如果子类没有实现+initialize,就会调用父类的+initialize,所以父类的+initialize可能被调用多次
    • 如果分类实现了+initialize,就会覆盖类本身的+initialize调用

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

  • category
    • 给类添加新的方法
    • 是运行期决定的
    • 不能给类添加成员变量(通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性)-- 注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的[内存]class_ro_t布局已经确定
  • extension
    • 可以给类添加成员变量,但是是私有的
    • 可以給类添加方法,但是是私有的
    • 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。 伴随着类的产生而产生,也随着类的消失而消失 必须有类的源码才可以给类添加
    • 所以对于系统一些类,如nsstring,就无法添加类扩展 不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。

image.png

image.png

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

  • 消息转发机制

    • 消息转发机制是相对于消息传递机制而言的。
    • 1.消息(传递)机制
      • RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。
      • 对于C语言,函数的调用在编译的时候会决定调用哪个函数。编译完成之后直接顺序执行,无任何二义性。OC的函数调用称为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(也就是说,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
      • [obj makeText];首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method,若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
    • 2.消息转发机制(可以间接实现多重继承)
      • 当向someObject发送某消息,但runtime在当前类和父类中都找不到对应方法的实现时,runtime并不会立即报错使程序崩溃,而是依次执行下列步骤:
      • 1.动态方法解析:向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法。(迷茫请搜索:@dynamic)
      • 2.快速消息转发:检查该类是否实现了 forwardingTargetForSelector: 方法,若实现了则调用这个方法。若该方法返回值对象非nil或非self,则向该返回对象重新发送消息。
      • 3.标准消息转发:runtime发送methodSignatureForSelector消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。
  • 优劣

    • 其他语言一般都是静态绑定,就是在编译期就能决定运行时所调用的函数; 而OC的消息转发机制是动态绑定,在运行期才能确定调用函数
    • 优点:可以动态新增方法、改变篡改 receiver等
    • 缺点:执行效率相对慢、安全性较低。

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

  • 方法调用的时候,会转变为objc_msgSend或者其他msgSend方法,这个方法的的第一个参数是方法receiver接受者、第二个参数是方法的SEL(方法名),其余都是附加参数。 - objc_msgSend 汇编实现中CacheLookup 宏就是查询方法是否缓存,在此之前会判断receiver是否为空,空则不处理,再次会获取对象的isa指向,获取对应类对象
    • 1、检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。
    • 2、检查receiver是否为nil。

9.IMPSELMethod的区别和使用场景

  • IMP:是方法的实现,即:一段c函数
  • SEL:是方法名
  • Method:是objc_method类型指针,它是一个结构体
struct objc_method {
    SEL _Nonnull method_name                                OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • 使用场景:
    • 实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名
    • 我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP, 函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。

10._objc_msgForward函数是做什么的,直接调用它将会发生什么?

  • _objc_msgForwardIMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
  • 直接调用:会跳过方法查询步骤,直接进入转发。
  • 例子:JSPatch 中利用方法替换,把要监听的方法替换为_objc_msgForward, 然后在forwardInvocation方法中获取NSInvocation对象,包含方法的参数信息, 然后传递到js中

内存管理

1.weak的实现原理?SideTable的结构是什么样的

    • 查看汇编可以得知,添加__weak修饰会走到objc_initWeak函数,这个过程是由LLVM来决定的。
  • 接着走到storeWeak
    • 在这里 先获取到对象的 散列表 也就是SideTable
    • 然后进行加锁
  • 接着进入weak_register_no_lock
    • 这里 location指针,也就是weak指针
    • referent:newObject, NSObject 就是对象
    • 核心就是根据referent对象去查找weak_table中的weak_entries链表中的weak_entry_t对象entry
    • 如果存在则将weak指针存入weak_entry_t中的referrers链表中,存储时 需要遵循 3/4 两倍扩容规则
    • 不存在则根据object对象weak指针创建一个新的entry,再将entry插入weak_tableweak_entries链表中。
  • 注: 在使用weak对象时,会短暂的进行引用计数+1,但使用过后会调用objc_release方法进行进行引用计数-1,所以多次调用打印方法时,引用计数始终是没有变化
  • 弱引用表结构图: image.png

11.关联对象的应用?系统如何实现关联对象的

  • 如何实现: 通过objc_setAssociatedObject把object、key、value、policy入参,函数内部处理:
    • 1、通过AssociationManager单例获取全局AssociationHashMap哈希表
    • 2、新建ObjectAssociationMap C++map类,通过object参数的地址取反后作为Key, ObjectAssociationMap对象作为value存到全局AssociationHashMap表中
    • 3.其中ObjectAssociationMap 哈希map是以参数key为key,ObjcAssociation C++类为value来存储;而ObjcAssociation的成员变量就是参数value和policy
  • 应用:
    • 1、因为关联对象的销毁是伴随着实例对象销毁,通过监控关联对象的销毁间接监控对象销毁,可做一些自动释放操作,如为 KVO 创建一个关联的观察者。
    • 2、为现有的类添加私有变量以帮助实现细节;
    • 3、为现有的类添加公有属性;

12.关联对象的如何进行内存管理的?关联对象如何实现weak属性

  • 内存管理:
    • 关联对象会在对象释放的时候自动释放,即对象的dealloc中会检测类是否有关联对象,有就调用_object_remove_assocations函数释放
  • 关联对象如何实现weak属性:
    • 1、新增NSObject类,声明一个weak修饰的 id属性
    • 2、在对应分类中新增关联对象,用weak修饰,在set和get方法中通过新增的NSObject类包装此关联对象进行存取

13.Autoreleasepool的原理?所使用的的数据结构是什么

  • 数据结构: 由一个个page大小为4096字节的AutoreleasePoolPage节点组成的双向链表。
  • 自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接
  • 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPudh和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法
  • 每次调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个POOL_BOUNDARY,并返回插入POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况
    • 当page存在,且不满时,调用add方法将对象添加至page的next指针处,并next递增
    • 当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中
    • 当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
  • 4.当执行pop操作时,会传入一个值,这个值就是push操作的返回值,即POOL_BOUNDARY的内存地址token。所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release释放token之前的对象,并把next指针到正确位置

14.ARC的实现原理?ARC下对retain & release做了哪些优化

  • ARC的实现原理:
    • 通过编译期自动插入retain、release、autorelease来控制引用计数,当引用计数为0的时候会自动释放对象;
    • 其中objc_retain会将引用计数存储在isa.extra_rc变量(8个字节),当extra_rc溢出(上溢)后就会将extra_rc的一半引用计数迁移到SideTable的RefcountMap中。
    • objc_release会先处理isa.extra_rc变量的引用计数,当处理完(下溢)后把SideTable的RefcountMap中引用计数转回到extra_rc中。
  • retain,release做了哪些优化
    • TaggedPointer 指针优化
    • !newisa.nonpointer:未优化的 isa 的情况下retain或者release
    • newisa.nonpointer:已优化的 isa , 这其中又分 extra_rc 溢出区别 我把

image.png

15.ARC下哪些情况会造成内存泄漏

  • CF类型内存
    • ARC 可以帮忙管理 Objective-C 对象, 但是不支持 Core Foundation 对象的管理
    • 注意以creat,copy作为关键字的函数都是需要释放内存的,注意配对使用。比如:CGColorCreate<-->CGColorRelease
  • MRC内存使用
  • 循环引用
    • block引起的循环引用。
    • 引用大循环
      • 就像前面说的,引用循环可能是一个大循环。我遇到过一种情况,就是给UITableViewCell设置block属性响应事件,在block中强引用了self,导致self->tableView->cell->self形成循环。有时候随着代码量的增大,逻辑的负责,很容易形成一个很大的循环引用,最后造成内存泄漏。
    • NSTimer的使用
      • NSTimer会对它的target持有强引用,如果NSTimer不释放掉,就会一直持有它的target的强引用,如果这个NSTimer在被target强引用,会一直都释放不掉,造成内存泄露。
      • 解决办法
          1. 使用invalidate结束timer运行 ,加在didMoveToParentViewController方法里
          1. 中介者模式: 包装一下target,使得timer绑定另外一个不是self的target对象来打破这层强持有关系
          1. NSProxy虚基类, 我们不用self来响应timer方法的target,而是用NSProxy来响应
          1. __weak
    • 单例也会造成内存泄漏
      • 如果一个单例持有一个block,block内又使用了当前这个ViewController类,会引起循环引用。所以单例持有的代码块中要用弱引用, 原因是:单例不会被释放掉,它会一直持有block,导致该block所在的ViewController释放不掉。
  • performSelector的内存问题
    • performSelector 的动态绑定
      • 正是由于动态,编译器不知道即将调用的 selector 是什么,不了解方法签名和返回值,甚至是否有返回值都不懂,所以编译器无法用 ARC 的内存管理规则来判断返回值是否应该释放。因此,ARC 采用了比较谨慎的做法,不添加释放操作,即在方法返回对象的引用计数可能不会减少,从而可能导致内存泄露
      • 所以如果你使用的 selector 有返回值,一定要处理掉 手动释放(置为 nil)。
SEL selector;
  if (/* some condition */) {
  selector = @selector(newObject);
 } else if (/* some other condition */) {
  selector = @selector(copy);
 } else {
   selector = @selector(someProperty);
}
id ret = [object performSelector:selector];
  • performSelector afterDelay 延时操作

    • [self performSelector:@selector(method1:) withObject:self.tableLayer]的时候,系统会将tableLayer的引用计数加1,执行完这个方法时,还会将tableLayer的引用计数减1,有时切换场景时延时函数已经被调用但还没有执行,这时tableLayer的引用计数并没有减少到0,也就导致了切换场景dealloc方法没有被调用,出现了内存泄露。
    • 解决方法时在还没来得及执行的延时函数 [NSObject cancelPreviousPerformRequestsWithTarget:self]
  • 代理未清空引起野指针

    • 一般自己写的一些delegate,我们会用weak,而不是assign,weak的好处是当对应的对象被回收时,指针也会自动被设置为nil
  • 循环未结束

    • 如果某个ViewController中有无限循环,也会导致即使ViewController对应的view关掉了,ViewController也不能被释放。
    • 解决 -(void)viewWillDisappear:(BOOL)animated {[self.view.layer removeAllAnimations];}
  • ** try...catch 的使用**

    • 但如果 doSomethingMayThrowException 方法抛出了异常,那么 object 对象就无法释放。如果 object 对象持有了重要且稀缺的资源,就可能会造成严重后果。
  • 大次数循环内存暴涨问题

    • 该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

其他

1.Method Swizzle注意事项

  • 1.避免交换父类方法
    • 如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。 所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。
  • 2.交换方法应在+load方法 方法交换应当在调用前完成交换
  • 3.交换方法应该放到dispatch_once中执行
  • 4.交换的分类方法应该添加自定义前缀,避免冲突 这个毫无疑问
  • 5.交换的分类方法应调用原实现

2.属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗

  • property atomic 是采用 spinlock_t 也就是俗称的自旋锁实现的。
  • atomic通过这种方法,在运行时保证 set,get方法的原子性。也才仅仅是保证了set,get方法的原子性。这种线程是不安全的。

3.iOS 中内省的几个方法有哪些?内部实现原理是什么

- (BOOL)isKindOfClass:(Class)aClass; //判断是否是这个类或者这个类的子类的实例
+ (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass; //判断是否是这个类的实例
+ (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;  //判断是否遵守某个协议
+ (BOOL)conformsToProtocol:(Protocol *)protocol; //判断某个类是否遵守某个协议
- (BOOL)respondsToSelector:(SEL)aSelector;  //判读实例是否有这样方法
+ (BOOL)instancesRespondToSelector:(SEL)aSelector; //判断类是否有这个方法

内部实现原理

  • 1.isKindOfClass:
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
	
- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

判断是否是某个类或者是这个类的子类

  • 2.isMemberOfClass:
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

拿到isa指针对比

  • 3.conformsToProtocol:
+ (BOOL)conformsToProtocol:(Protocol *)protocol {
    if (!protocol) return NO;
    for (Class tcls = self; tcls; tcls = tcls->superclass) {
        if (class_conformsToProtocol(tcls, protocol)) return YES;
    }
    return NO;
}

- (BOOL)conformsToProtocol:(Protocol *)protocol {
    if (!protocol) return NO;
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (class_conformsToProtocol(tcls, protocol)) return YES;
    }
    return NO;
}
// 两个方法最终还是去isa->data()->protocols 拿到相关协议然后判断是否存在相关协议 如下代码:
BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen)
{
    protocol_t *proto = newprotocol(proto_gen);  
    if (!cls) return NO;
    if (!proto_gen) return NO;
    mutex_locker_t lock(runtimeLock);
    checkIsKnownClass(cls);
    ASSERT(cls->isRealized())
    for (const auto& proto_ref : cls->data()->protocols) {
        protocol_t *p = remapProtocol(proto_ref);
        if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) {
            return YES;
        }
    }
    return NO;
}
// 这里可以清晰的看到for循环 取出相关protocol指针 然后通过指针和传入的参数生成的`proto`对比
  • 4.respondsToSelector:
+ (BOOL)respondsToSelector:(SEL)sel {
    return class_respondsToSelector_inst(self, sel, self->ISA());
}

- (BOOL)respondsToSelector:(SEL)sel {
    return class_respondsToSelector_inst(self, sel, [self class]);
}

这个源码比较麻烦 我简单叙述一下吧 实际上调用栈比较深就是一直寻找到当前实例能响应哪些方法,当前类没有就去父类,父类没有则直到元类.

respondsToSelector:
	|__ class_respondsToSelector_inst()
		|__ lookUpImpOrNil()
			|__ lookUpImpOrForward()
				返回IMP结果

4.classobjc_getClassobject_getclass 方法有什么区别?

image.png

class:返回类对象 
Class objc_getClass(const char *aClassName) 根据类名返回对于的类对象 
Class object_getClass(id obj) 根据obj参数(instance对象、class对象、meta-class对象)返回类对象、元类对象、基元类对象