本篇内容不作为任何题目的解答,仅仅是个人学习记录,如有错误还请指正。
iOS基础必备
1:讲讲你对noatomic & nonatomic的理解
atomic
的seter
/getter
内部实现是用了互斥锁来保证seter
/getter
在多线程中的安全,但atomic
修饰的对象是自定义的,可能并没有加锁,在多线程中atomic
修饰对象并不能保证线程安全。
nonatomic
的setter
/getter
方法的实现并没有加互斥锁,所以nonatomic
修饰的对象是非线程安全的,同时nonatomic
的setter
/getter
方法也是非线程安全的,但也正因为没有互斥锁所以性能要比atomic
好。
(举例:当多个线程同时调用同一属性的读取方法时,线程1会get
到一个确定的值,但是get
的值不可控,可能是线程2或者线程3...set
之后的值,也可能是之前的值)
2:被 weak 修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable 么?里面的结构可以画出来么?
被weak
修饰的对象在被释放时候会被自动置为nil
。
runtime
维护了一个weak
表,用于存储指向某个对象的所有weak
指针。weak
表其实是一个hash
哈希表,Key
是所指对象的地址,Value
是weak
指针的地址数组(这个地址的值是所指对象指针的地址)。
1、初始化时:runtime
会调用objc_initWeak
函数,初始化一个新的weak
指针指向对象的地址。
2、添加引用时:objc_initWeak
函数会调用 objc_storeWeak()
函数, objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有weak
指针地址的数组,然后遍历这个数组把其中的数据设为nil
,最后把这个entry
从weak
表中删除,最后清理对象的记录。
在runtime
内存空间中,SideTables
是一个64个元素长度的hash
数组,里面存储了SideTable
。SideTables
的hash
键值就是一个对象obj
的address
。
因此可以说,一个obj
,对应了一个SideTable
。但是一个SideTable
,会对应多个obj
。因为SideTable
的数量只有64个,所以会有很多obj
共用同一个SideTable
。而在一个SideTable
中,又有两个成员,分别是RefcountMap refcnts
(引用计数的 hash
表,其key
是obj
的地址,而value
,则是obj
对象的引用计数)和weak_table_t weak_table
(weak
引用全局hash
表),weak_table
则存储了弱引用obj
的指针的地址,其本质是一个以obj
地址为key
,弱引用obj
的指针的地址作为value
的hash
表。hash
表的节点类型是weak_entry_t
。

3:block 用什么修饰?strong 可以?
因为block
变量默认是声明为栈变量的,为了能够在block
的声明域外使用,所以要把block
copy
到堆,所以说为了block
属性声明和实际的操作一致,block
用copy
修饰。
对于strong
修饰符:
首先,在OC
中的三种类型的block
:
_NSConcreateGlobalBlock
全局的静态block
,不会访问任何外部变量。_NSConcreateStackBlock
保存在栈中的block
,当函数返回时会被销毁。_NSConcreateMallocBlock
保存在堆区的block
,当引用计数为0时被销毁。
在MRC
环境下,block
存放在全局区、堆区、栈区,ARC
环境下只存放在全局区、堆区(栈区block
,在ARC
情况下会自动拷贝到堆区)。
所以在MRC
环境下,如果block
要访问外部变量,就必须用copy
修饰,而在ARC
环境下,如果block
访问了外部变量,系统会自动将栈区block
拷贝到堆区,所以也可以使用strong
。只不过copy
是MRC
遗留下来的,习惯而已
4:block 为什么能够捕获外界变量? __block做了什么事?
block
中使用到的局部变量,都会在编译时动态创建的block
结构体中创建一个与局部变量名称一样的实例变量,该实例变量存储着外部的局部变量的值,而当执行block
时,再将这里存储下来的值取出来。
__block
所起到的作用就是只要观察到该变量被block
所持有,就将外部变量在栈中的内存地址拷贝堆中。__block
将变量包装成对象,然后在把捕获到的变量封装在block
的结构体里面,block
内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。
5:谈谈你对事件的传递链和响应链的理解
传递链:
(1)发生触摸事件后,系统会利用Runloop
将该事件加入到一个由UIApplication
管理的队列事件中.
(2)UIApplication
会从事件队列中取出最前面的事件,并将事件分发下去处理,发送事件给应用程序的主窗口UIWindow
.
(3)主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
(4)遍历找到合适的视图控件后,就会调用视图控件的touches
方法来处理事件:touchesBegin
...此子控件就是我们需要找到的第一响应者
响应链:
(1)判断第一响应者能否响应事件,如果第一响应者能进行响应,则事件在响应链中的传递终止。如果第一响应者不能响应,则将事件传递给 nextResponder
也就是通常的superview
进行事件响应
(2)如果事件继续上传至UIWindow
并且无法响应,它将会把事件继续上报给UIApplication
(3)如果事件继续上报至UIApplication
并且也无法响应,它将会将事件上报给其Delegate
(4)如果最终事件依旧未被响应则会被系统抛弃

注: 穿透事件,扩大响应区域。
6:谈谈 KVC 以及 KVO 的理解?
KVC
(key-value-coding
)键值编码,是一种间接访问实例变量的方法。提供一种机制来间接访问对象的属性。
1、给私有变量赋值。
2、给控件的内部属性赋值(如自定义UITextFiled
的clearButton
,或placeholder
的颜色,一般可利用runtime
获取控件的内部属性名,Ivar *ivar = class_getInstanceVariable
获取实例成员变量)。
[textField setValue:[UIColor redColor] forKeyPath:@"placeholderLabel.textColor"];
3、结合Runtime
,model
和字典的转换(setValuesForKeysWithDictionary
,class_copyIvarList
获取指定类的Ivar
成员列表)
KVO
是一种基于KVC
实现的观察者模式。当指定的被观察的对象的属性更改了,KVO
会以自动或手动方式通知观察者。

监听 ScrollView
的 contentOffSet
属性
[scrollview addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
7:RunLoop 的作用是什么?它的内部工作机制了解么?
RunLoop
基本作用:
(1).使程序一直运行并接受用户输入:
程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop
,RunLoop
保证主线程不会被销毁,也就保证了程序的持续运行。
(2)决定程序在何时应该处理哪些Event
比如:touches
事件,timer
事件,selector
事件
(3)节省CPU
时间 程序运行起来,没有任何事件源的时候,RunLoop
会告诉CPU
,要进入休眠状态,这时CPU
就会将其资源释放出来去做其他的事情,当有事件源来的时候RunLoop
就会被唤醒处理事件源。

8:苹果是如何实现 autoreleasepool的?
autoreleasepool
以一个队列数组的形式实现,主要通过下列三个函数完成:
(1).objc_autoreleasepoolPush
(2).objc_autoreleasepoolPop
(3).objc_autorelease
通过函数名就可以知道,对autorelease
分别执行push
,和pop
操作。销毁对象时执行release
操作。
iOS -- Autorelease & AutoreleasePool
9:谈谈你对 FRP (函数响应式) 的理解
10:平时开发有没有玩过 Instrument ?
Instruments
里面工具很多,常用的有:
(1).Time Profiler
:性能分析,用来检测应用CPU
的使用情况.可以看到应用程序中各个方法正在消耗CPU
时间。
(2).Zoombies
:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能
(3).Allocations
:用来检查内存,写算法的那批人也用这个来检查
(4).Leaks
:检查内存,看是否有内存泄漏
(5).Core Animation
:评估图形性能,这个选项检查了图片是否被缩放,以及像素是否对齐。被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。黄色、紫色越多,性能越差。
Runtime
1:什么是 isa,isa 的作用是什么?
2:一个实例对象的isa 指向什么?类对象指向什么?元类isa 指向什么?
是一个Class
类型的指针.
每个实例对象有个isa
的指针,他指向对象的类,而Class
里也有个isa
的指针, 指向meteClass
(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass
)也是类,它也是对象。元类也有isa指针,它的isa
指针最终指向的是一个根元类(root meteClass
).根元类的isa
指针指向本身,这样形成了一个封闭的内循环。

3:objc 中类方法和实例方法有什么本质区别和联系?
类方法:
(1).类方法是属于类对象的
(2).类方法只能通过类对象调用
(3).类方法中的self
是类对象
(4).类方法可以调用其他的类方法
(5).类方法中不能访问成员变量
(6).类方法中不定直接调用对象方法
实例方法:
(1).实例方法是属于实例对象的
(2).实例方法只能通过实例对象调用
(3).实例方法中的self
是实例对象
(4).实例方法中可以访问成员变量
(5).实例方法中直接调用实例方法
(6).实例方法中也可以调用类方法(通过类名)
4:load 和 initialize 的区别?
共同点:
(1).+load
和+initialize
会被自动调用,不能手动调用它们。
(2).子类实现了+load
和+initialize
的话,会隐式调用父类的+load
和+initialize
方法
(3).+load
和+initialize
方法内部使用了锁,因此它们是线程安全的。
不同点:
+load
方法是在runtime
加载类、分类的时候调用(根据函数地址直接调用,默认执行),每个类,分类的+load
方法都只会调用一次。
先执行父类的+load
方法,再执行子类的+load
方法,而分类的+load
方法会在它的主类的+load
方法之后执行。(分类按照编译顺序先后调用)
+initialize
方法在第一次给某个类发送消息时调用(通过objc_msgSend
调用),并且只会调用一次,是懒加载模式,此时所有的类都已经加载到了内存中,如果这个类一直没有使用,就不会调用+initialize
方法。
先执行父类的+initialize
方法,再执行子类的+initialize
方法,如果子类没有实现+initialize
方法时,会调用父类的+initialize
方法,所以父类的+initialize
方法会调用多次,如果分类实现了+initialize
方法,就会'覆盖掉'类本身的+initialize
方法。
+load
方法一般是用来交换方法Method Swizzle
,+initialize
方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作
5:_objc_msgForward 函数是做什么的?直接调用会发生什么问题?
_objc_msgForward
是 IMP
类型(函数指针),用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。
_objc_msgForward
消息转发做的几件事:
(1).调用resolveInstanceMethod:
方法 (或 resolveClassMethod:
)。允许用户在此时为该 Class
动态添加实现。如果有实现了,则调用并返回YES
,那么重新开始objc_msgSend
流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod
。如果仍没实现,继续下面的动作。
(2).调用forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil
对象。否则返回nil
,继续下面的动作。注意,这里不要返回self
,否则会形成死循环。
(3).调用methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。如果能获取,则返回非nil
:创建一个NSlnvocation
并传给forwardInvocation:
。
(4).调用forwardInvocation:
方法,将第3步获取到的方法签名包装成 Invocation
传入,如何处理就在这里面了,并返回非nil
。
(5).调用doesNotRecognizeSelector:
,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
一旦调用_objc_msgForward
,将跳过查找IMP
的过程,直接触发“消息转发”,如果调用了_objc_msgForward
,即使这个对象确实已经实现了这个方法,也会告诉objc_msgSend
没有找到这个方法的实现。最常见的场景是:你想获取某方法所对应的NSInvocation
对象。
6:简述下 Objective-C 中调用方法的过程
先来看一下Method
在头文件中的定义:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
Method
被定义为一个objc_method
指针,在 objc_method
结构体中,包含一个 SEL 和一个IMP
,其中,SEL
是一个指向objc_selector
的指针,其实就是一个保存方法名的字符串。IMP
是一个“函数指针”,就是用来找到函数地址,然后执行函数。所以,Method
建立了SEL
和IMP
的关联,当对一个对象发送消息时,会通过给出的SEL
去找到IMP
,然后执行。
再加上继承的情况,可以总结出:当向一个对象发送消息时,会去这个类的方法缓存里寻找(cache methodLists
)如果有缓存则跳到方法实现,否则继续在这个类的 methodLists
中查找相应的SEL
,如果查不到,则通过super_class
指针找到父类,再去父类的缓存列表和方法列表里查找,层层递进,直到找到基类为止。最后仍然找不到,才会走消息转发的流程。
7:能否想向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量;
因为编译后的类已经注册在runtime
中,类结构体中的objc_ivar_list
实例变量的链表 和instance_size
实例变量的内存大小已经确定,同时runtime
会调用 class_setIvarLayout
或class_setWeakIvarLayout
来处理strong weak
引用。所以不能向存在的类中添加实例变量;
运行时创建的类是可以添加实例变量,调用 class_addIvar
函数。但是得在调用 objc_allocateClassPair
之后,objc_registerClassPair
之前。
8:谈谈你对切面编程的理解
AOP
: Aspect Oriented Programming
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
AOP
是OOP
的延续,函数式编程的一种衍生范型。(利用 Objective-C Runtime
的黑魔法Method Swizzling
。)
优势:对业务逻辑的各个部分进行隔离,降低业务逻辑各部分之间的耦合度,提高程序的可重用性,提高了开发的效率。
网络&多线程
1:HTTP的缺陷是什么?
(1).通信过程中使用的是未加密的明文,内容会被取抓。
(2).对于服务器或者客户端来说,不会验证通信方的身份,因此有可能遭到中间人的伪装。
(3).无法验证所发送报文的完整性和安全性,而且报文的内容也有可能是中间人篡改之后发送过来的。
2:谈谈三次握手,四次挥手!为什么是三次握手,四次挥手?
3:socket 连接和 Http 连接的区别
HTTP
协议是基于TCP
连接的,是应用层协议,主要解决如何包装数据。HTTP
连接是一种短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源,而且服务器不能主动给客户端响应。
Socket
是对TCP/IP
协议的封装,Socket
本身并不是协议,而是一个调用接口(API
),通过Socket
,我们才能使用TCP/IP
协议。Socket
连接是一种长连接,客户端跟服务器端直接使用Socket
进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据。Socket
默认连接超时时间是30秒,默认大小是8K(理解为一个数据包大小)。
4:HTTPS,安全层除了SSL还有,最新的? 参数握手时首先客户端要发什么额外参数
5:HTTPS是什么?握手过程,SSL原理,非对称加密了解多少
6:什么时候POP网路,有了 Alamofire 封装网络 URLSession为什么还要用Moya ?
POP
:面向协议编程
面向协议 = 协议 + 扩展 + 继承
通过协议、扩展做功能划分,降低模块间的耦合,增强代码的可扩展性。iOS
中有一个不足之处就是多重继承,而协议正好能够解决多重继承的问题。在Swift中结构体变的更加强大了,不仅能定义属性,还能定义方法,还能多重继承协议,这是OC
所不提供的。
如果已经使用Alamofire
进行抽象URLSession
,为什么不使用某些方法来抽象URL
,参数等的实质呢,因此,Moya
的基本思想是,我们需要一些网络抽象层,以充分封装实际直接调用Alamofire
的层。

7:如何实现 dispatch_once
dispatch_once
能保证任务只会被执行一次,即使同时多线程调用也是线程安全的。常用于创建单例、swizzeld method
等功能。
@implementation ZHClass
+ (id)sharedInstance {
static ZHClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

8:能否写一个读写锁?谈谈具体的分析
信号量方式:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.semaphore = dispatch_semaphore_create(1);
for (NSInteger i = 0; i < 10; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
}
}
- (void)read{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%s",__func__);
dispatch_semaphore_signal(self.semaphore);
}
- (void)write{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%s",__func__);
dispatch_semaphore_signal(self.semaphore);
}
pthread_rwlock_t
方式:
@property (nonatomic,assign) pthread_rwlock_t rwlock;
//初始化读写锁
pthread_rwlock_init(&_rwlock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 3; i ++) {
dispatch_async(queue, ^{
[[[NSThread alloc]initWithTarget:self selector:@selector(readPthreadRWLock) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(writePthreadRWLock) object:nil]start];
});
}
//读
- (void)readPthreadRWLock{
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"读文件");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
// 写
- (void)writePthreadRWLock{
pthread_rwlock_wrlock(&_rwlock);
NSLog(@" 写入文件");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
//销毁锁
- (void)dealloc{
pthread_rwlock_destroy(&_rwlock);
}
dispatch_barrier
方式:
//异步队列
self.testqueue = dispatch_queue_create("rw.thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
dispatch_async(queue, ^{
[self readBarryier];
[self readBarryier];
[self readBarryier];
[self writeBarrier];
});
}
- (void)readBarryier{
dispatch_async(self.testqueue, ^{
NSLog(@"读文件 %@",[NSThread currentThread]);
sleep(2);
});
}
- (void)writeBarrier{
dispatch_barrier_async(self.testqueue, ^{
NSLog(@"写入文件 %@",[NSThread currentThread]);
sleep(1);
});
}
9:什么时候会出现死锁?如何避免?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称。
死锁的产生满足一些特定条件:
(1).互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放。
(2).请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
(3).不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。
(4).循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
10:有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说
(1).NSLock
实现了最基本的互斥锁,遵循了 NSLocking
协议,通过lock
和unlock
来进行锁定和解锁。
(2).NSRecursiveLock
递归锁,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁
(3).NSCondition
是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。
(4).NSConditionLock
对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁。它和 NSCondition
很像,但实现方式是不同的。
(5).pthread_mutex
,互斥锁是一种超级易用的互斥锁,使用的时候,只需要初始化一个pthread_mutex_t
用pthread_mutex_lock
来锁定 pthread_mutex_unlock
来解锁,当使用完成后,记得调用 pthread_mutex_destroy
来销毁锁。
(6).pthread_rwlock
,读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。
当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。
(7).dispatch_semaphore
,信号量机制实现锁,等待信号,和发送信号,当有多个线程进行访问的时候,只要有一个获得了信号,其他线程的就必须等待该信号释放。
(8).@synchronized
,一个便捷的创建互斥锁的方式,它做了其他互斥锁所做的所有的事情。
应当针对不同的操作使用不同的锁:
当进行文件读写的时候,使用pthread_rwlock
较好,文件读写通常会消耗大量资源,而使用互斥锁同时读文件的时候会阻塞其他读文件线程,而 pthread_rwlock
不会。
当性能要求较高时候,可以使用pthread_mutex
或者 dispath_semaphore
,由于OSSpinLock
不能很好的保证线程安全,而在只有在iOS10
中才有 os_unfair_lock
,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。
对于NSLock
及其子类,速度来说NSLock
< NSCondition
< NSRecursiveLock
< NSConditionLock
。
持续更新中..........