iOS面试题及答案.及添加React Native面试题 随时更新
阅读原文时间:2021年04月20日阅读:1
  • 第一大类OC面试题
    =========

一.设计模式是什么? 你知道哪些设计模式,并简要叙述?

MVC是最普遍认知的设计模式,MVC模式将页面的逻辑分为3块:Model(模型数据业务)、View(UI展示业务)、Controller(协调者-控制器)

这样的划分很好理解,维护时,只要找到对应的那一块进行修改就好了。

在iOS开发中,UIKIt框架是将控制器Controller与View进行绑定了的,每个控制器都有View对象,代码添加UI子控件细节或者在xib与storyboard中子视图可以直接与controller进行关联,都会导致控制器中难以避免很多本该View去负责的UI子控件细节处理放在了控制器Controller里面;而在Controller里面本身要处理的请求、控制器生命周期函数要处理的事情比较多的情况下,控制器就变得很臃肿。实际上这个设计模式在iOS中为:M-VC

MVVM设计模式介绍

M=Model, V=V+C, VM = ViewModel.  为什么会叫ViewModel?

先看这样划分后的分工:

View :UI界面的创建及根据传递的Model来更新视图的逻辑 。

Controller :负责控制器本身的生命周期,协调各个部分的绑定关系以及必要的逻辑处理。 

ViewModel :网络请求、返回数据逻辑和缓存读写。

 Model :用来将数据模型化,是数据查看更清晰,使用更方便。
总结来说就是:MVVM由MVP和WPF结合演变过来的,MVVM模式是将业务分为3块M-V-新对象,由于这个新对象是为View量身定制的(即它是视图的模型),被称为ViewModel。MVVM的核心是双向绑定。

MVVM的双向绑定

绑定的意思就是讲两个对象建立关联,其中一个改变另一个自动跟着变。假设Model与View绑定就意味着Model改变时View自动跟着变,不需要手动赋值那一步---即响应式

单向绑定:一般指模型数据变化触发对应的视图数据变化。

双向绑定:指模型数据,视图数据任意一方变化,都会触发另一方的同步变化。
双向绑定如何实现?


通信图
1. 实际开发中的做法:让Controller拥有View和ViewModel属性,VM拥有Model属性;Controller或者View来接收ViewModel发送的Model改变的通知

2. 用户的操作点击或者Controller的视图生命周期里面让ViewModel去执行请求,请求完成后ViewModel将返回数据模型化并保存,从而更新了Model;Controller和View是属于V部分,即实现V改变M(V绑定M)。如果不需要请求,这直接修改Model就是了。

3. 第2步中的Model的改变,VM是知道的(因为持有关系),只需要Model改变后发一个通知;Controller或View接收到通知后(一般是Controller先接收再赋值给View),根据这个新Model去改变视图就完成了M改变V(M绑定V)
使用RAC(RactiveCocoa)框架实现绑定可以简单到一句话概括:

ViewModel中创建好请求的信号RACSignal, Controller中订阅这个信号,在ViewModel完成请求后订阅者调用sendNext:方法,Controller里面订阅时写的block就收到回调了。


结论:主体使用MVC,局部看情况使用MVVM设计模式,这样比较适用于当前的iOS开发。

他们之间的结构关系如下:

MVVM 的优势

低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上

可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑

独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计

可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试

MVVM 的弊端

数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。

对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:

数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。

转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。

只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。

调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。

同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

二 .属性引用self.xx与_xx的区别

iOS的@property已经独揽了@synthesize的功能主要有三个作用:

1.生成了成员变量get/set方法的声明
2.生成了私有的带下划线的的成员变量因此子类不可以直接访问,但是可以通过get/set方法访问。那么如果想让定义的成员变量让子类直接访问那么只能在.h文件中定义成员变量了,因为它默认是@protected
3.生成了get/set方法的实现

值得注意的是:
如果已经手动实现了get和set方法(两个都实现)的话Xcode不会再自动生成带有下划线的私有成员变量了
因为xCode自动生成成员变量的目的就是为了根据成员变量而生成get/set方法的,但是如果get和set方法缺一个的话都会生成带下划线的变量

@property
声明的属性默认会生成一个_类型的成员变量,同时也会生成setter/getter方法。苹果将默认编译器从GCC转换为LLVM(low level virtual machine),才不再需要为属性声明实例变量了。在没有更改之前,属性的正常写法需要 成员变量 + @property + @synthesize 成员变量 三个步骤。 

上面我们说到了属性与成员变量、
@property 以及 @synthesize之间的联系与区别。
同时,我们提到了self.xx和_xx的一点区别,其中self.xx是调用的xx属性的get/set方法,而_xx则只是使用成员变量_xx,并不会调用get/set方法。两者的更深层次的区别在于,通过存取方法访问比直接访问多做了一些其他的事情(例如内存管理,复制值等).
例如如果属性在@property中属性的修饰符有retain,那么当使用self.xx的时候相应的属性的引用计数器由于生成了setter方法而进行加1操作,此时的retaincount为2。

在一个类中用self.xx  是调用set和get的方法​​,对这个类中xx进行读取。在内存管理中,会引用计数+1。可以兼容懒加载

三.frame 和 bounds 有什么不同?

frame指的是:该view在父view坐标系统中的位置和大小。(参照点是父view的坐标系统)
bounds指的是:该view在本身坐标系统中的位置和大小。(参照点是本身坐标系统)

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

答:Objective-C的类不可以多重继承;可以实现多个接口(协议);Category是类别;一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原有类的关系。

五.@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@property 的本质是什么?
    @property = ivar + getter + setter;
“属性” (property)有两大概念:ivar(实例变量)、getter+setter(存取方法)

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

六.@property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?

属性可以拥有的特质分为四类:
1.原子性--- nonatomic 特质
2.读/写权限---readwrite(读写)、readonly (只读)
3.内存管理语义---assign、strong、 weak、unsafe_unretained、copy
4.方法名---getter=<name> 、setter=<name>
5.不常用的:nonnull,null_resettable,nullable

七.属性关键字 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?

答:
1). readwrite 是可读可写特性。需要生成getter方法和setter方法。
2). readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
3). assign 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。
4). retain(MRC)/strong(ARC) 表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
5). copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。
6). nonatomic 非原子操作。决定编译器生成的setter和getter方法是否是原子操作,atomic表示多线程安全,一般使用nonatomic,效率高。

八.什么情况使用 weak 关键字,相比 assign 有什么不同?

1.在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
2.自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。

IBOutlet连出来的视图属性为什么可以被设置成weak?因为父控件的subViews数组已经对它有一个强引用。

不同点:
assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。
weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。

九.怎么用 copy 关键字?

 用途:
 1. NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
 2. block 也经常使用 copy 关键字。

 说明:
 block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

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

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

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

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

十一.浅拷贝和深拷贝的区别?

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

十二.这个写法会出什么问题:@property (nonatomic, copy) NSMutableArray *arr;

问题:添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃。

原因:是因为 copy 就是复制一个不可变 NSArray 的对象,不能对 NSArray 对象进行添加/修改。

十三.如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
具体步骤:
    1. 需声明该类遵从 NSCopying 协议
    2. 实现 NSCopying 协议的方法。

十四.常见的 Objective-C 的数据类型有那些,和C的基本数据类型有什么区别?如:NSInteger和int

答:
Objective-C的数据类型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,这些都是class,创建后便是对象,而C语言的基本数据类型int,只是一定字节的内存空间,用于存放数值;NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(NSInteger的定义typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定是本身是int还是long。

十五.id 声明的对象有什么特性?IOS开发之__bridge,__bridge_transfer和__bridge_retained

答:id 声明的对象具有运行时的特性,即可以指向任意类型的Objcetive-C的对象。

我们先来看一下ARC无效的时候,我们写id类型转void*类型的写法:

id obj = [[NSObject alloc] init];
void *p = obj;
反过来,当把void*对象变回id类型时,只是简单地如下来写,

id obj = p;
[obj release];
但是上面的代码在ARC有效时,就有了下面的错误:

    error: implicit conversion of an Objective-C pointer
        to ’void *’ is disallowed with ARC
        void *p = obj;
                  ^

    error: implicit conversion of a non-Objective-C pointer
        type ’void *’ to ’id’ is disallowed with ARC
        id o = p;
                ^

__bridge
为了解决这一问题,我们使用 __bridge 关键字来实现id类型与void*类型的相互转换。看下面的例子。

id obj = [[NSObject alloc] init];

void *p = (__bridge void *)obj;

id o = (__bridge id)p;
将Objective-C的对象类型用 __bridge 转换为 void* 类型和使用 __unsafe_unretained 关键字修饰的变量是一样的。被代入对象的所有者需要明确对象生命周期的管理,不要出现异常访问的问题。

除过 __bridge 以外,还有两个 __bridge 相关的类型转换关键字:

__bridge_transfer
__bridge_retained
接下来,我们将看看这两个关键字的区别。

__bridge_retained
先来看使用 __bridge_retained 关键字的例子程序:

id obj = [[NSObject alloc] init];

void *p = (__bridge_retained void *)obj;

从名字上我们应该能理解其意义:类型被转换时,其对象的所有权也将被变换后变量所持有。如果不是ARC代码,类似下面的实现:

id obj = [[NSObject alloc] init];

void *p = obj;
[(id)p retain];

可以用一个实际的例子验证,对象所有权是否被持有。

void *p = 0;

{
    id obj = [[NSObject alloc] init];
    p = (__bridge_retained void *)obj;
}

NSLog(@"class=%@", [(__bridge id)p class]);
出了大括号的范围后,p 仍然指向一个有效的实体。说明他拥有该对象的所有权,该对象没有因为出其定义范围而被销毁。

__bridge_transfer
相反,当想把本来拥有对象所有权的变量,在类型转换后,让其释放原先所有权的时候,需要使用 __bridge_transfer 关键字。文字有点绕口,我们还是来看一段代码吧。

如果ARC无效的时候,我们可能需要写下面的代码。

// p 变量原先持有对象的所有权
id obj = (id)p;
[obj retain];
[(id)p release];
那么ARC有效后,我们可以用下面的代码来替换:

// p 变量原先持有对象的所有权
id obj = (__bridge_transfer id)p;
可以看出来,__bridge_retained 是编译器替我们做了 retain 操作,而 __bridge_transfer 是替我们做了 release1。

Toll-Free bridged
在iOS世界,主要有两种对象:Objective-C 对象和 Core Foundation 对象0。Core Foundation 对象主要是有C语言实现的 Core Foundation Framework 的对象,其中也有对象引用计数的概念,只是不是 Cocoa Framework::Foundation Framework 的 retain/release,而是自身的 CFRetain/CFRelease 接口。

这两种对象间可以互相转换和操作,不使用ARC的时候,单纯的用C原因的类型转换,不需要消耗CPU的资源,所以叫做 Toll-Free bridged。比如 NSArray和CFArrayRef, NSString和CFStringRef,他们虽然属于不同的 Framework,但是具有相同的对象结构,所以可以用标准C的类型转换。

比如不使用ARC时,我们用下面的代码:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (CFStringRef)string;
同样,Core Foundation类型向Objective-C类型转换时,也是简单地用标准C的类型转换即可。

但是在ARC有效的情况下,将出现类似下面的编译错误:

    Cast of Objective-C pointer type ‘NSString *’ to C pointer type ‘CFStringRef’ (aka ‘const struct __CFString *’) requires a bridged cast
    Use __bridge to convert directly (no change in ownership)
    Use __bridge_retained to make an ARC object available as a +1 ‘CFStringRef’ (aka ‘const struct __CFString *’)
错误中已经提示了我们需要怎样做:用 __bridge 或者 __bridge_retained 来转型,其差别就是变更对象的所有权。

正因为Objective-C是ARC管理的对象,而Core Foundation不是ARC管理的对象,所以才要特意这样转换,这与id类型向void*转换是一个概念。也就是说,当这两种类型(有ARC管理,没有ARC管理)在转换时,需要告诉编译器怎样处理对象的所有权。

上面的例子,使用 __bridge/__bridge_retained 后的代码如下:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge CFStringRef)string;
只是单纯地执行了类型转换,没有进行所有权的转移,也就是说,当string对象被释放的时候,cfString也不能被使用了。

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge_retained CFStringRef)string;
...
CFRelease(cfString); // 由于Core Foundation的对象不属于ARC的管理范畴,所以需要自己release
使用 __bridge_retained 可以通过转换目标处(cfString)的 retain 处理,来使所有权转移。即使 string 变量被释放,cfString 还是可以使用具体的对象。只是有一点,由于Core Foundation的对象不属于ARC的管理范畴,所以需要自己release。

实际上,Core Foundation 内部,为了实现Core Foundation对象类型与Objective-C对象类型的相互转换,提供了下面的函数。

CFTypeRef  CFBridgingRetain(id  X)  {
    return  (__bridge_retained  CFTypeRef)X;
}

id  CFBridgingRelease(CFTypeRef  X)  {
    return  (__bridge_transfer  id)X;
}
所以,可以用 CFBridgingRetain 替代 __bridge_retained 关键字:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = CFBridgingRetain(string);
...
CFRelease(cfString); // 由于Core Foundation不在ARC管理范围内,所以需要主动release。
__bridge_transfer
所有权被转移的同时,被转换变量将失去对象的所有权。当Core Foundation对象类型向Objective-C对象类型转换的时候,会经常用到 __bridge_transfer 关键字。

CFStringRef cfString = CFStringCreate...();
NSString *string = (__bridge_transfer NSString *)cfString;

// CFRelease(cfString); 因为已经用 __bridge_transfer 转移了对象的所有权,所以不需要调用 release
同样,我们可以使用 CFBridgingRelease() 来代替 __bridge_transfer 关键字。

CFStringRef cfString = CFStringCreate...();
NSString *string = CFBridgingRelease(cfString);

十六.Objective-C 如何对内存管理的,说说你的看法和解决方法?

答:Objective-C的内存管理主要有三种方式ARC(自动内存计数)、手动内存计数、内存池。
1). 自动内存计数ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。
2). 手动内存计数MRC:遵循内存谁申请、谁释放;谁添加,谁释放的原则。
3). 内存释放池Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。内存池的释放操作分为自动和手动。自动释放受runloop机制影响。

十七.AES和DES加密

1、对称加密:

需要对加密和解密使用相同密钥的加密算法。由于其速度快,对称性加密通常在消息发送方需要加密大量数据时使用。对称性加密也称为密钥加密。 
所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。 
因此 [1] 加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。因为加密和解密都使用同一个密钥,如何把密钥安全地传递到解密者手上就成了必须要解决的问题。 

DES

DES(全程Data Encryption Standard)即数据加密标准,是一种使用密钥加密的块算法,1977年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。需要注意的是,在某些文献中,作为算法的DES称为数据加密算法(Data Encryption Algorithm,DEA),已与作为标准的DES区分开来。


DES代码

.h文件

// 加密方法
+ (NSString*)encrypt:(NSString*)plainText;
// 解密方法
+ (NSString*)decrypt:(NSString*)encryptText;
.m文件

// 加密方法
+ (NSString*)encrypt:(NSString*)plainText {
    NSData* data = [plainText dataUsingEncoding:NSUTF8StringEncoding];
    size_t plainTextBufferSize = [data length];
    const void *vplainText = (const void *)[data bytes];

    CCCryptorStatus ccStatus;
    uint8_t *bufferPtr = NULL;
    size_t bufferPtrSize = 0;
    size_t movedBytes = 0;

    bufferPtrSize = (plainTextBufferSize + kCCBlockSize3DES) & ~(kCCBlockSize3DES - 1);
    bufferPtr = malloc( bufferPtrSize * sizeof(uint8_t));
    memset((void *)bufferPtr, 0x0, bufferPtrSize);

    const void *vkey = (const void *) [gkey UTF8String];
    const void *vinitVec = (const void *) [gIv UTF8String];

    ccStatus = CCCrypt(kCCEncrypt,
                       kCCAlgorithm3DES,
                       kCCOptionPKCS7Padding,
                       vkey,
                       kCCKeySize3DES,
                       vinitVec,
                       vplainText,
                       plainTextBufferSize,
                       (void *)bufferPtr,
                       bufferPtrSize,
                       &movedBytes);

    NSData *myData = [NSData dataWithBytes:(const void *)bufferPtr length:(NSUInteger)movedBytes];
    NSString *result = [GTMBase64 stringByEncodingData:myData];
    return result;
}

// 解密方法
+ (NSString*)decrypt:(NSString*)encryptText {
    NSData *encryptData = [GTMBase64 decodeData:[encryptText dataUsingEncoding:NSUTF8StringEncoding]];
    size_t plainTextBufferSize = [encryptData length];
    const void *vplainText = [encryptData bytes];

    CCCryptorStatus ccStatus;
    uint8_t *bufferPtr = NULL;
    size_t bufferPtrSize = 0;
    size_t movedBytes = 0;

    bufferPtrSize = (plainTextBufferSize + kCCBlockSize3DES) & ~(kCCBlockSize3DES - 1);
    bufferPtr = malloc( bufferPtrSize * sizeof(uint8_t));
    memset((void *)bufferPtr, 0x0, bufferPtrSize);

    const void *vkey = (const void *) [gkey UTF8String];
    const void *vinitVec = (const void *) [gIv UTF8String];

    ccStatus = CCCrypt(kCCDecrypt,
                       kCCAlgorithm3DES,
                       kCCOptionPKCS7Padding,
                       vkey,
                       kCCKeySize3DES,
                       vinitVec,
                       vplainText,
                       plainTextBufferSize,
                       (void *)bufferPtr,
                       bufferPtrSize,
                       &movedBytes);

    NSString *result = [[NSString alloc] initWithData:[NSData dataWithBytes:(const void *)bufferPtr
                                                                      length:(NSUInteger)movedBytes] encoding:NSUTF8StringEncoding];
    return result;
}
.m文件需要导入的头文件及宏定义

#import "DESUtil.h"
#import <CommonCrypto/CommonCryptor.h>
#import "GTMBase64.h"
#define gkey            @"0123456789ABCDEFGHI"
#define gIv             @"01234567"
使用的话 只需更改gkey及gIv。

重点: 
- 如果密码位数少于等于64位,加密结果与DES相同; 
- 秘钥长度128位,192位,即16或24个字符组成的字符串; 
- 常用ECB 和 BCB 模式加密计算。


AES

AES(全称Advance Encryption Standard)高级加密标准,在密码学中又成Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES。AES属于块加密(Block Cipher),块加密中有CBC、ECB、CTR、OFB、CFB等几种工作模式。常用的为CBC和ECB模式,

ECB模式

这种模式是将整个明文分成若干段相同的小段,然后对每一小段进行加密。

CBC模式

这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。
需要注意的是CBC模式需要传一个16位的向量值,不传的话,默认为空,空的话就属于ECB模式。

AES代码(CBC模式)

.h文件

NSString * aesEncryptString(NSString *content, NSString *key);
NSString * aesDecryptString(NSString *content, NSString *key);
NSData * aesEncryptData(NSData *data, NSData *key);
NSData * aesDecryptData(NSData *data, NSData *key);
.m文件

NSData * cipherOperation(NSData *contentData, NSData *keyData, CCOperation operation) {
    NSUInteger dataLength = contentData.length;

    void const *initVectorBytes = [kInitVector dataUsingEncoding:NSUTF8StringEncoding].bytes;
    void const *contentBytes = contentData.bytes;
    void const *keyBytes = keyData.bytes;

    size_t operationSize = dataLength + kCCBlockSizeAES128;
    void *operationBytes = malloc(operationSize);
    size_t actualOutSize = 0;

    CCCryptorStatus cryptStatus = CCCrypt(operation,
                                          kCCAlgorithmAES,
                                          kCCOptionPKCS7Padding,
                                          keyBytes,
                                          kKeySize,
                                          initVectorBytes,
                                          contentBytes,
                                          dataLength,
                                          operationBytes,
                                          operationSize,
                                          &actualOutSize);

    if (cryptStatus == kCCSuccess) {
        return [NSData dataWithBytesNoCopy:operationBytes length:actualOutSize];
    }
    free(operationBytes);
    return nil;
}

NSString * aesEncryptString(NSString *content, NSString *key) {
    NSData *contentData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    NSData *encrptedData = aesEncryptData(contentData, keyData);
    return [encrptedData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}

NSString * aesDecryptString(NSString *content, NSString *key) {
    NSData *contentData = [[NSData alloc] initWithBase64EncodedString:content options:NSDataBase64DecodingIgnoreUnknownCharacters];
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    NSData *decryptedData = aesDecryptData(contentData, keyData);
    return [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
}

NSData * aesEncryptData(NSData *contentData, NSData *keyData) {
    return cipherOperation(contentData, keyData, kCCEncrypt);
}

NSData * aesDecryptData(NSData *contentData, NSData *keyData) {
    return cipherOperation(contentData, keyData, kCCDecrypt);
}
.m文件中需要定义两个常量

NSString const *kInitVector = @"16-Bytes--String";
size_t const kKeySize = kCCKeySizeAES128;
第一个为向量值,如果直接传空字符串,则为ECB模式,否则为CBC。本例中为CBC模式。

十八.Category(类别)、 Extension(扩展)和继承的区别

区别:
1. 分类有名字,类扩展没有分类名字,是一种特殊的分类。
2. 分类只能扩展方法(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法。
3. 继承可以增加,修改或者删除方法,并且可以增加属性。

十九.网络、Http与Https

1.网络基础

001 问题:为什么要学习网络编程?
        回答:(1)网络编程是一种实时更新应用数据的常用手段
             (2)网络编程是开发优秀网络应用的前提和基础

    002 网络基本概念
        2-1 客户端(就是手机或者ipad等手持设备上面的APP)
        2-2 服务器(远程服务器-本地服务器)
        2-3 请求(客户端索要数据的方式)
        2-4 响应(需要客户端解析数据)
        2-5 数据库(服务器的数据从哪里来)

2.Http
001 URL
        1-1 如何找到服务器(通过一个唯一的URL)
        1-2 URL介绍
            a. 统一资源定位符
            b. url格式(协议\主机地址\路径)
                协议:不同的协议,代表着不同的资源查找方式、资源传输方式
                主机地址:存放资源的主机(服务器)的IP地址(域名)
                路径:资源在主机(服务器)中的具体位置

        1-3 请求协议
            【file】访问的是本地计算机上的资源,格式是file://(不用加主机地址)
            【ftp】访问的是共享主机的文件资源,格式是ftp://
            【mailto】访问的是电子邮件地址,格式是mailto:
            【http】超文本传输协议,访问的是远程的网络资源,格式是http://(网络请求中最常用的协议)

    002 http协议
        2-1 http协议简单介绍
            a.超文本传输协议
            b.规定客户端和服务器之间的数据传输格式
            c.让客户端和服务器能有效地进行数据沟通

        2-2 http协议优缺点
            a.简单快速(协议简单,服务器端程序规模小,通信速度快)
            b.灵活(允许传输各种数据)
            c.非持续性连接(1.1之前版本是非持续的,即限制每次连接只处理一个请求,
              服务器对客户端的请求做出响应后,马上断开连接,这种方式可以节省传输时间)
        2-3 基本通信过程
            a.请求:客户端向服务器索要数据
            b.响应:服务器返回客户端相应的数据

    003 GET和POST请求
        3-1 http里面发送请求的方法
        GET(常用)、POST(常用)、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT、PATCH

        3-2 GET和POST请求的对比【区别在于参数如何传递】
            GET
            在请求URL后面以?的形式跟上发给服务器的参数,多个参数之间用&隔开,比如
            http://ww.test.com/login?username=123&pwd=234&type=JSON
            由于浏览器和服务器对URL长度有限制,因此在URL后面附带的参数是有限制的,通常不能超过1KB

            POST
            发给服务器的参数全部放在请求体中
            理论上,POST传递的数据量没有限制(具体还得看服务器的处理能力)

        3-3 如何选择【除简单数据查询外,其它的一律使用POST请求】
            a.如果要传递大量数据,比如文件上传,只能用POST请求
            b.GET的安全性比POST要差些,如果包含机密\敏感信息,建议用POST
            c.如果仅仅是索取数据(数据查询),建议使用GET
            d.如果是增加、修改、删除数据,建议使用POST
    004 iOS中发送http请求的方案
        4-1 苹果原生
            NSURLConnection 03年推出的古老技术
            NSURLSession    13年推出iOS7之后,以取代NSURLConnection【重点】
            CFNetwork       底层技术、C语言的

        4-2 第三方框架
            ASIHttpRequest
            AFNetworking        【重点】
            MKNetworkKit

    005 http请求通信过程
        5-1 请求
            【包括请求头+请求体·非必选】
        5-2 响应
            【响应头+响应体】
        5-3 通信过程
            a.发送请求的时候把请求头和请求体(请求体是非必须的)包装成一个请求对象
            b.服务器端对请求进行响应,在响应信息中包含响应头和响应体,响应信息是对服务器端的描述,
              具体的信息放在响应体中传递给客户端
        5-4 状态码
            【200】:请求成功
            【400】:客户端请求的语法错误,服务器无法解析
            【404】:无法找到资源
            【500】:服务器内部错误,无法完成请求  

3.Https

1.https简单说明
  1) HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),
      是以安全为目标的HTTP通道,简单讲是HTTP的安全版。
  2) 即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 
      它是一个URI scheme(抽象标识符体系),句法类同http:体系。用于安全的HTTP数据传输。
  3) https:URL表明它使用了HTTP,但HTTPS存在不同于HTTP的默认端口及一个加密/身份验证层(在HTTP与TCP之间)。

2.HTTPS和HTTP的区别主要为以下四点:
  1) https协议需要到ca申请证书,一般免费证书很少,需要交费。
  2) http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
  3) http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  4) http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

3.简单说明
  1) HTTPS的主要思想是在不安全的网络上创建一安全信道,并可在使用适当的加密包和服务器证书可被验证且可被信任时,
    对窃听和中间人攻击提供合理的保护。
  2)HTTPS的信任继承基于预先安装在浏览器中的证书颁发机构(如VeriSign、Microsoft等)
   (意即“我信任证书颁发机构告诉我应该信任的”)。
  3)因此,一个到某网站的HTTPS连接可被信任,如果服务器搭建自己的https 也就是说采用自认证的方式来建立https信道,
    这样一般在客户端是不被信任的。
  4)所以我们一般在浏览器访问一些https站点的时候会有一个提示,问你是否继续。

4.对开发的影响。
4.1 如果是自己使用NSURLSession来封装网络请求,涉及代码如下。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] 
                                          delegate:self 
                                          delegateQueue:[NSOperationQueue mainQueue]];

    NSURLSessionDataTask *task =  [session dataTaskWithURL:[NSURL URLWithString:@"https://www.apple.com"] 
                                           completionHandler:^(NSData *data, 
                                                               NSURLResponse *response, 
                                                               NSError *error) {
        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    }];
    [task resume];
}

/*
 只要请求的地址是HTTPS的, 就会调用这个代理方法
 我们需要在该方法中告诉系统, 是否信任服务器返回的证书
 Challenge: 挑战 质问 (包含了受保护的区域)
 protectionSpace : 受保护区域
 NSURLAuthenticationMethodServerTrust : 证书的类型是 服务器信任
 */
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
        completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, 
                                    NSURLCredential *))completionHandler
{
    //    NSLog(@"didReceiveChallenge %@", challenge.protectionSpace);
    NSLog(@"调用了最外层");
    // 1.判断服务器返回的证书类型, 是否是服务器信任
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        NSLog(@"调用了里面这一层是服务器信任的证书");
        /*
         NSURLSessionAuthChallengeUseCredential = 0,                     使用证书
         NSURLSessionAuthChallengePerformDefaultHandling = 1,            忽略证书(默认的处理方式)
         NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,     忽略书证, 并取消这次请求
         NSURLSessionAuthChallengeRejectProtectionSpace = 3,            拒绝当前这一次, 下一次再询问
         */
//        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        NSURLCredential *card = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential , card);
    }
}

4.2 如果是使用AFN框架,那么我们不需要做任何额外的操作,AFN内部已经做了处理。

4 URL中文转码问题

//1.确定请求路径

    NSString *urlStr = @"http://120.25.226.186:32812/login2?username=哈哈哈&pwd=123";
    NSLog(@"%@",urlStr);
    //中文转码操作
    urlStr =  [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSLog(@"%@",urlStr);

    NSURL *url = [NSURL URLWithString:urlStr];

二十. 设计一个简单Memory Cache 使用LRU算法

一.Swift语言代码

 class Node(object):
    def __init__(self, key=None, value=None, next=None, prev=None):
        self.key = key
        self.value = value
        self.next = next
        self.prev = prev
class LRUCache(object):
    def __init__(self, capacity):
        """
        :type capacity: int
        """
        self.capacity = capacity
        #带有头节点的单个链表
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#总是将新节点放在尾部
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;#也将修改后的节点移到尾部
        self.head = Node()
        self.tail = self.head
        self.head.next = self.tail
        # <key, node.prev>
        self.hash_table = {}
    def pop_front(self):
        del self.hash_table[self.head.next.key]
        p_next = self.head.next.next
        self.head.next = p_next
        #更新新前端节点的引用
        self.hash_table[self.head.next.key] = self.head
    def append(self, node):
        self.hash_table[node.key] = self.tail
        self.tail.next = node
        self.tail = node
    def move_to_end(self, prev):
        node = prev.next
        if node == self.tail:
            return
        # 断开节点
        prev.next = node.next
        node.next = None
        self.hash_table[prev.next.key] = prev
        # 追加节点
        self.append(node)
    def get(self, key):
        """
        :type key: int
        :rtype: int
        """
        if key not in self.hash_table:
            return -1
        prev = self.hash_table[key]
        val = prev.next.value
        self.move_to_end(prev)
        return val
    def put(self, key, value):
        """
        :type key: int
        :type value: int
        :rtype: void
        """
        if key in self.hash_table:
            prev = self.hash_table[key]
            prev.next.value = value
            self.move_to_end(prev)
        else:
            self.append(Node(key, value))
            if len(self.hash_table) > self.capacity:
                self.pop_front()


二.OC语言代码

  struct Node{
    int _key;
    int _value;
    Node* _next;
    Node(int key,int value,Node* next):_key(key),_value(value),_next(next){}
};

class LRUCache{
public:
    LRUCache(int capacity) {
        _capacity   = capacity;
        _size       = 0;
        _last       = 0;
        _cur_begin  = _begin = (char *) malloc(sizeof(Node)*(capacity+1));
        _head       = new (_cur_begin) Node(0,0,NULL);//在指定内存上构造对象
        _cur_begin += sizeof(Node);
    }
    ~LRUCache(){
        if(_begin!=NULL){
            while(_cur_begin > _begin){
                _cur_begin -= sizeof(Node);
                ((Node*)_cur_begin)->~Node();//先释放内存上的对象
            }
            free(_begin);//再释放内存
        }
    }
    int get(int key) {
        int value             = -1;//初始时假设key对应的结点不存在
        Node* pre_node_of_key = umap_prenodes[key];//key对应的结点的前驱结点
        if(pre_node_of_key !=NULL){//key结点存在
            Node* node             = pre_node_of_key->_next;//key对应的结点
            pre_node_of_key->_next = node->_next;
            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;
            }
            node->_next            = _head->_next;
            if(node->_next!=NULL){//node有后继,更新后继的前驱结点
                umap_prenodes[node->_next->_key] = node;
            }
            _head->_next           = node;
            umap_prenodes[key]     = _head;              
            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }

            value = node->_value;
        }
        return value;
    }

    void set(int key, int value) {
        Node* node            = NULL;
        Node* pre_node_of_key = umap_prenodes[key];//key对应的结点的前驱结点
        if(pre_node_of_key != NULL){//key对应的结点存在,孤立key对应的结点,也就是从链表中把结点取出来,重新链接链表
            node                   = pre_node_of_key->_next;//key对应的结点
            pre_node_of_key->_next = node->_next;

            if(pre_node_of_key->_next!=NULL){
                umap_prenodes[pre_node_of_key->_next->_key] = pre_node_of_key;//更新前驱
            }

            node->_value           = value; //重置结点值

            /*更新_last*/
            if(_last == key ){
                _last = ( pre_node_of_key == _head ? key : pre_node_of_key->_key ); 
            }
        }else{//结点不存在

            if(_capacity == 0){//缓冲区为空
                return ;
            }

            if(_size == _capacity){//缓存满,重用最后一个结点

                Node* pre_node_of_last    = umap_prenodes[_last];//最后一个结点的前驱结点

                umap_prenodes[pre_node_of_last->_next->_key] = NULL;

                node                      = new (pre_node_of_last->_next) Node(key,value,NULL);//重用最后一个结点

                pre_node_of_last->_next   = NULL;//移出最后一个结点

                _last = ( pre_node_of_last == _head ? key : pre_node_of_last->_key ); //更新指向最后一个结点的key

            }else{//缓冲未满,使用新结点

                node    = new (_cur_begin) Node(key,value,NULL);
                _cur_begin += sizeof(Node);
                _size++;
                if(_size==1){
                    _last = key;
                }
            }
        }

        /*把node插入到第一个结点的位置*/
        node->_next            = _head->_next;
        if(node->_next!=NULL){//node有后继,更新后继的前驱结点
            umap_prenodes[node->_next->_key] = node;
        }
        _head->_next           = node;
        umap_prenodes[key]     = _head;  

    }

private:
    int   _size;
    int   _capacity;
    int   _last;//_last是链表中最后一个结点的key
    Node* _head;
    unordered_map<int,Node*> umap_prenodes;//存储key对应的结点的前驱结点,链表中第一个结点的前驱结点为_head

    char* _begin;//缓存的起始位置 
    char* _cur_begin;//用于分配结点内存的起始位置
};

二十一.为什么我们常见的delegate属性都用是week而不是retain/strong?

答:是为了防止delegate两端产生不必要的循环引用。
@property (nonatomic, weak) id<UITableViewDelegate> delegate;

二十二.delete Notification KVO 区别

一.delegate的优势:
  1.非常严格的语法。所有将听到的事件必须是在delegate协议中有清晰的定义。
  2.如果delegate中的一个方法没有实现那么就会出现编译警告/错误
  3.协议必须在controller的作用域范围内定义
  4.在一个应用中的控制流程是可跟踪的并且是可识别的;
  5.在一个控制器中可以定义定义多个不同的协议,每个协议有不同的delegates
  6.没有第三方对象要求保持/监视通信过程。
  7.能够接收调用的协议方法的返回值。这意味着delegate能够提供反馈信息给controller
 缺点:
  1.需要定义很多代码:1.协议定义;2.controller的delegate属性;3.在delegate本身中实现delegate方法定义
  2.在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash
  3.在一个controller中有多个delegate对象,并且delegate是遵守同一个协议,但还是很难告诉多个对象同一个事件,不过有可能。

二.notification
  优势:
  1.不需要编写多少代码,实现比较简单;
  2.对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单
  3.controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息
 缺点:
  1.在编译期不会检查通知是否能够被观察者正确的处理; 
  2.在释放注册的对象时,需要在通知中心取消注册;
  3.在调试的时候应用的工作以及控制过程难跟踪;
  4.需要第三方对喜爱那个来管理controller与观察者对象之间的联系;
  5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况;
  6.通知发出后,controller不能从观察者获得任何的反馈信息。

三.KVO
 优点:
  1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
  2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
  3.能够提供观察的属性的最新值以及先前值;
  4.用key paths来观察属性,因此也可以观察嵌套对象;
  5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
 缺点:
  1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
  2.对属性重构将导致我们的观察代码不再可用;
  3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向;
  4.当释放观察者时不需要移除观察者。

二十三.开发中常用的锁有如下几种:

(1).@synchronized

 - (void)lock1 {
    @synchronized (self) {
        // 加锁操作
    }
}

(2).NSLock 对象锁

- (void)lock2 {
    NSLock *xwlock = [[NSLock alloc] init];
    XWLogBlock logBlock = ^ (NSArray *array) {
        [xwlock lock];
        for (id obj in array) {
            NSLog(@"%@",obj);
        }
        [xwlock unlock];
    };

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *array = @[@1,@2,@3];
        logBlock(array);
    });
}

死锁
- (void)lock5 {
    NSLock *commonLock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^XWRecursiveBlock)(int);

        XWRecursiveBlock = ^(int  value) {
            [commonLock lock];
            if (value > 0) {
                NSLog(@"加锁层数: %d",value);
                sleep(1);
                XWRecursiveBlock(--value);
            }
            NSLog(@"程序退出!");
            [commonLock unlock];
        };

        XWRecursiveBlock(3);
    });
}

(3).NSRecursiveLock 递归锁

- (void)lock4 {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^XWRecursiveBlock)(int);

        XWRecursiveBlock = ^(int  value) {
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"加锁层数: %d",value);
                sleep(1);
                XWRecursiveBlock(--value);
            }
            NSLog(@"程序退出!");
            [recursiveLock unlock];
        };

        XWRecursiveBlock(3);
    });
}


(4).NSConditionLock 条件锁

- (void)lock11 {
    NSConditionLock *conditionLock = [[NSConditionLock alloc] init];
    NSMutableArray *arrayM = [NSMutableArray array];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionLock lock];

        for (int i = 0; i < 6; i++) {
            [arrayM addObject:@(i)];
            NSLog(@"异步下载第 %d 张图片",i);
            sleep(1);
            if (arrayM.count == 4) {
                [conditionLock unlockWithCondition:4];
            }
        }
    });

    dispatch_async(dispatch_get_main_queue(), ^{
        [conditionLock lockWhenCondition:4];
        NSLog(@"已经获取到4张图片->主线程渲染");
        [conditionLock unlock];
    });
}

(5).pthread_mutex 互斥锁(C语言)

 __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"+++++ 线程1 start");
        pthread_mutex_lock(&mutex);
        sleep(2);
        pthread_mutex_unlock(&mutex);
        NSLog(@"+++++ 线程1 end");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"----- 线程2 start");
        pthread_mutex_lock(&mutex);
        sleep(3);
        pthread_mutex_unlock(&mutex);
        NSLog(@"----- 线程2 end");
    });
}

(6).dispatch_semaphore 信号量实现加锁(GCD)

- (void)lock7 {
//    dispatch_semaphore_create //创建一个信号量 semaphore
//    dispatch_semaphore_signal //发送一个信号 信号量+1
//    dispatch_semaphore_wait   // 等待信号 信号量-1

    /// 需求: 异步线程的两个操作同步执行

    dispatch_semaphore_t semaphone = dispatch_semaphore_create(0);
    NSLog(@"start");

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"async .... ");
        sleep(5);
        /// 线程资源 + 1
        dispatch_semaphore_signal(semaphone);//信号量+1
    });
    /// 当前线程资源数量为 0 ,等待激活
    dispatch_semaphore_wait(semaphone, DISPATCH_TIME_FOREVER);
    NSLog(@"end");
}


(7).OSSpinLock

二十四.KVC的底层实现?

当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

二十五.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: 也会被调用。
6.补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

二十六.你是否接触过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:));

二十七.const、static、extern  inline简介

const作用:限制类型. &nbsp;&nbsp;
* 1.const仅仅用来修饰右边的变量(基本数据变量p,指针变量*p)
* 2.被const修饰的变量是只读的。

static和extern简单使用
* "static作用":* 修饰局部变量:
1.延长局部变量的生命周期,程序结束才会销毁。
2.局部变量只会生成一份内存,只会初始化一次。
3.改变局部变量的作用域。
* 修饰全局变量
1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改
2.避免重复定义全局变量

static与const联合使用
* static与const作用:声明一个只读的静态变量
* 开发使用场景:在"一个文件中"经常使用的字符串常量,可以使用static与const组合


* "extern作用":
* 只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量
* "extern工作原理":
* 先在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。

extern与const联合使用
* 开发中使用场景:在"多个文件中"经常使用的同一个字符串常量,可以使用extern与const组合。
* 原因:
* static与const组合:在每个文件都需要定义一份静态全局变量。
* extern与const组合:只需要定义一份全局变量,多个文件共享。
*&nbsp; 全局常量正规写法:开发中便于管理所有的全局变量,通常搞一个GlobeConst文件,里面专门定义全局变量,统一管理,要不然项目文件多不好找。

static自不用多说,表示在当前文件中应用,如 static A, 在其它文件中也可以出现static A.不会导致重名的错误.
inline.内联函数.
作用:替代宏.

虽然static inline修饰的是函数(或者方法,swift出来后,我觉着方法==函数,朋友们不用咬文嚼字,鄙视我辈了).但它在这里就是宏的作用,即你可以将CGFloatFromPixel当作一个宏.
当然inline函数与宏有区别,inline可以:

解决函数调用效率的问题:
函数之间调用,是内存地址之间的调用,当函数调用完毕之后还会返回原来函数执行的地址。函数调用有时间开销,内联函数就是为了解决这一问题。
不用inline修饰的函数, 汇编时会出现 call 指令.调用call指令就是就需要:
(1)将下一条指令的所在地址入栈
(2)并将子程序的起始地址送入PC(于是CPU的下一条指令就会转去执行子程序).

为什么inline能取代宏?

优点相比于函数:

inline函数避免了普通函数的,在汇编时必须调用call的缺点:取消了函数的参数压栈,减少了调用的开销,提高效率.所以执行速度确比一般函数的执行速度要快.
2)集成了宏的优点,使用时直接用代码替换(像宏一样);

优点相比于宏:

1)避免了宏的缺点:需要预编译.因为inline内联函数也是函数,不需要预编译.

2)编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。

3)可以使用所在类的保护成员及私有成员。

inline内联函数的说明

1.内联函数只是我们向编译器提供的申请,编译器不一定采取inline形式调用函数.
2.内联函数不能承载大量的代码.如果内联函数的函数体过大,编译器会自动放弃内联.
3.内联函数内不允许使用循环语句或开关语句.
4.内联函数的定义须在调用之前.

二十八. iOS开发中nil、Nil、NULL和[NSNull null]的区别

1、nil--- 当一个对象置为nil时,这个对象的内存地址就会被系统收回。置空之后是不能进行retain,copy等跟引用计数有关的任何操作的。

2、Nil--- nil完全等同于Nil,只不过由于编程习惯,人们一般把对象置空用nil,把类置空用Nil。

3、NULL--- 这个是从C语言继承来的,就是一个简单的空指针

4、[NSNull null]

这个才是重点:[NSNull null]和nil的区别在于,nil是一个空对象,已经完全从内存中消失了,而如果我们想表达“我们需要有这样一个容器,但这个容器里什么也没有”的观念时,我们就用到[NSNull null],它就是为“值为空的对象”。如果你查阅开发文档你会发现NSNull这个类是继承NSObject,并且只有一个“+ (NSNull *) null;”类方法。这就说明NSNull对象拥有一个有效的内存地址,所以在程序中对它的任何引用都是不会导致程序崩溃的。
 

二十九.UIView 和 CALayer 的关系

UIView 有一个名叫 layer ,类型为 CALayer 的对象属性,它们的行为很相似,主要区别在于:CALayer 继承自 NSObject ,不能够响应事件。

这是因为 UIView 除了负责响应事件 ( 继承自 UIReponder ) 外,它还是一个对 CALayer 的底层封装。可以说,它们的相似行为都依赖于 CALayer 的实现,UIView 只不过是封装了它的高级接口而已。

1.职责不同
UIVIew 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UI。

2.需要复用
在 macOS 和 App 系统上,NSView 和 UIView 虽然行为相似,在实现上却有着显著的区别,却又都依赖于 CALayer 。在这种情况下,只能封装一个 CALayer 出来。

CALayerDelegate

你可以使用 delegate (CALayerDelegate) 对象来提供图层的内容,处理任何子图层的布局,并提供自定义操作以响应与图层相关的更改。如果图层是由 UIView 创建的,则该 UIView 对象通常会自动指定为图层的委托。

注意:

在 iOS 中,如果图层与 UIView 对象关联,则必须将此属性设置为拥有该图层的 UIView 对象。
delegate 只是另一种为图层提供处理内容的方式,并不是唯一的。UIView 的显示跟它图层委托没有太大关系。

三十.iOS开发中id,NSObject *,id,instancetype四者有什么区别

instancetype 和 id 都是万能指针,指向对象。
不同点:
1.id在编译的时候不能判断对象的真实类型,instancetype在编译的时候可以判断对象的真实类型

2.id可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype只能作为返回值类型

3、id和instancetype都能省去具体类型,提高代码的通用性。而NSObject *则没有这种功能。

4、instancetype只能用于方法的返回类型,而id用处和NSObject *类似。

5、instancetype会告诉编译器当前的类型,这点和NSObject *类似,但id对于编译器却是无类型的,调用任何方法不会给出错误提示。

6、对于init方法,id和instancetype是没有区别的。因为编译器会把id优化成instancetype。当明确返回的类型就是当前Class时,使用instancetype能避免id带来的编译不出的错误情况。

7、NSObject *和id都是仅包含了一个Class isa。但NSObject 包含了更多方法的定义。

8、instancetype应是对id和NSObject *两者不足的一个补充。

三十一.什么是谓词?

谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。
//定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

三十二.isa指针问题

isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。
元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。
同时注意的是:元类(meteClass)也是类,它也是对象。
元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。
根元类的isa指针指向本身,这样形成了一个封闭的内循环。

三十三.如何访问并修改一个类的私有属性?

1.KVC

我们可以用setValue:的方法设置私有属性,并利用valueForKey:的方法访问私有属性。假设我们有一个类Person,并且这个类有一个私有属性name。看代码:

 Person * ls = [[Person alloc] init];

 [ls setValue:@"wo" forKey:@"name"];

2.runtime

我们可以利用runtime获取某个类的所有属性(私有属性、非私有属性),在获取到某个类的属性后就可以对该属性进行访问以及修改了。

三十四.一个objc对象的isa的指针指向什么?有什么作用?

isa 指的就是 是个什么,对象的isa指向类,类的isa指向元类(meta class),元类isa指向元类的根类。isa帮助一个对象找到它的方法。isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

三十五.isKindOfClass、isMemberOfClass、selector作用分别是什么

isKindOfClass:作用是某个对象属于某个类型或者继承自某类型。
isMemberOfClass:某个对象确切属于某个类型。
selector:通过方法名,获取在内存中的函数的入口地址。

三十六.delegate 和 notification 的区别

1). 二者都用于传递消息,不同之处主要在于一个是一对一的,另一个是一对多的。
2). notification通过维护一个array,实现一对多消息的转发。
3). delegate需要两者之间必须建立联系,不然没法调用代理的方法;notification不需要两者之间有联系。

三十七.iOS开发之layoutSubviews的作用和调用机制

1、layoutSubviews作用

layoutSubviews是对subviews重新布局。比如,我们想更新子视图的位置的时候,可以通过调用layoutSubviews方法,即可以实现对子视图重新布局。 
layoutSubviews默认是不做任何事情的,用到的时候,需要在子类进行重写。

2、layoutSubviews调用机制

①、直接调用setLayoutSubviews。
②、addSubview的时候触发layoutSubviews。
③、当view的frame发生改变的时候触发layoutSubviews。
④、第一次滑动UIScrollView的时候触发layoutSubviews。
⑤、旋转Screen会触发父UIView上的layoutSubviews事件。
⑥、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
注意: 
init初始化不会触发layoutSubviews,但是使用initWithFrame进行初始化时,当rect的值不为CGRectZero时,也会触发。

3、其他

①、- (void)layoutSubviews; 
这个方法,默认没有做任何事情,需要子类进行重写;
②、- (void)setNeedsLayout; 
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用;
③、- (void)layoutIfNeeded; 
如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。

三十八.block的注意点

1). 在block内部使用外部指针且会造成循环引用情况下,需要用__week修饰外部指针:
    __weak typeof(self) weakSelf = self; 
2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。
    __strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block内部改变外部栈区变量的话,需要在用__block修饰外部变量。

三十九.BAD_ACCESS在什么情况下出现?

答:这种问题在开发时经常遇到。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。

四十.lldb(gdb)常用的控制台调试命令?

1). p 输出基本类型。是打印命令,需要指定类型。是print的简写
    p (int)[[[self view] subviews] count]
2). po 打印对象,会调用对象description方法。是print-object的简写
    po [self view]
3). expr 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
4). bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
5). br l:是breakpoint list的简写

四十一.iOS中常用的数据存储方式有哪些?

数据存储有四种方案:NSUserDefault、KeyChain、file、DB。
    其中File有三种方式:plist、Archive(归档)
    DB包括:SQLite、FMDB、CoreData

四十二.iOS的沙盒目录结构是怎样的?

沙盒结构:
1). Application:存放程序源文件,上架前经过数字签名,上架后不可修改。
2). Documents:常用目录,iCloud备份目录,存放数据。(这里不能存缓存文件,否则上架不被通过)
3). Library:
        Caches:存放体积大又不需要备份的数据。(常用的缓存路径)
        Preference:设置目录,iCloud会备份设置信息。
4). tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。

四十三.iOS多线程技术有哪几种方式?

答:pthread、NSThread、GCD、NSOperation

四十四.用dispatch_source实现可取消的定时器

1 在开发的过程中,定时器是一个必不可少的功能,我们可以用NStimer,CADisplayLink,GCD Timer
CADisplayLink 是一个保持屏幕同频率的计时器类,一般用在动画或者视频的渲染,不是作为定时器事件来用的。
NSTimer不多讲,这个刚入门的iOS开发者用的大多都是这个,而且大部分情况下能够实现我们的功能。比如取消已经在队列的任务。这个就需要用到GCD Timer了
而且GCD相对于NStimer有很多优势
1&nbsp;GCD的定时器和NSTimer是不一样的,NSTimer受RunLoop影响,但是GCD的定时器不受影响,因为RunLoop也是基于GCD的
2&nbsp;dispatch_source_t支持的类型比较多,不仅仅是timer,还有以下类型
1 Timer dispatch source:定期产生通知
2 Signal dispatch source:UNIX信号到达时产生通知
3 Descriptor dispatch source:各种文件和socket操作的通知&nbsp;数据可读&nbsp;&nbsp;数据可写&nbsp;文件在文件系统中被删除、移动、重命名&nbsp;文件元数据信息改变
4 Process dispatch source:进程相关的事件通知&nbsp;&nbsp;当进程退出时&nbsp;当进程发起fork或exec等调用&nbsp;信号被递送到进程
5 &nbsp;Mach port dispatch source:Mach相关事件的通知
6&nbsp;Custom dispatch source:
学会GCD Timer你不会吃亏
下面只介绍dispatch_source定时器的实现 先上代码

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW,HXMessageShowTime * NSEC_PER_SEC),10 * NSEC_PER_SEC  , 0);
        dispatch_source_set_event_handler(timer, ^{

                        [self doSomething];
                        dispatch_cancel(timer);

             });
                    dispatch_resume(timer);
         }];

1 首先创建一个dispatch_source_create&nbsp;timer类型的信号量。
2 调用dispatch_source_set_timer设置信号的频率
3 用dispatch_source_set_event_handler设置定时器出发的事件
4 启用定时器。
5 在handler里面完成我们要做的工作,取消定时器。

我们也可以在程序的任何地方随时dispatch_cancel 这个定时器,这样就能取消定时器事件了

四十五.写出使用GCD方式从子线程回到主线程的方法代码

答:dispatch_sync(dispatch_get_main_queue(), ^{ });

四十六.如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

// 使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); 
// 当并发队列组中的任务执行完毕后才会执行这里的代码
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合并图片
});

四十七.dispatch_barrier_async(栅栏函数)的作用是什么?

函数定义:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
作用:
    1.在它前面的任务执行结束后它才执行,它后面的任务要等它执行完成后才会开始执行。
    2.避免数据竞争

// 1.创建并发队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
// 2.向队列中添加任务
dispatch_async(queue, ^{  // 1.2是并行的
    NSLog(@"任务1, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"任务2, %@",[NSThread currentThread]);
});

dispatch_barrier_async(queue, ^{
    NSLog(@"任务 barrier, %@", [NSThread currentThread]);
});

dispatch_async(queue, ^{   // 这两个是同时执行的
    NSLog(@"任务3, %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"任务4, %@",[NSThread currentThread]);
});

// 输出结果: 任务1 任务2 ——》 任务 barrier ——》任务3 任务4 
// 其中的任务1与任务2,任务3与任务4 由于是并行处理先后顺序不定。

四十八. RunLoop总结深度理解

  1. ###  RunLoop的结构组成
  • 主要有以下六种状态:

  • kCFRunLoopEntry -- 进入runloop循环

  • kCFRunLoopBeforeTimers -- 处理定时调用前回调

  • kCFRunLoopBeforeSources -- 处理input sources的事件

  • kCFRunLoopBeforeWaiting -- runloop睡眠前调用

  • kCFRunLoopAfterWaiting -- runloop唤醒后调用

  • kCFRunLoopExit -- 退出runloop

RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):

在CF中,和RunLoop相关的结构有下面几个类:(RunLoop应用场景)

1.CFRunLoopRef
2.CFRunLoopModeRef
3.CFRunLoopSourceRef
4.CFRunLoopTimerRef
5.CFRunLoopObserverRef

RunLoop的组成结构如下图: 

CFRunLoopRef 与 NSRunLoop之间的转换时toll-free的。关于RunLoop的具体实现代码,我们会在下面提到。

RunLoop提供了如下功能(括号中CF**表明了在CF库中对应的数据结构名称):

1.RunLoop(CFRunLoop)使你的线程保持忙碌(有事干时)或休眠状态(没事干时)间切换(由于休眠状态的存在,使你的线程不至于意外退出)。
2.RunLoop提供了处理事件源(source0,source1)机制(CFRunLoopSource)。
3.RunLoop提供了对Timer的支持(CFRunLoopTimer)。
4.RunLoop自身会在多种状态间切换(run,sleep,exit等),在状态切换时,RunLoop会通知所注册的5.Observer(CFRunLoopObserver),使得系统可以在特定的时机执行对应的操作。相关的如AutoreleasePool 的Pop/Push,手势识别等。
RunLoop在run时,会进入如下图所示的do while循环: 

(1)Thread & RunLoop(RunLoop和线程之间有什么关系?)

1.RunLoop和Thread是一一对应的(key: pthread value:runLoop)
2.Thread默认是没有对应的RunLoop的,仅当主动调用Get方法时,才会创建
3.所有Thread线程对应的RunLoop被存储在全局的__CFRunLoops字典中。同时,主线程在static CFRunLoopRef __main,子线程在TSD中,也存储了线程对应的RunLoop,用于快速查找。
这里有一点要弄清,Thread和RunLoop不是包含关系,而是平等的对应关系。Thread的若干功能,是通过RunLoop实现的。另一点是,RunLoop自己是不会Run的,需要我们手动调用Run方法(Main RunLoop会由系统启动),我们的RunLoop才会跑圈。静止(注意,这里的静止不是休眠的意思)的RunLoop是不会做任何事情的
 

(2)RunLoopMode(NSDefaultRunLoopMode | NSRunLoopCommonModes)

每次RunLoop开始Run的时候,都必须指定一个Mode,称为RunLoopMode。

如,timer是基于RunLoop实现的,我们在创建timer时,可以指定timer的mode:

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
       NSLog(@"do timer");
    }];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // 指定timer在common modes(default mode + event tracking mode) 下运行

这也就解释了,为什么当我们在滑动scrollview的时候,timer事件不会被回调。因为如果我们将timer添加到默认的主线程 的defaultmode时,当用户滑动scrollview的时候,main RunLoop 会切换到event tracking mode下来接收处理密集的滑动事件,这时候,添加在default mode下的timer是不会被触发的。解决方法就是,我们将timer添加到common modes下,让其在default mode和Event tracking mode下面都可以被调用。

(3)RunLoop Source

苹果文档将RunLoop能够处理的事件分为Input sources和timer事件。下面这张图取自苹果官网,不要注意那些容易让人混淆的细节,只看Thread , Input sources 和 Timer sources三个大方块的关系即可,不要关注里面的内容。

source0 VS source1

相同 
1. 均是__CFRunLoopSource类型,这就像一个协议,我们甚至可以自己拓展__CFRunLoopSource,定义自己的source。 
2. 均是需要被Signaled后,才能够被处理。 
3. 处理时,均是调用__CFRunLoopSource._context.version(0?1).perform,其实这就是调用一个函数指针。

不同

source0需要手动signaled,source1系统会自动signaled
source0需要手动唤醒RunLoop,才能够被处理: CFRunLoopWakeUp(CFRunLoopRef rl)。而source1 会自动唤醒(通过mach port)RunLoop来处理。
Source1 由RunLoop和内核管理,Mach Port驱动。 
Source0 则偏向应用层一些,如Cocoa里面的UIEvent处理,会以source0的形式发送给main RunLoop。

(4)Timer

我们经常使用的timer有几种?

NSTimer & PerformSelector:afterDelay:(由RunLoop处理,内部结构为CFRunLoopTimerRef)
GCD Timer(由GCD自己实现,不通过RunLoop)
CADisplayLink(通过向RunLoop投递source1 实现回调)

关于Timer的计时,是通过内核的mach time或GCD time来实现的。在RunLoop中,NSTimer在激活时,会将休眠中的RunLoop通过_timerPort唤醒,(如果是通过GCD实现的NSTimer,则会通过另一个CGD queue专用mach port),之后,RunLoop会调用来回调到timer的fire函数。

(5)Observer

Observer的作用是可以让外部监听RunLoop的运行状态,从而根据不同的时机,做一些操作。 
系统会在APP启动时,向main RunLoop里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

四十九. Runtime常用的几种方法

1.相关函数

// 遍历某个类所有的成员变量      // 遍历某个类所有的方法        // 获取指定名称的成员变量
(1)class_copyMethodList      (2)class_copyIvarList     (3)class_getInstanceVariable


// 获取成员变量名              // 获取成员变量类型编码         // 获取某个对象成员变量的值
(4)ivar_getName              (5)ivar_getTypeEncoding      (6)object_getIvar


// 设置某个对象成员变量的值      // 给对象发送消息
(7)object_setIvar            (8) objc_msgSend


2.相关应用

(1)更改属性值  (2)动态添加属性  (3)动态添加方法  (4)交换方法的实现

(5)拦截并替换方法  (6)在方法上增加额外功能  (7)归档解档  (8)字典转模型

3.代码实现

3.1 更改属性值用

runtime 修改一个对象的属性值

unsigned int count = 0;
    // 动态获取类中的所有属性(包括私有)
    Ivar *ivar = class_copyIvarList(_person.class, &count);
    // 遍历属性找到对应字段
    for (int i = 0; i < count; i ++) {
        Ivar tempIvar = ivar[i];
        const char *varChar = ivar_getName(tempIvar);
        NSString *varString = [NSString stringWithUTF8String:varChar];
        if ([varString isEqualToString:@"_name"]) {
            // 修改对应的字段值
            object_setIvar(_person, tempIvar, @"更改属性值成功");
            break;
        }
    }

3.2 动态添加属性用

 runtime 为一个类添加属性, iOS 分类里一般会这样用, 我们建立一个分类, NSObject+NNAddAttribute.h, 并添加以下代码:

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

这样只要引用 NSObject+NNAddAttribute.h, 用 NSObject 创建的对象就会有一个 name 属性, 我们可以直接这样写:

    NSObject *person = [NSObject new];
    person.name = @"以梦为马";

3.3 动态添加方法

person 类中没有 coding 方法,我们用 runtime 给 person 类添加了一个名字叫 coding 的方法,最终再调用coding方法做出相应. 下面代码的几个参数需要注意一下:

- (void)buttonClick:(UIButton *)sender {
    /*
     动态添加 coding 方法
     (IMP)codingOC 意思是 codingOC 的地址指针;
     "v@:" 意思是,v 代表无返回值 void,如果是 i 则代表 int;@代表 id sel; : 代表 SEL _cmd;
     “v@:@@” 意思是,两个参数的没有返回值。
     */
    class_addMethod([_person class], @selector(coding), (IMP)codingOC, "v@:");
    // 调用 coding 方法响应事件
    if ([_person respondsToSelector:@selector(coding)]) {
        [_person performSelector:@selector(coding)];
        self.testLabelText = @"添加方法成功";
    } else {
        self.testLabelText = @"添加方法失败";
    }
}

// 编写 codingOC 的实现
void codingOC(id self,SEL _cmd) {
    NSLog(@"添加方法成功");
}

3.4 交换方法的实现

某个类有两个方法, 比如 person 类有两个方法, coding 方法与 eating 方法, 我们用 runtime 交换一下这两个方法, 就会出现这样的情况, 当我们调用 coding 的时候, 执行的是 eating, 当我们调用 eating 的时候, 执行的是 coding, 如下面的动态效果图.

Method oriMethod = class_getInstanceMethod(_person.class, @selector(coding));
    Method curMethod = class_getInstanceMethod(_person.class, @selector(eating));
    method_exchangeImplementations(oriMethod, curMethod);

3.5 拦截并替换方法

这个功能和上面的其实有些类似, 拦截并替换方法可以拦截并替换同一个类的, 也可以在两个类之间进行, 我这里用了两个不同的类, 下面是简单的代码实现.

    _person = [NNPerson new];
    _library = [NNLibrary new];
    self.testLabelText = [_library libraryMethod];
    Method oriMethod = class_getInstanceMethod(_person.class, @selector(changeMethod));
    Method curMethod = class_getInstanceMethod(_library.class, @selector(libraryMethod));
    method_exchangeImplementations(oriMethod, curMethod);

3.6 在方法上增加额外功能

这个使用场景还是挺多的, 比如我们需要记录 APP 中某一个按钮的点击次数, 这个时候我们便可以利用 runtime 来实现这个功能. 我这里写了个 UIButton 的子类, 然后在 + (void)load 中用 runtime 给它增加了一个功能, 核心代码及实现效果图如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self.class, @selector(sendAction:to:forEvent:));
        Method cusMethod = class_getInstanceMethod(self.class, @selector(customSendAction:to:forEvent:));
        // 判断自定义的方法是否实现, 避免崩溃
        BOOL addSuccess = class_addMethod(self.class, @selector(sendAction:to:forEvent:), method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSuccess) {
            // 没有实现, 将源方法的实现替换到交换方法的实现
            class_replaceMethod(self.class, @selector(customSendAction:to:forEvent:), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            // 已经实现, 直接交换方法
            method_exchangeImplementations(oriMethod, cusMethod);
        }
    });
}

3.7 归档解档

当我们使用 NSCoding 进行归档及解档时, 如果不用 runtime, 那么不管模型里面有多少属性, 我们都需要对其实现一遍 encodeObject 和 decodeObjectForKey 方法, 如果模型里面有 10000 个属性, 那么我们就需要写 10000 句encodeObject 和 decodeObjectForKey 方法, 这个时候用 runtime, 便可以充分体验其好处(以下只是核心代码, 具体代码请见 demo).

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    // 获取类中所有属性
    Ivar *ivars = class_copyIvarList(self.class, &count);
    // 遍历属性
    for (int i = 0; i < count; i ++) {
        // 取出 i 位置对应的属性
        Ivar ivar = ivars[i];
        // 查看属性
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        // 利用 KVC 进行取值,根据属性名称获取对应的值
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int count = 0;
        // 获取类中所有属性
        Ivar *ivars = class_copyIvarList(self.class, &count);
        // 遍历属性
        for (int i = 0; i < count; i ++) {
            // 取出 i 位置对应的属性
            Ivar ivar = ivars[i];
            // 查看属性
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            // 进行解档取值
            id value = [aDecoder decodeObjectForKey:key];
            // 利用 KVC 对属性赋值
            [self setValue:value forKey:key];
        }
    }
    return self;
}

3.8 字典转模型

字典转模型我们通常用的都是第三方, MJExtension, YYModel 等, 但也有必要了解一下其实现方式: 遍历模型中的所有属性,根据模型的属性名,去字典中查找对应的 key,取出对应的值,给模型的属性赋值。

/** 字典转模型 **/
+ (instancetype)modelWithDict:(NSDictionary *)dict {
    id objc = [[self alloc] init];
    unsigned int count = 0;
    // 获取成员属性数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    // 遍历所有的成员属性名
    for (int i = 0; i < count; i ++) {
        // 获取成员属性
        Ivar ivar = ivarList[i];
        // 获取成员属性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSString *key = [ivarName substringFromIndex:1];
        // 从字典中取出对应 value 给模型属性赋值
        id value = dict[key];
        // 获取成员属性类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // 判断 value 是不是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            Class modalClass = NSClassFromString(ivarType);
            // 字典转模型
            if (modalClass) {
                // 字典转模型
                value = [modalClass modelWithDict:value];
            }
        }
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;
                // 获取数组中字典对应的模型
                NSString *type = [idSelf arrayContainModelClass][key];
                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }
                // 把模型数组赋值给value
                value = arrM;
            }
        }
        // KVC 字典转模型
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

五十._objc_msgForward 函数是做什么的,直接调用它将会发生什么?

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

五十一.什么是 TCP / UDP ?

TCP:传输控制协议。
UDP:用户数据协议。

TCP 是面向连接的,建立连接需要经历三次握手,是可靠的传输层协议。
UDP 是面向无连接的,数据传输是不可靠的,它只管发,不管收不收得到。
简单的说,TCP注重数据安全,而UDP数据传输快点,但安全性一般。

五十二.静态库和动态库的区别

一.使用静态库的好处(.a)

1.模块化,分工合作
2.避免少量改动经常导致大量的重复编译连接
3.也可以重用,注意不是共享使用
4.静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;
5.链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
(静态库问题)
   静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在O-C代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。

(静态库问题解决)
  框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。

二.动态库使用有如下好处(.framework)

1.使用动态库,可以将最终可执行文件体积缩小
2.使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
3.使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
4.动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
5.链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存。

三.共同特点

1 注意理解:无论是.a静态库还.framework静态库,我们需要的都是二进制文件+.h+其它资源文件的形式,不同的是,.a本身就是二进制文件,需要我们自己配上.h和其它文件才能使用,而.framework本身已经包含了.h和其它文件,可以直接使用。

2 图片资源的处理:两种静态库,一般都是把图片文件单独的放在一个.bundle文件中,一般.bundle的名字和.a或.framework的名字相同。.bundle文件很好弄,新建一个文件夹,把它改名为.bundle就可以了,右键,显示包内容可以向其中添加图片资源。

3 category是我们实际开发项目中经常用到的,把category打成静态库是没有问题的,但是在用这个静态库的工程中,调用category中的方法时会有找不到该方法的运行时错误(selector not recognized),解决办法是:在使用静态库的工程中配置other linker flags的值为-ObjC。

4 如果一个静态库很复杂,需要暴露的.h比较多的话,就可以在静态库的内部创建一个.h文件(一般这个.h文件的名字和静态库的名字相同),然后把所有需要暴露出来的.h文件都集中放在这个.h文件中,而那些原本需要暴露的.h都不需要再暴露了,只需要把.h暴露出来就可以了。

五十三.OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?

// 创建线程的方法
- [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
- [self performSelectorInBackground:nil withObject:nil];
- [[NSThread alloc] initWithTarget:nil selector:nil object:nil];
- dispatch_async(dispatch_get_global_queue(0, 0), ^{});
- [[NSOperationQueue new] addOperation:nil];

// 主线程中执行代码的方法
- [self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
- dispatch_async(dispatch_get_main_queue(), ^{});
- [[NSOperationQueue mainQueue] addOperation:nil];

五十四.用伪代码写一个线程安全的单例模式

static id _instance;
+ (id)allocWithZone:(struct _NSZone *)zone {
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       _instance = [super allocWithZone:zone];
   });
   return _instance;
}

+ (instancetype)sharedData {
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       _instance = [[self alloc] init];
   });
   return _instance;
}

- (id)copyWithZone:(NSZone *)zone {
   return _instance;
}

五十五.在手势对象基础类UIGestureRecognizer的常用子类手势类型中哪两个手势发生后,响应只会执行一次?

答:UITapGestureRecognizer,UISwipeGestureRecognizer是一次性手势,手势发生后,响应只会执行一次。

五十六.请简单的介绍下APNS发送系统消息的机制

APNS优势:杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的行为,由iOS系统和APNS进行长连接替代。
APNS的原理:
    1). 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
    2). 应用程序接收到设备令牌并发送给自己的后台服务器
    3). 服务器把要推送的内容和设备发送给APNS
    4). APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示

五十七.oc和swift的区别

1.Swift和Objective-C的联系

Swift和Objective-C共用一套运行时环境,Swift的类型可以桥接到Objective-C(下面我简称OC),反之亦然。
其次就是,OC之前积累的很多类库,在Swift中大部分依然可以直接使用,当然,Swift3之后,一些语法改变了很多,不过还是有迹可循的。OC出现过的绝大多数概念,比如引用计数、ARC、属性、协议、接口、初始化、扩展类、命名参数、匿名函数等,在Swift中继续有效(可能最多换个术语)。Swift大多数概念与OC一样。当然Swift也多出了一些新兴概念,这些在OC中是没有的,比如范型、元组等。

2. Swift比Objective-C有什么优势

1、Swift容易阅读,语法和文件结构简易化。
2、Swift更易于维护,文件分离后结构更清晰。
3、Swift更加安全,它是类型安全的语言。
4、Swift代码更少,简洁的语法,可以省去大量冗余代码
5、Swift速度更快,运算性能更高。

3. Swift目前存在的缺点

1、版本不稳定,之前升级Swift3大动刀,苦了好多人
2、使用人数比例偏低,目前还是OC为主
3、社区的开源项目偏少,毕竟OC独大好多年,很多优秀的类库都不支持Swift,不过这种状况正在改变,现在有好多优秀的Swift的开源类库了
4、公司使用的比例不高,很多公司以稳为主,还是在使用OC开发,很少一些在进行混合开发,更少一些是纯Swift开发。
5、偶尔开发中遇到的一些问题,很难查找到相关资料,这是一个弊端。
6、纯Swift的运行时和OC有本质区别,一些OC中运行时的强大功能,在纯Swift中变无效了。
7、对于不支持Swift的一些第三方类库,如果非得使用,只能混合编程,利用桥接文件实现。

4. Swift其他功能说明

1 Swift的内存管理

Swift使用自动引用计数(ARC)来简化内存管理,与OC一致。

2 Swift的可选项类型(Optionals)介绍

Swift引入了可选项类型,用于处理变量值不存在的情况。Optionals类似于OC中指向nil的指针,但是适用于所有数据类型,而非仅仅局限于类,Optionals相比于OC中的nil指针,更加安全和简明,并且也是Swift诸多最强大功能的核心。

3 Swift中的 !和 ?

这两个符号是用来标记这个变量的值是否可选,!表示可选变量必须保证转换能够成功,否则报错,但定义的变量可以直接使用;?表示可选变量即使转换不成功也不会报错,变量值为nil,如果转换成功,要使用该变量时,后面需要加!进行修饰。

4 Swift中范型的简单说明

范型是用来使代码能安全工作,swift中的范型可以在函数数据和普通数据类型中使用,例如类、结构体或枚举。范型可以解决代码复用的问题,

5 Swift的访问权限变更

swift新增了两种访问权限,权限更细化。具体查看这里:
访问权限由大到小依次为:open,public,internal(默认),fileprivate,private

fileprivate

在原有的swift中的 private其实并不是真正的私有,如果一个变量定义为private,在同一个文件中的其他类依然是可以访问到的。这个场景在使用extension的时候很明显。

这样带来了两个问题:
(1)当我们标记为private时,意为真的私有还是文件内可共享呢?
(2)当我们如果意图为真正的私有时,必须保证这个类或者结构体在一个单独的文件里。否则可能同文件里其他的代码访问到。
由此,在swift 3中,新增加了一个 fileprivate来显式的表明,这个元素的访问权限为文件内私有。

过去的private对应现在的fileprivate。现在的private则是真正的私有,离开了这个类或者结构体的作用域外面就无法访问。
所以fileprivate > private .
open

open则是弥补public语义上的不足。

现在的pubic有两层含义:
(1)这个元素可以在其他作用域被访问
(2)这个元素可以在其他作用域被继承或者override
继承是一件危险的事情。尤其对于一个framework或者module的设计者而言。在自身的module内,类或者属性对于作者而言是清晰的,能否被继承或者override都是可控的。但是对于使用它的人,作者有时会希望传达出这个类或者属性不应该被继承或者修改。这个对应的就是 final。

final的问题在于在标记之后,在任何地方都不能override。而对于lib的设计者而言,希望得到的是在module内可以被override,在被import到其他地方后其他用户使用的时候不能被override。
通俗的理解public和open就是:

public:可以被任何人访问,但其他module中不可以被override和继承,而在本module内可以被override和继承。
open:可以被任何人使用,包括override和继承。
internal

internal是系统默认访问级别,internal修饰符可写可不写。

(1)internal访问级别所修饰的属性或方法在源代码所在的整个模块都可以访问。
(2)如果是框架或者库代码,则在整个框架内部都可以访问,框架由外部代码所引用时,则不可以访问。
(3)如果是App代码,也是在整个App代码,也是在整个App内部可以访问。
5. Swift Foundation框架

为了方便使用,Swift的基本类型都可以无缝转换到 Foundation 框架中的对应类型。

因为 Cocoa 框架所接受和返回的基本数据类型都是自身框架内的类型,也就是 Foundation 中所定义的像 NSString,NSNumber,NSArray 等这些东西。而脱离 Cocoa 框架进行 app 开发是不可能的事情。因此我们在使用 Swift 开发 app 时无法避免地需要在 Swift 类型和 Foundation 类型间进行转换。如果需要每次显式地书写转换的话,大概就没人会喜欢用 Swift 了。还好 Swift 与 Foundation 之间的类型转换是可以自动完成的,这使得通过 Swift 使用 Cocoa 时顺畅了很多。

而且这个转换不仅是自动的,而且是双向的,而且无论何时只要有可能,转换的结果会更倾向于使用 Swift 类型。也就是说,只要你不写明类型是需要 NS 开头的类型的时候,你都会得到一个 Swift 类型。

Swift中的类型和OC的类型对应关系

String - NSString
Int, Float, Double, Bool 以及其他与数字有关的类型 - NSNumber
Array - NSArray
Dictionary - NSDictionary
6. Swift便捷的函数式编程

Swift提供了Map、FlatMap、Filter、Reduce等函数方法,能够大大方便我们对对象处理。

Map

var results = [1,3,5,7]
let results = values.map ({ (element) -> Int in
    return element * 2
})
//"[2, 6, 10, 14]"
Filter:

var values = [1,3,5,7,9]
let flattenResults = values.filter{ $0 % 3 == 0}
//[3, 9]
Reduce

var values = [1,3,5]
let initialResult = 0
var reduceResult = values.reduce(initialResult, combine: { (tempResult, element) -> Int in
    return tempResult + element
})
print(reduceResult)
//9
7. 其他补充 - swift独有

范围运算符

a...b 表示 [a,b] 包括a和b 。 (如3...5  就是范围取3,4,5)
a..<b 表示 [a,b) 包括a,不包括b 。 (如3...5  就是范围取3,4)
常见的如for循环:for i in 0...9{}
独有的元组类型

元组(tuples)把多个值组合成一个复合值。元组内的值可以使任意类型,并不要求是相同类型

var value = (Int,String) = (x:15,y:"abc")
swift中使用let定义常量,var定义变量

使用常量,更加安全,不能够被修改,在需要对对象进行修改的时候 只能用var修饰.

if let 、 guard let 的用法

缩减代码量,安全处理数据逻辑。

8. 细节使用区别

1、swift不分.h和.m文件 ,一个类只有.swift一个文件,所以整体的文件数量比起OC有一定减少。
2、swift句尾不需要分号 ,除非你想在一行中写三行代码就加分号隔开。
3、swift数据类型都会自动判断 , 只区分变量var 和常量let
4、强制类型转换格式不同 OC强转:(int)a Swift强转:Int(a)
5、关于BOOL类型更加严格 ,Swift不再是OC的非0就是真,而是true才是真false才是假
6、swift的 循环语句中必须加{} 就算只有一行代码也必须要加
7、swift的switch语句后面可以跟各种数据类型了 ,如Int、字符串都行,并且里面不用写break(OC好像不能字符串)
8、swift if后的括号可以省略: if a>b {},而OC里 if后面必须写括号。
9、swift打印 用print("") 打印变量时可以 print("(value)"),不用像OC那样记很多%@,d%等。
10、Swift3的【Any】可以代表任何类型的值,无论是类、枚举、结构体还是任何其他Swift类型,这个对应OC中的【id】类型。

五十八.GCD 与NSOperation  区别

1.GCD的核心是C语言写的系统服务,执行和操作简单高效,因此NSOperation底层也通过GCD实现,换个说法就是NSOperation是对GCD更高层次的抽象,这是他们之间最本质的区别.因此如果希望自定义任务,建议使用NSOperation;

2.依赖关系,NSOperation可以设置两个NSOperation之间的依赖,第二个任务依赖于第一个任务完成执行,GCD无法设置依赖关系,不过可以通过dispatch_barrier_async来实现这种效果;

3.KVO(键值对观察),NSOperation和容易判断Operation当前的状态(是否执行,是否取消),对此GCD无法通过KVO进行判断;

4.优先级,NSOperation可以设置自身的优先级,但是优先级高的不一定先执行,GCD只能设置队列的优先级,无法在执行的block设置优先级;

5.继承,NSOperation是一个抽象类实际开发中常用的两个类是NSInvocationOperation和NSBlockOperation,同样我们可以自定义NSOperation,GCD执行任务可以自由组装,没有继承那么高的代码复用度;

6.效率,直接使用GCD效率确实会更高效,NSOperation会多一点开销,但是通过NSOperation可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己;

关于主要的区别都已经总结,根据实际开发中来说,GCD使用情况较多,简单高效,从变成原则上来看,应该是使用高层次的抽象,避免使用低层次的抽象,那么无疑我们应该选择NSOperation,因为复杂的任务可以自己通过NSOperation实现,日常还是GCD的天下,毕竟GCD有更高的并发和执行能力。

NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD

五十九.检查内存泄漏

项目的代码很多,前两天老大突然跟我说项目中某一个ViewController的dealloc()方法没有被调用,存在内存泄漏问题,需要排查原因,解决内存泄漏问题。由于刚加入项目组不久,对出问题的模块的代码还不太熟悉,所以刚拿到问题时觉得很棘手,再加上作为一个iOS菜鸟,对内存泄漏的排查方法和原因确实基本上不了解。所以,也借着这样的机会,我研究了一下关于iOS开发中内存泄漏的排查方法和原因分析。

  首先,补充两个基本概念的解释:

  • 内存溢出 (out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
  • 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

一、排查方法

静态分析方法(Analyze)和动态分析方法(Instrument的leak)。

Analyze 优点:

1、使用操作容易。

2、能够在编码阶段,开发自行进行代码检查。早期发现代码隐患。

3、直接分析源代码来发现程序中的错误,而不需要实际运行。

4、自动检测Objective-C程序中的BUG,发现内存泄露和其它问题。

5、内存问题发现越早,解决的代价就越小。

主要分析以下四种问题:

1、逻辑错误:访问空指针或未初始化的变量等;

2、内存管理错误:如内存泄漏等;

3、声明错误:从未使用过的变量;

4、Api调用错误:未包含使用的库和框架。

Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。
4). Leaks:检查内存,看是否有内存泄露。

1.1 静态内存泄漏分析方法

  通过xcode打开项目,然后点击product-->Analyze,如下图左侧的图所示,这样就开始对项目进行静态内存泄漏分析,分析结果如下图右侧的图所示。根据分析结果进行休整之后在进行分析就好了。

       

  静态分析方法能发现大部分的问题,但是只能是静态分析结果,有一些并不准确,还有一些动态分配内存的情形并没有进行分析。所以仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,如果需要,我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。

1.2 动态内存泄漏分析方法

  分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过xcode打开项目,然后点击product-->profile,如下图左侧图所示。

       

  按上面操作,build成功后跳出Instruments工具,如上图右侧图所示。选择Leaks选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作,工具显示效果如下:

  点击左上角的红色圆点,这时项目开始启动了,由于leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。

  选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call Tree 和Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。

二、内存泄漏的原因分析

  在目前主要以ARC进行内存管理的开发模式,导致内存泄漏的根本原因是代码中存在循环引用,从而导致一些内存无法释放,这就会导致dealloc()方法无法被调用。主要原因大概有一下几种类型。

2.1 ViewController中存在NSTimer

如果你的ViewController中有NSTimer,那么你就要注意了,因为当你调用

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self  selector:@selector(updateTime:) userInfo:nil  repeats:YES];

时的 target:self 就增加了ViewController的return count,如果你不将这个timer invalidate,将别想调用dealloc。

  • 理由:这时  target: self,增加了ViewController的retain count
    self强引用timertimer强引用self。造成循环引用。
  • 解决方案:在恰当时机调用[timer invalidate]即可。

2.2 ViewController中的代理delegate

  一个比较隐秘的因素,你去找找与这个类有关的代理,有没有强引用属性?如果你这个VC需要外部传某个Delegate进来,来通过Delegate+protocol的方式传参数给其他对象,那么这个delegate一定不要强引用,尽量assign或者weak,否则你的VC会持续持有这个delegate,直到它自身被释放。

  • 理由:如果代理用strong修饰,ViewController(self)会强引用ViewView强引用delegatedelegate内部强引用ViewController(self)。造成内存泄漏。
  • 解决方案:代理尽量使用weak修饰。

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end

@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id delegate;

- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画
 

2.3 ViewController中Block

  这个可能就是经常容易犯的一个问题了,Block体内使用实例变量也会造成循环引用,使得拥有这个实例的对象不能释放。因为该block本来就是当前viewcontroller的一部分,现在盖子部门又强引用self,导致循环引用无法释放。 例如你这个类叫OneViewController,有个属性是NSString *name; 如果你在block体中使用了self.name,或者_name,那样子的话这个类就没法释放。 要解决这个问题其实很简单,就是在block之前申明当前的self引用为弱引用即可。

  • 理由:如果block被当前ViewController(self)持有,这时,如果block内部再持有ViewController(self),就会造成循环引用。

  • 解决方案:在block外部对弱化self,再在block内部强化已经弱化的weakSelf

    //MRC下代码如下
    __block Viewcontroller *weakSelf = self;
    //ARC下代码如下
    __weak Viewcontroller *weakSelf = self;

__weak typeof(self) weakSelf = self;

    [self.operationQueue addOperationWithBlock:^{

        __strong typeof(weakSelf) strongSelf = weakSelf;

        if (completionHandler) {
            
            KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
            
            completionHandler([strongSelf serialReaderWithRequest:request]);
        }
    }];

2.4 ViewController的子视图对self的持有

这个问题也是我的项目中内存泄漏的问题所在。我们有时候需要在子视图或者某个cell中点击跳转等操作,需要在子视图或cell中持有当前的ViewController对象,这样跳转之后的back键才能直接返回该页面,同时也不销毁当前ViewController。此时,你就要注意在子视图或者cell中对当前页面的持有对象不能是强引用,尽量assign或者weak,否则会造成循环引用,内存无法释放。

六十.__block和__weak的区别

__block

1.__block对象在block中是可以被修改、重新赋值的。
2.__block对象在block中不会被block强引用一次,从而不会出现循环引用问题。

__weak

使用了__weak修饰符的对象,作用等同于定义为weak的property。自然不会导致循环引用问题,因为苹果文档已经说的很清楚,当原对象没有任何强引用的时候,弱引用指针也会被设置为nil。

两者区别

1.__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。 
2.__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。 
3.__block对象可以在block中被重新赋值,__weak不可以。 

__weak&nbsp;本身是可以避免循环引用的问题的,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个&nbsp;__strong&nbsp;的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题

__block&nbsp;本身无法避免循环引用的问题,但是我们可以通过在 block 内部手动把 blockObj 赋值为 nil 的方式来避免循环引用的问题。另外一点就是&nbsp;__block&nbsp;修饰的变量在 block 内外都是唯一的,要注意这个特性可能带来的隐患。

_六十一._iOS中的事件的产生和传递

1.事件的产生

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
2.事件的传递

触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

应用如何找到最合适的控件来处理事件?

1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:

不允许交互:userInteractionEnabled = NO
隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注 意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。

响应者链条

响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条。
一般默认做法是控件将顺着响应者链条向上传递,将事件交给上一个响应者处理。
那么如何判断当前响应者的上一个响应者是谁呢?
判断当前是否为控制器的view,如果是,上一个响应者就是控制器,如果不是,上一个响应者就是父控件。


传递的过程为:
1.判断当前是否为控制器的view,是,事件就传递给控制器,不是,事件就传递给父控件。
2.在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给window对象处理。
3.如果window对象也不处理,则将事件传递给UIApplication对象。
4.如果UIApplication对象也不处理,则将事件丢弃。

hitTest:withEvent:

这是iOS事件的传递和响应中最重要的方法之一,在前边也有提到,现在来具体的介绍一下这个方法。
只要事件一传递给一个控件,这个控件就会调用自己的hitTest:withEvent:方法。
他的作用就是寻找并返回最适合的view,无论这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后就调用该方法。
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

六十二.说说你理解weak属性?

weak实现原理:

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

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

2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

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

2.当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?

1、调用objc_release

2、因为对象的引用计数为0,所以执行dealloc

3、在dealloc中,调用了_objc_rootDealloc函数

4、在_objc_rootDealloc中,调用了object_dispose函数

5、调用objc_destructInstance

6、最后调用objc_clear_deallocating,详细过程如下:

a. 从weak表中获取废弃对象的地址为键值的记录

b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil

c. 将weak表中该记录删除

d. 从引用计数表中删除废弃对象的地址为键值的记录

第三方框架

AFNetworking 底层原理分析

1). AFHTTPRequestOperationManager:内部封装的是 NSURLConnection, 负责发送网络请求, 使用最多的一个类。(3.0废弃)
2). AFHTTPSessionManager:内部封装是 NSURLSession, 负责发送网络请求,使用最多的一个类。
3). AFNetworkReachabilityManager:实时监测网络状态的工具类。当前的网络环境发生改变之后,这个工具类就可以检测到。
4). AFSecurityPolicy:网络安全的工具类, 主要是针对 HTTPS 服务。
5). AFURLRequestSerialization:序列化工具类,基类。上传的数据转换成JSON格式
(AFJSONRequestSerializer).使用不多。
6). AFURLResponseSerialization:反序列化工具类;基类.使用比较多:
7). AFJSONResponseSerializer; JSON解析器,默认的解析器.
8). AFHTTPResponseSerializer; 万能解析器; JSON和XML之外的数据类型,直接返回二进制数据.对服务器返回的数据不做任何处理.
9). AFXMLParserResponseSerializer; XML解析器;

AFNetworking是封装的NSURLSession的网络请求,由五个模块组成:分别由NSURLSession,Security,Reachability,Serialization,UIKit五部分组成

NSURLSession:网络通信模块(核心模块) 对应 AFNetworking中的 AFURLSessionManager和对HTTP协议进行特化处理的AFHTTPSessionManager,AFHTTPSessionManager是继承于AFURLSessionmanager的

Security:网络通讯安全策略模块 对应 AFSecurityPolicy

Reachability:网络状态监听模块 对应AFNetworkReachabilityManager

Seriaalization:网络通信信息序列化、反序列化模块 对应 AFURLResponseSerialization

UIKit:对于iOS UIKit的扩展库

SDWebImage内部实现原理

详细解说:

图片解释:内存层面的相当是个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。

1、入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

2、进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:,交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:.

3、如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

4、SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

5、如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

6、根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7、如果硬盘中有该图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片。

8、如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

9、共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

10、图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

11、imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

12、通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

13、将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

总结解说:
1.首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
2.如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
3.如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
4.下载后的图片会加入缓存中,并写入磁盘中
5.整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来

SDWebImage原理:
调用类别的方法:
1. 从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用。
2. 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。
3. 从网络上获取,使用,缓存到内存,缓存到沙盒。

六十三.iOS开发几大算法资料整理

CSDN八大内部排序算法介绍

github上搜集的几大算法原理和实现代码,只有JavaScript、Python、Go、Java的实现代码

github上搜集的几大算法时间复杂度和空间复杂度比较

iOS 开发中常用的排序(冒泡、选择、快速、插入、希尔、归并、基数)算法 几种常用算法OC实现(他的归并排序好像写的有点问题)

几大算法文字理解和OC代码实现

1. 冒泡排序算法(Bubble Sort)

相邻元素进行比较,按照升序或者降序,交换两个相邻元素的位置 是一种“稳定排序算法”

1.1 网上文字理论

是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

1.2 算法步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

1.3 动图演示

bubbleSort.gif

1.4 什么时候最快

当输入的数据已经是正序时。

1.5 什么时候最慢

当输入的数据是反序时。

1.6 冒泡排序代码示例

- (void)bubbleSortWithArray:(NSMutableArray *)array {
    for (int i = 0; i < array.count - 1; i++) {          //外层for循环控制循环次数         for (int j = 0; j < array.count - 1 - i; j++) {             //内层for循环控制交换次数             if ([array[j] integerValue] > [array[j + 1] integerValue]) {
                [array exchangeObjectAtIndex:j withObjectAtIndex:j + 1];
            }
        }
    }
}

2. 快速排序算法(quick sort)

快速排序图文理解,通过哨兵站岗理解快速排序

2.1 网上文字理解

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案: 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

2.2 算法步骤

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

2.3 动图演示

quickSort.gif

2.4 快速排序代码示例

- (void)quickSortArray:(NSMutableArray *)array
            leftIndex:(NSInteger)left
           rightIndex:(NSInteger)right {
   if (left > right) {
       return;
   }
   NSInteger i = left;
   NSInteger j = right;
   //记录基准数 pivoty
   NSInteger key = [array[i] integerValue];
   while (i < j) {        //首先从右边j开始查找(从最右边往左找)比基准数(key)小的值<---        while (i < j && key <= \[array\[j\] integerValue\]) {            j--;        }        //如果从右边j开始查找的值\[array\[j\] integerValue\]比基准数小,则将查找的小值调换到i的位置        if (i < j) {            array\[i\] = array\[j\];        }                //从i的右边往右查找到一个比基准数小的值时,就从i开始往后找比基准数大的值 --->
       while (i < j && [array[i] integerValue] <= key) {
           i++;
       }
       //如果从i的右边往右查找的值[array[i] integerValue]比基准数大,则将查找的大值调换到j的位置
       if (i < j) {
           array[j] = array[i];
       }
   }
   //将基准数放到正确的位置,----改变的是基准值的位置(数组下标)---
   array[i] = @(key);
   //递归排序
   //将i左边的数重新排序
   [self quickSortArray:array leftIndex:left rightIndex:i - 1];
   //将i右边的数重新排序
   [self quickSortArray:array leftIndex:i + 1 rightIndex:right];
}
 

3. 选择排序算法(select sort)

它的改进(相比较冒泡算法)在于:先并不急于调换位置,先从A[0]开始逐个检查,看哪个数最小就记下该数所在的位置P,等一躺扫描完毕,再把A[P]和A[0]对调,这时A[0]到A[n]中最小的数据就换到了最前面的位置。是一个“不稳定排序算法”

它是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。

选择排序算法一: 直接选择排序(straight select sort)

3.1 算法步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

3.2 动图演示

selectionSort.gif

3.3 直接选择排序示例代码

- (void)selectSortWithArray:(NSMutableArray *)array {
    for (int i = 0; i < array.count; i++) {         for (int j = i + 1; j < array.count; j++) {             if (array[i] > array[j]) {
                [array exchangeObjectAtIndex:i withObjectAtIndex:j];
            }
        }
    }
}

选择排序算法二:

堆排序(heap sort 涉及到完全二叉树的概念)

参考了网上搜罗的java堆排序写法和概念,计算机语言通用,OC也能实现
堆排序理解(java例子)

网上文字理解

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
    堆排序的平均时间复杂度为 Ο(nlogn)。

算法步骤

  1. 创建一个堆 H[0……n-1];
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  4. 重复步骤 2,直到堆的尺寸为 1。

动图演示

heapSort.gif

堆排序代码示例

- (void)heapSortWithArray:(NSMutableArray *)array {
    //循环建立初始堆
    for (NSInteger i = array.count * 0.5; i >= 0; i--) {
        [self heapAdjustWithArray:array parentIndex:i length:array.count];
    }
    //进行n-1次循环,完成排序
    for (NSInteger j = array.count - 1; j > 0; j--) {
        //最后一个元素和第一个元素进行交换
        [array exchangeObjectAtIndex:j withObjectAtIndex:0];
        //筛选R[0]结点,得到i-1个结点的堆
        [self heapAdjustWithArray:array parentIndex:0 length:j];
        NSLog(@"第%ld趟:", array.count - j);
        [self printHeapSortResult:array begin:0 end:array.count - 1];
    }
}

- (void)heapAdjustWithArray:(NSMutableArray *)array
                parentIndex:(NSInteger)parentIndex
                     length:(NSInteger)length {
    NSInteger temp = [array[parentIndex] integerValue]; //temp保存当前父结点
    NSInteger child = 2 * parentIndex + 1; //先获得左孩子
    
    while (child < length) {         //如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点         if (child + 1 < length && [array[child] integerValue] < [array[child + 1] integerValue]) {             child++;         }                  //如果父结点的值已经大于孩子结点的值,则直接结束         if (temp >= [array[child] integerValue]) {
            break;
        }
        
        //把孩子结点的值赋值给父结点
        array[parentIndex] = array[child];
        
        //选取孩子结点的左孩子结点,继续向下筛选
        parentIndex = child;
        child = 2 * child + 1;
    }
    array[parentIndex] = @(temp);
}

- (void)printHeapSortResult:(NSMutableArray *)array
                      begin:(NSInteger)begin
                        end:(NSInteger)end {
    for (NSInteger i = 0; i < begin; i++) {

    }
    for (NSInteger i = begin; i <= end; i++) {              }     //打印堆排序     NSLog(@"堆排序升序结果是--->%@",array);
}
 

4.  插入排序(insert sort)

4.1 网上文字理解

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

4.2 算法步骤

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)

4.3 动图演示

insertionSort.gif

4.4 插入排序代码示例

- (void)insertSortWithArray:(NSMutableArray *)array {
    NSInteger j;
    for (NSInteger i = 1; i < array.count; i++) {         //取出每一个待插入的数据,从array[1]开始查找         NSInteger temp = [array[i] integerValue];                  for (j = i - 1; j >= 0 && temp < [array[j] integerValue]; j--) {
            //如果之前的数比temp大,就将这个数往后移动一个位置,留出空来让temp插入,和整理扑克牌类似
            [array[j + 1]  integerValue] = [array[j] integerValue]];
            array[j] = [NSNumber numberWithInteger:temp];
        }
    }
}

5. 归并排序(merge sort)

5.1 网上文字理解

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

在《数据结构与算法 JavaScript 描述》中,作者给出了自下而上的迭代方法。

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

5.2 算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

5.3 动图演示

mergeSort.gif

5.4 归并排序代码示例 参考简书作者OC代码

//自顶向下的归并排序
/**
 递归使用归并排序,对array[left…right]的范围进行排序
 @param array 数组
 @param left 左边界
 @param right 右边界
 */
- (void)mergeSortWithArray:(NSMutableArray *)array
                      left:(NSInteger)left
                     right:(NSInteger)right {
    //判断递归到底的情况
    if (left >= right) {
        //这时候只有一个元素或者是不存在的情况
        return;
    }
    //中间索引的位置
    NSInteger middle = (right + left) / 2;
    //对 left --- middle 区间的元素进行排序操作
    [self mergeSortWithArray:array left:left right:middle];
    //对 middle + 1 ---- right 区间的元素进行排序操作
    [self mergeSortWithArray:array left:middle + 1 right:right];
    //两边排序完成后进行归并操作
    [self mergeSortWithArray:array left:left middle:middle right:right];
}

/**
 对 [left middle] 和 [middle + 1 right]这两个区间归并操作
 @param array 传入的数组
 @param left 左边界
 @param middle 中间位置
 @param right 右边界
 */
- (void)mergeSortWithArray:(NSMutableArray *)array
                      left:(NSInteger)left
                    middle:(NSInteger)middle
                     right:(NSInteger)right {
    //拷贝一个数组出来
    NSMutableArray *copyArray = [NSMutableArray arrayWithCapacity:right - left + 1];
    for (NSInteger i = left; i <= right; i++) {         //这里要注意有left的偏移量,所以copyArray赋值的时候要减去left         copyArray[i - left] = array[i];     }          NSInteger i = left, j = middle + 1;     //循环从left开始到right区间内给数组重新赋值,注意赋值的时候也是从left开始的,不要习惯写成了从0开始,还有都是闭区间     for (NSInteger k = left; k <= right; k++) {         //当左边界超过中间点时 说明左半部分数组越界了 直接取右边部分的数组的第一个元素即可         if (i > middle) {
            //给数组赋值 注意偏移量left 因为这里是从left开始的
            array[k] = copyArray[j - left];
            //索引++
            j++;
        } else if (j > right) {//当j大于右边的边界时证明有半部分数组越界了,直接取左半部分的第一个元素即可
            array[k] = copyArray[i - left];
            //索引++
            i++;
        } else if (copyArray[i - left] > copyArray[j - left]) {//左右两半部分数组比较
            //当右半部分数组的第一个元素要小时 给数组赋值为右半部分的第一个元素
            array[k] = copyArray[j - left];
            //右半部分索引加1
            j++;
        } else {//右半部分数组首元素大于左半部分数组首元素
            array[k] = copyArray[i - left];
            i++;
        }
    }
}

6. 希尔排序(shell sort)

6.1 网上文字理解

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

6.2 算法步骤

  1. 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  2. 按增量序列个数 k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

6.4 希尔排序代码示例

- (void)shellAscendingOrderSort:(NSMutableArray *)ascendingArr {
&nbsp; &nbsp; NSMutableArray *buckt = [self createBucket];
&nbsp; &nbsp; NSNumber *maxnumber = [self listMaxItem:ascendingArr];
&nbsp; &nbsp; NSInteger maxLength = numberLength(maxnumber);
&nbsp; &nbsp; for (int digit = 1; digit <= maxLength; digit++) {
&nbsp; &nbsp; &nbsp; &nbsp; // 入桶
&nbsp; &nbsp; &nbsp; &nbsp; for (NSNumber *item in ascendingArr) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *mutArray = buckt[baseNumber];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [mutArray addObject:item];
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; NSInteger index = 0;
&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < buckt.count; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *array = buckt[i];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while (array.count != 0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSNumber *number = [array objectAtIndex:0];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ascendingArr[index] = number;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [array removeObjectAtIndex:0];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; index++;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; NSLog(@"希尔升序排序结果:%@", ascendingArr);
}

- (NSMutableArray *)createBucket {
&nbsp; &nbsp; NSMutableArray *bucket = [NSMutableArray array];
&nbsp; &nbsp; for (int index = 0; index < 10; index++) {
&nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *array = [NSMutableArray array];
&nbsp; &nbsp; &nbsp; &nbsp; [bucket addObject:array];
&nbsp; &nbsp; }
&nbsp; &nbsp; return bucket;
}

- (NSNumber *)listMaxItem:(NSArray *)list {
&nbsp; &nbsp; NSNumber *maxNumber = list[0];
&nbsp; &nbsp; for (NSNumber *number in list) {
&nbsp; &nbsp; &nbsp; &nbsp; if ([maxNumber integerValue] < [number integerValue]) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; maxNumber = number;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; return maxNumber;
}

NSInteger numberLength(NSNumber *number) {
&nbsp; &nbsp; NSString *string = [NSString stringWithFormat:@"%ld", (long)[number integerValue]];
&nbsp; &nbsp; return string.length;
}

- (NSInteger)fetchBaseNumber:(NSNumber *)number digit:(NSInteger)digit {
&nbsp; &nbsp; if (digit > 0 && digit <= numberLength(number)) {
&nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *numbersArray = [NSMutableArray array];
&nbsp; &nbsp; &nbsp; &nbsp; NSString *string = [NSString stringWithFormat:@"%ld", [number integerValue]];
&nbsp; &nbsp; &nbsp; &nbsp; for (int index = 0; index < numberLength(number); index++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [numbersArray addObject:[string substringWithRange:NSMakeRange(index, 1)]];
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; NSString *str = numbersArray[numbersArray.count - digit];
&nbsp; &nbsp; &nbsp; &nbsp; return [str integerValue];
&nbsp; &nbsp; }
&nbsp; &nbsp; return 0;
}

7. 基数排序(radix sort)

7.1 文字理解

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

7.2 基数排序 vs 计数排序 vs 桶排序

基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

7.3 动图演示

radixSort.gif

7.4 基数排序代码示例

- (void)radixAscendingOrderSort:(NSMutableArray *)ascendingArr {
&nbsp; &nbsp; NSMutableArray *buckt = [self createBucket];
&nbsp; &nbsp; NSNumber *maxnumber = [self listMaxItem:ascendingArr];
&nbsp; &nbsp; NSInteger maxLength = numberLength(maxnumber);
&nbsp; &nbsp; for (int digit = 1; digit <= maxLength; digit++) {
&nbsp; &nbsp; &nbsp; &nbsp; // 入桶
&nbsp; &nbsp; &nbsp; &nbsp; for (NSNumber *item in ascendingArr) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSInteger baseNumber = [self fetchBaseNumber:item digit:digit];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *mutArray = buckt[baseNumber];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [mutArray addObject:item];
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; NSInteger index = 0;
&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < buckt.count; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSMutableArray *array = buckt[i];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while (array.count != 0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; NSNumber *number = [array objectAtIndex:0];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ascendingArr[index] = number;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [array removeObjectAtIndex:0];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; index++;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; NSLog(@"基数升序排序结果:%@", ascendingArr);
}

8. 计数排序(counting sort)

8.1 文字理解

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.2 动图演示

countingSort.gif

8.3 计数排序代码示例(无)

9. 桶排序(bucket sort)

9.1 文字理解

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

9.2 什么时候最快

当输入的数据可以均匀的分配到每一个桶中。

9.3 什么时候最慢

当输入的数据被分配到了同一个桶中。

六十四.​在 Objective-C 中,enum 建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型。

//定义一个枚举(比较严密)
typedef NS_ENUM(NSInteger, BRUserGender) {
    BRUserGenderUnknown,    // 未知
    BRUserGenderMale,        // 男性
    BRUserGenderFemale,        // 女性
    BRUserGenderNeuter        // 无性
};

六十五.谈谈 UITableView 的优化

1). 正确的复用cell。
2). 设计统一规格的Cell
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
4). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!
5). 减少子视图的层级关系
6). 尽量使所有的视图不透明化以及做切圆操作。
7). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示。
8). 使用调试工具分析问题。

如何实行cell的动态的行高

如果希望每条数据显示自身的行高,必须设置两个属性,1.预估行高,2.自定义行高。
设置预估行高 tableView.estimatedRowHeight = 200。
设置定义行高 tableView.estimatedRowHeight = UITableViewAutomaticDimension。 
如果要让自定义行高有效,必须让容器视图有一个自下而上的约束。

六十六.什么是野指针、空指针?

野指针:不知道指向了哪里的指针叫野指针。即指针指向不确定,指针存的地址是一个垃圾值,未初始化。
空指针:不指向任何位置的指针叫空指针。即指针没有指向,指针存的地址是一个空地址,NULL。

六十七.什么是 OOA / OOD / OOP ?

OOA(Object Oriented Analysis) &nbsp; --面向对象分析
OOD(Object Oriented Design) &nbsp; &nbsp; --面向对象设计
OOP(Object Oriented Programming)--面向对象编程
  • 第二大类开发之 Swift 常问面试题
    --------------------

(一)Swift 与 Objective-C 的联系与区别?

Swift和Objective-C 共用一套运行时环境,Swift 的类型可以桥接到Objective-C(下面我简称OC),反之亦然。两者可以互相引用混合编程。
其次就是,OC 之前积累的很多类库,在 Swift 中大部分依然可以直接使用,当然,Swift3之后,一些语法改变了很多,不过还是有迹可循的。OC出现过的绝大多数概念,比如引用计数、ARC、属性、协议、接口、初始化、扩展类、命名参数、匿名函数等,在Swift中继续有效(可能最多换个术语)。Swift大多数概念与OC一样。当然Swift也多出了一些新兴概念,这些在OC中是没有的,比如范型、元组等。

(二)Swift 比 Objective-C 有什么优势?

  1. Swift 容易阅读,语法和文件结构简易化。
  2. Swift 更易于维护,文件分离后结构更清晰。
  3. Swift 更加安全,它是类型安全的语言。
  4. Swift 代码更少,简洁的语法,可以省去大量冗余代码。
  5. Swift 速度更快,运算性能更高。

(三)Swift目前存在的缺点

  1. 版本不稳定,之前升级Swift3大动刀,苦了好多人。
  2. 使用人数比例偏低,目前还是OC的天下。
  3. 社区的开源项目偏少,毕竟OC独大好多年,很多优秀的类库都不支持Swift,不过这种状况正在改变,现在有好多优秀的Swift的开源类库了。
  4. 公司使用的比例不高,很多公司以稳为主,还是在使用OC开发,很少一些在进行混合开发,更少一些是纯Swift开发。
  5. 偶尔开发中遇到的一些问题,很难查找到相关资料,这是一个弊端。
  6. 纯Swift的运行时和OC有本质区别,一些OC中运行时的强大功能,在纯Swift中变无效了。
  7. 对于不支持Swift的一些第三方类库,如果非得使用,只能混合编程,利用桥接文件实现。

(四)Swift 相比 Objective-C 独有的语法

  1. 范围运算符
    a…b 表示 [a,b] 包括a和b 。 (如3…5 就是范围取3,4,5)
    a..<b 表示 [a,b) 包括a,不包括b 。 (如3…5 就是范围取3,4)
    常见的如for循环:for i in 0…9{}

  2. 独有的元组类型
    元组(tuples)把多个值组合成一个复合值。元组内的值可以使任意类型,并不要求是相同类型。eg:

    var value = (Int,String) = (x:15,y:"abc")

  3. swift中使用let定义常量,var定义变量
    使用常量,更加安全,不能够被修改,在需要对对象进行修改的时候 只能用var修饰.

  4. if let 、 guard let 的用法
    缩减代码量,安全处理数据逻辑。

(五)Swift 相比 Objective-C 细节使用区别

  1. swift不分.h和.m文件 ,一个类只有.swift一个文件,所以整体的文件数量比起OC有一定减少。
  2. swift句尾不需要分号 ,除非你想在一行中写三行代码就加分号隔开。
  3. swift数据类型都会自动判断 , 只区分变量var 和常量let
  4. 强制类型转换格式不同 OC强转:(int)a Swift强转:Int(a)
  5. 关于BOOL类型更加严格 ,Swift不再是OC的非0就是真,而是true才是真false才是假
  6. swift的 循环语句中必须加{} 就算只有一行代码也必须要加
  7. swift的switch语句后面可以跟各种数据类型了 ,如Int、字符串都行,并且里面不用写break(OC好像不能字符串)
  8. swift if后的括号可以省略: if a>b {},而OC里 if后面必须写括号。
  9. swift打印 用print("") 打印变量时可以 print("(value)"),不用像OC那样记很多%@,d%等。
  10. Swift3的【Any】可以代表任何类型的值,无论是类、枚举、结构体还是任何其他Swift类型,这个对应OC中的【id】类型。

(六)Swift 是面向对象还是函数式的编程语言?

Swift 既是面向对象的,又是函数式的编程语言。
说 Swift 是面向对象的语言,是因为 Swift 支持类的封装、继承、和多态,从这点上来看与 Java 这类纯面向对象的语言几乎毫无差别。
说 Swift 是函数式编程语言,是因为 Swift 支持 map, reduce, filter, flatmap 这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。

(七)请说明并比较以下关键词:Open, Public, Internal, File-private, Private

Swift 有五个级别的访问控制权限,从高到底依次为比如 Open, Public, Internal, File-private, Private。
他们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。比如一个 private 的 class 中不能含有 public 的 String。反之,低级别的变量却可以定义在高级别的变量中。比如 public 的 class 中可以含有 private 的 Int。

  • Open 具备最高的访问权限。其修饰的类和方法可以在任意 Module 中被访问和重写;它是 Swift 3 中新添加的访问权限。
  • Public 的权限仅次于 Open。与 Open 唯一的区别在于它修饰的对象可以在任意 Module 中被访问,但不能重写。
  • Internal 是默认的权限。它表示只能在当前定义的 Module 中访问和重写,它可以被一个 Module 中的多个文件访问,但不可以被其他的 Module 中被访问。
  • File-private 也是 Swift 3 新添加的权限。其被修饰的对象只能在当前文件中被使用。例如它可以被一个文件中的 class,extension,struct 共同使用。
  • Private 是最低的访问权限。它的对象只能在定义的作用域内使用。离开了这个作用域,即使是同一个文件中的其他作用域,也无法访问。

(八)请说明并比较以下关键词:strong, weak, unowned

Swift 的内存管理机制与 Objective-C一样为 ARC(Automatic Reference Counting)。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。

  • strong 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。
  • weak 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不亏崩溃
  • unowned 与弱引用本质上一样。唯一不同的是,对象在释放后,依然有一个无效的引用指向对象,它不是 Optional 也不指向 nil。如果继续访问该对象,程序就会崩溃。

加分回答:

  • weak 和 unowned 的引入是为了解决由 strong 带来的循环引用问题。简单来说,就是当两个对象互相有一个强指向去指向对方,这样导致两个对象在内存中无法释放(详情请参考第3章第3节第8题)。

weak 和 unowned 的使用场景有如下差别:

  • 当访问对象时该对象可能已经被释放了,则用 weak。比如 delegate 的修饰。
  • 当访问对象确定不可能被释放,则用 unowned。比如 self 的引用。
  • 实际上为了安全起见,很多公司规定任何时候都使用 weak 去修饰。

(九)在Swift和Objective-C的混编项目中,如何在Swift文件中调用Objective-C文件中已经定义的方法?如何在Objective-C文件中调用Swift文件中定义的方法?

  • Swift中若要使用Objective-C代码,可以在ProjectName-Bridging-Header.h里添加Objective-C的头文件名称,Swift文件中即可调用相应的Objective-C代码。一般情况Xcode会在Swift项目中第一次创建Objective-C文件时自动创建ProjectName-Bridging-Header.h文件。
  • Objective-C中若要调用Swift代码,可以导入Swift生成的头函数ProjectName-Swift.h来实现。
  • Swift文件中若要规定固定的方法或属性暴露给Objective-C使用,可以在方法或属性前加上@objc来声明。如果该类是NSObject子类,那么Swift会在非private的方法或属性前自动加上@objc。

(十)用Swift 将协议(protocol)中的部分方法设计成可选(optional),该怎样实现?

@optional@required 是 Objective-C 中特有的关键字。
Swift中,默认所有方法在协议中都是必须实现的。而且,协议里方法不可以直接定义 optional。先给出两种解决方案:

  • 在协议和方法前都加上 @objc 关键字,然后再在方法前加上 optional 关键字。该方法实际上是把协议转化为Objective-C的方式然后进行可选定义。示例如下:

    @objc protocol SomeProtocol {
    func requiredFunc()
    @objc optional func optionalFunc()
    }

  • 用扩展(extension)来规定可选方法。Swift中,协议扩展(protocol extension)可以定义部分方法的默认实现,这样这些方法在实际调用中就是可选实现的了。示例如下:

    protocol SomeProtocol {
    func requiredFunc()
    func optionalFunc()
    }
    extension SomeProtocol {
    func optionalFunc() {
    print(“Dumb Implementation”)
    }
    }
    Class SomeClass: SomeProtocol {
    func requiredFunc() {
    print(“Only need to implement the required”)
    }
    }

(十一)swift中,如何阻止一个方法属性,属性,下标被子类改写?

在类的定义中使用final关键字声明类、属性、方法和下标。final声明的类不能被继承,final声明的属性、方法和下标不能被重写。

(十二)swift中,实现一个将整形数组全部转化成对应的字符串数组(eg: [1,2,3,4,5] -> ["1","2","3","4","5"])

var sampleArray: [Int] = [1,2,3,4,5]
sampleArray.map {
    String($0)
}
//["1", "2", "3", "4", "5"]

(十三)swift中,关键字 guard 和 defer 的用法

guard也是基于一个表达式的布尔值去判断一段代码是否该被执行。与if语句不同的是,guard只有在条件不满足的时候才会执行这段代码。

guard let name = self.text else {  return }

defer的用法是,这条语句并不会马上执行,而是被推入栈中,直到函数结束时才再次被调用。

defer {
   //函数结束才调用
}

(十四)open与public的区别

  • public:可以别任何人访问,但是不可以被其他module复写和继承。
  • open:可以被任何人访问,可以被继承和复写。

(十五)struct与class 的区别

  • struct是值类型,class是引用类型

    • 值类型的变量直接包含它们的数据,对于值类型都有它们自己的数据副本,因此对一个变量操作不可能影响另一个变量。
    • 引用类型的变量存储对他们的数据引用,因此后者称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。
    • 二者的本质区别:struct是深拷贝,拷贝的是内容;class是浅拷贝,拷贝的是指针
  • property的初始化不同:class 在初始化时不能直接把 property 放在 默认的constructor 的参数里,而是需要自己创建一个带参数的constructor;而struct可以,把属性放在默认的constructor 的参数里。

  • 变量赋值方式不同:struct是值拷贝;class是引用拷贝。

  • immutable变量:swift的可变内容和不可变内容用var和let来甄别,如果初始为let的变量再去修改会发生编译错误。struct遵循这一特性;class不存在这样的问题

  • mutating function: struct 和 class 的差別是 struct 的 function 要去改变 property 的值的时候要加上 mutating,而 class 不用。

  • 继承: struct不可以继承,class可以继承。

  • struct比class更轻量:struct分配在栈中,class分配在堆中。

(十六)swift把struct作为数据模型

16.1优点

  1. 安全性: 因为 Struct 是用值类型传递的,它们没有引用计数。
  2. 内存: 由于他们没有引用数,他们不会因为循环引用导致内存泄漏。
  3. 速度: 值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快很多!
  4. 拷贝:Objective-C 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),而值类型的拷贝则非常轻松!
  5. 线程安全: 值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。

16.2 缺点

  1. Objective-C与swift混合开发:OC调用的swift代码必须继承于NSObject。

  2. 继承:struct不能相互继承。

  3. NSUserDefaults:Struct 不能被序列化成 NSData 对象。

  4. 如何设置实时渲染?

    @IBDesignable让Interface Bulider在特定视图上执行实时渲染
    复制代码

  5. 异步同步任务的区别?

  6. 同步:等待任务完成,一个接一个,顺可预测(Predictable Execution Order),通常情况在Main
    异步:不分先后执行顺序完成任务,顺序不可预测(Unpredictable Order),通常在Background
    复制代码

  7. 什么是NSError对象? NSError有三部分组成,分别为 Domain Code UserInfor Domain是一个字符串,标记一个错误域

  8. NSError(domain: <#String#>, code: <#Int#>, userInfo: <#[String : Any]?#>)
    复制代码

  9. 什么是Enum? enum 是一种类型,包含了相关的一组数据

  10. 为什么使用synchronized? 保证在一定时间内,只有一个线程访问它

  11. strong, weak,copy 有什么不同? strong:引用计数会增加 weak:不会增加引用计数 Copy: 意味着我们在创建对象时复制该对象的值

  12. 什么是ABI? 应用程序二进制接口

  13. 在Cocoa中有哪些常见的设计模式 创造性:单例(Singleton) 结构性: 适配器(Adapter) 行为:观察者(Observer)

  14. Realm数据库的好处 a. 开源的DB framework b. 快 c. ios 安卓都可以使用

  15. Swift 优势是什么? a. 类型安全 b. 闭包 c. 速度快

  16. 什么是泛型? 泛型可以让我们定义出灵活,且可重用的函数和类型,避免重复代码

  17. 解释 Swift 中的 lazy? lazy是 Swift 中的一个关键字,他可以延迟属性的初始化时间,知道用到这个属性时,才去加载它

  18. 解释什么是 defer? 延迟执行,当你离开当前代码块,就会去执行

  19. KVC 和 KCO 的区别? KVC: 它是一种用间接方式访问类的属性的机制 KVO: 它是一种观察者模式,被观察的对象如果有改变,观察者就会收到通知

  20. Gurad的好处? 可以使语句变得更简洁,避免嵌套很多层,可以使用break,return提前退.

  • 第三大类React Native面试题
    ----------------------------------------------------------------------

1.React Native相对于原生的ios和Android有哪些优势。

react native一套代码可以开发出跨平台app, 减少了人力、节省了时间、避免了 iOS 与 Android 版本发布的时间差,开发新功能可以更迅速。等等

2.React Native的优点和缺点在哪里。

缺点:内存、转化为原生的

3.父传子,子传父数据传递方式。

props state refs 方面回答

4.如何实现底部TabBar的高度不一样呢。(类似新浪微博底部加号)

主要考察flex布局绝对定位问题

5.请您简单介绍一下redux、mobx。

redux ==> action/reducer/store
mobx ==>数据双向绑定

6.当你调用setState的时候,发生了什么事。

当调用 setState 时,React会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。 这将启动一个称为和解(reconciliation)的过程。 和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。 为此,React将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。 一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )。 通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间。

7.React中Element 和 Component 有何区别。

简单地说,一个 React element 描述了你想在屏幕上看到什么。 换个说法就是,一个 React element 是一些 UI 的对象表示。 一个 React Component 是一个函数或一个类, 它可以接受输入并返回一个 React element (通常是通过 JSX ,它被转化成一个 createElement 调用)。

8.shouldComponentUpdate 应该做什么

其实这个问题也是跟reconciliation有关系。 “和解( reconciliation )的最终目标是以最有效的方式,根据新的状态更新用户界面”。 如果我们知道我们的用户界面(UI)的某一部分不会改变, 那么没有理由让 React 很麻烦地试图去弄清楚它是否应该渲染。 通过从 shouldComponentUpdate 返回 false, React 将假定当前组件及其所有子组件将保持与当前组件相同

9.描述事件在React中的处理方式

为了解决跨浏览器兼容性问题, 您的 React 中的事件处理程序将传递 SyntheticEvent 的实例, 它是 React 的浏览器本机事件的跨浏览器包装器。 这些 SyntheticEvent 与您习惯的原生事件具有相同的接口,除了它们在所有浏览器中都兼容。 有趣的是,React 实际上并没有将事件附加到子节点本身。 React 将使用单个事件监听器监听顶层的所有事件。 这对于性能是有好处的,这也意味着在更新DOM时,React 不需要担心跟踪事件监听器

10.reactJS的props.children.map函数来遍历会收到异常提示,为什么。应该如何遍历。

this.props.children 的值有三种可能

  1. 当前组件没有子节点,它就是 undefined;
  2. 有一个子节点,数据类型是 object;
  3. 有多个子节点,数据类型就是 array.

系统提供React.Children.map()方法安全的遍历子节点对象

11.XSS与CSRF介绍

XSS是一种跨站脚本攻击,是属于代码注入的一种,攻击者通过将代码注入网页中,其他用户看到会受到影响(代码内容有请求外部服务器);
CSRF是一种跨站请求伪造,冒充用户发起请求,完成一些违背用户请求的行为(删帖,改密码,发邮件,发帖等)

12.在使用redux过程中,如何防止定义的action-type的常量重复。

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。 Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象 Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

13 PureComponent 为什么比Component高效?

追查:这两个组件的实现在ReactBaseClasses.js中间,除掉注释后如下

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.isReactComponent = {};

ReactComponent.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};


function ReactPureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;

ReactPureComponent.prototype = new ComponentDummy();
ReactPureComponent.prototype.constructor = ReactPureComponent;
_assign(ReactPureComponent.prototype, ReactComponent.prototype);
ReactPureComponent.prototype.isPureReactComponent = true;

module.exports = {
  Component: ReactComponent,
  PureComponent: ReactPureComponent
};

发现Component就只实现了构造方法,定义了setState方法就完了。而ReactPureComponent更狠,就只是用js原型模拟继承的方法继承了Component,然后定义属性isPureReactComponent为true。全局搜索isPureReactComponent属性,发现在ReactCompositeComponent.js中有使用,这个类就是管理组件的更新、加载等的。关键代码在updateComponent方法中,如下

var shouldUpdate = true;    // 这个变量决定是否需要重新渲染组件
  if (!this._pendingForceUpdate) {
    var prevState = inst.state;
    shouldUpdate = willReceive || nextState !== prevState;
    // inst代表组件实例,这个判断条件就是组件是否自己实现了shouldComponentUpdate方法
    if (inst.shouldComponentUpdate) {
      if (__DEV__) {
        shouldUpdate = measureLifeCyclePerf(
          () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
          this._debugID,
          'shouldComponentUpdate',
        );
      } else {
        shouldUpdate = inst.shouldComponentUpdate(
          nextProps,
          nextState,
          nextContext,
        );
      }
    } else {// 组件没有实现shouldComponentUpdate方法,且是PureComponent,采用shallowEqual浅比较
      if (this._compositeType === ReactCompositeComponentTypes.PureClass) {
        shouldUpdate = !shallowEqual(prevProps, nextProps) ||
          !shallowEqual(inst.state, nextState);
      }
    }
  }

shallowEqual的实现在shallowEqual.js中,大意就是作浅比较,也就是对象数组等只比较对象所处的地址是否相等,而不比较具体的内容,因为深层次递归比较对象内容是否一致很耗费性能。

结论
PureComponent是Component的子类,当PureComponent手动实现了shouldComponentUpdate方法时两个组件没有区别,但若没有手动实现该方法,则PureComponent采用默认的shallowEqual比较对象是否相等性能更佳。由此可能引发的页面不刷新现象可以采用别的办法解决,如重新生成新的对象、采用immutable.js对象等

14 TextInput首次focus时键盘出现缓慢,卡顿

追查:原因就是键盘是懒加载模式,初次出现时需要先初始化键盘视图耗费时间,要想缩减首次耗时间隔,可以事先就让键盘初始化完毕。js端没想到如何做,但是原生端可以在didFinishLaunchingWithOptions方法中写:

  UITextField *textField = [[UITextField alloc] init];
  [self.window addSubview:textField];
  [textField becomeFirstResponder];
  [textField resignFirstResponder];
  [textField removeFromSuperview];

键盘消失与否

TextInput聚焦时弹出了键盘,点击非TextInput空白处键盘是不会消失的,若想实现该功能只需要让TextInput嵌入在ScrollView中即可。
那么问题又来了,这样做之后,除了TextInput外屏幕上任意地方点击键盘都会先消失,导致例如页面上有个按钮A,点击A时会先退下键盘,再次点击才能触发A的事件,很扯淡。解决方法大体如下:

_addEvent = (event) => {
  this.events.push(event.nativeEvent.target);
};

_onStartShouldSetResponder(event) {
  const target = event.nativeEvent.target;
  if (!this.events.includes(target)) {
    Keyboard.dismiss();
  }
  return false;
}

render() {
  return (
    <ScrollView keyboardShouldPersistTaps="always">
      <View
        style={{ alignItems: 'center', flex: 1, height: SCREEN_HEIGHT }}
        onStartShouldSetResponder={(event) => this._onStartShouldSetResponder(event)}
      >
        <Button
          text="登陆"
          onLayout={(event) => this._addEvent(event)}
        />
      </View>
    </ScrollView>
  );
}

ScrollView的keyboardShouldPersistTaps属性设置为always,则键盘不再拦截点击事件,点击空白处键盘不会自动消失。
onStartShouldSetResponderCapture是点击事件发生时调用,询问该视图是否要拦截事件,自定义处理,当点击屏幕除了指定位置外都退下键盘。指定位置A(比如登录按钮)点击时,键盘不退下。
A的onLayout在视图布局完成回调,event.nativeEvent.target能唯一的标识该组件。

15 redux中的reducer为何要返回Object.assign

在redux-devtools中,我们可以查看到redux下所有通过reducer更新state的记录,每一个记录都对应着内存中某一个具体的state,让用户可以追溯到每一次历史操作产生与执行时,当时的具体状态,这也是使用redux管理状态的重要优势之一.

  • 若不创建副本,redux的所有操作都将指向内存中的同一个state,我们将无从获取每一次操作前后,state的具体状态与改变,
  • 若没有副本,redux-devtools列表里所有的state都将被最后一次操作的结果所取代.我们将无法追溯state变更的历史记录.
    创建副本也是为了保证向下传入的this.props与nextProps能得到正确的值,以便我们能够利用前后props的改变情况以决定如何render组件

16 使用redux时,不能在componentWillReceiveProps方法中使用action。

原因:有时候一个action改变数据后,我们希望拿到改变后的数据做另外一个action,比如初始化action读取硬盘中的数据到内存,然后用该参数进行请求网络数据action。此时我们可以在componentWillReceiveProps方法中拿到参数,若此时发出再发出action,则数据返回后改变reducer会再次进入componentWillReceiveProps方法,又继续发出action,陷入死循环。可以如下解决

componentWillReceiveProps(nextProp) {
    if(nextProp.app.user && nextProp.app.sessionId && !this.isFirstLoad){
        this.props.action(nextProp.app);   // action操作
        this.isFirstLoad = true;
    }
}

17 navigation导航栏下方那根黑线是什么?

追查:发现在iphone7plus模拟器中黑线看不到,但是iphone6模拟器能看见。查看源代码,在navigation组件中的Header.js第300行找到了黑线样式定义,

let platformContainerStyles;
if (Platform.OS === 'ios') {
    platformContainerStyles = {
        borderBottomWidth: StyleSheet.hairlineWidth,  // hairlineWidth为当前分辨率下能显示的最小宽度,模拟器下可能看不见
        borderBottomColor: 'rgba(0, 0, 0, .3)',
    };
} else {
    platformContainerStyles = {
        shadowColor: 'black',
        shadowOpacity: 0.1,
        shadowRadius: StyleSheet.hairlineWidth,
        shadowOffset: {
            height: StyleSheet.hairlineWidth,
        },
        elevation: 4,
    };
}

可见在ios中下方黑线使用边框的形式实现,而安卓则是设置图层阴影。若想隐藏该线,ios中设置headerStyle的borderBottomWidth为0,安卓中设置elevation/shadowOpacity为0.
同上,可在TabBarBottom.js中180行找到tabbar上方那跟线的默认设置,更改则可在TabNavigator中的tabBarOptions的style中设置borderTopWidth和borderTopColor

18 为何需要render的组件被保存到数组中需要设置key?

追查:跟虚拟DOM和Diff算法有关。
一次DOM操作流程包括,拿到页面所有DOM节点,拿到css样式表,生成render树,布局计算节点位置,渲染等操作。 传统应用,一个操作如果需要改变10个DOM节点,则会相应的进行10次DOM操作,很多重复浪费性能。
虚拟DOM就是刚开始就将所有的DOM节点转换成js的相关代码保存到内存中,一个操作改变10次DOM节点全部在内存中完成,再将内存中的js转换为实际的DOM节点渲染,性能高。
虚拟DOM一个操作中10次改变DOM节点,每次只是改变了必要的那一个节点,不需要全部改变,为了减少时间复杂度,引入Diff算法,只比较节点改变了的那一点,进行增删改操作等。比如现在的render树是A、B、C节点,想再A节点后面插入D节点,若没有key,React无法区分各个节点,只能根据渲染树的排列依次卸载B、装载D、卸载C、装载B、装载C,效率低下。如果ABC节点都有key,则React就能根据key找出对应的节点,直接渲染A、D、B、C,效率高。

19 Diffing算法相关

在任何一个单点时刻 render() 函数的作用是创建 React 元素树。在下一个 state 或props 更新时,render() 函数将会返回一个不同的 React 元素树。 React 通过Diffing算法找出两颗元素树的差异,更新必须的部分,其假定规则是:
a、DOM 节点跨层级的移动操作特别少,可以忽略不计。
b、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
c、对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

具体的比较如下:
1、tree diff,DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。若有节点跨层级的移动,性能会受到影响
2、component diff,如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
3、element diff,当节点处于同一层级时,默认情况下,当递归一个 DOM 节点的子节点时,React 只需同时遍历所有的孩子节点并更改不同点,如在列表组件追加几个item时,性能不错。但是当如下

<ul>
  <li>1</li>
  <li>2</li>
</ul>
<ul>
  <li>3</li>
  <li>1</li>
  <li>2</li>
</ul>

React 将会改变每一个子节点而没有意识到需要保留 

  • 1和 2

  • 两个子树。这很低效。为了解决这个问题,React 支持一个 key 属性(attributes)。当子节点有了 key ,React 使用这个 key 去比较原来的树的子节点和之后树的子节点。例如,添加一个 key 到我们上面那个低效的例子中可以使树的转换变高效

    <ul>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>
    <ul>
      <li key="2014">3</li>
      <li key="2015">1</li>
      <li key="2016">2</li>
    </ul>

    现在 React 知道有'2014' key 的元素是新的, key为'2015' 和'2016'的两个元素仅仅只是被移动而已,效率变高很多。要注意key必须具备唯一性。若将数组中的索引作为 key ,如果存在重新排序时,性能将会很差,应该避免这种情况。

20 高阶组件(HOC)

高阶组件是重用组件逻辑的一项高级技术。高阶组件并不是React API的一部分。高阶组件源自于React生态。具体来说,高阶组件是一个函数,能够接受一个组件并返回一个新的组件,例如Redux的connect函数。
HOC存在的问题:
1、组件的静态方法不会被传递,需要自行传递处理
2、refs不会被传递,应该避免此,或者用自定义属性传递
3、react-native-fetch-blob的POST请求不成功。
4、js传到原生端的函数(ios中叫block)只能执行一次,否则崩溃。

21.React Native 中组件的生命周期

如图,可以把组件生命周期大致分为三个阶段:

  • 第一阶段:是组件第一次绘制阶段,如图中的上面虚线框内,在这里完成了组件的加载和初始化;
  • 第二阶段:是组件在运行和交互阶段,如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面;
  • 第三阶段:是组件卸载消亡的阶段,如图中右下角的虚线框中,这里做一些组件的清理工作。

组件的生命周期详解

1.创建阶段

  • 该阶段主要发生在创建组件类的时候,在这个阶段中会初始化组件的属性类型和默认属性。通常会将固定的内容放在这个过程中进行初始化和赋值。
  • 在 ES6 中统一使用 static 成员来实现。

1

2

3

4

static defaultProps = {

autoPlay: false``,

maxLoop: 10,

};

2,实例化阶段

该阶段主要发生在实例化组件类的时候,也就是该组件类被调用的时候触发。这个阶段会触发一系列的流程,按执行顺序如下:

  • constructor:构造函数,这里主要对组件的一些状态进行初始化。
  • componentWillMount:准备加载组件,可以在这里做一些业务初始化操作,或者设置组件状态。
  • render:生成页面需要的 DOM 结构,并返回该结构。
  • componentDidMount:组件加载成功并被成功渲染出来后执行。一般会将网络请求等加载数据的操作放在这里进行,保证不会出现 UI 上的错误。

3,运行(更新)阶段

该阶段主要发生在用户操作之后或者父组件有更新的时候,此时会根据用户的操作行为进行相应的页面结构的调整。这个阶段也会触发一系列的流程,按执行顺序如下:

  • componentWillReceiveProps:当组件接收到新的 props 时,会触发该函数。在该函数中,通常可以调用 this.setState 方法来完成对 state 的修改。
  • shouldComponentUpdate:该方法用来拦截新的 props 或 state,然后根据事先设定好的判断逻辑,做出最后要不要更新组件的决定。
  • componentWillUpdate:当上面的方法拦截返回 true 的时候,就可以在该方法中做一些更新之前的操作。
  • render:根据一系列的 diff 算法,生成需要更新的虚拟 DOM 数据。(注意:在 render 中最好只做数据和模板的组合,不应进行 state 等逻辑的修改,这样组件结构会更加清晰)
  • componentDidUpdate:该方法在组件的更新已经同步到 DOM 中去后触发,我们常在该方法中做 DOM 操作。

4,销毁阶段

该阶段主要在组件消亡的时候触发。

  • componentWillUnmount:当组件要被从界面上移除时就会调用。可以在这个函数中做一些相关的清理工作,例如取消计时器、网络请求等。

二、生命周期函数详细介绍

1,constructor

(1)函数原型

1

constructor(props)

(2)基本介绍

  • 它是组件的构造函数。它的第一个语句必须是 super(props)。
  • 构造函数将在组件被加载前最先调用,并且仅调用一次。

(3)常见用途

构造函数最大的作用,就是在这里定义状态机变量。

2,componentWillMount

(1)函数原型

1

componentWillMount()

(2)基本介绍

  • 在组件的生命周期中,这个函数只会被执行一次。
  • 这个函数无参数并且不需要任何返回值。
  • 它在初始渲染(render 函数被 React Native 框架调用执行)前被执行,当它执行完后, render 函数会马上被 React Native 框架调用执行。注意:如果在这个函数里调用 setstate 函数改变了某些状态机变量的值, React Native 框架不会执行渲染操作,而是等待这个函数执行完成后再执行初始渲染。
  • 如果子组件也有 componentWillMount 函数,它会在父组件的 componentWillMount 函数之后被调用。

(3)常见用途

如果我们需要从本地存储中读取数据用于显示,那么在这个函数里进行读取是一个很好的时机。

3,render

(1)函数原型

1

render()

(2)基本介绍

  • render 是一个组件必须有的方法,用于界面渲染。
  • 这个函数无参数,返回 JSX 或者其他组件来构成 DOM。注意:只能返回一个顶级元素。

4,componentDidMount

(1)函数原型

1

componentDidMount()

(2)基本介绍

  • 在组件的生命周期中,这个函数只会被执行一次。
  • 这个函数无参数并且不需要任何返回值。
  • 它在初始渲染执行完成后会马上被调用。在组件生命周期的这个时间点之后,开发者可以通过子组件的引用来访问、操作任何子组件。
  • 如果子组件也有 componentDidMount 函数,它会在父组件的 componentDidMount 函数之前被调用。

(3)常见用途

如果 React Native 应用需要在程序启动并显示初始界面后从网络侧获取数据,那么把从网络侧获取数据的代码放在这个函数里是一个不错的选择。

5,componentWillReceiveProps

(1)函数原型

1

componentWillReceiveProps(nextProps)

(2)基本介绍

  • 组件的初始渲染执行完成后,当组件接收到新的 props 时,这个函数将被调用。
  • 这个函数不需要返回值。接收一个 object 参数, object 里是新的 props。
  • 如果新的 props 会导致界面重新渲染,这个函数将在渲染前被执行。在这个函数中,老的 props 可以通过 this.props 访问,新的 props 在传入的 object 中。
  • 如果在这个函数中通过调用 this.setState 函数改变某些状态机变量的值, React Native 框架不会执行对这些状态机变量改变的渲染,而是等 componentWillReceiveProps 函数执行完成后一起渲染。

注意:
当 React Native 初次被渲染时,componentWillReceiveProps 函数并不会被触发,这种机制是故意设计的。

6,shouldComponentUpdate

(1)函数原型

1

boolean shouldComponentUpdate(nextProps, nextState)

(2)基本介绍

  • 组件的初始渲染执行完成后,当组件接收到新的 state 或者 props 时这个函数将被调用。
  • 该函数接收两个 object 参数,其中第一个是新的 props,第二个是新的 state。
  • 该函数需要返回一个布尔值,告诉 React Native 框架针对这次改变,是否需要重新渲染本组件。默认返回 true。如果此函数返回 false,React Native 将不会重新渲染本组件,相应的,该组件的 componentWillUpdate 和 componentDidUpdate 函数也不会被调用。

(3)常见用途

  • 这个函数常常用来阻止不必要的重新渲染,提高 React Native 应用程序性能。
  • 比如我们可以在该函数中比较新老版本的 state 和 props,判断是否需要进行重新渲染。下面是一个简单的使用样例:

1

2

3

4

shouldComponentUpdate(nextProps, nextState) {

if``(``this``.state.inputedNum.length < 3) return false``;

return true``;

}

7,componentWillUpdate

(1)函数原型

1

componentWillUpdate(nextProps, nextState)

(2)基本介绍

  • 组件的初始渲染执行完成后, React Native 框架在重新渲染该组件前会调用这个函数。
  • 该函数不需要返回值,接收两个 object 参数,其中第一个是新的 props,第二个是新的 state。
  • 我们可以在这个函数中为即将发生的重新渲染做一些准备工作,但不能在这个函数中通过 this.setState 再次改变状态机变量的值。如果需要改变,则在 componentWillReceiveProps 函数中进行改变。

8,componentDidUpdate

(1)函数原型

1

componentDidUpdate(prevProps, prevState)

(2)基本介绍

  • 组件的初始渲染执行完成后,React Native 框架在重新渲染该组件完成后会调用这个函数。
  • 该函数不需要返回值,接收两个 object 参数,其中第一个是渲染前的 props,第二个是渲染前的 state。

9,componentWillUnmount

(1)函数原型

1

componentWillUnmount()

(2)基本介绍

  • 在组件被卸载前,这个函数将被执行。
  • 这个函数没有参数,也没不需要返回值。

(3)常见用途

如果组件申请了某些资源或者订阅了某些消息,那么需要在这个函数中释放资源,取消订阅。

三、完整的样例

下面通过一个简单的文本显示组件,来演示组件各个环节的运作流程。同时这里把组件整个生命周期中所有会触发的方法都列出来了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

import React, { Component } from 'react'``;

import {

AppRegistry,

StyleSheet,

TextInput,

View,

Text,

Clipboard

} from 'react-native'``;

export default class Main extends Component {

//构造函数

constructor(props) {

super``(props);

console.log(``"constructor"``);

//初始化状态值

this``.state = {message: "欢迎访问 hangge.com"``}

}

//准备加载组件

componentWillMount() {

console.log(``"componentWillMount"``);

}

//渲染界面

render() {

console.log(``"render"``);

return (

<View style={styles.container}>

<Text style={styles.info}>

{``this``.state.message}

</Text>

</View>

);

}

//组件加载成功并渲染出来

componentDidMount() {

console.log(``"componentDidMount"``);

}

//组件接收到新的 props 时触发

componentWillReceiveProps(nextProps) {

console.log(``"componentWillReceiveProps"``);

}

//决定是否需要更新组件

shouldComponentUpdate(nextProps, nextState) {

console.log(``"shouldComponentUpdate"``);

}

//组件重新渲染前会调用

componentWillUpdate(nextProps, nextState) {

console.log(``"componentWillUpdate"``);

}

//组件重新渲染后会调用

componentDidUpdate(prevProps, prevState) {

console.log(``"componentDidUpdate"``);

}

//组件被卸载前会调用

componentWillUnmount() {

console.log(``"componentWillUnmount"``);

}

}

const styles = StyleSheet.create({

container:{

flex:1,

marginTop:40,

alignItems:``'center'``,

},

info:{

fontSize:20,

},

});

AppRegistry.registerComponent(``'HelloWorld'``, () => Main);

控制台输出信息如下(由于样例中我没有更新状态,也没有销毁组件。所以也就没有后面两个阶段): 

22.当你调用setState的时候,发生了什么事?

当调用 setState 时,React会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。
 这将启动一个称为和解(reconciliation)的过程。 和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。 
为此,React将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。
 一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )。 
通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间

23.props和state相同点和不同点

1.不管是props还是state的改变,都会引发render的重新渲染。 2.都能由自身组件的相应初始化函数设定初始值。

不同点 1.初始值来源:state的初始值来自于自身的getInitalState(constructor)函数;props来自于父组件或者自身getDefaultProps(若key相同前者可覆盖后者)。

2.修改方式:state只能在自身组件中setState,不能由父组件修改;props只能由父组件修改,不能在自身组件修改。

3.对子组件:props是一个父组件传递给子组件的数据流,这个数据流可以一直传递到子孙组件;state代表的是一个组件内部自身的状态,只能在自身组件中存在。

24.shouldComponentUpdate 应该做什么

其实这个问题也是跟reconciliation有关系。 “和解( reconciliation )的最终目标是以最有效的方式,根据新的状态更新用户界面”。 
如果我们知道我们的用户界面(UI)的某一部分不会改变, 那么没有理由让 React 很麻烦地试图去弄清楚它是否应该渲染。 
通过从 shouldComponentUpdate 返回 false, React 将假定当前组件及其所有子组件将保持与当前组件相同

25.reactJS的props.children.map函数来遍历会收到异常提示,为什么?应该如何遍历?

this.props.children 的值有三种可能:

1.当前组件没有子节点,它就是 undefined; 
2.有一个子节点,数据类型是 object ; 
3.有多个子节点,数据类型就是 array 。

系统提供React.Children.map()方法安全的遍历子节点对象

26.加载bundle的机制

要实现RN的脚本热更新,我们要搞明白RN是如何去加载脚本的。 
在编写业务逻辑的时候,我们会有许多个js文件,打包的时候RN会将这些个js文件打包成一个叫index.android.bundle(ios的是index.ios.bundle)的文件,所有的js代码(包括rn源代码、第三方库、业务逻辑的代码)都在这一个文件里,启动App时会第一时间加载bundle文件,所以脚本热更新要做的事情就是替换掉这个bundle文件。

runtime思维导图

2、多线程相关面试问题

多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径, 从技术角度来看,一个线程就是一个需要管理执行代码的内核级和应用级数据结 构组合。

多线程思维导图

3、RunLoop相关面试问题

我相信大多数开发者一样,迷惑于runloop,最初只了解可以通过runloop一些监听事件的通知来做一些事情,优化性能。关于runloop源码的基础知识,可以参考下面的思维导图:

runloop思维导图

4、设计模式相关面试问题

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

设计模式思维导图

5、架构/框架相关面试问题

架构/框架思维导图

6、算法相关面试问题

算法思维导图

7、第三方库相关面试问题

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器