iOS基础 (iOS面试题一)

657 阅读51分钟

如果你不知道你接下来需要补充学习哪一部分知识,做点面试题吧。

基础部分

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

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

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

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

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

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

2、说一下线程之间的通信

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

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

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)

8、http 的 POST 和 GET 啥区别?

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

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

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

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

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

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

http + ssl/tls = https

主要介绍一下,ssl的验证过程  保证安全和数据完整性

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、聊一聊二叉树搜索(Binary search tree)

参考

网络知识整理

leetCode

剑指offer

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 告诉编译器:属性的 settergetter 方法由用户自己实现,不自动生成。

@property有两个对应的词,

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

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

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

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

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

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 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopyingNSMutableCopying 协议。
具体步骤:
    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的子类,还有一些继承自NSProxyNSObject *可指向的类型是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?


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 ObjectsMalloc 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方法, 找到直接调用. 如果是BOOLint等内建值类型, 会做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、每一个指针代表一个加入到释放池的对象 或者是哨兵对象,哨兵对象是在 @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 和 NSOperationNSThread 

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

weakassign都是引用计数不变,两个的差别在于,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参数就是需要绘制的区域,这个区域之外的不需要进行绘制。例如上例中,就可以用CGRectIntersectsRectCGRectIntersectionCGRectContainsRect判断是否需要绘制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学习


给大家推荐一个优秀的iOS交流平台,平台里的伙伴们都是非常优秀的iOS开发人员,我们专注于技术的分享与技巧的交流,大家可以在平台上讨论技术,交流学习。欢迎大家的加入(想要进入的可加小编微信)。 17512010526


作者:就叫yang
链接:https://www.jianshu.com/p/8ede4692978d
来源:简书