iOS 知识点整理

iOS App 安全

本地数据安全

  • 对于主动存储在app内的重要的、有价值的、涉及隐私的信息需要加密处理,增加攻击者破解难度。
  • 对于一些非主动的存储行为如网络缓存,涉及重要信息,做到用完即删。
  • Sqlite敏感数据要么对数据加密(优:使用简单,无需三方库支持;缺:每次存储都有加密解密过程,增加APP资源消耗),要么对整库加密(优:对数据库整体操作,减少资源消耗。缺:需要使用第三库),如SQLCipher
  • KeyChain数据的读取。Keychain是一个拥有有限访问权限的SQLite数据库(AES256加密),可以为多种应用程序或网络服务存储少量的敏感数据(如用户名、密码、加密密钥等)。可以在越狱的设备上使用keychain_dumper来解密。
  • 剪切板的缓存要及时清理

网络请求使用https,防止http劫持或使用httpdns

尽量使用WKWebView代替UIWebView

加大反编译的难度

  • 字符串加密
  • 类名方法名混淆
  • 反调试

防止抓包

  • 判断是否设置了代理
  • 数据加密

断点续传怎么实现?需要设置什么?

所谓断点续传,就是要从文件已经下载的地方开始继续下载。所以在客户端/浏览器传给Web服务器的时候要多加一条信息–从哪里开始。下面是用自己编的一个”浏览器”来传递请求信息给Web服务器,要求从2000070字节开始。

GET /down.zip HTTP/1.1
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

仔细看一下就会发现多了一行RANGE: bytes=2000070-这一行的意思就是告诉服务器down.zip这个文件从2000070字节开始传,前面的字节不用传了。服务器收到这个请求以后,返回的信息如下:

206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT

和前面服务器返回的信息比较一下,就会发现增加了一行:Content-Range=bytes 2000070-106786027/106786028返回的代码也改为206了,而不再是200了。

HTTP请求的哪些方法用过?什么时候选择get、post、put、delete?

http本质上就是应用层的一套协议,制定之初就考虑到增删改查这样的应用场景,预设了几个method(get post put delete …)用来区别资源的执行动作。只要双方使用http协议,对一个url使用某个method访问,服务器就知道对应什么逻辑。只是一直以来没那么严格的使用而已。

get最常用,通常向Web服务器发请求“获取”资源;post则是向Web服务器传输一些数据包来获取资源;两者方法严格来说都是“索取”行为。

delete通过http请求删除指定的url上资源;put通过http请求创建资源。

TCP 三次握手和四次挥手

三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

四次挥手

三次握手耳熟能详,所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,具体流程如下图

iOS里面有哪些数据存储方法

NSUserDefaults类

一般用于一些基本的用户设置,数据量很小数据存储。

NSUserDefaults类除了可以存储数组、字典、NSdata外,还可以直接存储OC基本类型属性。

Plist文件

类似NSUserDefaults类

归档

能对自定义的对象进行存储,可用于较大数据存储

数据库(Sqlite、Core Data、Realm …)

对于大型的数据存储,再使用上面的方法显然不适用了,该使用数据库了,但不适合直接存储图片、文件或二进制数据。

手动存放沙盒

高度可定制化,可存储任意数据

MVVM如何实现绑定

KVO

键值观察Key-Value-Observer就是观察者模式。

观察者模式的定义:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

KVO同KVC一样,通过 isa-swizzling 技术来实现。当观察者被注册为一个对象的属性的观察对象的isa指针被修改,指向一个中间类,而不是在真实的类。其结果是,isa指针的值并不一定反映实例的实际类。

所以不能依靠isa指针来确定对象是否是一个类的成员。应该使用class方法来确定对象实例的类。

Delegate

Delegate本质是一种程序设计模型,iOS中使用Delegate主要用于两个页面之间的数据传递

Block

Block本质是一种程序设计模型,可以代替delegate

Notification

NSNotification是用来在类之间传递消息参数的。

block与protocol相比的优点

block的作用:保存一段代码,到恰当的时候调用,很多时候block是代理的一种优化方案

1、block比protocol更灵活,更高聚合,低耦合。
例如AFN的网络框架中,就可以将“准备请求参数”的代码和“处理后台返回数据”的代码放在一起。

2、block的灵活还体现在他可以当作方法参数以及返回值。
Block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值。

在OC中,`()`block是以()的形式去执行的,猜想如果返回一个block的话,那么我就可以用()来实现链式编程这种效果了吧!

Delegate 、NSNotification 、KVO之间的联系及其区别

区别:

delegate方法往往需要关注返回值, 也就是delegate方法的结果。

delegate只是一对一,而NSNotification、KVO可以一对多,两者都没有返回值。

各自特点:

NSNotification的特点,就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活。

KVO只能检测类中的属性,并且属性名都是通过NSString来查找,编译器不会帮你检错和补全,所以比较容易出错。

delegate方法最典型的特征是往往需要关注返回值。

进程和线程的区别

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

线程:是进程的一个执行单元,是进程内调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

一个程序至少一个进程,一个进程至少一个线程。

进程线程的区别:

地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
     一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

     进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程是处理器调度的基本单位,但是进程不是。
两者均可并发执行。

优缺点:

线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(多CPU系统)上运行。
进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。

进程间通信的方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  2. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  3. 共享内存SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  4. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  5. 套接字Socket:套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同系统间的进程通信。
  6. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

卡顿检测

为了实现卡顿的检测,首先需要注册RunLoop的监听回调(注册RunLoopObserverCallback函数),保存RunLoop状态;当主线程处在Before Waiting状态的时候,通过派发任务到主线程来设置标记位的方式处理常态下的卡顿检测。

发布出去的版本,怎么收集crash日志

三方crash收集系统,bugly等等

app内监听signal,保存sginal引发的中断错误,并上报

1
2
3
4
5
6
7
8
9
signal(SIGHUP, SignalExceptionHandler);
signal(SIGINT, SignalExceptionHandler);
signal(SIGQUIT, SignalExceptionHandler);
signal(SIGABRT, SignalExceptionHandler);
signal(SIGILL, SignalExceptionHandler);
signal(SIGSEGV, SignalExceptionHandler);
signal(SIGFPE, SignalExceptionHandler);
signal(SIGBUS, SignalExceptionHandler);
signal(SIGPIPE, SignalExceptionHandler);

app内监听exception,保存错误,并上报

1
2
3
4
void HandleException(NSException *exception) {

}
NSSetUncaughtExceptionHandler(&HandleException)

在block里面使用属性会造成循环引用吗?怎么解决?

1.self->属性
2.block外弱引用self,block内强引用弱引用后的self

PING命令使用的是什么协议?

使用的是ICMP协议,是“Internet Control Message Protocol”(Internet控制消息协议)的缩写,是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。

PING命令是用于检测网络连接性、可到达性和名称解析的疑难问题的主要TCP/IP命令.

MTU

最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。

因为协议数据单元的包头和包尾的长度是固定的,MTU越大,则一个协议数据单元的承载的有效数据就越长,通信效率也越高。MTU越大,传送相同的用户数据所需的数据包个数也越低。

MTU也不是越大越好,因为MTU越大, 传送一个数据包的延迟也越大;并且MTU越大,数据包中 bit位发生错误的概率也越大。

MTU越大,通信效率越高而传输延迟增大,所以要权衡通信效率和传输延迟选择合适的MTU。

MRC与ARC区别

MRC手动内存管理

引用计数器:在MRC时代,系统判定一个对象是否销毁是根据这个对象的引用计数器来判断的。

  1. 每个对象被创建时引用计数都为1
  2. 每当对象被其他指针引用时,需要手动使用[obj retain];让该对象引用计数+1。
  3. 当指针变量不在使用这个对象的时候,需要手动释放release这个对象。 让其的引用计数-1.
  4. 当一个对象的引用计数为0的时候,系统就会销毁这个对象。

在MRC模式下必须遵循谁创建,谁释放,谁引用,谁管理

在MRC下使用ARC,在Build Phases的Compile Sources中选择需要使用MRC方式的.m文件,然后双击该文件在弹出的会话框中输入 -fobjc-arc

ARC自动内存管理

WWDC2011和iOS5所引入自动管理机制——自动引用计数(ARC),它不是垃圾回收机制而是编译器的一种特性。ARC管理机制与MRC手动机制差不多,只是不再需要手动调用retain、release、autorelease;当你使用ARC时,编译器会在在适当位置插入release和autorelease;ARC时代引入了strong强引用来带代替retain,引入了weak弱引用。

在ARC工程中如果要使用MRC的需要在工程的Build Phases的Compile Sources中选择需要使用MRC方式的.m文件,然后双击该文件在弹出的会话框中输入 -fno-objc-arc

KVC的作用

  1. KVC可以给对象的私有变量赋值(UIPageControl)

    使用注意:

    1. 设置key/keyPath位置的字符串必须保证有对应的属性(或者_属性)
    2. setValue:forKey:和setValue:forKeyPath区别
1
2
3
4
5
6
Person *p = [[Person alloc] init];
p.book = [[Book alloc] init];
[p setValue:@"18" forKey:@"age”];       //不报错
[p setValue:@"20" forKeyPath:@"age”]; //不报错
[p setValue:@"300" forKey:@"book.price”];     //报错
[p setValue:@"300" forKeyPath:@"book.price”]; //不报错
  1. 用于字典转模型(MJExtension框架)

    使用注意:

    1. 必须保证字典中对应key在模型中能找到对应的属性
    2. 模型中的属性可以在字典中没有对应的Key
    3. 通过KVC取出私有变量的值
    4. 模型对象转字典

使用method swizzling要注意什么?

进行版本迭代的时候需要进行一些检验,防止系统库的函数发生了变化

一个 objc 对象如何进行内存布局(考虑有父类的情况)?

每一个对象内部都有一个 isa 指针,指向他的类对象,类对象中存放着本对象的:

对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)。

成员变量的列表。

属性列表。

类对象内部也有一个 isa 指针指向元对象(meta class),元对象内部存放的是类方法列表。

类对象内部还有一个 superclass 的指针,指向他的父类对象。

所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中。

runtime 如何通过 selector 找到对应的 IMP 地址(分别考虑实例方法和类方法)?Selector、Method 和 IMP 的有什么区别与联系?

对于实例方法,每个实例的 isa 指针指向着对应类对象,而每一个类对象中都一个对象方法列表。

对于类方法,每个类对象的 isa 指针都指向着对应的元对象,而每一个元对象中都有一个类方法列表。方法列表中记录着方法的名称,方法实现,以及参数类型,其实 selector 本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现。

Selector、Method 和 IMP 的关系可以这样描述:在运行期分发消息,方法列表中的每一个实体都是一个方法(Method),它的名字叫做选择器(SEL),对应着一种方法实现(IMP)。

1
2
3
4
5
6
7
8
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
SEL method_name; // 方法选择器。
char *method_types; // 存储着方法的参数类型和返回值类型。
IMP method_imp; // 函数指针。
}

objc 中的类方法和实例方法有什么本质区别和联系?

类方法:

类方法是属于类对象的
类方法只能通过类对象调用
类方法中的 self 是类对象
类方法可以调用其他的类方法
类方法中不能访问成员变量
类方法中不能直接调用对象方法

实例方法:

实例方法是属于实例对象的
实例方法只能通过实例对象调用
实例方法中的 self 是实例对象
实例方法中可以访问成员变量
实例方法中直接调用实例方法
实例方法中也可以调用类方法(通过类名)

objc_msgSend、_objc_msgForward 都是做什么的?OC 中的消息调用流程是怎样的?

objc_msgSend 是用来做消息发送的。在 OC 中,对方法的调用都会被转换成内部的消息发送执行对 objc_msgSend 方法的调用

_objc_msgForward 是 IMP 类型(函数指针),用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会尝试做消息转发。

在消息调用的过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用 _objc_msgForward函数指针代替 IMP。最后,执行这个 IMP。

当调用一个 NSObject 对象不存在的方法时,并不会马上抛出异常,而是会经过多层转发,层层调用对象的 resolveInstanceMethod:forwardingTargetForSelector:methodSignatureForSelector:forwardInvocation:等方法。其中最后forwardInvocation:是会有一个 NSInvocation 对象,这个 NSInvocation 对象保存了这个方法调用的所有信息,包括 Selector 名,参数和返回值类型,最重要的是有所有参数值,可以从这个 NSInvocation 对象里拿到调用的所有参数值。

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

不能向编译后得到的类中增加实例变量,能向运行时创建的类中添加实例变量。

因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。

运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

OC Runtime

OC的反射机制

  • 对对象进行操作的方法一般以object_开头
  • 对类进行操作的方法一般以class_开头
  • 对类或对象的方法进行操作的方法一般以method_开头
  • 对成员变量进行操作的方法一般以ivar_开头
  • 对属性进行操作的方法一般以property_开头开头
  • 对协议进行操作的方法一般以protocol_开头

OC消息机制和消息转发机制

objc_msgSend

当找不到方法时
resolveInstanceMethod:(加方法class_addMethod([self class], sel, (IMP)speak, "V@:");)、forwardingTargetForSelector:(重定向Target)、methodSignatureForSelector:forwardInvocation:

OC关联对象

给类加属性,在类别中不能直接加属性
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

1
2
3
4
5
6
7
8
9
10
11
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. */
};

OC isa swizzling

KVO 系统自动自成NSKVONotifying_XX类,在要观察的属性的SET方法里加入- (void)willChangeValueForKey:(NSString )key;- (void)didChangeValueForKey:(NSString )key ;两个方法,调用方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

OC method swizzling

替换成自定义的方法,可以在自定义的方法内调用原方法,实现切面编程

iOS Runloop

Runloop 基于 线程(pthread)来管理的,主线程自动创建一个Runloop,其它线程只能通过在该线程调用 CFRunLoopGetCurrent() 获取。

iOS 提供了5种 Runloop Modes:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)(公开)

    默认,大部分情况,使用此Mode。

  • NSConnectionReplyMode(私有)

    跟 NSConnection 相关的Mode

  • NSModalPanelRunLoopMode(私有)

    跟 Modal panels 相关事件的Mode

  • NSEventTrackingRunLoopMode(私有)

    跟用户互动相关事件的Mode

  • NSRunLoopCommonModes(kCFRunLoopCommonModes)(公开)

    Mode组,包括除Coonection外的其它三个Mode(Default、Modal、Event tracking)

每个 Mode 都包含 Source0(只包含了一个回调(函数指针),它并不能主动触发事件),Source1(包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息),Observers,Timers 四个Item,一个Item可以被同时加入多个 Mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

每一个 Runloop 都会选择一种Mode启动运行。其过程如下:

  1. 通知Runloop已进入
  2. 通知Runloop有Timer事件待处理,处理Timer事件
  3. 通知Runloop有Source0事件待处理
  4. 处理Souce0事件
  5. 如有Source1事件待处理,则分发该事件
  6. 即将休眠
  7. 如有Source1事件到达或Timer事件待处理或Runloop超时,则从休眠中唤醒
  8. 通知线程被唤醒
  9. 如有Timer事件待处理,则重启Runloop;如有Source1事件待处理,则分发该事件
  10. 通知Runloop已退出

runloop 和线程有什么关系?

首先,iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。过去苹果有份文档标明了 NSThread 只是 pthread_t 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自mach thread (基于pthread_t)。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np()或 [NSThread mainThread]来获取主线程;也可以通过 pthread_self()或[NSThread currentThread]来获取当前线程。CFRunLoop 是基于 pthread 来管理的。

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()和 CFRunLoopGetCurrent()。

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

runloop 的 mode 作用是什么?

在 CoreFoundation 里面关于 RunLoop 有 5 个类,分别对应不同的概念:

CFRunLoopRef,对应 runloop。

CFRunLoopModeRef,对应 runloop mode。CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装

CFRunLoopSourceRef,对应 source,表示事件产生的地方。Source 有两个版本:Source0 和 Source1。Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef,对应 timer,是基于时间的触发器。它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。

CFRunLoopObserverRef,对应 observer,表示观察者。每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

kCFRunLoopEntry,即将进入Loop
kCFRunLoopBeforeTimers,即将处理 Timer
kCFRunLoopBeforeSources,即将处理 Source
kCFRunLoopBeforeWaiting,即将进入休眠
kCFRunLoopAfterWaiting,刚从休眠中唤醒
kCFRunLoopExit,即将退出Loop

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

线程的运行的过程中需要去处理不同情境的不同事件,mode 则是这个情景的标识,告诉当前应该响应哪些事件。一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

聊一聊iOS 中的离屏渲染?

GPU 渲染机制:CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
GPU 屏幕渲染有以下两种方式:

1)On-Screen Rendering,意为当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。
2)Off-Screen Rendering,意为离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

特殊的离屏渲染:如果将不在 GPU 的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU 渲染。如果我们重写了 drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地 完成,渲染得到的 bitmap 最后再交由 GPU 用于显示。

备注:Core Graphics 通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程。

离屏渲染的触发方式:

1)shouldRasterize(光栅化),光栅化是比较特别的一种。光栅化概念:将图转化为一个个栅格组成的图象。光栅化特点:每个元素对应帧缓冲区中的一像素。shouldRasterize = YES 在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的 layer 及其 sublayers 没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES 这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度。相当于光栅化是把 GPU 的操作转到 CPU 上了,生成位图缓存,直接读取复用。当你使用光栅化时,你可以开启 Color Hits Green and Misses Red 来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费。例如经常打交道的 TableViewCell,因为 TableViewCell 的重绘是很频繁的(因为 Cell 的复用),如果 Cell 的内容不断变化,则 Cell 需要不断重绘,如果此时设置了 cell.layer 可光栅化,则会造成大量的离屏渲染,降低图形性能。

2)masks(遮罩)

3)shadows(阴影)

4)edge antialiasing(抗锯齿)

5)group opacity(不透明)

6)复杂形状设置圆角等

7)渐变

为什么会使用离屏渲染:当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前(下一个 VSync 信号开始前)不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论 CPU 还是 GPU)。所以当使用离屏渲染的时候会很容易造成性能消耗,因为离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
Instruments 监测离屏渲染:

1)Color Offscreen-Rendered Yellow,开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
2)Color Hits Green and Misses Red,如果 shouldRasterize 被设置成 YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。

iOS 版本上的优化:

1)iOS 9.0 之前 UIimageView、UIButton 设置圆角都会触发离屏渲染。

2)iOS 9.0 之后 UIButton 设置圆角会触发离屏渲染,而 UIImageView 里 png 图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

事件传递链,页面上一个按钮,按钮和它的superView有一样的action,为什么只执行button的action?

hitTest方法:首先会通过调用自身的 pointInside 方法判断用户触摸的点是否在当前对象的响应范围内,如果 pointInside 方法返回 NO hitTest方法直接返回 nil

如果 pointInside 方法返回 YES hitTest方法接着会判断自身是否有子视图.如果有则调用顶层子视图的 hitTest 方法 直到有子视图返回 View

如果所有子视图都返回 nil hitTest 方法返回自身.

runtime的应用

拦截系统自带的方法调用(Method Swizzling黑魔法)
实现给分类增加属性
实现字典的模型和自动转换
aspect 切面编程

iOS应用导航模式有哪些?

平铺模式,一般由scrollView和pageControl组合而成的展示方式。手机自带的天气比较典型。

标签模式,tabBar的展示方式,这个比较常见。

树状模式,tableView的多态展示方式,常见的9宫格、系统自带的邮箱等展现方式。

iOS单元测试框架有哪些?

OCUnit 是 OC 官方测试框架, 现在被 XCTest 所取代。

XCTest 是与 Foundation 框架平行的测试框架。

GHUnit 是第三方的测试框架。

OCMock 都是第三方的测试框架。

Object-c 的类可以多重继承么?可以实现多个接口么?Category 是什 么?重写一个类的方式用继承好还是分类好?为什么?

Object-c 的类不可以多重继承;可以实现多个代理,通过实现多个代理
可以完成 C++的多重继承;Category 是类别,一般情况用分类好,用
Category 去重写类的方法,仅对本 Category 有效,不会影响到其他
类与原有类的关系。

谈谈对性能优化的看法,如何做?

1、程序 logging 不要太长

2、相同数据不做重复获取

3、昂贵资源要重用(cell、sqlite、date)

4、良好的编程习惯和程序设计:选择正确的数据结构和算法来进行编程、选择适合的数据存储(plist、SQLite)、优化 SQLite 查询语句

5、数据资源方面的优化(缓存和异步加载)

Profile工具测试,不要猜

runtime 如何实现 weak 属性

1、初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。

2、添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数,objc_storeWeak() 的作用是更新指针指向(指针可能原来指向着其他对象,这时候需要将该 weak 指针与旧对象解除绑定,会调用到 weak_unregister_no_lock),如果指针指向的新对象非空,则创建对应的弱引用表,将 weak 指针与新对象进行绑定,会调用到 weak_register_no_lock。在这个过程中,为了防止多线程中竞争冲突,会有一些锁的操作。

3、释放时:调用 clearDeallocating 函数,clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。

BAD_ACCESS 在什么情况下出现?

访问了野指针。比如对一个已经释放的对象执行了 release,访问已经释放对象的成员变量或者发消息。

死循环。

聊一聊 TCP 的拥塞控制相关过程?

TCP 的拥塞控制主要是四个算法:1)慢启动;2)拥塞避免;3)拥塞发生;4)快速恢复。

慢启动算法

慢启动的算法如下(cwnd 全称 Congestion Window):

  1. 连接建好的开始先初始化 cwnd = 1,表明可以传一个 MSS(Max Segment Size)大小的数据。
  2. 每当收到一个 ACK,cwnd++; 呈线性上升。
  3. 每当过了一个 RTT,cwnd = cwnd*2; 呈指数上升。
  4. 还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入「拥塞避免算法」。

所以,我们可以看到,如果网速很快的话,ACK 也会返回得快,RTT 也会短,那么,这个慢启动就一点也不慢。

拥塞避免算法

前面说过,还有一个 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入拥塞避免算法。一般来说 ssthresh 的值是 65535 字节,当 cwnd 达到这个值时后,算法如下:

  1. 收到一个 ACK 时,cwnd = cwnd + 1/cwnd。
  2. 当每过一个 RTT 时,cwnd = cwnd + 1。

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。

拥塞状态时的算法

当丢包的时候,会有两种情况:

  1. 等到 RTO 超时,重传数据包。TCP 认为这种情况太糟糕,反应也很强烈。

    sshthresh = cwnd/2。
    cwnd 重置为 1。
    进入慢启动过程。
    
  2. 快速重传(Fast Retransmit)算法,也就是在收到 3 个 duplicate ACK 时就开启重传,而不用等到 RTO 超时。

    TCP Tahoe 的实现和 RTO 超时一样。
    TCP Reno的实现是:
    
    cwnd = cwnd/2。
    
    sshthresh = cwnd。
    进入快速恢复算法(Fast Recovery)。
    

上面我们可以看到 RTO 超时后,sshthresh 会变成 cwnd 的一半,这意味着,如果 cwnd<=sshthresh 时出现的丢包,那么 TCP 的 sshthresh 就会减了一半,然后等 cwnd 又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP 是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。

快速恢复算法

TCP Reno 这个算法定义在 RFC5681。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有 3 个 Duplicated Acks 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。注意,正如前面所说,进入 Fast Recovery 之前,cwnd 和 sshthresh 已被更新:

cwnd = cwnd /2
sshthresh = cwnd

然后,真正的 Fast Recovery 算法如下:

cwnd = sshthresh + 3 * MSS (3 的意思是确认有 3 个数据包被收到了)。

重传 Duplicated ACKs 指定的数据包。

如果再收到 duplicated ACKs,那么 cwnd = cwnd + 1。

如果收到了新的 ACK,那么 cwnd = sshthresh,然后就进入了拥塞避免的算法了。

如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是它依赖于 3 个重复的 ACKs。注意,3 个重复的 ACKs 并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到 RTO 超时。于是,进入了恶梦模式:超时一个窗口就减半一下。多个超时会超成 TCP 的传输速度呈级数下降,而且也不会触发 Fast Recovery 算法了。
1995 年,TCP New Reno(参见 RFC 6582 )算法提出来,主要就是在没有 SACK 的支持下改进 Fast Recovery 算法:

当 sender 这边收到了 3 个 Duplicated ACKs,进入 Fast Retransimit 模式,开发重传重复 ACKs 指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的 ACK 会把整个已经被 sender 传输出去的数据 ACK 回来。如果没有的话,说明有多个包丢了。我们叫这个 ACK 为 Partial ACK。

一旦 Sender 这边发现了 Partial ACK 出现,那么,sender 就可以推理出来有多个包被丢了,于是乎继续重传 sliding window 里未被 ack 的第一个包。直到再也收不到了 Partial ACK,才真正结束 Fast Recovery 这个过程。

这个 Fast Recovery 的变更是一个非常激进的玩法,他同时延长了 Fast Retransmit 和 Fast Recovery 的过程。

聊一聊你知道的几种查找树?

AVL 树:平衡二叉搜索树。它的平衡度也最好,左右高度差可以保证在「-1,0,1」,基于它的平衡性,它的查询时间复杂度可以保证是 O(log(n))。但每个节点要额外保存一个平衡值,或者说是高度差。这种树是二叉树的经典应用,现在最主要是出现在教科书中。AVL 的平衡算法比较麻烦,需要左右两种 rotate 交替使用。

红黑树:平衡二叉搜索树。也就是说,如果从高度差来说,红黑树是大于 AVL 的,其实也就代表着它的实际查询时间(最坏情况)略逊于 AVL 的。数学证明红黑树的最大深度是 2log(n+1)。其实最差情况它从根到叶子的最长路可以是最短路的两倍,但也不是很差,所以它的查询时间复杂度也是 O(log(n))。从实现角度来说,保存红黑状态,每个节点只需要一位二进制,也就是一个 bit。红黑树是工业界最主要使用的二叉搜索平衡树:Java 用它来实现 TreeMap;C++ 用它来实现 std::set/map/multimap;著名的 Linux 进程调度 Completely Fair Scheduler,用红黑树管理进程控制块;epoll 在内核中的实现,用红黑树管理事件块;nginx 中,用红黑树管理 timer。

二叉搜索树:查找的时间复杂度是 O(log(n)),最坏情况下的时间复杂度是 O(n)。二叉搜索树有一个缺点就是,树的结构是无法预料的,随意性很大,它只与节点的值和插入的顺序有关系,往往得到的是一个不平衡的二叉树。在最坏的情况下,可能得到的是一个单支二叉树,其高度和节点数相同,相当于一个单链表,对其正常的时间复杂度有 O(log(n)) 变成了 O(n)。

B/B+ 树:N 叉平衡树。每个节点可以有更多的孩子,新的值可以插在已有的节点里,而不需要改变树的高度,从而大量减少重新平衡和数据迁移的次数,这非常适合做数据库索引这种需要持久化在磁盘,同时需要大量查询和插入操作的应用。

Trie 树:Trie 树并不是平衡树,也不一定非要有序。查询和插入时间复杂度都是 O(n)。是一种以空间换时间的方法。当节点树较多的时候,Trie 树占用的内存会很大。它主要用于前缀匹配,比如字符串。如果字符串长度是固定或者说有限的,那么 Trie 树的深度是可控制的,你可以得到很好的搜索效果,而且插入新数据后不用平衡。比如 IP 选路,也是前缀匹配,一定程度会用到 Trie 树。

如何优化 App 的的包大小?

  1. 资源优化

    1. 删除无用图片
    2. 删除重复资源
    3. 压缩图片资源
    4. 用 LaunchScreen.storyboard 替换启动图片
    5. 本地大图片都使用 webp
    6. 资源按需加载,非必要资源都等到使用时再从服务端拉取
  2. 编译选项优化

    1. Optimization Level 在 release 状态设置为 Fastest/Smallest。
    2. Strip Debug Symbols During Copy 在 release 状态设置为 YES。
    3. Strip Linked Product 在 release 状态设为 YES。
    4. Make String Read-Only 在 release 状态设为 YES。
    5. Dead Code Stripping 在 release 状态设为 YES。
    6. Deployment PostProcessing 在 release 状态设为 YES。
    7. Symbols hidden by default 在 release 状态设为 YES。
  3. 可执行文件优化

    1. 三方库优化
      1. 删除不使用的三方库。
      2. 功能用的少但是体积大的三方库可以考虑自己重写。
      3. 合并功能重复的三方库。
    2. 代码分析
      1. 去掉无用的类及文件。
      2. 清理 import。
      3. 去掉空方法。
      4. 去掉无用的 log。
      5. 去掉无用的变量。
  4. 苹果官方的策略

    1. App Thinning
      使用 xcasset 管理图片
    2. 开启 Bitcode

循环引用的产生原因,以及解决方法

原因:
两个对象相互引用
多个对象的“环”引用

方法:

事先知道存在循环引用的地方,在合理的位置主动断开一个引用,是对象回收。

使用弱引用的方法。

数据库建表的时候索引有什么用?

创建索引可以大大提高系统的性能。

  1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
  2. 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  3. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
  4. 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
  5. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

TCP和UDP的区别于联系

TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;

UDP为用户数据报协议,非连接的不可靠的点到多点的通信;

TCP侧重可靠传输,UDP侧重快速传输。

App启动过慢,你可能想到的因素有哪些?

  1. App启动过程

    1. 解析Info.plist
      1. 加载相关信息,例如如闪屏
      2. 沙箱建立、权限检查
    2. Mach-O加载
      1. 如果是胖二进制文件,寻找合适当前CPU类别的部分
      2. 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
      3. 定位内部、外部指针引用,例如字符串、函数等
      4. 执行声明为attribute((constructor))的C函数
      5. 加载类扩展(Category)中的方法
      6. C++静态对象加载、调用ObjC的 +load 函数
    3. 程序执行
      1. 调用main()
      2. 调用UIApplicationMain()
      3. 调用applicationWillFinishLaunching
  2. 影响启动性能的因素

    1. main()函数之前耗时的影响因素

      1. 动态库加载越多,启动越慢。
      2. ObjC类越多,启动越慢
      3. C的constructor函数越多,启动越慢
      4. C++静态对象越多,启动越慢
      5. ObjC的+load越多,启动越慢
    2. main()函数之后耗时的影响因素

      1. 执行main()函数的耗时
      2. 执行applicationWillFinishLaunching的耗时

        rootViewController及其childViewController的加载、view及其subviews的加载

七层网络协议

  1. 应用层:
    1. 用户接口、应用程序;
    2. Application典型设备:网关;
    3. 典型协议、标准和应用:TELNET、FTP、HTTP
  2. 表示层:
    1. 数据表示、压缩和加密presentation
    2. 典型设备:网关
    3. 典型协议、标准和应用:ASCLL、PICT、TIFF、JPEG|MPEG
    4. 表示层相当于一个东西的表示,表示的一些协议,比如图片、声音和视频MPEG。
  3. 会话层:
    1. 会话的建立和结束;
    2. 典型设备:网关;
    3. 典型协议、标准和应用:RPC、SQL、NFS、X WINDOWS、ASP
  4. 传输层:
    1. 主要功能:端到端控制Transport;
    2. 典型设备:网关;
    3. 典型协议、标准和应用:TCP、UDP、SPX
  5. 网络层:
    1. 主要功能:路由、寻址Network;
    2. 典型设备:路由器;
    3. 典型协议、标准和应用:IP、IPX、APPLETALK、ICMP;
  6. 数据链路层:
    1. 主要功能:保证无差错的疏忽链路的data link;
    2. 典型设备:交换机、网桥、网卡;
    3. 典型协议、标准和应用:802.2、802.3ATM、HDLC、FRAME RELAY;
  7. 物理层:
    1. 主要功能:传输比特流Physical;
    2. 典型设备:集线器、中继器
    3. 典型协议、标准和应用:V.35、EIA/TIA-232.

内存的使用和优化的注意事项

重用问题

尽量把views设置为不透明

不要使用太复杂的XIB/Storyboard

选择正确的数据结构

延迟加载

数据缓存

处理内存警告

避免反复处理数据

使用Autorelease Pool

正确选择图片加载方式

与 NSURLConnection 相比,NSURLsession 改进哪些?

可以配置每个 session 的缓存,协议,cookie,以及证书策略(credential policy),甚至跨程序共享这些信息

session task。它负责处理数据的加载以及文件和数据在客户端与服务端之间的上传和下载。NSURLSessionTask 与 NSURLConnection 最大的相似之处在于它也负责数据的加载,最大的不同之处在于所有的 task 共享其创造者 NSURLSession 这一公共委托者(common delegate)

使用drawRect有什么影响?

缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIView实例,那就会很糟糕了

这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制

-------------本文结束感谢您的阅读-------------