iOS开发——面试题1

基础部分

1、线程和进程有什么区别

进程是一个程序执行的实例,是资源分配的最小单位

线程是进程中的一个实例,是操作系统可以识别的最小执行和调度单位

那么,线程和进程与堆、栈之间的关系?

栈是线程独有的,保存其运行状态和局部自动变量,栈空间是线程安全的,栈被自动分配到进程的内存空间,栈内存无需开发管理,系统自动管理

堆在操作系统初始化进程的时候分配,运行过程可以要求更多额外的堆内存,但是需要返回,不然呢就是内存泄露

2、线程之间的通信

例如在多线程并发条件下,为了让线程之间可以更方便的共同完成一个任务,需要一些协调通信,采取的通信方式就是 等待、唤起。

也就是  wait()  和 notify()、 notifyAll()

3、几个线程问题

 1、串行队列  不管是不是async  或者 sync  你要执行 即使是async中等待 也得等着走完,因为就一个队列在进行

3、并行队列,如果async sleep等待,其他的可以同步进行  不需要等他,但是如果是sync等待在队列前面, 那还是乖乖等着。毕竟队列同步的话顺序执行,即使是同步并行,只要是sync 同步 ,就在一个线程里跑 就是串行执行的,如果是异步就会开启线程可以利用并行队列并行执行。

4、iOS 几种锁的性能问题

https://www.jianshu.com/p/b1edc6b0937a

@synchronized 性能最差

自旋锁最好,dispatch_semaphore 信号较好,然后是NSLock性能较好

os_unfair_lock  互斥锁  替换自旋锁

苹果通过这个处理了优先级反转的问题

**临界区:**指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

**自旋锁:**是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种`忙等待`。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

**互斥锁(Mutex):**是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。

**读写锁:**是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

**信号量(semaphore):**是一种更高级的同步机制,`互斥锁`可以说是semaphore在仅取值0/1时的特例。`信号量`可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

**条件锁:**就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

4、iOS通过MachPort向特定线程发送通知


mach kernel属于苹果内核,RunLoop依靠它实现了休眠和唤醒而避免了CPU的空转:  mach_port

Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装

3、当用一个不存在的key来查找两个不同长度的字典,那么哪个效率会高?

表面上看可能是一样快,因为字典底层都用了哈希表,查找的时间复杂度为 O(1),(最差的时候是O(n))都是一样的,但是可能会由于两个哈希表的负载因子不同,倒是查找的时间也是不同的。

4、什么是指针常量和常量指针

指针常量是 常量,指针修饰它,这个常量的值是一个指针 int a; int *const b = &a;

常量指针本质是指针,常量修饰它  const int *p; 

5、不借用第三个变量,如何交换两个变量的值?

算术运算

int a,b;
a=10;b=12;
a=b-a; //a=2;b=12
b=b-a; //a=2;b=10
a=b+a; //a=12;b=10

位运算 异或

int a=10,b=12; //a=1010^b=1100;
a=a^b; //a=0110^b=1100;
b=a^b; //a=0110^b=1010;
a=a^b; //a=1100=12;b=1010;

栈实现

int exchange(int x,int y) 
{ 
stack S; 
push(S,x); 
push(S,y); 
x=pop(S); 
y=pop(S); 
}

6、用递归算法求1到n的和

func add(n: Int) -> Int {
    var sum = 0
    if n > 0 {
        sum = n + add(n: n - 1)
    } else {
        sum = 0
    }
    return sum
}

7、100个数字,求最大值的时间复杂度

需要一轮遍历   O(n)
那递归算法的时间复杂度是多少:nlogn

8、http 的 POST 和 GET 啥区别?

GET请求的数据会附在URL之后

POST把提交的数据则放置在是HTTP包的包体中

GET请求URL受浏览器影响 所以有长度限制

POST没有,一般服务器会做POST数据长度的限制

POST的数据传输不是直接拼接URL 所以相对安全一些

9、http和https的区别,说一下http和https的请求过程?

http + ssl/tls = https

主要介绍一下,ssl的验证过程  保证安全和数据完整性
可以补充一下PKI证书体系的部分

10、如何用HTTP实现长连接?

web端:
Connection:keep-alive

服务器在闲置时候会向客户端发生侦测包,默认闲置时间是2个小时

移动端:
基于tcp的长连接,socket编程技术

12、聊下HTTP post的body体使用form-urlencoded和multipart/form-data的区别。

application/x-www-form-urlencoded:窗体数据被编码为名称/值对。这是标准的编码格式。

multipart/form-data:窗体数据被编码为一条消息,页上的每个控件对应消息中的一个部分

13、通信底层原理

OSI采用了分层的结构化技术,共分七层:

物理层:为设备间的数据通信提供传输媒体和互连设备,光纤、无线信道等等

数据链路层:为网络层提供数据传送服务的,包括链路连接的建立、拆除和分离;对帧的收发顺序控制

网络层:数据传送的单位是分组或者包,网络层在给两个不同地理位置的主机之间提供

传输层:定义了一些传输数据的协议和端口号,TCP, UDP;主要从下层接收的数据进行分段和传输,到达目的地后再重组

会话层:通过传输层建立数据传输通道,主要在你的系统之间发起会话或者接受会话请求(IP、MAC、主机名称)

表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取,主要做的就是把应用层提供的信息变换为能够共同理解的形式,提供字符代码,数据格式,控制信息格式,加密等的统一表示。

应用层:为用户的应用程序提供网络服务

TCP/IP 采用四层结构:

网络接口层:硬件、帧头帧尾的添加

网络互联层:确定目标计算机的IP地址

传输层:TCP,确定如何传输

应用层:app

14、介绍一下XMPP?

XMPP是一种以XML为基础的开放式实时通信协议。

XMPP 是一种很类似于http协议的一种数据传输协议,它的过程就如同“解包装–〉包装”的过程,用户只需要明白它接受的类型,并理解它返回的类型,就可以很好的利用xmpp来进行数据通讯。基于可扩展标记语言(XML)的协议 

XMPP基本结构:客户端 服务器 网关 

通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能。网关承担着与异构即时通信系统的互联互通,异构系统可以包括SMS(短信),MSN,ICQ等。基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML。

XMPP核心协议通信的基本模式就是先建立一个stream,然后协商一堆安全之类的东西,中间通信过程就是客户端发送XML Stanza,一个接一个的。服务器根据客户端发送的信息以及程序的逻辑,发送XML Stanza给客户端。但是这个过程并不是一问一答的,任何时候都有可能从一方发信给另外一方。通信的最后阶段是关闭流,关闭TCP/IP连接。

客户端1  <--> XMPP服务器  <--> 客户端2

两个客户端可以分别和服务器通信,但是客户端之间的通信必须经过服务器

用于一些即时通信

15、ssl / tls证书 作用

保障通信双方的可靠性,通信的安全和数据的完整性

https和ssl在握手方向有什么区别?

一个是连接握手,一个是安全校验握手,描述一下两者握手过程

具体原理见参考中的 网络知识整理。

16、socket连接和 http 连接区别

Http是基于Tcp的,而Socket是一套编程接口让我们更方便的使用Tcp/Ip协议;

Http是应用层协议,在Tcp/Udp上一层。

1、Http是基于"请求-响应"的,服务器不能主动向客户端推送数据,只能借助客户端请求到后向客户端推送数据,而Sokcet双方随时可以互发数据;

2、Http不是持久连接的,Socket用Tcp是持久连接;

3、Http基于Tcp,Socket可以基于Tcp/Udp;

4、Http连接是通过Socket实现的;

5、Http连接后发送的数据必须满足Http协议规定的格式:请求头、请求头和请求体,而Socket连接后发送的数据没有格式要求。

Socket的实现原理及 Socket之间是如何通信的

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

建立网络通信连接至少要一对端口号(socket)。

socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;

HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

socket分为客户端和服务端,客户端发送连接请求,服务端等待连接请求

当服务端socket监听到客户端socket的请求时,就响应客户端套接字的请求,建立一个新的线程,把服务端套接字描述发送给客户端,一旦客户端确认了此描述,双方正式建立连接,而服务端socket继续处于监听状态,等待其他连接请求

17、说一下HTTP协议以及经常使用的code码的含义。

一些常见的状态代码为:

200 - 服务器成功返回网页
300 - 重定向之类
404 - 请求的网页不存在
503 - 服务器暂时不可用

18、网络拥塞控制、tcp的慢启动

不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。

简单来说 拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载。

原理:

请求发送,每次按窗口数发送数据,收到一个确认就把窗口值加一,逐渐递增,这就是慢开始算法

当网络拥塞,窗口重新回 1 最大慢开始门限变为出现问题的网络拥塞窗口值的一半 这就是拥塞避免算法

然后再次循环。

19、TCP 三次握手、四次挥手,为什么 断开连接是4次挥手呢


因为TCP连接的时候,最后一次握手表示收到服务器确认的请求可以携带需要发给服务器的数据,三次是最短可能

四次挥手是确保客户端 没有消息要发给服务端,服务端也没有消息要发给客户端了,也可以不用四次,但是就会增加空等待的资源浪费

20、事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。

触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback()内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。

21、对象的创建

Person *p = [Person alloc];
Person *p1 = [p init];
Person *p2 = [p init];
NSLog(@"p ==> %@", p);
NSLog(@"p1 ==> %@", p1);
NSLog(@"p2 ==> %@", p2);

输出地址都是一样的

init 仅仅是把 alloc 创建的对象返回,

new 相当于 alloc + init

alloc 调用calloc 分配内存,接着就是 initInstanceIsa(cls, hasCxxDtor)  初始化isa

扩展:

一个NSObject对象占用多少内存????

new一个NSObject对象解析出来其实就是一个结构体,里面有个isa指针
struct NSObject_IMP {
        Class isa;
};
isa  指针在64位架构占用8个字节

一个指针占用多少个字节???
64位计算器 8个字节,32位计算器  4个字节,地址就是指针,指针指向地址,而地址是内存单元的编号,一个指针占用几个字节,等于是一个地址的内存单元编号有多长。

4*8 = 32

那一个对象占的就是  8 字节

22、Tagged Pointer

NSString 相关类说明表格

类名 存储区域 初始化的引用计数(retainCount) 作用描述
NSString 堆区 1 开发者常用的不可变字符串类,编译期间会转换到其他类型
NSMutableString 堆区 1 开发者常用的可变字符串类,编译期间会转换到其他类型
__NSCFString 堆区 1 可变字符串 NSMutableString 类,编译期间会转换到该类型
__NSCFConstantString 堆区 2^64-1 不可变字符串 NSString 类,编译期间会转换到该类型
NSTaggedPointerString 栈区 2^64-1 Tagged Pointer对象,并不是真的对象

https://www.jianshu.com/p/dcbf48a733f9

23、isa是什么

isa成员变量在64位CPU架构下是8字节,且排在objc_class结构体的前8字节。

objc_object的结构可以说明,当系统为一个对象分配好内存,并初始化实例变量后,在这些对象的实例变量的结构体中的第一个就是isa

isa将对象和类关联起来,起到了中间桥梁的作用。

1、isa是isa_t结构,采用 联合体+位域 的搭配来设计:在不同的位上显示不同的内容,以此来节省储存空间,进而优化内存。
2、isa包含了cls和bits两个成员变量,这两个成员变量在64位CPU架构下的长度都是8字节,所以isa在64位CPU架构下的长度也是8字节。
3、isa的位域上存储了一些对象与类的信息,并将对象与类关联起来,起到中间桥梁的作用。
4、isa指向:

对象的isa指针 指向 对象的所属类(如person对象的isa指向Person类)
类的isa指针 指向 类的元类(如Person类的isa指向Person元类)
元类的isa指针 指向 根元类(如Person元类的isa指向NSObject元类)

根元类的isa指针 指向自身(是个圆圈)

元类的继承关系向上传递(如Teacher元类 继承自 Person元类)

24、方法缓存原理

1、cache_t 能缓存调用过的方法
2、cache_t 的三个成员变量
   _buckets : struct bucket_t * , 也就是指针数组,表示一系列的哈希桶(已调用的方法的 SEL 和 IMP 就缓存在这),一个桶可以存一个方法。
   _mask : 侧面反映哈希桶的总数
   _occupied : 代表当前已经缓存的方法数

3、当缓存的方法达到临界点(桶总数的3/4)时,下次再缓存新的方法时,首先会丢弃旧的桶,同时开辟新的内存,也就是扩容(扩容后就全是新桶,每个方法都需要重新缓存),_occupied此时为1。

4、当多个线程同时调用一个方法时
   多线程读缓存 : 读缓存由汇编实现,无锁且高效,优于并没有改变 _buckets 和 _mask, 所以并无安全隐患
   多线程写缓存 : OC 用一个全局的互斥锁来保证不会出现写两次缓存的情况
   多线程读写缓存 : OC使用了ldp汇编指令、编译内存屏障技术、内存垃圾回收技术等多种手段来解决多线程读写的无锁处理方案,既保证了安全,又提升了系统的性能。

25、block原理

1、block要如何hook?

如果想要Hook住系统的所有Block调用,需要解决如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

fishhook  __Block_copy

fishhook原理

将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。

将内部函数的指针在动态链接时指向系统方法的地址。

这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。

ASLR  引入之前 方法地址是固定的,很容易被攻击串改

2、block的本质

struct __main_block_impl_0 {

  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, __Block_byref_valPtr_0 *_valPtr,  int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

关键结构体命名规则:前部分是__所在内部方法名_block_impl_方法体内第几个block

如:__main_block_impl_0

main方法内的第1个block

类似第二个命名为__main_block_impl_1

__main_block_impl_0 的函数地址赋给 block,就是我们平时的block定义

__block_impl 结构体:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtrl;  指向我们写在block里面内容函数的地址
}

block的执行是通过  impl  找到 FuncPtrl 然后职级调用函数地址

3、block的捕获列表:

局部变量、static变量; 全局变量 不捕获

__main_block_impl_0 中的参数 _val,_valPtr  可以看到val已经被捕获,valPtr是以指针的方式捕获进去的

__block 修饰符

添加 __block 修饰符后,源码如下, val变成一个结构体了

struct __block_byref_val_0{
    void *__isa;
    __block_byref_val_0 *__forwarding;
    int _flags;
    int __size;
    int val;
}

__forwarding用于指向真实变量地址

4、OC Block 和 Swift闭包的区别

1、OC的block在编译的时候就执行了拷贝,swift 的闭包要到执行的时候才会发生拷贝,swift可以让他提前捕获

var i = 1
let closure = {
    [i] in
    print("closure \(i)")
}

加上[i] 就行

5、block的isa指针指向哪里呢

指向_NSConcreteStackBlock 类对象

block最终都是继承自NSBlock类型

6、三类block

在内存角度来看,block分为 全局 、栈 和 堆 三种类型,

有强引用的block就属于堆内存block,   __NSMallocBlock__

只用到外部局部变量、成员属性变量、没有强指针引用的block属于栈block   __NSStackBlock__

只引用全局变量或静态变量的block,生命周期和程序生命周期一样的block就是全局block   __NSGlobalBlock__

26、KVO本质(简)

person在呗添加KVO监听之后发生了什么:

person  ->   NSKVONotifying_Person

isa指针会指向 person 子类  NSKVONotifying_Person

NSKVONotifying_Person 里的get、set方法进行了重写,class方法也重写了

KVO的本质是什么:

当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

如何手动触发KVO
答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

27、iOS常见崩溃

1、野指针

2、访问不存在的方法

3、数组越界、数组插入nil,字典key为nil

4、观察者未释放

5、masonry布局在add之前调用

6、死锁超时

7、Timer repeat的时候释放timer  回调找不到对象

8、非主线程刷新UI

9、字符串操作crash

28、weak的具体实现

weak修饰对象不增加引用计数,系统通过一个hash表来实现对象的弱引用

__weak obj1 = obj;
编译成
objc_initWeak(&obj1,obj)

objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }
    return storeWeak
        (location, (objc_object*)newObj);
}

location是weak指针的地址,newObj是被指向的对象的地址。

weak表

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

全局hash表,使用不定类型对象地址作为key,用weak_entry_t类型结构体对象作为value。它也是个结构体,它负责维护和存储指向一个对象的所有弱引用hash表

整体流程就是:

1、objc_initWeak
2、storeWeak
- oldTable  newTable  获取新旧引用散列,调用sideTables构造
- lockTwo  加锁,解决选择竞争
- weak_unregister 解除旧对象与弱引用表关联绑定
- weak_register  - 构建新的引用计数表,返回一个 newObj

weak指针的销毁

dealloc调用_objc_rootDealloc调用objc_object::rootDealloc()
objc_object::rootDealloc()调用object_dispose
object_dispose调用objc_destructInstance
objc_destructInstance调用obj->clearDeallocating()
clearDeallocating里面有分支sidetable_clearDeallocating()和clearDeallocating_slow(),但是这2个函数里面其实都是调用了weak_clear_no_lock。 

所以我们来看看weak_clear_no_lock干了啥

从weak表中获取废弃对象的地址为键值的记录
将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
将weak表中该记录删除
从引用计数表中删除废弃对象的地址为键值的记录

总结
runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

稍微详细版
初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
添加引用时:objc_initWeak函数会调用 storeWeak() 函数, storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

29、启动优化的过程

pre-main

初始化空间,加载镜像,载入动态库链接器,链接动态库,objcsetup,callloadmethod,

动态库越多,启动越慢,+load方法多也会影响

优化方向:静态库,cocoapods  :linkage => :static指令 将所有的动态库转为静态库。 要注意资源获取的方式问题。

合并动态库:cocoapods-pod-merge  支持合并动态库,要注意import方式会改变,作用域前缀要加上

main

main函数以后就是尽量减少第一个页面展示前的工作,有些组件库自己的工作可以派发到库自己处理,或者异步处理

启动项热拔插

30、HandyJSON原理

Objective-C通过class_copyPropertyList方法加上KVC机制 很容易实现JSON 反序列化

Swift的反射 Mirror是只读的

在内存上为实例的属性赋值

具体实现:

Swift中,一个类实例的内存布局是有规律的:

1、32位机器上,类前面有4+8个字节存储meta信息,64位机器上,有8+8个字节;
2、内存中,字段从前往后有序排列;
3、如果该类继承自某一个类,那么父类的字段在前;
4、Optional会增加一个字节来存储.None/.Some信息;
5、每个字段需要考虑内存对齐;

对一个实例:

1、获取它的起始指针,移动到有效起点;
2、通过Mirror获取每一个字段的字段名和字段类型,计算Model的每个属性字段占位大小;
3、根据字段名在JSON中取值,转换为和字段一样的类型,通过指针写入;
4、根据本字段类型的占位大小和下一个字段类型计算下一个字段的对齐起点;
5、移动指针,继续处理;

31、分类的方法覆盖

类似ViewDidLoad写在分类里面也会覆盖主类

load方法不会覆盖,如果做得方法替换针对同一个对象 会有影响

32、通知


1、通知是分线程的,如果在子线程发出的通知,接受者接到之后的方法执行会在相应的线程完成

2、同时注册多个监听者,要注意顺序,可以同时给一个对象加多个接受者

3、要监听所有通知,那么 name设置为nil , 包括系统通知在内都会监听

4、iOS 9 以后不移除对于的监听者也没事

5、hook底层objc_msgSend 监听每个方法的时间长度

监听每个方法的时间长度

16、app如何接收到触摸事件的

1. 首先,手机中处理`触摸事件`的是硬件系统进程 ,当硬件系统进程识别到触摸事件后,会将这个事件`进行封装`,并通过`machPort`,将封装的事件发送给当前活跃的APP进程。
2. 由于APP的主线程中runloop注册了这个machPort端口,就是用于接收处理这个事件的,所以这里APP收到这个消息后,开始寻找`响应链`。
3. 寻找到响应链后,开始`分发事件`,它会优先发送给`手势集合`,来过滤这个事件,一旦手势集合中其中一个手势识别了这个事件,那么这个事件将不会发送给响应链对象。
4. 手势没有识别到这个事件,事件将会发送给响应链对象`UIResponser`。

17、利用 runloop 解释一下页面的渲染的过程?

1、当我们调用 [UIView setNeedsDisplay] 时,这时会调用当前 View.layer[view.layer setNeedsDisplay]方法。

这等于给当前的 layer 打上了一个脏标记,而此时并没有直接进行绘制工作。而是会到当前的 Runloop 即将休眠,也就是 beforeWaiting 时才会进行绘制工作。

紧接着会调用 [CALayer display],进入到真正绘制的工作。CALayer 层会判断自己的 delegate 有没有实现异步绘制的代理方法 displayer:,这个代理方法是异步绘制的入口,如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束。

CALayer 内部会创建一个 Backing Store,用来获取图形上下文。接下来会判断这个 layer 是否有 delegate。

如果有的话,会调用 [layer.delegate drawLayer:inContext:],并且会返回给我们 [UIView DrawRect:] 的回调,让我们在系统绘制的基础之上再做一些事情。

如果没有 delegate,那么会调用 [CALayer drawInContext:]

以上两个分支,最终 CALayer 都会将位图提交到 Backing Store,最后提交给 GPU

至此绘制的过程结束。

2、UI在什么时候渲染?

因为UI不是立即渲染的,CA在runloop中注册了一个即将进入休眠的observer,在休眠之前对已提交的请求进行集中渲染。

3、卡顿问题,一次runloop绘制任务太多仍会存在卡顿问题,放到下个Runloop,以减少单次runloop绘制任务。(其实重用UI、子线程处理耗时的非UI操作基本上能处理大多数常见卡顿)。runloop中将一个任务放到第二次runloop中执行。最简单使用[self performSelector:@selector(xxxxx) withObject:nil afterDelay:0],而GCD的话,是不一定的。

4、Runloop支持线程唤醒的事件类型

  1. 基于端口的事件
    如触摸事件,系统将触摸事件封装,并且通过进程之间端口通讯传递到我们的进程。

    [[NSRunLoop currentRunLoop] addPort:port forMode:NSRunLoopCommonModes];
    
    
  2. 自定义事件

    [self performSelector:@selector(taskDo) onThread:thread withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
    
    
  3. 基于时间的定时事件

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    

5、异步绘制的ASDisplayKit 为啥要用set去管理,无需查找,事务去重 事务也无顺序

6、系统通知也是无序的

18、几个数据结构问题

1、NSArray与NSSet的区别?

  • NSArray内存中存储地址连续,而NSSet不连续
  • NSSet效率高,内部使用hash查找;NSArray查找需要遍历
  • NSSet通过anyObject访问元素,NSArray通过下标访问

2、NSHashTable与NSMapTable?

  • NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
  • NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;可以在访问成员时copy

(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。
NSMapTable与NSDictionary的区别:同上)

OC 部分

extern的作用

告诉编译器,这个全局变量在本文件找不到就去其他文件去找。如有必要需要使用#import "x.h"这样编译器才知道到哪里去找。使用extern前要保证对应变量被编译过,想要访问全局变量可以使用extern关键字(全局变量定义不能有static修饰)。

比如 A文件中 我声明的全局变量 NSInteger age = 10; 但是属性也不能直接获取。 如下在B文件中可以获取到 :

extern NSInteger age;
age ++;

NSLog(@"%d",age); // 11

如果不想让age被找到,声明为static

const的作用

常量定义,修饰一个常量

int a = 1;
int b = 2;

int const *p = &a
// 如果const修饰的是*p,那么*p的值是不能改变的,也就是p中存放的a的地址中的值无法改变,但是p的值是可以改变的(也就是p此时可以改变指向)
p = &b;
printf("---");
printf("%p",&b);
printf("---");
printf("%p",p);
printf("---");
printf("%d",*p);

//输出 ---0x7ffeea7e89f8---0x7ffeea7e89f8---2

int *const p = &a;
// 如果const修饰的是p,那么p的值是不能改变的,也就是p中存放的a的地址无法改变(p是int类型的指针变量)。但是*p是可以变化的,我们并没有用const去修饰*p,所以可以通过*p去改变a的值
*p = b;

static的作用

static NSInteger staticValue = 0;

static关键字修饰局部变量:

当static关键字修饰局部变量时,只会初始化一次且在程序中只有一份内存

关键字static不可以改变局部变量的作用域,但可延长局部变量的生命周期(直到程序结束才销毁)

static关键字修饰全局变量:

当static关键字修饰全局变量时,作用域仅限于当前文件,外部类是不可以访问到该全局变量的(即使在外部使用extern关键字也无法访问)

如果需要直接访问  需要引用头文件

宏定义

宏定义属于预编译指令,在程序运行之前已经编译好了的

#define M_PI  3.14159265358979323846264338327950288

#define SELF(x)  x  //NSLog(@"Hello %@",SELF(name));

#define PLUS(x,y) x + y  //printf("%d",PLUS(3,2));

#define MIN(A,B) A < B ? A : B  // int a = MIN(1,2);

#define NSLog(format, ...) do { \                                                                           fprintf(stderr, "<%s : %d> %s\n",                                          \
             [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \
             __LINE__, __func__);                                                        \
             (NSLog)((format), ##__VA_ARGS__);                                           \
              fprintf(stderr, "-------\n");                                              \
             } while (0)

1、block分几种?分别是怎么样产生的?block的实质是什么?

在内存角度来看,block分为 全局 、栈 和 堆 三种类型,

有强引用的block就属于堆内存block, 

只用到外部局部变量、成员属性变量、没有强指针引用的block属于栈block

只引用全局变量或静态变量的block,生命周期和程序生命周期一样的block就是全局block

block的实质是一个对象,一个结构体

2、__block修饰的变量为什么能在block里面能改变其值?

__block修饰符标记后,block就会访问标记变量本身内存地址,而未标记对象则访问截获拷贝后的变量的内存地址

3、block应该用copy关键字还是strong关键字?

block 使用 copy 是从 MRC 遗留下来的“传统”

在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区。

在 ARC 中写不写都行

对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

4、@property 的本质是什么?

@property = ivar + getter + setter;

“属性” (property)有两大概念:ivar(实例变量)、getter+setter(存取方法)

“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

5、ivar、getter、setter 是如何生成并添加到类中的

引申一个问题:@synthesize 和 @dynamic 分别有什么作用?


完成属性(@property)定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。

我们也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。
@synthesize lastName = _myLastName;

或者通过 @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。

@property有两个对应的词,

一个是@synthesize(合成实例变量),一个是@dynamic。

如果@synthesize和@dynamic都没有写,那么默认的就是 @synthesize var = _var;

// 在类的实现代码里通过 @synthesize 语法可以来指定实例变量的名字。(@synthesize var = _newVar;)
1. @synthesize 的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
2. @dynamic 告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成(如,@dynamic var)。

5、用@property声明的 NSString / NSArray / NSDictionary 经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题?

用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

1. 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
2. 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。

//总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。

这里还有一个引申问题:

NSMutableArray 如果用 copy修饰了会出现什么问题?

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x600000a100c0'

由于使用的是copy属性,本身的可变属性默认有一个不可变的拷贝 NSArray ,所以我们用这个可变数组去添加元素的时候,找不到对应方法而发生crash。

6、浅拷贝和深拷贝的区别?

浅拷贝(copy):只复制指向对象的指针,而不复制引用对象本身。
深拷贝(mutableCopy):复制引用对象本身。内存中存在了两份独立对象本身,当修改A时,A_copy不变。

只有对不可变对象进行copy操作是指针复制(浅复制),其它情况都是内容复制(深复制)

8、如何让自己的类用copy修饰符

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
具体步骤:
    1. 需声明该类遵从 NSCopying 协议
    2. 实现 NSCopying 协议的方法。
        // 该协议只有一个方法: 
        - (id)copyWithZone:(NSZone *)zone;
        // 注意:使用 copy 修饰符,调用的是copy方法,其实真正需要实现的是 “copyWithZone” 方法。

9、ViewController生命周期

按照执行顺序排列:
1. initWithCoder:通过nib文件初始化时触发。
2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。     

//如果不是nib初始化 上面两个换成 initWithNibName:bundle:

3. loadView:开始加载视图控制器自带的view。
4. viewDidLoad:视图控制器的view被加载完成。  
5. viewWillAppear:视图控制器的view将要显示在window上。
6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
9. viewDidAppear:视图控制器的view已经展示到window上。 
10. viewWillDisappear:视图控制器的view将要从window上消失。
11. viewDidDisappear:视图控制器的view已经从window上消失。

10、OC的反射机制

1). class反射
    通过类名的字符串形式实例化对象。
        Class class = NSClassFromString(@"student"); 
        Student *stu = [[class alloc] init];
    将类名变为字符串。
        Class class =[Student class];
        NSString *className = NSStringFromClass(class);
2). SEL的反射
    通过方法的字符串形式实例化方法。
        SEL selector = NSSelectorFromString(@"setName");  
        [stu performSelector:selector withObject:@"Mike"];
    将方法变成字符串。
        NSStringFromSelector(@selector*(setName:));

11、self 和 super

self 是类的隐藏参数,指向当前调用方法的这个类的实例。
super是一个Magic Keyword,它本质是一个编译器标示符,和self是指向的同一个消息接收者。
不同的是:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。

12、id 和 NSObject*的区别

id是一个 objc_object 结构体指针,定义是
typedef struct objc_object *id
id可以理解为指向对象的指针。所有oc的对象 id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。

NSObject *指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。

不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject *可指向的类型是id的子集。

引申: id 和 instancetype 的区别

instancetype的作用,就是使那些非关联返回类型的方法返回所在类的类型!

相同点:
都可以作为方法的返回类型

不同点:
instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象
instancetype只能作为返回值,不能像id那样作为参数

13、NSDictionary的实现原理是什么?

一:字典原理

NSDictionary(字典)是使用hash表来实现key和value之间的映射和存储的

方法:- (void)setObject:(id)anObject forKey:(id)aKey;

Objective-C中的字典NSDictionary底层其实是一个哈希表

引申:字典的查询工作原理

字典的工作原理 ?怎100w个中是怎么快速去取value?

位图算法:

给定10亿个不重复的正int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那10亿个数当中。

解法:遍历40个亿数字,映射到BitMap中,然后对于给出的数,直接判断指定的位上存在不存在即可。

14、你们的App是如何处理本地数据安全的(比如用户名的密码)?

本地尽量不存储用户隐私数据、敏感信息

使用如AES256加密算法对数据进行安全加密后再存入SQLite中

或者数据库整体加密

存放在keychain里面

向Keychain中存储数据时,不要使用kSecAttrAccessibleAlways,而是使用更安全的kSecAttrAccessibleWhenUnlocked或kSecAttrAccessibleWhenUnlockedThisDeviceOnly选项。 

AES  DES

15、遇到过BAD_ACCESS的错误吗?你是怎样调试的?

90%的错误来源在于对一个已经释放的对象进行release操作, 或者说对一个访问不到的地址进行访问,可能是由于些变量已经被回收了,亦可能是由于使用栈内存的基本类型的数据赋值给了id类型的变量。

例如:


id x_id = [self performSelector:@selector(returnInt)];

- (int)returnInt { return 5; }

上面通过id去接受int返回值,int是存放在栈里面的,堆内存地址如何找得到,自然就是 EXC_BAD_ACCESS。

处理方法

1、xcode可以用僵尸模式打印出对象 然后通过对象查找对应的代码位置

1、Edit Scheme - Diagnositics - Memory Management 勾选 Zombie Objects 和 Malloc Stack

2、会打印出 
cyuyan[7756:17601127] *** -[UIViewController respondsToSelector:]: message sent to deallocated instance 0x7fe71240d390

这句开启僵尸模式后打出来的输出,包含了我们需要的 进程pid、崩溃地址,终端通过下面命令查看堆栈日志来找到崩溃代码

3、查找日志
sudo malloc_history 7756 0x7fe71240d390

2、在 other c flags中加入-D FOR_DEBUG(记住请只在Debug Configuration下加入此标记)。这样当你程序崩溃时,Xcode的console上就会准确地记录了最后运行的object的方法。重写一个object的respondsToSelector方法,打印报错前的

#ifdef _FOR_DEBUG_  
-(BOOL) respondsToSelector:(SEL)aSelector {  
    printf("SELECTOR: %s\n", [NSStringFromSelector(aSelector) UTF8String]);  
    return [super respondsToSelector:aSelector];  
}  
#endif

3、通过instruments的Zombies

引申:怎么定位到野指针的地方。如果还没定位到,这个对象被提前释放了,怎么知道该对象在什么地方释放的

一种是多线程,一种是野指针。这两种Crash都带随机性,我们要让随机crash变成不随机

把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个作用。

1、Edit Scheme - Diagnositics - Memory Management 勾选 Malloc Scribble

暂时未解决

16、如何设计一个通知中心

单例设计一个NotificationCenter,NSPointerArray 保存 observer,对象销毁 observer自动变null

17、KVO、KVC的实现原理

KVC( 键值编码 )实现

1.KVC是基于runtime机制实现的

2、可以访问私有成员变量、可以间接修改私有变量的值

[object setValue:@"134567" forKey:@"uid"];

就会被编译器处理成:
// 首先找到对应sel
SEL sel = sel_get_uid("setValue:forKey:");
// 根据object->isa找到sel对应的IMP实现指针
IMP method = objc_msg_lookup (object->isa,sel);
// 调用指针完成KVC赋值
method(object, sel, @"134567", @"uid");

KVC键值查找原理

setValue:forKey:搜索方式

1、首先搜索setKey:方法.(key指成员变量名, 首字母大写)
2、上面的setter方法没找到, 如果类方法accessInstanceVariablesDirectly返回YES. 那么按 _key, _isKey,key, iskey的顺序搜索成员名。(这个类方法是NSKeyValueCodingCatogery中实现的类方法, 默认实现为返回YES)
3、如果没有找到成员变量, 调用setValue:forUnderfinedKey:

valueForKey:的搜索方式

1、首先按getKey, key, isKey的顺序查找getter方法, 找到直接调用. 如果是BOOL、int等内建值类型, 会做NSNumber的转换.
2、上面的getter没找到, 查找countOfKey, objectInKeyAtindex, KeyAtindexes格式的方法. 如果countOfKey和另外两个方法中的一个找到, 那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法.
3、还没找到, 查找countOfKey, enumeratorOfKey, memberOfKey格式的方法. 如果这三个方法都找到, 那么就返回一个可以响应NSSet所有方法的代理集合.
4、还是没找到, 如果类方法accessInstanceVariablesDirectly返回YES. 那么按 _key, _isKey, key, iskey的顺序搜索成员名.
5、再没找到, 调用valueForUndefinedKey.

KVO实现 键值观察、观察者模式的一种应用

简答

1.KVO是基于runtime机制实现的

2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制

3.如果原类为Person,那么生成的派生类名为NSKVONotifying_Person

4.每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

5.键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

深入

1.Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:?NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter?方法,setter?方法会负责在调用原?setter?方法之前和之后,通知所有观察对象属性值的更改情况。

2.NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;

3.所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。

4.(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。)?因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

5.子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法: 被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath?的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath?的属性值已经变更;之后,?observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter?方法这种继承方式的注入是在运行时而不是编译时实现的。

19、category为什么不能添加属性?

category 它是在运行期决议的,因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。

但是category则完全不一样,它是在运行期决议的。 
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的。

那为什么 使用Runtime技术中的关联对象可以为类别添加属性。

其原因是:关联对象都由AssociationsManager管理,AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。

如合清理关联对象?

runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。(详见Runtime的源码)

Objective-C Associated Objects 的实现原理

20、说一下runloop和线程的关系

runloop与线程是一一对应的

runloop是来管理线程的

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

21、说一下autoreleasePool的实现原理

autoreleasePool是一个延时release的机制, 在自动释放池被销毁或耗尽时,会向池中的所有对象发送release消息,释放所有autorelease对象。

ARC下,我们使用@autoreleasepool{}来使用一个自动释放池

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage作为结点以双向链表的形式组合而成。整个链表以堆栈的形式运作。
1、每一个指针代表一个加入到释放池的对象 或者是哨兵对象,哨兵对象是在
_objc_autoreleasePoolPush方法调用的时候插入的

2、然后  @autoreleasepool{} 闭包中对象被插入
2、当自动释放池 pop的时候,所有哨兵对象之后的对象都会release

3、链表会在一个Page空间占满时进行增加,一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

主线程:

主线程runloop中注册了两个Observer,回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个oberver监听 当从休眠状态即将进入loop的时候 ,这个时候,构建自动释放池

第二个oberver监听 当准备进入休眠状态的时候,调用 objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池

子线程:

runloop默认不开启,不会自动创建自动释放池,在需要使用自动释放池的时候,需要我们手动创建、添加自动释放池,此时如果所有的异步代码都写在自动释放池中,也可以理解为当子线程销毁的时候,自动释放池释放

自动释放池(sunnyxx)

自动释放池

22、说一下简单工厂模式,工厂模式以及抽象工厂模式?

简单工厂模式:根据外部信息就可以决定创建对象,所有产品都通过工厂判断就创建,体系结构很明显,缺点就是集中了所有的产品创建逻辑,耦合太重。

工厂模式:产品的各自创建逻辑下发到各自的工厂类中,一定程度达到解耦合。 多态性,产品构建逻辑可以具体到对应的产品工厂类中,更加清晰。 当我需要新产品的时候,只需要添加一个新的产品工厂,实现抽象工厂的产品产出方法,产出对应的产品。不影响客户逻辑。

抽象工厂模式:当有多个产品线,需要多个工厂分别生产不同的产品线产品,这个时候我们抽象出工厂逻辑,产品也抽象出产品类型,工厂抽象类只需要构建返回抽象产品的方法即可,更深程度的解耦。具体的什么工厂产什么产品逻辑下发到实际工厂实现。 即使添加新产品也不影响抽象工厂和抽象产品的逻辑。

23、如何设计一个网络请求库

网络请求库需要的功能:

1、在任意位置发起请求

2、请求表单的创建 (url拼接、参数填充、http请求方法确认)

3、UI-Loading

4、数据解析

5、异常处理

6、结果提示

自己分装的 一个 API 网络请求库 

24、说一下多线程,你平常是怎么用的?

常用的有 GCD 和 NSOperation 、NSThread 

NSThread 用于获取当前线程等操作

GCD 和 NSOperation 实现多线程操作不需要自己管理线程,操作简单

GCD block的使用方式比NSOperation 适合简单操作,NSOperation 对象级操作方法更多,更复杂操作适用

25、说一下UITableViewCell的卡顿你是怎么优化的?

一般简单的UITableViewCell都不会卡顿,TableView本身有Cell重用机制,但一些复杂的自适应高度的cell比较容易产生卡顿。

1、避免cell的过多重新布局,差别太大的cell之间不要选择重用。

2、提前计算并缓存cell的高度,内容

3、尽量减少动态添加View的操作

4、减少所有对主线程有影响的无意义操作

5、cell中的图片加载用异步加载,缓存等

6、局部更新cell

7、减少不必要的渲染时间,比如少用透明色之类的

28、什么是ARC?(ARC是为了解决什么问题诞生的?)


ARC全称是 Automatic Reference Counting,是Objective-C的内存管理机制。简单地来说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC的使用是为了解决对象retain和release匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

以前需要手动的通过retain去为对象获取内存,并用release释放内存。所以以前的操作称为MRC (Manual Reference Counting)。

29、请解释以下keywords的区别: assign vs weak, _block vs _weak

weak和assign都是引用计数不变,两个的差别在于,weak用于object type,就是指针类型,而assign用于简单的数据类型,如int BOOL 等。

assign看起来跟weak一样,其实不能混用的,assign的变量在释放后并不设置为nil(和weak不同),当你再去引用时候就会发生错误,崩溃,EXC_BAD_ACCESS.

assign 可以修饰对象么? 可以修饰,编译器不会报错,但是访问过程中对象容易野指针

__block 用于标记需要在block内部修改的变量,__weak 用于防止引用循环

30、使用atomic一定是线程安全的吗?

atomic只能保证操作也就是存取属性的时候的存取方法是线程安全的,并不能保证整个对象就是线程安全的。

比如NSMutableArray 设置值得时候是线程安全的,但是通过objectAtIndex访问的时候就不再是线程安全的了。还是需要锁来保证线程的安全。

31、描述一个你遇到过的retain cycle例子

VC中一个强引用block里面使用self

代理使用强引用

sqllite多线程抢写入操作

32、+(void)load; +(void)initialize; 有什么用处?方法分别在什么时候调用的?

+(void)load;

当类对象被引入项目时, runtime 会向每一个类对象发送 load 消息。
load 方法会在每一个类甚至分类被引入时仅调用一次,调用的顺序:父类优先于子类, 子类优先于分类。
由于 load 方法会在类被 import 时调用一次,而这时往往是改变类的行为的最佳时机,在这里可以使用例如 method swizlling 来修改原有的方法。
load 方法不会被类自动继承。

+(void)initialize;

也是在第一次使用这个类的时候会调用这个方法,也就是说 initialize 也是懒加载

总结:

在 Objective-C 中,runtime 会自动调用每个类的这两个方法
1.+load 会在类初始加载时调用
2.+initialize 会在第一次调用类的类方法或实例方法之前被调用
这两个方法是可选的,且只有在实现了它们时才会被调用
两者的共同点:两个方法都只会被调用一次

33、谈一谈消息发送 或者 对runtime的理解, 说一下工作中是如何使用runtime的?看过runtime源码吗?

runtime是 oc 语言特性,方法调用采用消息发送的方式,直到项目运行阶段才能最终确定,并且还可以动态添加成员变量与方法。

项目中用的多的runtime应该是方法实现的替换,动态属性的添加,KVO,performSelector,消息转发之类

34、如何高性能的给UIImageView加个圆角?

如何高性能的给 UIImageView 加个圆角?

不好的解决方案:使用下面的方式会强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响,会有卡顿的现象出现。

self.view.layer.cornerRadius = 5.0f;
self.view.layer.masksToBounds = YES;

正确的解决方案:使用绘图技术

- (UIImage *)circleImage {
    // NO代表透明
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
    // 获得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 添加一个圆
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextAddEllipseInRect(ctx, rect);
    // 裁剪
    CGContextClip(ctx);
    // 将图片画上去
    [self drawInRect:rect];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    // 关闭上下文
    UIGraphicsEndImageContext();
    return image;
}
还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的。

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
                       cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];

36、设计一个检测主线程卡顿的方案

卡顿的原因就是耗时长,设计一个检测主线程方法执行时间过长的方案

37、说几个你在工作中使用到的线程安全的例子

多线程同时操作同一个数据源的时候

AFNetworking 对于session的构建等都是线程安全的

38、用过哪些锁?哪些锁的性能比较高?谈下Objective C都有哪些锁机制,你一般用哪个?

常用的锁有 NSLock、@synchronized代码块、信号量 dispatch_semaphore_t

信号量性能最高

@synchronized代码块 最方便

32、说一下静态库和动态库之间的区别

静态库 

.a 、.framework 结尾
是一个已经编译好了的集合,使用的时候连接器会把静态库合并到可执行文件中。

动态库  
.tbd 或 .framework结尾

编译过程不会被链接到目标代码中, 只会将动态库头文件添加到目标app的可执行文件,程序运行的时候被添加在独立于app的内存区域。

36、说一下你对架构的理解? 技术架构如何搭建?

设计一个架构 需要考虑多个层次

1、代码风格、例如 代码整齐,一个类不能干两个事情,目录设定要清晰一眼就知道是干什么的,不要设置什么common module之类的目录,面向协议开发,瘦Controller啊等

2、规范业务块的分层,例如 MVC 或者 MVVM,统一的业务处理分层,让业务代码更清晰,耦合性也低

3、基础层的定义, 开发帮助库,例如 网络库,数据持久化库,路由库,要求易于扩展、易于测试,易于理解,让开发小伙伴上手快,接口方法设定要灵活,减少开发小伙伴的使用成本

4、组件化,一个架构本身也需要良好的封装,合理的组件化可以让功能更清晰,耦合性也更低,

大的组件化就是项目层级,把不常改动的基础库沉底,比如放pod中,经常扩展的内容放在工程里面,独立的业务块可以通过工程的方式依赖

小的组件化就是UI方面,统一封装管理UI轮子,避免一个东西出现很多份的情况

参考文章一

参考文章二

37、为什么一定要在主线程里面更新UI?

UIKit 不是线程安全的,容易产生UI更新上的混乱

39、讲讲你用Instrument优化动画性能的经历吧

core animation的使用

time profiler 的使用

40、loadView是干嘛用的?

self.view的初始化,根据xib初始化或者init初始化

41、viewWillLayoutSubView

controller layout触发的时候,开发者有机会去重新layout自己的各个subview。说UI熟悉的一定要知道。

当子View发生frame的变动的时候会触发layoutsubView,我们可以在这个方法中提前做一些预处理

42、GCD里面有哪几种Queue?你自己建立过串行queue吗?背后的线程模型是什么样的?

两种queue,串行和并行。

main queue是串行,global queue是并行。

有些开发者为了在工作线程串行的处理任务会自己建立一个serial queue。背后是苹果维护的线程池,各种queue要用线程都是这个池子里取的。

43、用过coredata或者sqlite吗?读写是分线程的吗?遇到过死锁没?咋解决的?

sqlite 一个线程A操作写入、一个线程B操作读取,在第一个线程等待写入的过程中也发起写入,写入操作在普通的事务操作 begin trancaction  commit transaction ,这种情况就会死锁

两个线程都争取写入操作,因为在A线程等待变成排他锁的过程中处于待定锁状态,并不会拒绝B线程的保留锁的获取,导致B线程一直不释放共享锁,A就一直得不到排他锁,造成死锁。

单个线程可以死锁(main thread里dispatch_sync到main queue),

多个线程直接也可以死锁(A,B线程互相持有对方需要的资源且互相等待)。

关于sqllite锁

44、NSString如何计算字符的个数?

- (int)myStrLength:(NSString *)str {
    int length = 0;
    char * p_str = [str cStringUsingEncoding:NSUTF8StringEncoding];
    for (int i = 0; i < [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; i++) {
        if (*p_str) {
            p_str++;
            length++;
        } else {
            p_str++;
        }
    }
    return length;
}

45、PKI体系(其实就是CA证书验证体系)当中加密和签名有什么区别?

签名密钥对用于数据的完整性检测,保证防伪造与防抵赖,签名私钥的遗失,并不会影响对以前签名数据的验证,因此,签名私钥无须备份,因此,签名密钥不需要也不应该需要第三方来管理,完全由持有者自己产生;

加密密钥对用于数据的加密保护,若加密私钥遗失,将导致以前的加密数据无法解密,这在实际应用中是无法接受的,加密私钥应该由可信的第三方(即通常所说的CA)来备份,以保证加密数据的可用性,因此,加密密钥对可以由第三方来产生,并备份。

一个加密 一个保证完整性

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

可以大大加快数据的检索速度,这也是创建索引的最主要的原因。

通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

49、iOS下如何实现指定线程数目的线程池?

使用信号量

GCD的信号量机制(dispatch_semaphore)

信号量是一个整型值,有初始计数值;可以接收通知信号和等待信号。当信号量收到通知信号时,计数+1;当信号量收到等待信号时,计数-1;如果信号量为0,线程会阻塞,直到线程信号量大于0,才会继续下去。

使用信号量机制可以实现线程的同步,也可以控制最大并发数。以下是控制最大并发数的代码。

dispatch_queue_t workConcurrentQueue = dispatch_queue_create("cccccccc", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("sssssssss",DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(serialQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(workConcurrentQueue, ^{
    NSLog(@"thread-info:%@开始执行任务%d",[NSThread currentThread],(int)i);
    sleep(1);
    NSLog(@"thread-info:%@结束执行任务%d",[NSThread currentThread],(int)i);
    dispatch_semaphore_signal(semaphore);});
});
}
NSLog(@"主线程...!");

说明:从执行结果中可以看出,虽然将10个任务都异步加入了并发队列,但信号量机制控制了最大线程并发数,始终是3个线程在执行任务。此外,这些线程也没有阻塞线程。

50、函数式编程当中的 first-class function是什么意思呢?

函数是一等公民

函数能像参数那样被传递到另一个函数、从另一个函数那像值一样被返回出来、函数可以赋值给变量或者存在数据结构中。

51.遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?

可能造成tableView卡顿的原因有:

1.最常用的就是cell的重用, 注册重用标识符

如果不重用cell时,每当一个cell显示到屏幕上时,就会重新创建一个新的cell

如果有很多数据的时候,就会堆积很多cell。

如果重用cell,为cell创建一个ID,每当需要显示cell 的时候,都会先去缓冲池中寻找可循环利用的cell,如果没有再重新创建cell

2.避免cell的重新布局

cell的布局填充等操作 比较耗时,一般创建时就布局好

如可以将cell单独放到一个自定义类,初始化时就布局好

3.提前计算并缓存cell的属性及内容

当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度

而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell

4.减少cell中控件的数量

尽量使cell得布局大致相同,不同风格的cell可以使用不用的重用标识符,初始化时添加控件,

不适用的可以先隐藏

5.不要使用ClearColor,无背景色,透明度也不要设置为0

渲染耗时比较长

6.使用局部更新

如果只是更新某组的话,使用reloadSection进行局部更

7.加载网络数据,下载图片,使用异步加载,并缓存

8.少使用addView 给cell动态添加view

9.按需加载cell,cell滚动很快时,只加载范围内的cell

10.不要实现无用的代理方法,tableView只遵守两个协议

11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

12.不要做多余的绘制工作。在实现drawRect:的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection或CGRectContainsRect判断是否需要绘制image和text,然后再调用绘制方法。

13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在bitmap context里先将其画一遍,导出成UIImage对象,然后再绘制到屏幕;

14.使用正确的数据结构来存储数据。

53、让你设计一种机制检测UIViewController的内存泄漏,你会怎么做?Instrument是如何检测内存泄漏的

swizzle NavigationController 的 push 和 pop方法

pop了控制器后过几秒钟进行一遍判断,如果为nil表示已销毁,没有则表示内存泄露

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
});

54、通过[UIImage imageNamed:]生成的对象什么时候被释放?

这种图片加载方式带有图片缓存的功能,使用这种方式加载图片后,图片会自动加入系统缓存中,并不会立即释放到内存。一些资源使程序中经常使用的图片资源,
使用这种方式会加快程序的运行减少IO操作,但对于项目中只用到一次的图片,如果采用这种方案加载,会增导致程序的内存使用增加。

非缓存的加载方式
(UIImage *)imageWithContentsOfFile:(NSString *)path
(UIImage *)
:(NSData *)data

55、applicationWillEnterForeground和applicationDidBecomeActive都会在哪些场景下被调用?举例越多越好。

后台进入前台

通知中心回来

正常启动app

56、如何终止正在运行的工作线程?

block 中 return;

[thread cancle]

57、穷举iOS下所有的本地持久化方案。

plist

preference  NSUserDefault

NSKeyedArchiver

SQLite3

coreData

沙盒

58、项目中网络层如何做安全处理

1、尽量使用https

https可以过滤掉大部分的安全问题。https在证书申请,服务器配置,性能优化,客户端配置上都需要投入精力,所以缺乏安全意识的开发人员容易跳过https,或者拖到以后遇到问题再优化。https除了性能优化麻烦一些以外其他都比想象中的简单,如果没精力优化性能,至少在注册登录模块需要启用https,这部分业务对性能要求比较低。

2、不要传输明文密码

不知道现在还有多少app后台是明文存储密码的。无论客户端,server还是网络传输都要避免明文密码,要使用hash值。客户端不要做任何密码相关的存储,hash值也不行。存储token进行下一次的认证,而且token需要设置有效期,使用refresh token去申请新的token。

3、Post并不比Get安全

事实上,Post和Get一样不安全,都是明文。参数放在QueryString或者Body没任何安全上的差别。在Http的环境下,使用Post或者Get都需要做加密和签名处理。

4、不要使用301跳转

301跳转很容易被Http劫持攻击。移动端http使用301比桌面端更危险,用户看不到浏览器地址,无法察觉到被重定向到了其他地址。如果一定要使用,确保跳转发生在https的环境下,而且https做了证书绑定校验。

5、http请求都带上MAC

所有客户端发出的请求,无论是查询还是写操作,都带上MAC(Message Authentication

Code)。MAC不但能保证请求没有被篡改(Integrity),还能保证请求确实来自你的合法客户端(Signing)。当然前提是你客户端的key没有被泄漏,如何保证客户端key的安全是另一个话题。MAC值的计算可以简单的处理为hash(request

params+key)。带上MAC之后,服务器就可以过滤掉绝大部分的非法请求。MAC虽然带有签名的功能,和RSA证书的电子签名方式却不一样,原因是MAC签名和签名验证使用的是同一个key,而RSA是使用私钥签名,公钥验证,MAC的签名并不具备法律效应。

6、http请求使用临时密钥

高延迟的网络环境下,不经优化https的体验确实会明显不如http。在不具备https条件或对网络性能要求较高且缺乏https优化经验的场景下,http的流量也应该使用AES进行加密。AES的密钥可以由客户端来临时生成,不过这个临时的AES

key需要使用服务器的公钥进行加密,确保只有自己的服务器才能解开这个请求的信息,当然服务器的response也需要使用同样的AES

key进行加密。由于http的应用场景都是由客户端发起,服务器响应,所以这种由客户端单方生成密钥的方式可以一定程度上便捷的保证通信安全。

7、AES使用CBC模式

不要使用ECB模式,记得设置初始化向量,每个block加密之前要和上个block的秘文进行运算。

59、假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中

方便在基类中处理公共逻辑;

方便在基类中处理缓存逻辑,以及其它一些公共逻辑;

方便做对象的持久化。

2.将界面的封装抽象到专门的类中

构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel

借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程。

4.专门构造存储类

专门来处理本地数据的存取。

5.整合常量

60、M、V、C相互通讯规则你知道的有哪些?

MVC 是一种设计思想,一种框架模式,是一种把应用中所有类组织起来的策略,它把你的程序分为三块,分别是:

M(Model):实际上考虑的是“什么”问题,你的程序本质上是什么,独立于 UI 工作。是程序中用于处理应用程序逻辑的部分,通常负责存取数据。

C(Controller):控制你 Model 如何呈现在屏幕上,当它需要数据的时候就告诉 Model,你帮我获取某某数据;当它需要 UI 展示和更新的时候就告诉 View,你帮我生成一个 UI 显示某某数据,是 Model 和 View 沟通的桥梁。

V(View):Controller 的手下,是 Controller 要使用的类,用于构建视图,通常是根据 Model 来创建视图的。

要了解 MVC 如何工作,首先需要了解这三个模块间如何通信。

MVC通信规则

http://cc.cocimg.com/api/uploads//20171127/1511752329535960.jpg

Controller to Model

可以直接单向通信。Controller 需要将 Model 呈现给用户,因此需要知道模型的一切,还需要有同 Model 完全通信的能力,并且能任意使用 Model 的公共 API。

Controller to View

可以直接单向通信。Controller 通过 View 来布局用户界面。

Model to View

永远不要直接通信。Model 是独立于 UI 的,并不需要和 View 直接通信,View 通过 Controller 获取 Model 数据

View to Controller

View 不能对 Controller 知道的太多,因此要通过间接的方式通信。

Target

action。首先 Controller 会给自己留一个 target,再把配套的 action 交给 View 作为联系方式。那么 View

接收到某些变化时,View 就会发送 action 给 target 从而达到通知的目的。这里 View 只需要发送

action,并不需要知道 Controller 如何去执行方法。

代理。有时候 View 没有足够的逻辑去判断用户操作是否符合规范,他会把判断这些问题的权力委托给其他对象,他只需获得答案就行了,并不会管是谁给的答案。

DataSoure。View 没有拥有他们所显示数据的权力,View 只能向 Controller 请求数据进行显示,Controller 则获取 Model 的数据整理排版后提供给 View。

Model 访问 Controller

同样的 Model 是独立于 UI 存在的,因此无法直接与 Controller 通信,但是当 Model 本身信息发生了改变的时候,会通过下面的方式进行间接通信。

Notification & KVO一种类似电台的方法,Model 信息改变时会广播消息给感兴趣的人 ,只要 Controller 接收到了这个广播的时候就会主动联系 Model,获取新的数据并提供给 View。

从上面的简单介绍中我们来简单概括一下 MVC 模式的优点。

1.低耦合性

2.有利于开发分工

3.有利于组件重用

4.可维护性

60、什么是MVVM,请设计View model需要考虑哪些?


M + V + VM , VM的作用主要用于简化Controller的负担,但是VM的设计中不可以没有C,其实应该是 M + V + C +VM , C 作为 关联 V 和 VM 的纽带, 最好不要直接关联VM。

参考

持久化方式学习整理

App生命周期知识学习整理

线程知识整理

事件响应链

运行时、消息转发相关

block知识整理

UIWindow、UIApplication

专题(持续更新)

iOS开发基础

iOS开发进阶

Swift学习

原文链接:[https://www.jianshu.com/p/8ede4692978d)

你可能感兴趣的:(iOS开发——面试题1)