iOS基础题
1. 分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
- 分类主要用来为某个类添加方法,属性,协议(我一般用来给系统的类添加方法或者把某个复杂的类按照功能拆分到不同的文件里)
- 扩展主要用来为某个类添加成员变量、属性、方法声明。(我一般用扩展来声明私有属性,或者把.h的只读属性重写成可读写的)
分类和扩展的区别:
- 分类是在运行时把分类信息合并到类信息中,而扩展是在编译时,就把信息合并到类中的
- 分类声明的属性,只会生成getter/setter方法的声明,不会自动生成成员变量和getter/setter方法的实现,而扩展会
- 分类不可用来为类添加实例变量,而扩展可以
- 分类可以为类添加方法的实现,而扩展只能声明方法,而不能实现
分类的局限性:
- 无法为类添加实例变量,但可以通过关联对象进行实现,注:关联对象中内存管理没有weak,用时需要注意野指针的问题,可以通过其他办法来实现,具体可参考iOS weak 关键字漫谈
- 分类的方法若和类中原本的实现重名,会覆盖原本方法的实现,注:并不是真正的覆盖
- 多个分类的方法重名,会调用最后编译的那个分类的实现
分类的结构体有哪些成员:
struct category_t {
const char *name; //名字
classref_t cls; //类的引用
struct method_list_t *instanceMethods;//实例方法列表
struct method_list_t *classMethods;//类方法列表
struct protocol_list_t *protocols;//协议列表
struct property_list_t *instanceProperties;//实例属性列表
// 此属性不一定真正的存在
struct property_list_t *_classProperties;//类属性列表
};
2. 讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
atomic的实现机制:
- atomic是property的修饰词之一,表示是原子性的,使用方式为
@property(atomic) int age;
,此时编译器会自动生成getter/setter方法,最终会调用objc_getProperty
和objc_setProperty
方法来进行存取属性。若此时属性用atomic
修饰的话,会在这两个方法的内部使用os_unfair_lock
来进行加锁,来保证读写的原子性。锁都在PropertyLocks中保存着(在iOS平台会初始化8个,mac平台64个),在用之前,会把锁都初始化好,在需要用到时,用对象的地址加上成员变量的偏移量为key,去PropertyLocks中去取。因此存取时用的时同一个锁,所以atomic能保证属性的存取时是线程安全的。注:由于锁是有限的,不同对象,不同属性的读取用的也可能是同一个锁
atomic为什么不能保证绝对的线程安全:
- atomic在getter/setter方法中加锁,仅保证了存取时的线程安全,假设我们的属性是
@property(atomic) NSMutableArray *array;
可变容器时,无法保证对容器的修改时线程安全的 - 在编译器自动产生的getter/setter方法,最终会调用
objc_getProperty
和objc_setProperty
方法来进行存取属性,在此方法内部保证了读写时的线程安全,当我们重写setter/getter方法时,就只能依靠自己在getter/setter中保证线程安全
3. 被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
被weak修饰的对象在被释放的时候会发生什么:
- 会把weak指针自动置为nil
weak是如何实现的:
- runtime会把被weak修饰的对象放到一个全局的哈希表中,用weak修饰的对象的内存地址为key,weak指针为值,在对象进行销毁时,通过对象自身地址去哈希表中查找到所有指向此对象的weak指针,并把所有的weak指针置为nil
sideTable的结构:
struct SideTable {
spinlock_t slock;//操作SideTable时用到的锁
RefcountMap refcnts;//引用计数器的值
weak_table_t weak_table;//存放weak指针的哈希表
};
4.关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
关联对象有什么应用:
- 一般用于在分类中给类添加实例变量
系统如何管理关联对象:
- 首先系统中有一个全局
AssociationsManager
,里面有个AssociationsHashMap
哈希表,哈希表中的key时对象的内存地址,value是ObjectAssociationMap
,也是一个哈希表,其中key是我们设置关联对象所设置的key,value是ObjcAssociation
,里面存放着关联对象设置的值和内存管理的策略。以void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
为例,首先会通过AssociationManager
获取AssociationsHashMap
,然后以object
的内存地址为key,从AssociationsHashMap
中取出ObjectAssociationMap
,若没有,则新创建一个,然后通过key获取旧值,以及通过key和policy生成新值objcAssociation(policy, new_value)
,把新值存放到ObjectAssociationMap
中,若新值不为nil
,并且内存管理策略为retain
,则会对新值进行一次retain
,若新值为nil
,则会删除旧值,若旧值不为空并且内存管理的策略是retain
,则对旧值进行一次release
其被释放的时候需要手动将所有的关联对象的指针置空么:
- 不需要,因为在对象的
dealloc
中,若发现对象有关联对象时,会调用_object_remove_associations
方法来移除所有的关联对象,并根据内存策略,来判断是否需要对关联的对象的值进行release
5. KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
KVO的底层实现:
- 当某个类的属性被观察时,系统会在运行时动态的创建一个该类的子类,并且将对象的
isa
指向这个子类 - 假设被观察的属性名是
name
,若父类里有setName:
或者_setName:
,那么在子类里重写这两个方法,若两个方法同时存在,则只会重写setName:
一个(这里和KVC的set搜索时的顺序时一样的) - 若被观察的类型是
NSString
,那么重写的方法的实现会指向_NSSetObjectValueAndNotify
这个函数,若是Bool
类型,那么重写的方法的实现会指向_NSSetBoolValueAndNotify
这个函数,这个函数里会调用willChangeValueForKey:
和didChangevlueForKey:
,并且会在这两个方法调用之间,调用父类set方法的实现 - 系统会在
willChangeValueForKey:
对observe里的change[old]赋值,取值是用valueForKey:
取值的,didChangevlueForKey:
对observe里的change[new]赋值,然后调用observe的这个方法- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary
*)change context:(nullable void *)context; - 当使用KVC赋值的时候,在
NSObject
里的setValue:forKey:
方法里,若父类不存在setName:
或这_setName:
这些方法,会调用_NSSetValueAndNotifyForKeyInIvar
这个函数,这个函数里同样也会调用willChangeValueForKey:
和didChangevlueForKey:
,若存在则调用
如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO):
举例:取消Person
类age
属性的默认KVO,设置age
大于18时,手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setAge:(NSInteger)age {
if (age > 18 ) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
} else {
_age = age;
}
}
6. Autoreleasepool
所使用的数据结构是什么?AutoreleasePoolPage
结构体了解么?
Autoreleasepool是由多个AutoreleasePoolPage以双向链表的形式连接起来的,
Autoreleasepool的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,在此期间,当有对象调用autorelsease时,会把对象添加到AutoreleasePoolPage中,若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位。
AutoreleasePoolPage结构如下:
class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一个存放autorelease对象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
uint32_t hiwat;
}
7. 讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的:
-
对象的结构体里存放着isa和成员变量,isa指向类对象。
类对象的isa指向元类,元类的isa指向NSObject的元类。
类对象和元类的结构体有isa、superclass、cache、bits,bits里存放着class_rw_t的指针。
放一张经典的图
为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里:
- 方法是每个对象互相可以共用的,如果每个对象都存储一份方法列表太浪费内存,由于对象的isa是指向类对象的,当调用的时候,直接去类对象中查找就行了。可以节约很多内存空间的
8. class_ro_t
和class_rw_t
的区别?
从字面上理解,class_ro_t
是只读,class_rw_t
可写可读。这两个变量共同点都是存储类的属性、方法、协议等信息的,不同的有两点:1、class_ro_t
还存储了类的成员变量,而class_rw_t
则没有,从这方面也验证了类的成员变量一旦确定了,就不能写了,就是分类不能增加成员变量的原因;2、class_ro_t
是在编译期就确定了固定的值,在整个运行时都只读不可写的状态,在运行时调用realizeClass
方法将class_ro_t
复制到class_rw_t
对应的变量上去。
9. iOS中内省的几个方法?class
方法和objc_getClass
方法有什么区别?
什么是内省:
在计算机科学中,内省是指计算机程序在运行时(Run time)检查对象(Object)类型的一种能力,通常也可以称作运行时类型检查。不应该将内省和反射混淆。相对于内省,反射更进一步,是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。
iOS中内省的几个方法:
-
isMemberOfClass
//对象是否是某个类型的对象 -
isKindOfClass
//对象是否是某个类型或某个类型子类的对象 -
isSubclassOfClass
//某个类对象是否是另一个类型的子类 -
isAncestorOfObject
//某个类对象是否是另一个类型的父类 -
respondsToSelector
//是否能响应某个方法 -
conformsToProtocol
//是否遵循某个协议
class
方法和object_getClass
方法有什么区别:
- 实例
class
方法就直接返回object_getClass(self)
,类class
方法直接返回self
,而object_getClass(类对象)
,则返回的是元类
10. 在运行时创建类的方法objc_allocateClassPair
的方法名尾部为什么是pair(成对的意思)?
因为此方法会创建一个类对象以及元类,正好组成一队
Class objc_allocateClassPair(Class superclass, const char *name,
size_t extraBytes){
...省略了部分代码
//生成一个类对象
cls = alloc_class_for_subclass(superclass, extraBytes);
//生成一个类对象元类对象
meta = alloc_class_for_subclass(superclass, extraBytes);
objc_initializeClassPair_internal(superclass, name, cls, meta);
return cls;
}
11. 一个int
变量被__block
修饰与否的区别?
int
变量被__block
修饰之后会生成一个结构体,复制int
的引用地址。达到修改数据,如__block int age
会被包装成下面这样:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding; //指向自己
int __flags;
int __size;
int age;//包装的具体的值
};
// age = 20;会被编译成下面这样
(age.__forwarding->age) = 20;
12. 为什么在block外部使用__weak
修饰的同时需要在内部使用__strong
修饰?
用__weak
修饰之后block
不会对该对象进行retain
,只是持有了weak
指针,在block
执行之前或执行的过程时,随时都有可能被释放,将weak
指针置位nil
,产生一些未知的错误。在内部用__strong
修饰,会在block
执行时,对该对象进行一次retain
,保证在执行时若该指针不指向nil
,则在执行过程中不会指向nil
。但有可能在执行执行之前已经为nil
了。
13. RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)?
什么是RunLoop:
- 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出。这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
RunLoop的作用是什么:
- 保持程序的持续运行,在iOS线程中,会在
main
方法给主线程创建一个RunLoop,保证主线程不被销毁 - 处理APP中的各种事件(如
touch
,timer
,performSelector
等) - 界面更新
- 手势识别
-
AutoreleasePool
- 系统在主线程RunLoop注册了2个
observer
- 第一个
observe
监听即将进入RunLoop,调用_objc_autoreleasePoolPush()
创建自动释放池 - 第二个
observe
监听两个事件,进入休眠之前
和即将退出RunLoop
- 在进入休眠之前的回调里,会先释放自动释放池,然后在创建一个自动释放池
- 在即将退出的回调里,会释放自动释放池
- 系统在主线程RunLoop注册了2个
- 线程保活
- 监测卡顿
RunLoop的内部逻辑:
14. 哪些场景可以触发离屏渲染?(知道多少说多少)
- 添加遮罩
mask
- 添加阴影
shadow
- 设置圆角并且设置
masksToBounds
为true
- 设置
allowsGroupOpacity
为true
并且layer.opacity
小于1.0
和有子layer
或者背景不为空 - 开启光栅化
shouldRasterize = true