前言:
最近看到大佬汇集的iOS面试题,个人感觉还不错,打算试着探索一下这些问题的答案,也巩固一下我自己基础知识。这篇文章先总结一下基础知识的答案吧。其中有些错误或不全的地方望指教。
-----------------------------------------持续更新中---------------------------------
iOS 基础题
1:分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
分类和扩展的作用
1:category的主要作用是为已经存在的类添加方法
下面也有其他作用可以了解下:
2:可以把类的实现分开在几个不同的文件里面,
(可以减少单个文件的体积
可以把不同的功能组织到不同的category里
可以由多个开发者共同完成一个类
可以按需加载想要的category)
3:模拟多继承
4:把framework的私有方法公开
扩展的作用:为一个类添加额外的原来没有变量,方法和属性
类别与类扩展的区别
1:extension在编译期决定,它就是类的一部分,
在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,
它伴随类的产生而产生,亦随之一起消亡。
extension一般用来隐藏类的私有信息,
你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension
但是category则完全不一样,它是在运行时候决定的.
类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
extension可以添加实例变量,而category是无法添加实例变量的
2:类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。
分类局限性
(1)无法向类中添加新的实例变量。
(2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。
(3)如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法
在runtime层,category用结构体category_t
typedef 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;
} category_t;
从源码中我们可以看出分类结构体成员:
1)类的名字(name)
2)类(cls)
3)category中所有给类添加的实例方法的列表(instanceMethods)
4)category中所有添加的类方法的列表(classMethods)
5)category实现的所有协议的列表(protocols)
6)category中添加的所有属性(instanceProperties)
参考链接:
分类和扩展说明参考
美团关于分类的源码解析说明
官方分类源码地址
2:atomic的实现机制;为什么不能保证绝对的线程安全
这个问题我觉得看这个就够了stackoverflow关于atomic和nonatomic的一个问题
当然也可以看别人根据stackoverflow这个问题总结好的中文说明
简单来说:atomic 会加一个锁来保障线程安全,也就是保证了读写操作是安全的,并且引用计数会 +1,来向调用者保证这个对象会一直存在.
但是不能保证线程安全,比如当线程A setter操作时,这时B线程的setter操作会等待。当A线程的setter结束后,B线程进行setter操作,
然后当A线程需要getter操作时,却有可能获得了在B线程中的值,这就破坏了线程安全
3:哪些场景可以触发离屏渲染?
首先我们要知道什么是离屏渲染:
离屏渲染Off-Screen Rendering 指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染会先在屏幕外创建新缓冲区,离屏渲染结束后,再从离屏切到当前屏幕
还有另外一种屏幕渲染方式-当前屏幕渲染On-Screen Rendering ,
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
以下方式会触发离屏幕渲染
1:使用系统提供的圆角效果也会触发离屏渲染.(masksToBounds = true&&cornerRadius>0才会引发离屏渲染)
2:重写drawRect
3:layer.shadow(Shawdow 可以通过指定路径来取消离屏渲染)
4:layer.mask(Mask 效果无法取消离屏渲染,使用混合图层的方法来模拟 mask 效果,性能各方面都是和无效果持平。)
5:layer.allowsGroupOpacity(GroupOpacity 是指 CALayer 的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性,
开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。)
layer.allowsEdgeAntialiasing(该属性用于消除锯齿,离屏渲染条件旋转视图并且设置layer.allowsEdgeAntialiasing = true)
6:layer.shouldRasterize(光栅化会触发离屏渲染,开启 Rasterization=true 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过 100ms 没有使用会从缓存里移除,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,原本拖后腿的离屏渲染就成为了助力)
参考:
离屏渲染优化详解
Instruments性能优化-Core Animation
绘制像素到屏幕上
界面流畅性优化
4:被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录.
objc_clear_deallocating该函数的动作如下:
1、从weak表中获取废弃对象的地址为键值的记录
2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3、将weak表中该记录删除
4、从引用计数表中删除废弃对象的地址为键值的记录
SideTable 这个结构体主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:
struct SideTable {
spinlock_t slock;//保证原子操作的自旋锁
RefcountMap refcnts;//引用计数的 hash 表
weak_table_t weak_table;//weak 引用全局 hash 表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
参考:
objc-weak.mm源码
weak 弱引用的实现方式
iOS 底层解析weak的实现原理
5:KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,
对象就神奇的变成了新创建的子类的实例
关闭默认的KVO重写方法
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}//如果返回NO,KVO无法自动运作,需手动触发
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey
: 和 didChangevlueForKey
。
在一个被观察属性发生改变之前, willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后,
observeValueForKey:ofObject:change:context:
会被调用,
并且 didChangeValueForKey:
也会被调用。如果可以手动实现这些调用,就可以实现手动触发
.
参考:
如何自己动手实现 KVO
apple用什么方式实现对一个对象的KVO
6:一个int变量被__block修饰与否的区别?
Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。
__block 所起到的作用就是只要观察到该变量被 block 所持有。
__block 后,实际上成为了一个结构体,block内截获了 该结构体的指针。
在block中使用自动变量时,使用的是 指针指向的结构体中的 自动变量。
ARC环境下,会被copy到堆上。(ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。
ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。)
MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上。
测试,其实最好的方法是动手测试,这边我只测试了ARC环境下的。我在.main.m
的测试代码如下:
__block int a1 = 1;
int a2 = 1;
NSLog(@"__block定义前a1:%p", &a1);
NSLog(@"__block定义前a2:%p", &a2);;
void (^foo)(void) = ^{
a1 = 2;
NSLog(@"block内部a1:%p", &a1);
NSLog(@"block内部a2:%p", &a2);
};
NSLog(@"重新定义后a1:%p", &a1);
NSLog(@"重新定义后a2:%p", &a2);
NSLog(@"foo =%@",foo);
foo();
——---------------------- 输出结果如下:-------------------------------
__block定义前a1:0x7fff53814128
__block定义前a2:0x7fff5381410c
重新定义后a1:0x60400003dd98
重新定义后a2:0x7fff5381410c
foo =<__NSMallocBlock__: 0x60c000244830>
block内部a1:0x60400003dd98
block内部a2:0x60400025dbd8
通知打印结果可以发现a1,a2blcok内部和定义前的地址字节数相差很大,堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,所以a1和a2在block内部都会被copy到堆上,只不过一个值的copy,一个是地址copy。
然后clang -rewrite-objc main.m
查看一下源码,如果clang -rewrite-objc
报错,可以像我一样尝试
xcrun -sdk iphonesimulator11.0 clang -rewrite-objc main.m
源码如下:
//加上__block 后,实际上成为了一个结构体,block内截获了 该结构体的指针
struct __Block_byref_a1_0 {
void *__isa;
__Block_byref_a1_0 *__forwarding;
int __flags;
int __size;
int a1;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a2;
////截获的结构体指针
__Block_byref_a1_0 *a1; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a2, __Block_byref_a1_0 *_a1, int flags=0) : a2(_a2), a1(_a1->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//指针引用
__Block_byref_a1_0 *a1 = __cself->a1; // bound by ref
//a2只是单纯的值拷贝,。Block仅仅捕获了a2的值,并没有捕获a2的内存地址。
int a2 = __cself->a2; // bound by copy
(a1->__forwarding->a1) = 2;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_2, &(a1->__forwarding->a1));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_3, &a2);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a1, (void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}
从源码中可以看出:
带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,很清楚看到了__block的引用过程。
而Block仅仅捕获了a2的值,并没有捕获a2的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量a2的值,
也无法改变Block外面自动变量a2的值
参考:
iOS中__block 关键字的底层实现原理
深入研究Block捕获外部变量和__block实现原理
7:为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰
_weak是为了解决循环引用问题,(如果block和对象相互持有就会形成循环引用)
而__strong在Block内部修饰的对象,会保证,在使用这个对象在block内,
这个对象都不会被释放,strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。
__strong主要是用在多线程中,防止对象被提前释放。
参考:
iOS __weak和__strong在Block中的使用
题外话:
有时候我们经常也会被问到block为什么 常使用copy关键字?
官方中有如下一段话:
总结别人的话来说:
block 使用
copy
是从 MRC遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。
如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”
8:讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里.
对象isa指向类对象,类对象的isa指向元类。元类isa指向根元类。
根元类的isa指针指向自己,superclass指针指向NSObject类
实例对象结构体只有一个isa变量,指向实例对象所属的类。
类对象有isa,superclass,方法,属性,协议列表,以及成员变量的
描述。
所有的对象调用方法都是一样的,没有必要存在对象中,对象可以有
无数个,类对象就有一个所以只需存放在类对象中
可以从官方objc.h源码里面找到实例定义
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
可以在runtime.h里面找到类对象的定义
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//向该类所继承的父类对象
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
//成员变量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;//方法列表
//用于缓存调用过的方法
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
//协议链表用来存储声明遵守的正式协议
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
}
参考:
iOS开发·runtime原理与实践: 基本知识篇
一个objc对象如何进行内存布局
9:iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?
题外话:原谅我看了这道面试题,第一次听说内省,才疏学浅,太菜了,只能好好搜索学习了一番。
内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法,用于查询运行时信息,以便根据对象的特征进行识别。
isKindOfClass:Class
检查对象是否是那个类或者其继承类实例化的对象
isMemberOfClass:Class
检查对象是否是那个类但不包括继承类而实例化的对象
respondToSelector:selector
检查对象是否包含这个方法
conformsToProtocol:protocol
检查对象是否符合协议,是否实现了协议中所有的必选方法。
object_getClass(obj)返回的是obj中的isa指针;
而[obj class]则分两种情况:
一:当obj为实例对象时,
[obj class]中class是实例方法:- (Class)class,
返回的obj对象中的isa指针,返回的是类对象;
二:当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身
可以在ViewController
通过简单代码验证一下
//currentClass现在是类对象
Class currentClass = [self class];
//都指向实例对象isa指定的类对象
NSLog(@"currentClass = %p getClass=%p",currentClass ,object_getClass(self));
//class指向类对象本身 getClass指向类对象isa指向元类
NSLog(@"currentClass = %p getClass=%p",[currentClass class],object_getClass(currentClass));
const char *getClassName = object_getClassName(currentClass);
//实例对象指向类,类执行元类,元类指向根元类,根元类指向自己
for (int i = 1; i < 5; i++) {
NSLog(@"Following the isa pointer %d times gives %p %@---%s", i, currentClass,currentClass,getClassName);
currentClass = object_getClass(currentClass);
getClassName = object_getClassName(currentClass);
}
输出结果如下:
currentClass = 0x10ab29198 getClass=0x10ab29198
currentClass = 0x10ab29198 getClass=0x10ab291c0
Following the isa pointer 1 times gives 0x10ab29198 ViewController---ViewController
Following the isa pointer 2 times gives 0x10ab291c0 ViewController---NSObject
Following the isa pointer 3 times gives 0x10b819e58 NSObject---NSObject
Following the isa pointer 4 times gives 0x10b819e58 NSObject---NSObject
参考
Objective-C的内省(Introspection)小结
10:RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
这一块平时用的比较少,了解不是很多。其有时间真的好好静下心来看一下相关东西了。
字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。
线程和 RunLoop 之间是一一对应的。
运行机制从官方文档说明
翻译过来如下:
1.通知观察者将要进入运行循环。
2.通知观察者将要处理计时器。
3.通知观察者任何非基于端口的输入源即将触发。
4.触发任何准备触发的基于非端口的输入源。
5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。
6.通知观察者线程即将睡眠。
7.将线程置于睡眠状态,直到发生以下事件之一:
- 事件到达基于端口的输入源。
- 计时器运行。
- 为运行循环设置的超时值到期。
- 运行循环被明确唤醒。
8.通知观察者线程被唤醒。
9.处理待处理事件。
- 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
- 如果输入源被触发,则传递事件。
- 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
10.通知观察者运行循环已退出。
这里借用一下这里的图片
参考
深入理解RunLoop
关于Runloop的原理探究及基本使用
11:谈谈消息转发机制实现
先会调用
objc_msgSend
方法,首先在Class中的缓存查找IMP,没有缓存则初始化缓存。如果没有找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,则执行消息转发。
1、调用resolveInstanceMethod:
方法。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程。这次对象会响应这个选择器,一般是因为它已经调用过了class_addMethod。如果仍没有实现,继续下面的动作。
2、调用forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作。注意这里不要返回self,否则会形成死循环。
3、调用methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil;传给一个NSInvocation并传给forwardInvocation:
。
4、调用forwardInvocation:
方法,将第三步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。
5、调用doesNotRecognizeSelector
:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤 。
参考:
Objective-C 消息发送与转发机制原理
深入浅出理解消息的传递和转发机制
-----------------------------------------未完待续-----------------------------------