iOS 转某博文阿里、头条面试
链接附上
https://www.jianshu.com/p/e87e0be2281f
以下对每道题做出我的理解,如有不对的地方请各位指正,共同进步
结构模型
介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
在Objective-C中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有isa指针。Class 是一个 objc_class 结构类型的指针, id是一个 objc_object 结构类型的指针。
每一个实例对象,其 isa 指针指向其类类对象,类对象的 isa 指针指向 metaclass,类对象的 superclass 指针指向其父类,一直指向根类对象,根类对象的 superclass 指针指向 nil;metaclass 的 isa 指针都指向 rootMetaclass, 而 rootMetaClass 的 isa 指针指向自己,其 superclass指针指向根类对象。依次来达到一个程序的闭合。实例对象存储的自身的值,而类对象存储的则是实例对象的方法列表、成员变量、协议等,根类对象存储的是类对象的方法。
为什么要设计metaclass
符合程序设计的原则,单一职责原则
class_copyIvarList & class_copyPropertyList区别
class_copyIvarList:可以获取类的属性列表以及成员变量列表
class_copyPropertyList:只能获取类的属性列表
class_rw_t 和 class_ro_t 的区别
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结构体的地址。
他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容
category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序
把分类的 实例方法、属性、协议 添加到类的实例对象中原本存储的 实例方法、属性、协议列表 的 前面 ;
把分类的类方法和协议添加到类的元类上。
这么做的目的就是保证分类方法 优先调用,注意,不是覆盖,而是共同存在在实例方法列表中,只是分类在前而已。
分类间的加载顺序取决于编译的顺序:编译在前则先加载,编译在后则后加载
category & extension区别,能给NSObject添加Extension吗,结果如何
- 分类
运行时决议
可以为系统类添加分类
添加实例方法
类方法
协议
属性 (只声明了 get 和 set 方法,但是却没有添加实例变量)- 拓展
编译时决议
只以声明的形式存在
不能为系统类添加拓展
声明私有属性
声明私有方法
声明私有成员变量
分类可以有声明,可以有实现
消息转发机制,消息转发机制和其他语言的消息机制优劣对比
https://juejin.im/post/5ae96e8c6fb9a07ac85a3860
在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么
- 首先检查这个selector是不是要忽略。比如Mac OS X开发,有了垃圾回收就不会理会retain,release这些函数。
- 检测这个selector的target是不是nil,OC允许我们对一个nil对象执行任何方法不会Crash,因为运行时会被忽略掉。
IMP、SEL、Method的区别和使用场景
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
在文档中,selector的定义都是这样声明,也就是说:selector是SEL的一个实例,只是在iOS中,selector的使用是如此的频繁,我们才会把他当成一个概念。
selector怎么理解呢?我们可以想想股票,比如市场上有如此多公司在纳斯达克上市,而且他们的名字又非常的长,或者有些公司的名称也是相似的,都是**有限公司。那当市场去指定一个股票的时候,效率会非常低,当你着急想买股票的时候,你会跟你的经纪人说:“hi,peter,给我买一百股Tuniu limited liability company的股票吗?”,也许等你说完,经纪人输入完,市场就变化了,所以纳斯达克通常用代码,比如“TOUR”.这里的selector有类似的作用,就是让我们能够快速找到对应的函数。
/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif
这个就比较好理解了,就是指向最终实现程序的内存地址的指针。
综上,在iOS的runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。
https://njafei.github.io/2017/05/03/Method-SEL-IMP/
load、initialize方法的区别什么?在继承关系中他们有什么区别
- 以main为分界,load方法在main函数之前执行,initialize在main函数之后执行
- load和initialize会被自动调用,不能手动调用它们。
- 子类实现了load和initialize的话,会隐式调用父类的load和initialize方法
- load和initialize方法内部使用了锁,因此它们是线程安全的。
- 子类中没有实现load方法的话,不会调用父类的load方法;而子类如果没有实现initialize方法的话,也会自动调用父类的initialize方法。
- load方法是在类被装在进来的时候就会调用,initialize在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不回调用到initialize方法。
load 调用顺序如下:
- 父类load先于类添加到loadable_classes列表,通过call_class_loads,调用列表中的load方法,这样父类的load先于类的load执行
当loadable_classes为空的时候,查看loadable_classes是否为空,如果不为空则调用call_category_loads加载分类中的load方法,这样分类的load在类之后执行
initialize调用顺序
- initialize 只会在对应类的方法第一次被调用时,才会调用,initialize 方法是在 alloc 方法之前调用的,alloc 的调用导致了前者的执行。
- initialize的调用栈中,直接调用其方法的其实是_class_initialize 这个C语言函数,在这个方法中,主要是向为初始化的类发送+initialize消息,不过会强制父类先发送。
- 与 load 不同,initialize 方法调用时,所有的类都已经加载到了内存中。
说说消息转发机制的优劣
优点:
优雅的消息传递机制
动态特性
Category
缺点:
不支持命名空间
蹩脚的KVO
蹩脚的多态
Runtime 的各种黑魔法
引用计数的内存管理方式(会有循环引用)
鬼畜的布尔类型
轻量的面向对象特性 (由于支持C中的东西,装入OO的容器需要wrap)
内存管理
weak的实现原理?SideTable的结构是什么样的
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
weak 的实现原理可以概括一下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
}
关联对象的应用?系统如何实现关联对象的
关键对象多用于在分类,因为分类中,不会存储实例变量,而我们要把实例变量关联进去,那么就需要关联对象技术了。具体代码如下:
-(void)setName:(NSString *)name {
// 设置 value,通过 key 和 value 建立映射,通过Policy策略将映射关系关联到设置的对象上
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name {
// 根据指定的 key,到 obj 中找到与当前 key 相对应的关联值
return objc_getAssociatedObject(self, @"name");
}
https://www.jianshu.com/p/32084d47963d
关联对象的如何进行内存管理的?关联对象如何实现weak属性
系统通过调用 objc_setAssociate的方法将对象包裹成一个 ObjcAssociation对象,将 value 和 policy,存进 ObjcAssociation对象中,然后将 ObjcAssociation 这个对象作为 value 存进 ObjcAssociationMap 中,在ObjcAssociationMap中有一个 selector,最终将ObjcAssociationMap存进AssociationsManagerHashMap 中,在AssociationsManagerHashMap中,有一个 disguise 指针指向我们的ObjcAssociationMap这样就完成对象的关联了。其内存管理也是通过引用计数进行管理的,释放的时候不需要我们手动释放,dealloc 方法内部会进行判断
Autoreleasepool的原理?所使用的的数据结构是什么
以栈为节点通过双向链表组合而成且和线程一一对应
// @autoreleasepool 经编译器改写会变成
void *ctx = autoreleasepoolPush()
{}
autoreleasepoolpop(ctx)
// autoreleasepoolPush() 内部会调用 autoreleasepoolPage::Push
// autoreleasepoolPage 的数据结构是
struct autoreleasePoolPage{
id *next;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
pthread_t const thread;
}
ARC的实现原理?ARC下对retain & release做了哪些优化
利用 llvm 和 runtime协同完成自动引用计数的管理。
这个问题太大,一时之间竟然想不出来怎么回答为好,所以还是从 关键字 strong、weak、autorelease 实现原理入手吧。
https://juejin.im/post/5ce2b7386fb9a07eff005b4c
ARC下哪些情况会造成内存泄漏
- 循环引用
- 悬垂指针
- 大循环引用
- 使用 CoreFoundation 框架未手动管理内存
- NSTimer
- performSelector的内存问题
https://www.jianshu.com/p/18d827d62fb8
其他
Method Swizzle注意事项
- 第一个风险是,需要在 +load 方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。
- 第二个风险是,被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。
- 第三个风险是,交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd。
- 第四个风险是,方法交换命名冲突。如果出现冲突,可能会导致方法交换失败。
属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗
atomic 内部是通过自旋锁的方式实现的,在其 set、get 方法的内部加入了自旋锁,所以 atomic 仅仅只能保证在set、get 的线程安全。当然仅仅只限于当前线程,如果在多个线程同时操作 set 方法,那么在其他线程访问的该值就是已经更新过后的,所以不能保证线程安全。
iOS 中内省的几个方法有哪些?内部实现原理是什么
判断对象类型:
-(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例
判断对象or类是否有这个方法
-(BOOL) respondsToSelector: 判读实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法
实现原理:
- (BOOL)isMemberOfClass:(Class)cls {
// 直接获取实例类对象并判断是否等于传入的类对象
return [self class] == cls;
}
- (BOOL)isKindOfClass:(Class)cls {
// 向上查询,如果找到父类对象等于传入的类对象则返回YES
// 直到基类还不相等则返回NO
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
class、objc_getClass、object_getclass 方法有什么区别?
- 1.当参数obj为Object实例对象
object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。 - 2.当参数obj为Class类对象
object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。 - 3.当参数obj为Metaclass类对象
object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。 - 4.obj为Rootclass类对象
object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。 - 总结:
经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。
NSNotification相关
苹果并没有开源相关代码,但是可以读下GNUStep的源码,基本上实现方式很具有参考性