iOS面试题
[toc]
一、设计基本原则
简述六大设计基本原则(也称 SOLID 五大原则)
单一职责原则 (SRP, Single Responsibility Principle)
- 定义: 一个类只负责一件事。
- 优点: 类的复杂度降低、可读性增强、易维护、变更引起的风险降低。
- 应用: 系统提供的UIView和CALayer的关系:UIView负责时间传递、事件响应;CALayer负责动画及展示
开闭原则(OCP, Open-Close Principle)
-
定义: 对修改关闭、对扩展开放.
- 设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类的代码.
-
优点: 灵活、稳定(不需修改内部代码,使得被破坏的程度大大下降)
-
关键: 抽象化
-
使用:
- 我们可以把把行为添加到一个协议中,使用时遵守这个协议即可。
- 添加类目(Category)方式创建
里氏替换原则 (LSP,Liskov Substitution Principle)
-
定义: 所有引用父类的地方必须能透明地使用其子类的对象。
- 通俗点说就是,父类可以被子类无缝替换,且原有功能不受任何影响
-
优点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法
- 提高代码的可重用性、扩张性,项目的开放性
-
缺点: 程序的可移植性降低,增加了对象间的耦合性
接口隔离原则(ISP, Interface Segregation Principle)
-
定义: 客户端不应该依赖它不需要的接口
- 使用多个专门的协议、而不是一个庞大臃肿的协议。
- 协议中的方法应当尽量少
-
例: UITableViewDataSource、UITableViewDelegate
-
优点: 解耦、增强可读性、可扩展性、可维护性
依赖倒置原则(DIP, Dependence Inversion Principle)
-
定义: 抽象不应该依赖于具体实现,具体实现可以依赖于抽象
-
核心思想: 面向接口编程
-
优点: 代码结构清晰,维护容易
-
实例: 平时我们使用 protocol 匿名对象模式就是依赖倒置原则的最好体现
迪米特法则(LOD, Law Of Demeter) / 最小知道原则 (LKP,Least Knowledge Principle)
-
定义: 一个对象应该对其他对象有尽可能少的了解。
- 也就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
-
迪米特法则应用:
- 外观模式(Facade)
- 中介者模式(Mediator)
- 匿名对象
-
优点: 使对象之间的耦合降到最底,从而使得类具有很好的可读性和可维护性。
设计基本原则特点总结
- 单一职责原则主要说明:类的职责要单一
- 开闭原则讲述的是:对扩展开放,对修改关闭
- 里氏替换原则强调:不要破坏继承体系
- 接口隔离原则讲解:设计接口的时候要精简
- 依赖倒置原则描述要:面向接口编程
- 迪米特法则告诉我们:要降低耦合
面向对象的三大特征
- 面向对象编程思想主要有三大特征,分别是:封装,继承 和 多态。
- 封装 是指把类中的细节进行包装,对外提供定义好的接口。封装对实现细节进行隐藏,使用者需要通过规定的访问来访问数据,这样避免了使用者进行不合理的赋值操作。
- 继承 是使用已存在的类定义作为基础建立新类的技术,新类的定义可增加新的数据或新的功能,也可以用父类的功能,但不能选择性的继承父类。在继承中,子类拥有父类非 private 的属性和方法;子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;子类可以用自己的方式实现父类的方法。继承使得系统在变化中有了延续性,同时继承也是封装过程中可变的因素,通过继承还可以缩小代码量。
- 多态 是指允许不同的子类类型对同一消息做出不同的行为。多态可以大量减少代码量的同时,提高代码的维护性和扩展性。
什么是 MVC 设计模式?
- 模型-视图-控制器(Model-View-Controller,MVC)是一种广泛应用于用户交互应用程序中的软件设计模式。iOS 中的 MVC 将软件系统分为 Model、View、Controller 三部分,其中
- Model 对象 封装了应用程序的数据,并定义操控和处理该数据的逻辑和运算;
- View 对象 是应用程序中用户可以看见的对象,其主要目的是显示来自应用程序 Model 对象的数据,并使该数据可被编辑;
- Controller 对象 在应用程序的一个或多个 View 对象和一个或多个 Model 对象之间充当媒介,Controller 可以直接访问 Model,Model 通过 Notification 和 KVO 机制与 Controller 间接通信;Controller 也可以直接控制View,View 通过 action 向 Controller 报告事件的发生,但 Model 和 View 不能互相通信。
什么是 MVVM?主要目的是什么?有哪些优点?
- MVVM,为 Model-View-ViewModel 的简写,本质是 MVC 设计模式的改进版,将视图处理逻辑从 C 中剥离出来给 V,剩下的业务逻辑部分被称做 View-Model。MVVM 模式和 MVC 模式一样,主要目的是分离视图和模型,其优点如下:
- 低耦合:View 可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候,Model 可以不变,当 Model 变化的时候 View 也可以不变;
- 可重用性:如果把一些视图逻辑放在一个 ViewModel 里面,则很多 view 可重用这段视图逻辑;
- 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计;
- 可测试:界面素来是较难于测试的,而现在测试可以针对 ViewModel 来写。
二、内存管理
-
规则
- 在iOS中,使用 “引用计数” 来管理OC对象的内存
- 新创建的OC对象,引用计数是1;
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- 当引用计数减为0,OC对象就会销毁,释放占用的内存空间
- 当调用 alloc、new、copy、mutableCopy 方法返回了一个对象,在不需要这个对象时,要调用
- release或者aoturelease释放
引用计数怎么存储?
- 可以直接存储在isa指针中
- 如果不够存储的话,会存储在SideTable结构体的refcnts散列表中
struct SideTable {
spinlock_t stock;
RefcountMap refcnts; // 存放着对象引用计数的散列表
weak_table_t weak_table;
}
ARC具体为引用计数做了哪些工作?
- 编译阶段自动添加代码
- ARC是LLVM编译器和Runtime系统相互协作的一个结果
- 编译器帮我们实现内存管理相关的代码
- Runtime在程序运行过程中处理弱引用
深拷贝与浅拷贝
-
概念:
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象,原对象的引用计数+1
- 完全拷贝:深拷贝的一种,能拷贝多层内容(使用归解档技术)
-
执行结果:
- copy:不可变拷贝,产生不可变副本
- mutableCopy:可变拷贝,产生可变副本
-
准则:不可变对象的copy方法是浅拷贝,其余都是深拷贝🚩🚩🚩🚩🚩
-
原因:
- 它是不可变对象,没有必要拷贝一份出来,指向同一块地址还节省内存
- 不可变对象调用copy返回他本身,不可变对象copy就相当于是retain
-
原因:
对象的拷贝
- 遵守协议
(<NSCopying, NSMutableCopying>)
- 实现协议方法
- (id)copyWithZone:(NSZone *)zone {
Person *person = [Person allocWithZone:zone];
person.name = self.name;
return person;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
Person *person = [Person allocWithZone:zone];
person.name = self.name;
return person;
}
集合对象的拷贝
- 对于集合类的可变对象来说,深拷贝并非严格意义上的深复制,只能算是单层深复制
- 即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然之乡员数组元素值,并没有另外复制一份),这就叫做单层深复制
- 对于集合类的对象如何实现每一层都深拷贝呢?
initWithArray:copyItems
2、归档解档技术
#import <Foundation/Foundation.h>
@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@end
#import "Person.h"
@implementation Person
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self.name = [aDecoder decodeObjectForKey:@"name"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
}
@end
自动释放池
- 底层结构:AutoreleasPool是通过以AutoreleasePoolPage为结点的 “双向链表” 来实现的
-
AutoreleasPool运行的三个过程:
- objc_autoreleasePoolPush()
- [objc autorelease]
- objc_autoreleasePoolPop(void *)
属性修饰词:Copy、Strong、Weak、Assign
-
copy:
- 会在内存里拷贝一份对象,两个指针指向不同的内存地址。
- 一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保可变对象变化时,对象中的字符串不被修改 ,应该在设置属性时拷贝一份。
- 而若用strong修饰,如果可变对象变化,对象中的字符串属性也会跟着变化。
-
strong:
- ARC下的strong等同于MRC下的retain都会把对象引用计数加1
-
weak:
- 修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用
- 在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰
- weak和strong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。
-
assign:
- 用于对基本数据类型进行赋值操作,不更改引用计数
- 也可以用来修饰对象,但是被assign修饰的对象在释放后,指针的地址还是存在的,指针并没有被置为nil,成为野指针✨
- 之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。
block属性为什么需要用copy来修饰?
- 因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。
- 他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。
- 通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
- 在ARC下写不写都行,编译器会自动对block进行copy操作。
代理为什么使用weak修饰?
- weak指明该对象并不负责保持delegate这个对象,delegate的销毁由外部控制;
- 如果用strong修饰,强引用后外界不能销毁delegate对象,会导致循环引用;
为什么NSMutableArray一般不用copy修饰?
- (void)setData:(NSMutableArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
拷贝完成后:可变数组->不可变数组,在外操作时(添加、删除等)会存在问题
什么是“僵尸对象”?
- 一个OC对象引用计数为0被释放后就变成僵尸对象,僵尸对象的内存已经被系统回收
- 虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用
- 它的内存是随时可能被别的对象申请而占用的
有哪些情况会出现内存泄漏
- block循环引用
- delegate循环引用问题
- NSTimer循环引用
- 地图类处理
- 线程保活target:self
- (void)dealloc底层执行了什么?
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj) {
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc() {
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && // 无优化过isa指针
!isa.weakly_referenced && // 无弱引用
!isa.has_assoc && // 无关联对象
!isa.has_cxx_dtor && // 无cxx析构函数
!isa.has_sidetable_rc)) { // 不存在引用计数器是否过大无法存储在isa中(使用 sidetable 来存储引用计数)
// 直接释放
assert(!sidetable_present());
free(this);
} else {
// 下一步
object_dispose((id)this);
}
}
// 如果不能快速释放,则调用 object_dispose()方法,做下一步的处理
static id _object_dispose(id anObject) {
if (anObject==nil) return nil;
objc_destructInstance(anObject);
anObject->initIsa(_objc_getFreedObjectClass ());
free(anObject);
return nil;
}
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor(); // 是否存在析构函数
bool assoc = obj->hasAssociatedObjects(); // 是否有关联对象
// This order is important.
if (cxx) object_cxxDestruct(obj); // 销毁成员变量
if (assoc) _object_remove_assocations(obj); // 释放动态绑定的对象
obj->clearDeallocating();
}
return obj;
}
/*
* clearDeallocating一共做了两件事
*
* 将对象弱引用表清空,即将弱引用该对象的指针置为nil
* 2、清空引用计数表
* - 当一个对象的引用计数值过大(超过255)时,引用计数会存储在一个叫 SideTable 的属性中
* - 此时isa的 has_sidetable_rc 值为1,这就是为什么弱引用不会导致循环引用的原因
*/
inline void objc_object::clearDeallocating() {
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
三、多线程
GCD
GCD核心概念:「任务」、「队列」
-
任务:
概念:指操作,线程中执行的那段代码,GCD主要放在block中;
执行任务的方式:「同步执行」、「异步执行」;
区别:是否等待队列的任务执行结束,是否具备开启新县城的能力;-
同步执行(sync)
- 同步添加任务到指定队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行
- 只能在当前线程中执行任务,不具备开启新线程的能力
-
异步执行(async)
- 异步添加任务到指定队列中,不会做任何等待,可以继续执行任务
- 可以在新的线程中执行任务,具备开启新县城的能力
- 异步执行虽然具有开启新线程的能力,但不一定开启新线程。(与任务指定的队列类型有关)
-
-
队列(Dispatch Queue)
概念:执行任务的等待队列,即用来存放任务的队列
结构:特殊的线性表,采用FIFO(先进先出)原则。即每读取一个任务,则从队列中释放一个任务-
串行队列:(Serial Dispatch Queue)
- 每次只有一个任务被执行,任务依次执行(只开启一个线程,一个任务执行完成后,再执行下一个任务)
-
并发队列:(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
- 并发队列的「并发」功能只有在异步(dispatch_async)方法下才有效
-
-
GCD使用步骤
- 创建一个队列(串行队列/并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行/异步执行)
-
死锁条件:
- 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列。
如何打造线程安全的NSMutableArray?
- 线程锁:使用线程锁在对数组读写时候加锁
- 派发队列:
《Effective Objective 2.0》中41条提出的观点,串行同步:将读取和写入都安排在同一个队列里,可保证数据同步。
如何异步下载多张小图最后合成一张大图?
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
什么是线程安全?
- 多线程操作过程中往往都是多个线程并发执行的,因此同一个资源可能被多个线程同时访问,造成资源抢夺。
- 线程安全就是多条线程同时访问一段代码,不会造成数据混乱的情况
如何设置常驻线程?🌟🌟
- 为当前线程开启一个 RunLoop (第一次调用 [NSRunLoop currentRunLoop] 方法时
实际是会先去创建一个 RunLoop ) - 向当前 RunLoop 中添加一个 Port/Source 等维持 RunLoop 的事件循环(如果
RunLoop 的 mode 中一个 item 都没有, RunLoop 会退出) - 启动该 RunLoop
在异步线程发送通知,在主线程接收通知。会不会有什么问题?
GCD线程是如何调度的
如何实现多个任务执行完后,再统一处理?
- 同步阻塞
- 栅栏函数
- 线程组
线程和线程之间如何通信?
-
线程通信的表现:
- 1个线程传递数据给另1个线程
- 在1个线程中执行完特定任务后,转到另1个线程继续执行任务
-
线程间通信常用方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
示例:
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 在子线程中调用download方法下载图片
[self performSelectorInBackground:@selector(download) withObject:nil];
}
- (void)download {
// 1.根据URL网络中下载图片
NSURL *urlstr=[NSURL URLWithString:@"fdsf"];
// 2、把图片转换为二进制的数据, 这一行操作会比较耗时
NSData *data=[NSData dataWithContentsOfURL:urlstr];
// 3、把数据转换成图片
UIImage *image=[UIImage imageWithData:data];
// 4、回到主线程中设置图片
[self performSelectorOnMainThread:@selector(settingImage:) withObject:image waitUntilDone:NO];
}
//设置显示图片
- (void)settingImage:(UIImage *)image {
self.iconView.image=image;
}
谈谈atomic的实现机制,为什么不能保证绝对线程安全?
-
实现机制:
- 编译器自动生成
getter/setter
方法中添加锁保证线程安全
- 编译器自动生成
-
为什么不能保证绝对安全?
- 在
getter/setter
中加锁,仅保证存取时线程安全,不会让你拿到一个崩溃的值 - 无法保证对容器的修改是线程安全的,例:假设属性是可变容器
(@property (atomic) NSMutableArray *array)
时 - 重写
getter/setter
方法时,只能依靠自己在getter/setter
中保证线程安全
- 在
- (void)setCurrentImage:(UIImage *)currentImage {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
- (UIImage *)currentImage {
return _currentImage;
}
- (void)setCurrentImage:(UIImage *)currentImage {
@synchronized(self) {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
}
}
}
- (UIImage *)currentImage {
@synchronized(self) {
return _currentImage;
}
}
进程和线程的区别
-
区别:
- 一个线程只能属于一个进程.
- 一个进程可以有多个线程,但至少有一个线程。
- 线程是操作系统可识别的最小执行和调度单位。
-
资源分配:
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 同一进程中的多个线程共享代码段、数据段、扩展段。
- 但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
Notification与线程相关
- 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。
- 换句话说就是在哪个线程发送通知,就在哪个线程接受通知。
如何实现在不同线程中post和转发一个Notification?
重定向的实现思路:
- 自定义一个通知队列(用数组类型),让它去维护那些我们需要重定向的Notification
- 我们仍然是像平常一样去注册一个通知的观察者,当Notification来了时,先看看post这个Notification的线程是不是我们所期望的线程
- 如果不是,则将这个Notification存储到我们的队列中,并发送一个信号(signal)到期望的线程中,来告诉这个线程需要处理一个Notification
- 指定的线程在收到信号后,将Notification从队列中移除,并进行处理
dispatch_once底层实现
线程锁
-
线程锁的作用:
- 我们在使用多线程的时候,多个线程可能会访问同一块资源,就很容易引发数据错乱和数据安全等问题
- 这时候就需要我们保证每次只有一个线程访问这一块资源
-
线程锁类型:
- 互斥锁
- 自旋锁
- 信号量
- 递归锁
- atomic
互斥锁
- 标记用来保证在任一时刻,只能有一个线程访问对象
NSLock
@synchronized (self)
自旋锁
- OSSpinLock(YYKit作者有一篇文章写它不安全,可以自己研究一下)
- os_unfair_lock
信号量(Semaphore - dispatch_semaphore_t)
- 多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用
- 在进入一个关键代码段之前,线程必须获取一个信号量;关键代码段完成后,该线程必须释放信号量
- 其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量
递归锁(NSRecursiveLock)
- 同一个线程可以多次加锁,不会造成死锁
atomic
- atomic 修饰的对象,系统会保证在其自动生成的
getter/setter
方法中的操作是完整的,不受其他线程的影响
线程不安全
- 如果有另一个线程同时在调
[name release]
,那可能就会crash,因为release
不受getter/setter
操作的限制 - 这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作
四、Block相关
Block
-
block本质
- block是将函数及其执行上下文封装起来的对象
关于block的截获特性,你是否有了解?block的截获变量特性是怎样的?
-
变量捕获机制分析:
- 对于“基本数据类型”的“局部变量”截获其值
- 对于“对象”类型的局部变量“连同所有权修饰符”一起截获
- 以“指针形式”截获局部静态变量(指针传递)
- 不截获全局变量、静态全局变量(直接访问)
-
改外部变量必要条件
- 将auto从栈copy到堆
原因:栈中内存管理是由系统管理,出了作用域就会被回收,堆中才是可以由程序员管理
- 将auto从栈copy到堆
对栈上的block进行copy之后,假如在mrc环境下,内存是否回泄漏?
- copy操作之后,堆上的block没有额外的成员变量指向它,正如我们alloc对象后,没有进行release,造成内存泄漏
为什么block会产生循环引用?
- 如果当前block对当前对象的某一成员变量进行截获,block会对当前对象有一个强引用
- 而当前block由于当前对象对其有一个强引用,产生了一个自循环引用的一个循环引用的问题
Block不允许修改外部变量的值原因
-
block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域---改变了作用域。
-
在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。
-
比如想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
-
所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
-
可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性---不允许直接修改。
如何实现对外部变量的捕获?
- 将变量设置为全局变量。原理:block内外可直接访问全局变量
- 加 static (放在静态存储区/全局初始化区)。原理是block内部对外部auto变量进行指针捕获
- 最优解:使用__block 关键字
__block
- 将auto变量封装为结构体(对象),在结构体内部新建一个同名的auto变量
- block内截获该结构体的指针
- 在block中使用自动变量时,使用指针指向的结构体中的自动变量
__block int var = 10;
void(^blk)(void) = ^{
var = 20;
};
blk();
转换后的代码:
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var; // 10 => 20 该结构体持有相当于原来自动变量的成员变量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_var_0 *var; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block在修改NSMutableArray需不需要添加__block
- 不需要
- 原因:
- 当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。
- 所以,在block里面对指针指向内容做的修改,在block外面也一样生效。
block是如何捕获局部变量的?
- block捕获外界变量时,在内部会自动生成同一个属性来保存
UIView动画中block回调里self要不要弱引用?
- 不需要,它不会造成循环引用,因为它是类方法。
- 之所以需要弱引用本身,是因为怕对象之间产生循环引用,当前控制器不可能强引用一个类,所以循环无法形成。
block里面会不会存在self为空的情况(weak strong的原理)?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
[strongself.dataSource reloadDataWithCompletion:nil];
}];
- 有时候weakSelf在block里在执行reloadDataWithCompletion还存在
- 但在执行reloadDataWithCompletion前,可能会被释放了
- 为了保证self在block执行过程里一直存在,对他强引用strongSelf
__block与__weak的区别
- __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型
- __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)
- __block对象可以在block中被重新赋值,__weak不可以。
多层block嵌套如何使用weakSelf?
__weak typeof(self) weakself = self;
[self wj_refresh_addRefreshHeader:^{
__strong typeof(weakself) strongself = weakself;
__weak typeof(self) weakSelf2 = strongself;
[strongself.dataSource reloadDataWithCompletion:^(BOOL result) {
__strong typeof(self) strongSelf2 = weakSelf2;
}];
}];
Masonry对于block内部引用self会不会造成循环引用?
- 不会
- 这个block没有copy,是在栈上,使用完直接释放了,
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
__weak来解决block中的循环引用,还有别的方法吗
- __block
- 将对象传进入修改
代理、Block利弊
-
与委托代理模式的代码相比,用block写出的代码更为整洁
-
代理优点:
- 代理语法清晰,可读性高,易于维护
- 它减少代码耦合性,使事件监听与事件处理分离
- 一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高
-
代理缺点:
- 实现代理的过程较繁琐
- 跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱
- 当多个对象同时传值时不易区分,导致代理易用性大大降低
-
block优点:
- 语法简洁,代码可读性和维护性较高
- 配合GCD优秀的解决多线程问题
-
block缺点:
- Block中得代码将自动进行一次retain操作,容易造成内存泄漏
- Block内默认引用为强引用,容易造成循环应用
-
运行成本:
- delegate运行成本低,block的运行成本高
- block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是假引用技术,使用完block置nil才会消除
- delegate只是保存了一个对象的指针,直接回调,没有额外的消耗。就像c的函数指针,只多了一个查表动作
五、Runtime
Runtime的相关术语
SEL、id、Class、Method、IMP、Cache、Property
介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
为什么要设计metaclass
class_copyIvarList & class_copyPropertyList区别
class_rw_t 和 class_ro_t 的区别
交互两个方法的现实有什么风险?
六、Runloop
- 什么是RunLoop?
- RunLoop 实际上是一个对象
- 这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)
- 从而保持程序的持续运行
- 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能
- 应用范畴
- 定时器(Timer)、PerformSelect
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
- 基本应用
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- runloop和线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
/*
* 从字典中获取,如果没有则直接创建
*/
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
NSTimer相关
NSTimer准吗?如果不准的话原因是什么?如何解决?
- 原因:
- NSTimer的触发时间到的时候,会在RunLoop中被检测一次;
- 如果在这一次的RunLoop中做了耗时的操作,会处于阻塞状态
- 时间超过了定时器的间隔时间,触发时间就会推迟到下一个runloop周期
- 解决方法:
- 在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作
- 使用CADisplayLink(一般用来做UI展示更新,同样存在runloop卡顿问题)
- 使用GCD定时器
使用NSTimer是如何处理循环引用的?
使用类方法
TODO(待填充);
谈谈常用的三种定时器优缺点(NSTimer、CADisplayLink、GCD定时器)✨✨✨✨✨
- NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
- 相比之下GCD的定时器会更加准时,因为GCD不是依赖RunLoop,而是由内核决定
- CADisplayLink和NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
在viewWillDisappear或者viewDidDisappear方法中将 timer = nil,是否还会造成循环引用?
- 问题一:如果只是想在离开此页时要释放,进入下一页时不要释放,场景就不适用了
- runloop->timer;controller->timer
如何利用runloop监控卡顿✨✨✨
七、KVO
KVO基础
KVO 的 全称Key-Value Observing,俗称“键值监听”,可以用于某个对象属性值的改变
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
- 利用runtimeAPI动态生成一个子类(NSKVONotifying_XXXX),并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会动用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey
- 父类原来的setter方法
- didChangeValueForKey
- 内部触发监听器(
ObserveValueForKeyPath:ofObject:change:context
)
如何手动触发KVO?
- 手动调用
willChangeValueForKey
- 修改成员变量值
- 手动调用
didChangeValueForKey
直接修改成员变量会触发KVO么?
- 不会触发KVO(原因看KVO的本质)
八、KVC
KVC基础
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
通过KVC修改属性会出发KVO么?
- 能触发KVO()
- KVC在修改属性时,会调用
willChangeValueForKey:和didChangeValueForKey:
方法
KVC的赋值和取值过程是怎样的?
- 赋值过程
- 首先会按照setKey、_setKey的顺序查找方法,找到方法,直接调用方法并赋值;
- 未找到方法,则调用
+ (BOOL)accessInstanceVariablesDirectly;
- 若accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
- 若accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;
- 取值过程
- 首先会按照getKey、key、isKey、_key的顺序查找方法,找到直接调用取值
- 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出异常;
- 若返回的YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到则取值;
- 找不到则抛出异常;
使用场景
- 单层字典模型转化:
[self.model setValuesForKeysWithDictionary:dict];
- 通过KVC修改未暴露的属性:
UILabel *placeholderLabel=[self.userTextField valueForKeyPath:@"placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
- 使用valueForKeyPath可以获取数组中的最小值、最大值、平均值、求和
CGFloat sum = [[array valueForKeyPath:@"@sum.floatValue"] floatValue];
CGFloat avg = [[array valueForKeyPath:@"@avg.floatValue"] floatValue];
CGFloat max =[[array valueForKeyPath:@"@max.floatValue"] floatValue];
CGFloat min =[[array valueForKeyPath:@"@min.floatValue"] floatValue];
- 数组内部去重
[dataArray valueForKeyPath:@"@distinctUnionOfObjects.self"]
- 数组合并(去重合并:distinctUnionOfArrays.self、直接合并:unionOfArrays.self)
NSArray *temp1 = @[@3, @2, @2, @1];
NSArray *temp2 = @[@3, @4, @5];
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@distinctUnionOfArrays.self"]);
NSLog(@"\n%@",[@[temp1, temp2] valueForKeyPath:@"@unionOfArrays.self"]);
输出两个数组:( 5, 1, 2, 3, 4 ), ( 3, 2, 2, 1, 3, 4, 5 )。
- 大小写转换(uppercaseString)及 打印字符串长度同样适用(length)
NSArray *array = @[@"name", @"w", @"aa", @"jimsa"];
NSLog(@"%@", [array valueForKeyPath:@"uppercaseString"]);
打印:
(NAME,W,AA,JIMSA)
九、Category
Category相关
Category的使用场合
- 将一个类拆成很多模块(其实就是解耦,将相关的功能放到一起)
Category的实现原理
- 通过runtime动态将分类的方法合并到类对象、元类对象中
- Category编译之后的底层结构是 struct_category_t , 里面存储着分类的对象方法、类方法、属性、协议信息
- 在程序运行的时候,runtime会将 Category 的数据,合并到类信息中(类对象、元类对象)
category和extension区别
- Extension在编译时,就把信息合并到类信息中
- Category是在运行时,才会将分类信息合并到类信息中
- 分类声明的属性,只会生成getter/setter方法声明,不会自动生成成员变量和getter/setter方法实现,而扩展会
- 分类不可用为类添加实例变量,而扩展可以
分类的局限性
- 无法为类添加实例变量,但可通过关联对象进行实现
- 分类的方法如果和类重名,会覆盖原来方法的实现
- 多个分类的方法重名,会调用最后编译的那个分类的实现
为什么category不能添加属性?使用Runtime就可以了?
- 分类没有自己的isa指针
- 类最开始生成了很多基本属性,比如IvarList,MethodList
- 分类只会将自己的method attach到主类,并不会影响到主类的IvarList
- 实例变量没有setter和getter方法。也没有自己的isa指针
- 关联对象都由AssociationsManager管理
- AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。
- 相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址
- 而这个map的value又是另外一个AssAssociationsHashMap,里面保存了关联对象的kv对
Category中有load方法么?load方法什么时候调用的?load方法能继承么?
-
有
-
+load方法会在runtime加载类、分类时调用;
-
每个类、分类的+load,在程序运行过程中只调用一次
-
调用顺序
-
先调用类的+load,(按照编译先后顺序,先编译,先调用),调用子类的+load之前会调用父类的+load
-
再调用分类的+load按照编译先后顺序调用(先编译,先调用)
test方法和load方法的本质区别?(+load方法为什么不会被覆盖)
- test方法是通过消息机制调用 objc_msgSend([MJPerson class], @selector(test))
- +load方法调用,直接找到内存中的地址,进行方法调用
load调用顺序
- +load方法会在runtime加载类、分类时调用
- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序
- 先调用类的+load方法,之后按照编译先后顺序调用(先编译,先调用,调用子类的+load之前会先调用父类的+load)
- 再调用分类的+load,之后按照编译先后顺序调用(先编译,先调用)
不同Category中存在同一个方法,会执行哪个方法?如果是两个都执行,执行顺序是什么样的?
- 根据Build Phases->Compile Sources中添加的文件顺序,后面的会覆盖前面的
load、initialize方法的区别是什么?它们在category中的调用顺序?以及出现继承时他们之间的调用过程?🌟🌟🌟🌟🌟
区别:
- 调用方式不同
- load是根据函数地址直接调用
- initialize是通过objc_msgSend调用
- 调用时刻
- load是runtime加载 类/分类 的时候调用(只会调用1次)
- initialize是类第一次接收消息时调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
- 调用顺序
- load:先调用类的load。先编译的类,优先调用load(调用子类的load之前,会先调用父类的load)
- 再调用分类的load(先编译的分类,优先调用load)
- initialize:先初始化父类, 再初始化子类(可能最终调用的是父类的initialize方法)
分类中方法替换
- category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
- category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面
- 这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。
为什么不能动态添加成员变量?
- 方法和属性并不“属于”类实例,而成员变量“属于”类实例
- “类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。
- 假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用
十、网络
TCP、UDP
TCP、UDP优点及缺点
-
TCP优点:( 可靠,稳定)
- 在传递数据之前,会有三次握手来建立连接,
- 在数据传递时,有确认、窗口、重传、拥塞控制机制,
- 在数据传完后,还会断开连接用来节约系统资源
-
TCP缺点:(慢,效率低,占用系统资源高)
- TCP在传递数据之前,要先建连接,这会消耗时间
- 在数据传递时(确认机制、重传机制、拥塞控制机制)等都会消耗大量的时间
- 因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击
-
UDP的优点:(快)
- UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制
- UDP是一个无状态的传输协议,所以它在传递数据时非常快
-
UDP的缺点:(不可靠,不稳定)
- 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包
TCP、UDP的区别
- TCP面向连接(如打电话要先拨号建立连接); UDP是无连接的,即发送数据
- TCP提供可靠的服务。通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流; UDP是面向报文的
- 每一条TCP连接只能是点到点的; UDP支持一对一,一对多,多对一和多对多的交互通信
Scoket连接和HTTP连接的区别
- HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。
- HTTP连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应,iPhone主要使用类NSURLConnection
- Socket连接:长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据
HTTP协议的特点,关于HTTP请求GET和POST的区别
- 特点:
- HTTP超文本传输协议,是短连接,是客户端主动发送请求,服务器做出响应,服务器响应之后,链接断开
- HTTP是一个属于应用层面向对象的协议,HTTP有两类报文:请求报文和响应报文
- HTTP请求报文:一个HTTP请求报文由请求行、请求头部、空行和请求数据4部分组成
- HTTP响应报文:由三部分组成:状态行、消息报头、响应正文
- GET请求
- 参数在地址后拼接,不安全(因为所有参数都拼接在地址后面)
- 不适合传输大量数据(长度有限制,为1024个字节)
- POST请求
- 参数在请求数据区放着,相对GET请求更安全
- 数据大小理论上没有限制
- 提交的数据放置在HTTP包的包体中
断点续传怎么实现的?
- 断点续传主要依赖于 HTTP 头部定义的 Range 来完成
- 有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源
- 当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小
TCP建立和断开连接过程
a、 三次握手
- 定义:
- 三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。 - 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。 - 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
b、 三次握手为什么是三次握手
- 如果是两次握手:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
c、 四次挥手
- 定义:
- 建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。
- 第一次:TCP客户端发送一个FIN报文,用来关闭客户到服务器的数据传送。
- 第二次:服务器收到这个FIN报文,它发回一个ACK报文,确认序号为收到的序号加1。和SYN一样,一个FIN报文将占用一个序号。
- 第三次:服务器关闭客户端的连接,发送一个FIN给客户端。
- 第四次:客户端发回ACK报文确认,并将确认序号设置为收到序号加1。
d、挥手为什么需要四次?
- 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
e、为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
- 虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
f、SYN的初始值(ISN initialization sequence number)是一个随机值
- SYN:同步标志 ,同步序列编号(Synchronize Sequence Numbers)。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把 TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号
- 客户端向服务器发送一个同步数据包请求建立连接,该数据包中,初始序列号(ISN)是客户端随机产生的一个值,确认号是0;
- 服务器收到这个同步请求数据包后,会对客户端进行一个同步确认。这个数据包中,序列号(ISN)是服务器随机产生的一个值,确认号是客户端的初始序列号+1;
- 客户端收到这个同步确认数据包后,再对服务器进行一个确认。该数据包中,序列号是上一个同步请求数据包中的确认号值,确认号是服务器的初始序列号+1。
- ISN代表什么?意义何在?
- ISN,发送方的字节数据编号的原点,让对方生成一个合法的接收窗口。
- ISN是固定不变的吗?
- 动态随机。
- ISN为何要动态随机?
- 增加安全性,为了避免被第三方猜测到,从而被第三方伪造的RST报文Reset。
TCP建立连接的三次握手中,第二次握手发送的包会包含的标记,最正确的描述是?
网络层相关
网络七层协议
Charles原理
HTTPS的连接建立流程
TCP分片 和 IP分片
Cookie和Session
-
Cookie
- HTTP协议是无状态的,服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
基于HTTP这种特点,就产生了cookie/session - cookie主要是用来记录用户状态,区分用户,状态保存在客户端
- HTTP协议是无状态的,服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
-
Session
- Web应用程序中还经常使用Session来记录客户端状态。Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。
-
区别
- cookie数据存放在客户的浏览器上,session数据放在服务器上。
- cookie相比session不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
- 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。而session存储在服务端,可以无限量存储
- 所以:将登录信息等重要信息存放为session;其他信息如果需要保留,可以放在cookie中
DNS是什么?DNS解析过程
-
定义:
- 域名系统(Domain Name System,DNS)
- 因特网上的主机,可以使用多种方式标识:
一种标识方法就是用它的主机名,比如·www.baidu.com、www.google.com、gaia.cs.umass.edu等
-
另外一种方式,就是直接使用定长的、有着清晰层次结构的IP地址
-
区别:
- 主机名:方便人们记忆和接受,但长度不一、没有规律的字符串,路由器并不方便处理
- IP地址:路由器方便处理,不便于人们记忆
-
为了折衷这两种方式,需要一种能进行主机名到IP地址转换的目录服务,就是 域名系统(Domain Name System,DNS)
-
作用:
- 将用户提供的主机名解析为IP地址
-
DNS解析过程(以www.163.com为例:)
- 打开浏览器,输入一个域名(www.163.com)。客户端会发出一个DNS请求到本地DNS服务器(本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动)
- 本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,直接返回结果。如果没有,向DNS根服务器进行查询。
- 根DNS服务器没有记录具体的域名和IP地址的对应关系,而是给出域服务器的地址,告诉他可以到域服务器上去继续查询
- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。
- .com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
- 最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,
- 本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
HTTP和HTTPS的区别?Https为什么更加安全?
- 传输信息安全性不同
- http协议:是超文本传输协议,信息是明文传输。(如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息)
- https协议:是具有安全性的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全
- 连接方式不同
- http协议:http的连接很简单,是无状态的。
- https协议:是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议。
- 端口不同
- http协议:使用的端口是80。
- https协议:使用的端口是443.
- 证书申请方式不同
- http协议:免费申请。
- https协议:需要到ca申请证书,一般免费证书很少,需要交费。
ipv6
- IPv4 是互联网协议( Internet Protocol, IP)的第四版,也是第一个被广泛使用,目前运用最多的互联网技术协议。
IPv4 地址格式是这个样子: 123.58.25.46 - IPv6 是 IPv4 的下一个版本 。 IPv6 地址长度为 128 位,地址空间增加了 2128-232 个,它在提高安全性方面相比前代有着较大的提升。此外,身份认证和隐私权也是 IPv6 的关键特性。
IPv6 地址格式是这个样子: 2001:da8:215:4009:250:56ff:fe97:40c7 。
十一、UI
Storyboard/Xib和纯代码UI相比,有哪些优缺点?
- storyboard/xib优点
- 简单直接。直接通过拖拽和点选即可完成配置。
- 跳转关系清楚
- 缺点:
- 协作冲突(多人提交代码)
- 很难做到页面继承和重用
- 不便于进行模块化管理
- 影响性能(多图层渲染)
Auto Layout
Auto Layout基本原理
- 一个约束本质上就是一个表示视图布局关系的线性方程。一个完整的约束方程式
控制器
描述一下 UIViewController 的生命周期
- 当 UIViewController 被创建并在屏幕上显示,其生命周期方法执行顺序为:
- alloc:创建对象,分配空间;
- init (initWithNibName|initWithCoder) :初始化对象,初始化数据;
- loadView: 完成一些关键 view 的初始化工作,加载 view;
- viewDidLoad:载入完成,可以进行自定义数据以及动态创建其他控件;
- viewWillAppear:表示视图即将出现在屏幕之前;
- viewWillLayoutSubviews:将要对子视图进行调整;
- viewDidLayoutSubviews:完成对子视图的调整;
- viewDidAppear:表示视图已在屏幕上完成渲染;
- viewWillDisappear:表示视图将要被移除,在程序执行完但屏幕上视图还未消失的时候执行;
- viewDidDisappear:表示视图已经被移除,在程序执行完屏幕视图也消失的时候执行;
- dealloc :视图被销毁,此处需要对在 init 和 viewDidLoad 中创建的对象进行释放;
- didReceiveMemoryWarning:内存警告。
如果页面 A 跳转到 页面 B,A 的 viewDidDisappear 方法和 B 的 viewDidAppear 方法哪个先调用?
- A -->viewWillDisappear
- B-->viewWillAppear
- A-->viewDidDisappear
- B-->viewDidAppear
离屏渲染,隐式动画和显式动画相关
离屏渲染触发条件
- 背景色、边框、背景色+边框,再加上圆角+裁剪,因为 contents = nil 没有需要裁剪处理的内容,所以不会造成离屏渲染。
- 一旦为contents设置了内容,无论是图片、绘制内容、有图像信息的子视图等,再加上圆角+裁剪,就会触发离屏渲染。
事件响应过程(响应链)
事件的传递 (寻找最合适的view的过程)
- 当一个事件发生后,事件会从父控件传给子控件 (UIApplication->UIWindow->UIView->initial view)
事件的响应
- 首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)
- 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件
- 如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递
- 一直到window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
事件顺序
- 事件的传递是从上到下(父控件到子控件)
- 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)
事件穿透
- 假设有一个黄色控件和白色控件,白色空间覆盖在黄色控件上
- 点击白色view想要黄色view来响应该事件,就是所谓的穿透
- 方法一
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint yellowPoint = [self convertPoint:point toView:_yellowView];
if ([_yellowView pointInside:yellowPoint withEvent:event]) {
return _yellowView;
}
return [super hitTest:point withEvent:event];
}
- 方法二
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint yellowPoint =[_yellowView convertPoint:point fromView:self];
if ([_yellowView pointInside:yellowPoint withEvent:event]){
return NO;
} else {
return [super pointInside:point withEvent:event];
}
}
手势识别
手势识别的过程
这里主要说的是关于runloop的概念点
- 当
_UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用Cancel,将当前的 touchesBegin/Move/End 系列回调打断 - 随后系统将对应的 UIGestureRecognizer 标记为待处理
- 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件
- 这个 Observer 的回调函数是
_UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer 的回调。 - 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理
十二、其他
数组和链表
数组和链表的区别
- 数组在内存上给出了连续的空间
- 链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个)
数组和链表的优缺点
- 数组:
- 优点: 使用方便,查询效率比链表高,内存为一连续的区域
- 缺点: 大小固定,不适合动态存储,不方便动态添加
- 链表:
- 优点: 可动态添加删除,大小可变
- 缺点: 只能通过顺次指针访问,查询效率低
静态库、动态库
什么是库?
- 共享代码,实现代码的复用,一般分为静态库和动态库。
静态库和动态库的区别
- 静态库(.a和.framework 样式):
- 链接时完整的拷贝到可执行文件,多次使用多次拷贝,造成冗余,使包变的更大
- 但是代码装载速度快,执行速度略比动态库快
- 动态库:(.dylib和.framework)
- 链接时不复制,程序运行时由系统加在到内存中,供系统调用,系统加在一次,多次使用,共用节省内存。
为什么framework既是静态又是动态?
- 系统的framework是动态的,自己创建的是静态的。
.a 和 .framework 的区别是什么?
- .a 是单纯的二进制文件,需要 .h文件配合,不能直接使用
- .framework是二进制文件+资源文件,可以直接使用。 .framework = .a + .h + sorrceFile(资源文件)
NSDictionary
NSDictionary底层实现原理
- 在OC中NSDictionary是使用hash表来实现key和value的映射和存储的。
hash表存储过程简单介绍: - 根据key值计算出它的hash值h;
- 假设箱子的个数是n,那么键值对应该放在第(h%n)个箱子中。
- 如果该箱子中已经有了键值对,就是用开放寻址法或者拉链法解决冲突。使用拉链法解决哈希冲突时,每个箱子其实是一个链表,属于同一个箱子的所有键值对都会排列在链表中。
十三、Objective-C 类和对象相关
语言基础
Objective-C 与 C、C++ 之间的联系和区别是什么?
- Objective-C 与 C++ 都是从 C 演化而来的面向对象语言,两者都兼容标准 C 语言。Objective-C 与 C++ 的区别主要有以下几点:
Objective-C 是完全动态的,而 C++是部分动态的;
Objective-C 不支持多重继承, 而 C++ 支持,不过 Objective-C 通过 proxy(代理) 或 Category(类别)可以更优雅地实现这一特性;
Objective-C 通过互相传递消息实现函数调用,而 C++ 直接进行函数调用;
Objective-C 采用 protocol 协议(非正式和正式)的形式来定义接口,而 C++ 采用虚函数的形式来定义接口;
Objective-C 没有 C++ 里有的构造函数和析构函数, 其对应物为 alloc-init/free。
如何理解 Objective-C 为动态运行时语言?
- 主要是将数据类型的确定由编译时,推迟到了运行时。简单来说,运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。
Objective-C 中是否支持垃圾回收机制?
Objective-C 是支持垃圾回收机制的,但是在 iOS 中不可用,iOS 开发只支持手动内存管理和 ARC(Automatic Reference Counting)。
Objective-C 中堆和栈的区别是什么?
堆空间的内存是动态分配的,一般用于存放 Objective-C 对象,并且需要手动释放内存,不及时回收容易产生内存泄露,ARC 环境下 Objective-C 对象由编译器管理,不需要手动释放;栈空间的内存由系统自动分配,一般存放非 Objective-C 对象的基本数据类型,例如 int、float 等,由系统编译器管理,不需要手动管理内存。
类、对象
OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象中
类与结构体的区别
- 结构体只能封装数据,而类还可以封装行为
- 赋值:结构体是拷贝,对象之间是地址
- 结构体变量分配在栈空间(如果是一个局部变量的情况下),而对象分配在堆空间
class、meta-class的结构
struct objc_class : objc_object {
Class ISA;
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用于获取具体的类信息
}
& FAST_DATA_MASK
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; // 类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
对象的isa指针指向哪里?superclass指针呢?
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class
- class的superclass指向父类的class(如果没有父类,superclass指针为nil)
- meta-class的superclass指向父类的meta-class
- 基类的meta-class的superclass指向基类的class
方法调用查找
- 对象方法的调用:通过instance的isa找到class,最后找到对象方法的实现进行调用
- 类方法的调用:当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
class对象的superclass指针
- Student : Person : NSObject
- 当Student的instance对象要调用Personal的对象方法时:
- 先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用
meta-class对象的superclass指针
- 当Student的class要调用Person的类方法时
- 先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用
对象相关
属性可以与set方法和get方法 三者同时存在吗,如果不行,请说明原因?
换句话说就是:iOS中同时重写属性的set与get方法时,为什么访问不了下划线属性?
- 原因:
- 属性的setter方法和getter方法是不能同时进行重写,
- 因为,一旦你同时重写了这两个方法,那么系统就不会帮你生成这个成员变量了
- 解决方式:
@synthesize authType = _authType;
- 意思是,将属性的setter,getter方法,作用于这个变量。
iOS如何实现多继承,代码书写一下
- 使用协议组合
- NSProxy
- 比如我有两个协议, 分别是 YFPerson,YFChild
#import <Foundation/Foundation.h>
@protocol YFPerson <NSObject>
@required
@property (nonatomic,copy,readonly)NSString *name;
- (NSInteger) age;
- (void)eat;
- (void)sleep;
@optional
- (void)play;
- (void)setName:(NSString *)newName;
@end
#import <Foundation/Foundation.h>
@protocol YFChild <NSObject>
@required
- (NSString *)nickname;
- (void)introduceMyselfWithName:(NSString *)name nickname:(NSString *)nickname age:(NSInteger)age;
@optional
- (void)study;
@end
- 那么, 我在新创建的一个 YFStudent 类中, 只要遵守上面两个协议, 实现协议里的方法, 就可以在一个类中,实现多个协议中的方法了.
- (NSString *)nickname
{
return @"龙儿";
}
- (NSInteger)age
{
return 19;
}
- (void)sleep{
NSLog(@"sleep");
}
- (void)eat{
NSLog(@"eat");
}
- (void)introduceMyselfWithName:(NSString *)name nickname:(NSString *)nickname age:(NSInteger)age
{
NSLog(@"我叫%@,小名%@,今天%@岁了", name,nickname,@(age));
}
- 这样, 我在控制器的 viewDidLoad 方法中,创建 YFStudent 对象, 然后就可以调协议中的任何方法了
- (void)viewDidLoad {
[super viewDidLoad];
YFStudent *student = [[YFStudent alloc]init];
student.name = @"小龙女";
[student eat];
[student sleep];
[student introduceMyselfWithName:student.name nickname:student.nickname age:student.age];
}
关键字和系统提供的类
简述__kindof关键字
- 表示当前类或者他的子类(iOS9推出的,一般用于消除警告)
- 声明数组存贮指定UIView类型的元素
- 如果元素被赋值为UIWebView或UIButton这样的子类型,编译器就会报警告
- 为了解决这个问题,__kindof就应运而生。
@property (nonatomic, strong) NSMutableArray<UIView *> *viewList;
👇
@property (nonatomic, strong) NSMutableArray <__kindof UIView *> * viewList;
新版Xcode这个问题已经优化,例子属于老实例,理解这个意思就行了
关于NSProxy
- 消除NSTimer循环引用
- 多继承
@interface LYBird ()
@property (nonatomic, copy) NSString *bridName;
@end
@implementation LYBird
- (void)onFly {
NSLog(@"%@正在飞翔", self.bridName);
}
@end
@interface LYFish ()
@property (nonatomic, copy) NSString *fishName;
@end
@implementation LYFish
- (void)onSwimming {
NSLog(@"%@正在游泳", self.fishName);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
LYBird *bird = [[LYBird alloc] init];
LYFish *fish = [[LYFish alloc] init];
LYProxy *proxy = [LYProxy alloc];
[proxy transformToObject:bird];
[proxy performSelector:@selector(setBridName:) withObject:@"鹰隼"];
[proxy performSelector:@selector(onFly)];
[proxy transformToObject:fish];
[proxy performSelector:@selector(setFishName:) withObject:@"🦈"];
[proxy performSelector:@selector(onSwimming)];
}
id和NSObject ,instancetype的区别?
- id和instancetype都可以做方法的返回值。
- id类型的返回值在编译期不能判断对象的真实类型,即非关联返回类型
- instancetype类型的返回值在编译期可以判断对象的真实类型,即关联返回类型。
- id可以用来定义变量, 可以作为返回值, 可以作为形参
- instancetype只能用于作为返回值。
nil、Nil、NULL、NSNull的区别?
- nil:指向一个对象的空指针
- Nil:指向一个类的空指针,
- NULL:指向其他类型(如:基本类型、C类型)的空指针, 用于对非对象指针赋空值.
- NSNull:在集合对象中,表示空值的对象.
NSNull在Objective-C中是一个类 .NSNull有 + (NSNull *)null; 单例方法.多用于集合(NSArray,NSDictionary)中值为空的对象.
NSArray *array = [NSArray arrayWithObjects: [[NSObject alloc] init], [NSNull null], @"aaa", nil, [[NSObject alloc] init], [[NSObject alloc] init], nil];
NSLog(@"%ld", array.count);// 输出 3,NSArray以nil结尾
十四、Swift
语言基础
对比 Objective-C,Swift 有什么优势?
- 对比 Objective-C,Swift 优势有如下几点:
- 语法简单易读、代码编写更加简洁、清晰、易于维护;
- 速度更快,运算性能更高;
- 是一门类型安全语言,代码里值的类型非常明确;
- 泛型、结构体、枚举都很强大;
- 拥有便捷的函数式编程。
在 Swift 中,为什么 map
函数必不可少?该在什么情况下使用它?
- map 函数属于高阶函数,使用高阶函数编程不仅可以优化代码,还能够并行执行。map 函数通常用于数组中的每个元素,通过指定的方法进行转换,返回一个泛型的数组。
在 Swift 中使用扩展可以完成什么任务?
- Swift 中的扩展可以动态地给类增加功能。在 Swift 中使用扩展可以完成的任务有如下几点:
- 添加计算型属性和计算静态属性;
- 定义实例方法和类型方法;
- 提供新的构造器;
- 定义下标;
- 定义和使用新的嵌套类型;
- 使一个已有类型符合某个接口。
在 Swift 中,什么时候用结构体,什么时候用类?
- 在 Swift 开发环境中,结构体和类都是构建代码所用的一种通用且灵活的构造体,都拥有属性,都可以定义方法,但两者存在很多不同的特性,类是引用类型、支持继承,而结构体是值类型、不支持继承。此外,由于结构体的方法调用是静态绑定,而类的方法调用是动态实现的,所以在运行时,结构体的性能更优。按照通用准则,当符合以下一条或多条情形时应考虑创建一个结构体:
- 结构体的主要目的是为了封装一些相关的简单数据值;
- 在赋予或者传递结构实例时,需要封装的数据值被拷贝而不是引用;
- 在其他的情况下,定义一个类,并创建这个类的实例,通过引用来管理和传递。
十五、App优化等
系统架构
iOS 系统可分为四级结构,由上至下分别为可触摸层(Cocoa Touch Layer)、媒体层(Media Layer)、核心服务层(Core Services Layer)、核心操作系统层(Core OS Layer),每个层级提供不同的服务。
低层级结构提供基础服务如文件系统、内存管理、I/O 操作等,高层级结构建立在低层级结构之上提供具体服务如 UI 控件、文件访问等。
可触摸层主要提供用户交互相关的服务如界面控件、事件管理、通知中心、地图,包含的框架主要有 UIKit Framework、Event Kit UI Framework、Notification Center Framework、Map Kit Framework等。
媒体层主要提供图像引擎、音频引擎、视频引擎框架。
核心服务层为程序提供基础的系统服务例如网络访问、浏览器引擎、定位、文件访问、数据库访问等,主要包含框架有 CFNetwork Framework、Core Data Framework、Core Location Framework 和 Web Kit Framework 等。
核心系统层为上层结构提供最基础的服务如操作系统内核服务、本地认证、安全、加速等,主要包含框架有 Accelerate Framework、External Accessory Framework 和 Security Framework 等。
简述一下 iOS 的系统架构
iOS 系统可分为四级结构,由上至下分别为 可触摸层(Cocoa Touch Layer)、媒体层(Media Layer)、核心服务层(Core Services Layer)、核心操作系统层(Core OS Layer),每个层级提供不同的服务。可触摸层主要提供用户交互相关的服务如界面控件、事件管理、通知中心、地图;媒体层主要提供图像引擎、音频引擎、视频引擎框架;核心服务层为程序提供基础的系统服务例如网络访问、浏览器引擎、定位、文件访问、数据库访问等;核心系统层为上层结构提供最基础的服务如操作系统内核服务、本地认证、安全、加速等。
Crash
Crash监控
- NSSetUncaughtExceptionHandler可以统计闪退
App启动
- App启动分为两种:
- 冷启动(Cold Launch):从零开始启动app
- 热启动(Warm Launch):app已在内存中,在后台存活,再次点击图标启动app
- 启动时间的优化,主要是针对冷启动进行优化
- 通过添加环境变量可以打印app的启动时间分析
- DYLD_PRINT_STATISTICS
- DYLD_PRINT_STATISTICS_DETAILS(比上一个详细)
- 一般400毫秒以内正常
打印结果:
Total pre-main time: 238.05 milliseconds (100.0%) // main函数调用之前(pre-main)总耗时
dylib loading time: 249.65 milliseconds (104.8%) // 动态库耗时
rebase/binding time: 126687488.8 seconds (18128259.6%)
ObjC setup time: 10.67 milliseconds (4.4%) // OC结构体准备耗时
initializer time: 52.83 milliseconds (22.1%) // 初始化耗时
slowest intializers : // 比较慢的加载
libSystem.B.dylib : 6.63 milliseconds (2.7%)
libBacktraceRecording.dylib : 6.61 milliseconds (2.7%)
libMainThreadChecker.dylib : 31.82 milliseconds (13.3%)
- 冷启动可以概括为3大阶段
- dyld
- runtime
- main
- dyld(dynamic link editor),Apple的动态连接器,可以装载Mach-O(可执行文件、动态库等)
- 装载app的可执行文件,同时递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完成后,会通知runtime进行下一步处理
- runtime所做的事情
- 调用map_images函数中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册objc类、初始化类对象等等)
- 调用C++静态初始化器和attribure((constructor))修饰的函数(JSONKit中存在具体应用)
- 到此为止,可执行文件和动态库中所有的符号(Class, Protocol, Selector, IMP...)都已按格式成功加载到内存中,被runtime所管理
- 总结
- app的启动由dylb主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是ApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
项目的优化、性能优化
App启动优化
- 依据App冷启动阶段进行优化
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少objc类、分类的数量、减少selector数量(定期清理不必要的类、分类)
- 减少C++虚构函数
- Swift尽量使用struct
- runtime
- 使用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、Objc的+load方法
- main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需加载
App 包体积优化
-
安装包瘦身(ipa):资源文件、可执行文件
-
资源文件(图片、音频、视频等)
- 采取无损压缩(使用工具)
- 去除没有用到的资源(https://github.com/tinymind/LSUnusedResources)
-
可执行文件瘦身:
- 编译器优化(Xcode相关配置)
- 利用AppCode(https://www.jetbrains.com/objc/) 检测未使用的代码:菜单栏 -> Code -> Inspect Code
- 生成LinkMap,可以查看可执行文件的具体组成
- 可借助第三方工具解析LinkMap文件:http://github.com/huanxsd/LinkMap
页面浏览速度优化
- json的处理(iOS 自带的NSJSONSerialization,Jsonkit,SBJson)
- 数据的分页(后端数据多的话,就要分页返回,例如网易新闻,或者 微博记录)
- 数据压缩(大数据也可以压缩返回,减少流量,加快反应速度)
- 内容缓存(例如网易新闻的最新新闻列表都是要缓存到本地,从本地加载,可以缓存到内存,或者数据库,根据情况而定)
- 延时加载tab(比如app有5个tab,可以先加载第一个要显示的tab,其他的在显示时候加载,按需加载
- 算法的优化(核心算法的优化,例如有些app 有个 联系人姓名用汉语拼音的首字母排序)
操作流畅度优化
- Tableview 优化(tableview cell的加载优化)
- ViewController加载优化(不同view之间的跳转,可以提前准备好数据)
数据持久化
SQLite
SQLite 数据库的特点有哪些?
- SQLite 数据库有以下特点:
- 轻量级;
- 独立性,没有依赖,无需安装和管理;
- 隔离性,全部在一个文件夹系统;
- 跨平台性, 支持众多操作系统;
- 多语言接口,支持众多编程语言;
- SQLite 事务是完全兼容 ACID 的,通过独占性和共享锁来实现独立事务的处理,允许从多个进程或线程安全访问。
请简述几种 SQLite 命令
- SQLite 的常用命令和其操作属性分为以下几种:
- CREATE : 创建一个新的表,一个表的视图,或者数据库中的其他对象;
- ALTER: 修改数据库中的某个已有的数据库对象,比如一个表;
- DROP: 删除整个表,或者表的视图,或者数据库中的其他对象;
- INSERT:创建一条记录;
- UPDATE: 修改记录;
- DELETE : 删除记录;
- SELECT:从一个或多个表中检索某些记录。
Core Data
Core Data 是什么?
- Core Data 是一个框架,提供了对象—关系映射(ORM)的功能,既能够将对象转化成数据,保存在 SQLite 数据库文件中,也可以按照需求将数据库中的数据还原成对象。
Core Data 是一个关系型数据库吗?
- Core Data 不是一个关系型数据库,也不是关系型数据库管理系统,只是为数据变更管理、对象存储、对象读取恢复的功能提供了支持。
Core Data 与 SQLite 有无必然联系?
Core Data 与 SQLite 是有联系的,Core Data 是基于 SQLite 数据库的一个封装,底层仍然是使用 SQLite 进行存储数据的。
对比分析 Core Data 和 SQLite 的优缺点
- SQLite 是使用最多的 轻量级开源数据库引擎,无配置、无服务要求的事务数据库引擎。其优点有 独立于服务器、零配置、多进程和线程下安全访问等。Core Data 更加关注于对象而不是传统的表数据库方法,相比于 SQLite,CoreData 集成化,数据动态加载,在取数据方面更快,不需要书写大量的 sql 语句,且还具备其他 sql 所不具备的优点,比如对 undo 的支持、多个 context 实现sketchbook 类似的功能等,但是 Core Data 数据模型升级兼容性较差 较差,不便于处理多对多的关系,自定义升级麻烦,并且效率低下。
测试
单元测试
- 测试可以分为黑盒测试和白盒测试,单元测试可以算是一种白盒测试的类型,其目的是提高软件开发的效率,维持代码的健康性,助力开发人员开发健壮、安全的应用程序。
应用程序的开发者对现有的模块编写相应的测试代码进行测试,包含测试用例的设计。
又程序的开发者自己编写测试代码并进行白盒测试,之后再交给测试团队进行相应的黑盒测试,这种方式有利于提升测试流程的完整性,从而保证应用程序的质量。
单元测试比较适合进行应用程序的业务逻辑和网络请求接口方面的测试。
单元测试的关键组件是测试用例,测试用例可以在最低可测试的单元对代码进行测试。
什么是单元测试?
单元测试是针对程序的最小单元进行正确性检验的测试工作。
本文参考文章及部分内容来源
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/ios-interview/r1rxtj/
作者:强子ly
链接:https://www.jianshu.com/p/24a9447d70f8
网友评论