这篇文章可能有点长,所以分了三篇文章记录。内容来自网上的面试题以及自己面试过程中遇到的问题总结,也会定期更新,不合理的地方欢迎指正。
- 更多技术题合集:
提升iOS开发技能学习网址:docs.qq.com/doc/DVWlQam9Qd3B1cEF2
为自己的面试,为自己的跳槽,加油吧 iOS开发
一、分类和扩展
-
分类和扩展有什么区别?
category
1、分类给类添加方法
2、不能通过正常模式给类添加属性,但是可以通过 runtime 添加
3、如果在分类中通过@property定义属性,那么只会对属性的 getter setter 方法进行声明,不会实现。同时也不会生成带下划线的成员变量
4、在运行时才会编译代码
extension
1、扩展可以看成是特殊的分类 匿名分类
2、可以给类添加属性,私有
3、可以给类添加方法,也是私有
4、在编译时期就会编译,与 .h 文件中的@interface和.m文件里的@implement一起形成了一个完整的类
5、扩展一般用来隐藏类的信息,所以使用扩展的前提是要有类的源码!所以针对系统自带的类,是无法使用扩展的。
为什么分类可以添加方法,而不能添加成员变量???
因为在运行时,类的内部布局早已经确定,如果添加实例变量,会破坏类的内部布局。
-
分类的结构体里面有哪些成员?
category 是一个指向类结构体的指针,其结构体的定义如下:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
可以 与 objc_class 的结构体进行对比:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
经过对比,发现少了一个 struct objc_ivar_list *ivars
成员变量列表!!!这也就说明了 分类是不能添加成员变量的。
-
分类加载和方法调用顺序
1、 加载:先加载原始类的 load() 方法 ,再去加载 分类中的 load() 方法,如果有多个分类,则按照编译顺序加载
2、调用:先调用分类中的方法,再去调用原始类中的方法,如果要是重名,则会覆盖原始类中的方法(因为在方法列表中,分类的方法会排在原始类中同名方法的前面)。
二、atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
-
atomic
atomic
1、会对属性的 setter/getter 方法进行加锁,这仅仅只能保证在 操作 setter/getter 方法是安全的。不能保证其他线程的安全
2、例如 : 线程1调用了某一属性的setter方法并进行到了一半,线程2调用其getter方法,那么会执行完setter操作后,在执行getter操作,线程2会获取到线程1 setter后的完整的值.
3、当几个线程同时调用同一属性的setter、getter方法时,会get到一个完整的值,但get到的值不可控。例如 : 线程1 调用getter ,线程2 调用setter,线程3 调用setter,这3个线程并行同时开始,线程1会get到一个值,但是这个值不可控,可能是线程2,线程3 set之前的原始值,可能是线程2 set的值,也可能是线程3 set的值
-
atomic是线程安全的吗?
不是,很多文章谈到atomic和nonatomic的区别时,都说atomic是线程安全,其实这个说法是不准确的.atomic只是对属性的getter/setter方法进行了加锁操作,这种安全仅仅是set/get 的读写安全,并非真正意义上的线程安全,因为线程安全还有读写之外的其他操作
比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash
-
nonatomic
nonatomic
系统生成的getter/setter方法没有加锁线程不安全,但更快当多个线程同时访问同一个属性,会出现无法预料的结果
-
atomic的seter getter内部实现
- (void)setCurrentImage:(UIImage *)currentImage
{
@synchronized(self) {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
}
- (UIImage *)currentImage
{
@synchronized(self) {
return _currentImage;
}
}
-
nonatomic的seter getter内部实现
- (void)setCurrentImage:(UIImage *)currentImage
{
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
- (UIImage *)currentImage
{
return _currentImage;
}
三、被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
参考:https://www.jianshu.com/p/b93d61418f17
这个问题在 数据结构&&算法里面做了解答
四、关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将其指针置空么?
我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 @property 并不能在分类中正确创建实例变量和存取方法。这时候就会用到关联对象。
分类中的 @property
@interface DKObject : NSObject
@property (nonatomic, strong) NSString *property;
@end
在使用上述代码时会做三件事:
- 生成带下划线的实例变量 _property
- 生成 getter 方法 - property
- 生成 setter 方法 - setProperty:
@implementation DKObject {
NSString *_property;
}
- (NSString *)property {
return _property;
}
- (void)setProperty:(NSString *)property {
_property = property;
}
@end
这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里,我们既然可以在类中使用 @property
生成一个属性,那么为什么在分类中不可以呢?
我们来做一个小实验:创建一个 DKObject 的分类 Category,并添加一个属性 categoryProperty
:
@interface DKObject (Category)
@property (nonatomic, strong) NSString *categoryProperty;
@end
看起来还是很不错的,不过 Build 一下这个 Demo,会发现有这么一个警告:
在这里的警告告诉我们 categoryProperty 属性的存取方法
需要自己手动去实现,或者使用 @dynamic 在运行时实现这些方法。
换句话说,分类中的 @property 并没有为我们生成实例变量以及存取方法
,而需要我们手动实现。
使用关联对象
Q:我们为什么要使用关联对象?
A:因为在分类中 @property 并不会自动生成实例变量以及存取方法
,所以一般使用关联对象为已经存在的类添加『属性
』。
以下是与关联对象有关的 API,并在分类中实现一个伪属性:
#import "DKObject+Category.h"
#import <objc/runtime.h>
@implementation DKObject (Category)
- (NSString *)categoryProperty {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setCategoryProperty:(NSString *)categoryProperty {
objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
这里的
_cmd
代指当前方法的选择子,也就是@selector(categoryProperty)
。
我们使用了两个方法 objc_getAssociatedObject
以及 objc_setAssociatedObject
来模拟『属性』的存取方法,而使用关联对象模拟实例变量。
在这里有必要解释两个问题:
- 为什么向方法中传入
@selector(categoryProperty)?
-
OBJC_ASSOCIATION_RETAIN_NONATOMIC
是干什么的?
关于第一个问题,我们需要看一下这两个方法的原型:
id objc_getAssociatedObject(id object, const void *key);
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
@selector(categoryProperty)
也就是参数中的key
,其实可以使用静态指针 static void *
类型的参数来代替,不过在这里,笔者强烈推荐使用 @selector(categoryProperty)
作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性
。
OBJC_ASSOCIATION_RETAIN_NONATOMIC
又是什么呢?如果我们使用 Command 加左键查看它的定义:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
从这里的注释我们能看到很多东西,也就是说不同的 objc_AssociationPolicy 对应了不通的属性修饰符:
objc_AssociationPolicy | modifier |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, strong |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | atomic, strong |
OBJC_ASSOCIATION_COPY | atomic, copy |
而我们在代码中实现的属性 categoryProperty 就相当于使用了 nonatomic 和 strong 修饰符。
在obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除
五、KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
实现原理:
- 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
- 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
- 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
KVO与Notification之间的区别:
notification是需要一个发送notification的对象,一般是notificationCenter,来通知观察者。
KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。
六、Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
最外层池子的顶端会有一个next指针。当链表容量满了,就会在链表的顶端,并指向下一张表。
Autorelease对象什么时候释放?
这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
例子:
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"sunnyxx"]; // str是一个autorelease对象,设置一个weak的引用来观察它
reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference);
// Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference);
// Console: (null)
}
当然,我们也可以手动干预Autorelease对象的释放时机:
- (void)viewDidLoad
{
[super viewDidLoad];
@autoreleasepool { NSString *str = [NSString stringWithFormat:@"sunnyxx"];
} NSLog(@"%@", str);
// Console: (null)
}
Autorelease原理
AutoreleasePoolPage
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
AutoreleasePoolPage是一个C++实现的类
- AutoreleasePool并没有单独的结构,而是由若干个
AutoreleasePoolPage以双向链表
的形式组合而成(分别对应结构中的parent指针和child指针)。 - AutoreleasePool是按
线程一一对应
的(结构中的thread指针指向当前线程)。 - AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的
地址
。 - 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的
下一个位置
。 - 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。
所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:
图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
释放时刻
每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:
1.根据传入的哨兵对象地址找到哨兵对象所处的page
2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理)
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
嵌套的AutoreleasePool
知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
七、class_ro_t 和 class_rw_t 的区别?
Class的结构
class_rw_t
class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
class_ro_t
class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容