iOS:程序员手册

分类(category),类扩展(extension):https://blog.csdn.net/u012946824/article/details/51799664
面试题:https://www.jianshu.com/p/980eb40d1b21
面试题:https://blog.csdn.net/hanangellove/article/details/45033453
BAT面试:https://www.cnblogs.com/fengmin/p/6101972.html

1、Runtime

Runtime详解:https://www.jianshu.com/p/6ebda3cd8052

Runtime应用:

1、关联对象(Objective-C Associated Objects)给分类增加属性
2、方法魔法(Method Swizzling)方法添加和替换和KVO实现
3、消息转发(热更新)解决Bug(JSPatch)
4、实现NSCoding的自动归档和自动解档
5、实现字典和模型的自动转换(MJExtension)

1.1、关联对象

我们都是知道分类是不能自定义属性和变量的,下面通过关联对象实现给分类动态添加属性。
关联对象Runtime提供了下面几个接口:

//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

参数解释

id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略

内存管理的策略

OBJC_ASSOCIATION_ASSIGN = 0, //关联对象的属性是弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //关联对象的属性是强引用并且关联对象不使用原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //关联对象的属性是copy并且关联对象不使用原子性
OBJC_ASSOCIATION_RETAIN = 01401, //关联对象的属性是copy并且关联对象使用原子性
OBJC_ASSOCIATION_COPY = 01403 //关联对象的属性是copy并且关联对象使用原子性

1.2、Method Swizzling

待补充,详见链接

1.3、消息转发

待补充,详见链接

1.4、实现NSCoding的自动归档和解档

原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。
核心方法:在Model的基类中重写方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

1.5、实现字典和模型的自动转换

原理描述:用runtime提供的函数遍历Model自身所有属性,如果属性在json中有对应的值,则将其赋值。
核心方法:在NSObject的分类中添加方法。

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        // 1、获取类的属性
        NSMutableArray *keys = [NSMutableArray array];
        // 获取属性集合及数量
        unsigned int outCount;
        objc_property_t *properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            // 通过property_getName函数获得属性的名字
            NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
        }
        // 释放properties指向的内存
        free(properties);
        // 2、根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;
}

2、RunLoop是什么?

Run loops 是线程相关的的基础框架的一部分。
一个 run loop 就是一个事件处理 的循环,用来不停的调度工作以及处理输入事件。
使用 run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。
Runloop还可以在loop在循环中的同时响应其他输入源,比如界面控件的按钮,手势等。

2.1、Runloop机制

一般主线程会自动运行RunLoop,我们一般情况下不会去管。在其他子线程中,如果需要我们需要去管理。使用RunLoop后,可以把线程想象成进入了一个循环;如果没有这个循环,子线程完成任务后,这个线程就结束了。所以如果需要一个线程处理各种事件而不让它结束,就需要运行RunLoop

详情链接:

https://www.jianshu.com/p/519baeebf35b
https://www.cnblogs.com/jiangzzz/p/5619512.html

2.2、autorelease

autorelease 自动释放,与之相关联的是一个自动释放池autoreleasepoolautorelease的变量会被放入自动释放池中。等到自动释放池释放时(drain)时,自动释放池中的自动释放变量会随之释放。ios系统应用程序在创建是有一个默认的autoreleasepool,程序退出时会被销毁。但是对于每一个RunLoop,系统会隐含创建一个autoreleasepool,所有的release pool会构成一个栈式结构,每一个RunLoop结束,当前栈顶的pool会被销毁。

2.2.1、autoreleasepool时候释放对象

当一个runloop在不停的循环工作,那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池,那么这两个方法来销毁要释放的对象。

3、内存区域及管理机制

3.1、内存的几大区域

栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。[先进后出]

堆区(heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。

BSS段 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放;。

常量区 存放常量字符串,程序结束后由系统释放;

代码段 存放函数的二进制代码,程序结束后由系统释放;

图片
图片

- 堆和栈的区别:

堆需要用户手动释放内存,而栈则是编译器自动释放内存

- OC中NSString的内存存储方式

@”xxx”方法生成的字符串分配在常量区,系统自动管理内存;【栈区】
initWithStringstringWithString后面接的是@”xxx”的话,创建字符串等同于直接复制字符串常量;【栈区】
initWithFormat: 和 stringWithFormat: 方法生成的字符串分配在【堆区】,并且两个的内存地址并不相同,也就是说同一段字符串在堆区中存储了两份。

3.2、内存管理机制

1、简述OC中内存管理机制?

管理机制:使用了一种叫做引用计数的机制来管理内存中的对象。OC中每个对象都对应着他们自己的引用计数,引用计数可以理解为一个整数计数器,当使用alloc方法创建对象的时候,持有计数会自动设置为1。当你向一个对象发送retain消息时,持有计数数值会增加1。相反,当你像一个对象发送release消息时,持有计数数值会减小1。当对象的持有计数变为0的时候,对象会释放自己所占用的内存,iphone os没有垃圾回收机制。

2、与retain配对使用的方法是dealloc还是release,为什么?

retain(引用计数加1)->release(引用计数减1)

3、需要与alloc配对使用的方法是dealloc还是release,为什么?

alloc(申请内存空间)->dealloc(释放内存空间)

4、readwritereadonlyassignretaincopynonatomicatomicstrongweak属性的作用?

readwrite:表示既有getter,也有setter (默认)
readonly: 表示只有getter,没有setter
assign: 简单赋值,不更改索引计数(默认)
retainrelease旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1。
copy:其实是建立了一个相同的对象,地址不同(retain:指针拷贝 copy:内容拷贝)
nonatomic:不考虑线程安全
atomic:线程操作安全(默认)
strong:(ARC下的)和(MRC)retain一样(默认)
weak:(ARC下的)和(MRC)assign一样, weak当指向的内存释放掉后自动nil化,防止野指针
autoreleasing:用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。

3.3、 内存泄漏的常见情况

3.3.1、NSTimer

NSTimer会造成循环引用,timer会强引用targetself,一般self又会持有timer作为属性,这样就造成了循环引用。那么,如果timer只作为局部变量,不把timer作为属性呢?同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timerinvalidate之前,self也就不会被释放。

所以我们要注意,不仅仅是把timer当作实例变量的时候会造成循环引用,只要申请了timer,加入了runloop,并且targetself,虽然不是循环引用,但是self却没有释放的时机。如下方式申请的定时器,self已经无法释放了。

NSTimer *timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(commentAnimation) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

解决这种问题的实现方法:增加startTimerstopTimer方法,在合适的时机去调用,比如可以在viewDidDisappearstopTimer,或者由这个类的调用者去设置。

3.3.2、__block

__block在MRC中是不会增加引用的,可是在ARC中会增加,所以在ARC中,只能使用__weak去打破循环引用。另外声明一点,并非所有的block都需要使用weak来打破循环引用,如果self没有持有block就不会造成循环引用(例如, 动画block块)。而有些地方之所以使用了__weak,是为了在[self dealloc]之后就不再执行了。在这种场景下使用weakself时,也需要注意,如果self被释放了会不会引起异常。

3.3.3、delegate循环引用问题

delegate循环引用问题比较基础,只需注意将代理属性修饰为weak即可。

3.3.4、大次数循环内存暴涨问题

for (int i = 0; i < 100000; i++) {
    NSString *string = @"Abc";
    string = [string lowercaseString];
    string = [string stringByAppendingString:@"xyz"];
    NSLog(@"%@", string);
}

该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        NSString *string = @"Abc";
        string = [string lowercaseString];
        string = [string stringByAppendingString:@"xyz"];
        NSLog(@"%@", string);
    }
}

3.3.5、非OC对象内存处理

CoreGraphics框架下的CGImageRefCGContextRef类型变量是非OC对象,其需要手动执行释放操作CGImageRelease(ref)CGContextRelease(ref)否则会造成大量的内存泄漏导致程序崩溃。其他的对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意。

3.3.6、地图类处理

若项目中使用地图相关类,一定要检测内存情况,因为地图是比较耗费App内存的,因此在根据文档实现某地图相关功能的同时,我们需要注意内存的正确释放,大体需要注意的有需在使用完毕(如:viewDidDisappear)时将地图、代理等置空为nil,注意地图中标注(大头针)的复用,并且在使用完毕时清空标注数组等。

self.mapView = nil;
self.mapView.delegate =nil;
self.mapView.showsUserLocation = NO;
[self.mapView removeAnnotations:self.annotations];
[self.mapView removeOverlays:self.overlays];
[self.mapView setCompassImage:nil];

4、如何收集用户的卡顿和崩溃信息

4.1、卡顿的原因

在iOS应用中,所有的UI操作及更新都是在主线程完成,并且主线程的runloop是逐个处理用户事件的(当然其他的runloop也一样),所以主线程必须等待上一次事件处理完成后才能继续响应下一次事件。绝大部分用户感知到的卡顿就是由于主线程阻塞了,在处理某次事件消耗了过长的时间,导致主线程处于等待状态,无法及时响应用户的下一次输入事件。由于iOS 上的 UIKit 只能在主线程进行处理,导致开发者在开发过程中不经意间在主线程做了一些消耗时间的工作,导致了应用卡顿。

4.2、避免卡顿

避免卡顿的黄金法则就是不要让主线程干重活,例如网络请求,读写大文件,复杂的运算等一些耗费大量系统资源及时间的任务。充分利用好 iOS 的多线程,如 NSThread、NSO peration Queue,GCD 等干重活,让主线程能及时迅速的响应用户事件。

4.3、Crash(崩溃)的原因

  1. 调用悬浮指针;
  2. 数组越界访问;
  3. 调用了未实现的方法;
  4. 调用的库函数版本高于本机;
  5. 返回空cell;
  6. 类释放时未remove通知,之后收到通知;
  7. 类释放时delegate未置空,之后被回调;
  8. 使用nil做初始化操作;
  9. NSRange访问越界;
  10. 对象对应关系异常;
  11. delegate先于tableview被置空,后收到关于table或者scroll的调用;
  12. 系统内存不足等。

4.4、收集卡顿和崩溃信息

4.4.1 设置捕捉异常的回调

在程序启动时加上一个异常捕获监听:NSSetUncaughtExceptionHandler (&UncaughtExceptionHandler),用来处理程序崩溃时的回调动作。将崩溃信息持久化在本地,下次程序启动时,将崩溃信息作为日志发送给开发者。

void HandleException(NSException *exception)
{
    // 异常的堆栈信息
    NSArray *stackArray = [exception callStackSymbols];
    // 出现异常的原因
    NSString *reason = [exception reason];
    // 异常名称
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
    [UncaughtExceptionHandler saveCreash:exceptionInfo];
}

void InstallUncaughtExceptionHandler(void)
{
    NSSetUncaughtExceptionHandler(&HandleException);
}

4.4.2、崩溃分析平台使用

项目中集成【友盟分析SDK: UMengAnalytics-NO-IDFA】,通过dSYMTools和崩溃的内存地址确定代码中崩溃的位置。

4.4.3 卡顿收集

1、寻找卡顿的切入点

线程的消息事件处理都是依赖于NSRunLoop来驱动,所以要知道线程正在调用什么方法,就需要从NSRunLoop来入手。NSRunLoop调用方法主要就是在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

2、量化卡顿的程度

要监控NSRunLoop的状态,需要使用到CFRunLoopObserverRef,通过它可以实时获得这些状态值的变化。需要另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手。为了让计算更精确,需要让子线程更及时的获知主线程NSRunLoop状态变化,所以dispatch_semaphore_t是个不错的选择,另外卡顿需要覆盖到多次连续小卡顿和单次长时间卡顿两种情景,所以判定条件也需要做适当优化。

3、记录卡顿的函数调用

监控到了卡顿现场,当然下一步便是记录此时的函数调用信息,此处可以使用一个第三方Crash收集组件PLCrashReporter,它不仅可以收集Crash信息也可用于实时获取各线程的调用堆栈。当检测到卡顿时,抓取堆栈信息,然后在客户端做一些过滤处理,便可以上报到服务器,通过收集一定量的卡顿数据后经过分析便能准确定位需要优化的逻辑,至此这个实时卡顿监控就实现了。(用到框架CrashReporter.framework)

示例代码下载: PerformanceMonitor.zip

5、多线程(NSThread、GCD、NSOperation)

参考链接:https://www.jianshu.com/p/2d57c72016c6

  • 进程与线程

一个程序至少有一个进程,一个进程至少有一个线程:
进程:一个程序的一次运行,在执行过程中拥有独立的内存单元,而多个线程共享一块内存
线程:线程是指进程内的一个执行单元。

区别:(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。(2) 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。(3) 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,,导致系统的开销明显大于创建或撤消线程时的开销。
举例说明 : 操作系统有多个软件在运行(QQ、office、音乐等),这些都是一个个进程,而每个进程里又有好多线程(比如QQ,可以同时聊天,发送文件等)。

  • 多线程的优点

(1)充分发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入“并行运算”状态;
(2)将耗时、轮询或者并发需求高等任务分配到其他线程执行,并由主线程负责统一更新界面会使得应用程序更加流畅,用户体验更好;
(3)当硬件处理器的数量增加,程序会运行更快,而无需做任何调整.

  • 多线程中会出现的问题

(1)临界资源:多个线程共享各种资源,然而有很多资源一次只能供一线程使用。一次仅允许一个线程使用的资源称为临界资源。
(2)临界区:访问临界资源的代码区;
(3)注意:

  • 如果有若干线程要求进入空闲的临界区,一次仅允许一个线程进入。
  • 任何时候,处于临界区内的线程不可多于一个。如已有线程进入自己的临界区,则其它所有试图进入临界区的线程必须等待。
  • 进入临界区的线程要在有限时间内退出,以便其它线程能及时进入自己的临界区。
  • 如果线程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。

(4)死锁:两个(多个)线程都要等待对方完成某个操作才能进行下一步,这时就会发生死锁。
(5)互斥锁:能够防止多线程抢夺造成的数据安全问题,但是需要消耗大量的资源
(6)原子属性:
atomic: 原子属性,为setter方法加锁,将属性以atomic的形式来声明,该属性变量就能支持互斥锁了。
nonatomic: 非原子属性,不会为setter方法加锁,声明为该属性的变量,客户端应尽量避免多线程争夺同一资源。
(7)上下文切换(Context Switch):当一个进程中有多个线程来回切换时,context switch用来记录线程执行状态。从一个线程切换到另一个线程时需要保存当前进程的状态并恢复另一个进程的状态,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。

5.1、NSThread

这是最轻量级的多线程的方法,使用起来最直观的多线程编程方法。但是因为需要自己管理线程的生命周期,线程同步,因此在实际项目中不推荐使用。使用方式如下:

// 获取当前线程
NSThread *current = [NSThread currentThread];
// 获取主线程
NSThread *main = [NSThread mainThread];

// 阻塞线程3秒
[NSThread sleepForTimeInterval:3];
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];

5.2、GCD(Grand Central Dispatch)

GCD是基于C语言底层API实现的一套多线程并发机制,非常的灵活方便,在实际的开发中使用很广泛。简单来说CGD就是把操作放在队列中去执行,只需定义好操作和队列就可以了,不需要直接控制线程的创建和销毁,线程的生命周期由队列来管理。

5.2.1、线程与队列

队列:负责操作的调度和执行,有先进先出(FIFO)的特点,也就是说先加入队列的操作先执行,后加入的后执行。

队列有两种:
1、串行队列:队列中的操作只会按顺序执行,你可以想象成单窗口排队。
2、并行队列:队列中的操作可能会并发执行,这取决与操作的类型,你可以想象成多窗口排队。

GCD中的队列类型:
- The main queue(主线程串行队列):与主线程功能相同,提交至Main queue的任务会在主线程中执行;
- Global queue(全局并发队列):全局并发队列由整个进程共享,有高、中(默认)、低、后台四个优先级别。
- Custom queue (自定义队列):可以为串行,也可以为并发。

//创建串行队列
dispatch_queue_t q = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
//创建并行队列
dispatch_queue_t q = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);

my_serial_queuemy_concurrent_queue是队列的名字标签,为了与其他的队列区分,在一个项目里面必须是唯一的。
DISPATCH_QUEUE_SERIAL表示串行队列;
DISPATCH_QUEUE_CONCURRENT表示并行队列;

操作同样也分两种类型:
1、同步操作:只会按顺序执行,执行顺序是确定的。
2、异步操作:在串行队列中执行顺序确定,在并行队列中执行顺序不确定。

使用block来定义操作要执行的代码,queue是已经定义好的队列,操作要加入的队列:

//定义同步操作
dispatch_sync(queue, ^{
    //要执行的代码   
});
//定义异步操作
dispatch_async(queue, ^{
    //要执行的代码     
});

同步、异步操作加入到串行和并行队列里面,执行的顺序和特点:

1、同步操作、串行和并行队列

同步操作不管加入到何种队列,只会在主线程按顺序执行;

// 串行队列
dispatch_queue_t q_serial = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t q_concurrent = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
// 同步操作
dispatch_sync(q_serial, ^{
    NSLog(@"串行队列>输出:%@", [NSThread currentThread]);
});
dispatch_sync(q_concurrent, ^{
    NSLog(@"并行队列>输出:%@", [NSThread currentThread]);
});

控制台输出

串行队列>输出:<NSThread: x7ff833505450>{number = 1, name = main} 
并行队列>输出:<NSThread: x7ff833505450>{number = 1, name = main} 

2、异步操作、串行队列

异步操作只在非主线程的线程执行,在串行队列中异步操作会在新建的线程中按顺序执行。因为是异步操作,所以会新建一个线程。又因为加入到串行队列中,所以所有的操作只会按顺序执行。

dispatch_queue_t q_serial = dispatch_queue_create("my_serial_queue", DISPATCH_QUEUE_SERIAL);
    for(int i = 0; i < 5; ++i){
        dispatch_async(q_serial, ^{
            NSLog(@"串行队列>输出%d: %@ ", i,[NSThread currentThread]);
        });
    }

控制台输出

串行队列>输出0:<NSThread: x7fb593d42f50>{number = 2, name = (null)} 
串行队列>输出1:<NSThread: x7fb593d42f50>{number = 2, name = (null)} 
串行队列>输出2:<NSThread: x7fb593d42f50>{number = 2, name = (null)} 
串行队列>输出3:<NSThread: x7fb593d42f50>{number = 2, name = (null)} 
串行队列>输出4:<NSThread: x7fb593d42f50>{number = 2, name = (null)} 

3、异步操作、并行队列

并行队列会给每一个异步操作新建线程,然后让所有的任务并发执行,完成顺序不定。

dispatch_queue_t q_concurrent = dispatch_queue_create("my_concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
    for(int i = 0; i < 5; ++i){
        dispatch_async(q_concurrent, ^{
            NSLog(@"并行队列 -- 异步任务 %@ %d", [NSThread currentThread], i);
        });
    }

5.2.2、GCD 栅栏方法:dispatch_barrier_async

我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
dispatch_barrier_async函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 任务1
    });
    dispatch_async(queue, ^{
        // 任务2
    });
    dispatch_barrier_async(queue, ^{
        // 任务 barrier
    });
    dispatch_async(queue, ^{
        // 任务3
    });
    dispatch_async(queue, ^{
        // 任务4
    });
}

5.2.3、 GCD 延时执行方法:dispatch_after

我们经常会遇到这样的需求:在指定时间(例如3秒)之后执行某个任务。可以用 GCDdispatch_after函数来实现。
需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

5.2.4、GCD 一次性代码:dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCDdispatch_once 函数。使用dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

5.2.5、GCD 快速迭代方法:dispatch_apply

通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_applydispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。

我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    NSLog(@"apply---begin");
    dispatch_apply(6, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}

因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是apply---end一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。

5.2.6、GCD 队列组:dispatch_group

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合 来实现
dispatch_group_async
调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。

1、dispatch_group_notify:监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
2、 dispatch_group_wait:暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
3、dispatch_group_enterdispatch_group_leave

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1;
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。
  • group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。

dispatch_group_enterdispatch_group_leave相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务。这里的dispatch_group_enterdispatch_group_leave组合,其实等同于dispatch_group_async

多线程 GCD实现线程池

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
 dispatch_group_t group = dispatch_group_create();
 for (id str in strings) {
    dispatch_group_async(group, queue, ^{
        NSAttributedString *result = [MLExpressionManager expressionAttributedStringWithString:str expression:expression];
            @synchronized(results){
                results[str] = result;
            }
        });
 }
 dispatch_group_notify(group, queue, ^{
    //重新排列
    NSMutableArray *resultArr = [NSMutableArray arrayWithCapacity:results.count];
    for (id str in strings) {
        [resultArr addObject:results[str]];
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        if (callback) {
           callback(resultArr);
        }
    });
});

5.2.7、GCD 信号量:dispatch_semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。

Dispatch Semaphore 提供了三个函数:

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意!!!:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务;
  • 保证线程安全,为线程加锁;

5.2.7.1、Dispatch Semaphore 线程同步

线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworkingAFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return tasks;
}

下面,我们来利用 Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务。

/** semaphore 线程同步 */
- (void)semaphoreSync {
    NSLog(@"输出当前线程1:%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"输出:semaphore---begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"输出当前线程2:%@",[NSThread currentThread]);      // 打印当前线程
        number = 100;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"输出:semaphore---end,number = %zd",number);
}

控制台输出:

输出当前线程1:<NSThread: 0x60400006bc80>{number = 1, name = main}
输出:semaphore---begin
输出当前线程2:<NSThread: 0x600000272300>{number = 3, name = (null)}
输出:semaphore---end,number = 100

Dispatch Semaphore 实现线程同步的代码可以看到:

semaphore---end 是在执行完 number = 100; 之后才打印的。而且输出结果 number 为 100。
这是因为异步执行不会做任何等待,可以继续执行任务。异步执行将任务1追加到队列之后,不做等待,接着执行dispatch_semaphore_wait方法。此时 semaphore == 0,当前线程进入等待状态。然后,异步任务1开始执行。任务1执行到dispatch_semaphore_signal之后,总信号量,此时 semaphore == 1dispatch_semaphore_wait方法使总信号量减1,正在被阻塞的线程(主线程)恢复继续执行。最后打印semaphore---end,number = 100。这样就实现了线程同步,将异步执行任务转换为同步执行任务。

5.2.7.2、Dispatch Semaphore 线程安全(为线程加锁)

线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 100; i++) {
     dispatch_async(queue, ^{
          // 信号量为1>>wait加锁
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
          // 信号量-1,等于0>>阻塞
          NSLog(@"i = %zd semaphore = %@", i, semaphore);
          // 信号量+1>>解锁
          dispatch_semaphore_signal(semaphore);
      });
}

注释:当线程1执行到dispatch_semaphore_wait这一行时,semaphore的信号量为1,所以使信号量-1变为0,并且线程1继续往下执行;如果当在线程1 NSLog这一行代码还没执行完的时候,又有线程2来访问,执行dispatch_semaphore_wait时由于此时信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会一直阻塞线程2(此时线程2处于等待状态),直到线程1执行完NSLog并执行完dispatch_semaphore_signal使信号量为1后,线程2才能解除阻塞继续住下执行。以上可以保证同时只有一个线程执行NSLog这一行代码。

5.2.7.3、使用 Dispatch Semaphore 控制并发线程数量

有点像[NSOperationQueue maxConcurrentOperationCount]。 在能保证灵活性的情况下,通常更好的做法是使用操作队列,而不是通过GCD和信号量来构建自己的解决方案。

void dispatch_async_limit(dispatch_queue_t queue,NSUInteger limitSemaphoreCount, dispatch_block_t block) {
    // 控制并发数的信号量
    static dispatch_semaphore_t limitSemaphore;
    // 专门控制并发等待的线程
    static dispatch_queue_t receiverQueue;
    // 使用 dispatch_once而非 lazy 模式,防止可能的多线程抢占问题
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        limitSemaphore = dispatch_semaphore_create(limitSemaphoreCount);
        receiverQueue = dispatch_queue_create("receiver", DISPATCH_QUEUE_SERIAL);
    });
    // 如不加 receiverQueue 放在主线程会阻塞主线程
    dispatch_async(receiverQueue, ^{
        // 可用信号量后才能继续,否则等待
        dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            !block ? : block();
            // 在该工作线程执行完成后释放信号量
            dispatch_semaphore_signal(limitSemaphore);
        });
    });
}

5.3、NSOperation & NSOperationQueue

虽然GCD的功能已经很强大了,但是它使用的API依然是C语言的。在某些时候,在面向对象的objective-c中使用起来非常的不方便和不安全。所以苹果公司把GCD中的操作抽象成NSOperation对象,把队列抽象成NSOperationQueue对象。

抽象为NSOperation & NSOperationQueue以后的好处有一下几点:

  • 代码风格统一了,我们不用在面向对象的objective-C中写面对过程的C语言代码了。
  • 我们知道在GCD中操作的执行代码都是写在匿名的block里面,那么我们很难做到给操作设置依赖关系以及取消操作。这些功能都已经封装到NSOperation对象里面了。
  • NSOperationQueue对象比GCD中队列更加的强大和灵活,比如:设置并发操作数量,取消队列中所有操作。

NSOperation分为NSInvocationOperationNSBlockOperation
1、NSInvocationOperation的使用

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction) object:nil];
[queue addOperation:op];// 把操作加入队列中即开始执行

2、NSBlockOperation的使用

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    [self operationAction];
}];
[queue addOperation:op];// 把操作加入队列中即开始执行

3、设置依赖关系(执行顺序)

NSOperation & NSOperationQueue中,我们不需要再像GCD那样定义操作的类型和队列的类型和控制操作的执行顺序了,你只需要直接设定操作的执行顺序就可以了。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@"op1"];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationAction:) object:@"op2"];
// op2在op1之后执行
[op2 addDependency:op1];//这里需要注意,一定要在addOperation之前设置依赖关系
[queue addOperation:op1];
[queue addOperation:op2];

5.4、多线程如何按设定顺序去执行任务(面试题)

1、线程依赖关系通过使用系统对GCD的进一步封装的类NSBlockOperation来实现;

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
}];
NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
}];
NSBlockOperation * operation3 = [NSBlockOperation blockOperationWithBlock:^{
}];
// 操作1执行完后,才能执行操作2
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
[queue addOperations:@[operation1,operation2,operation3] waitUntilFinished:NO];

2、NSOperationQueue串行队列依次执行,maxConcurrentOperationCount为1

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
 }];
NSInvocationOperation *operationB = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testInvocaionOperatation) object:nil];
[queue addOperation:operationA];
[queue addOperation:operationB];

3、GCD队列组(线程池)

// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create(0, 0);
// 使用函数添加任务(所有任务都是并发执行)
dispatch_group_enter(group);
// 任务A
dispatch_async(queue, ^{
        // 请求A
        if (success) {// 请求成功
            dispatch_group_leave(group);
        } else { // 失败
            dispatch_group_leave(group);
        }
 });
//   任务B
dispatch_group_enter(group);
dispatch_async(queue, ^{
      // 请求B
      if (success) { // 请求成功
          dispatch_group_leave(group);
      } else { // 失败
          dispatch_group_leave(group);
      }
 });
 // A,B执行完毕,不论成功失败。只要执行完毕就执行下方代码
 dispatch_group_notify(group, queue, ^{
        // 执行C操作。注意刷新UI等需要回到主线程。
      dispatch_async(dispatch_get_main_queue(), ^{
            // 刷新等操作。
      });
  });

4、GCD信号量

信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。
信号量为0则阻塞线程,大于0则不会阻塞。因此我们可以通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。
在GCD中有三个函数是semaphore的操作,分别是:
  dispatch_semaphore_create   创建一个semaphore
  dispatch_semaphore_signal   发送一个信号
  dispatch_semaphore_wait    等待信号
  简单的介绍一下这三个函数,第一个函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。
  

6、锁机制

锁机制在大多数编程语言中都是很常用的线程安全机制,你可以在关键的代码前后,或者只希望同时只能被一个线程执行的任务前后加上线程锁来避免因为多线程给程序造成不可预知的问题。

6.1、互斥锁

互斥锁扮演的角色就是代码或者说任务的栅栏,它将你希望保护的代码片段围起来,当其他线程也试图执行这段代码时会被互斥锁阻塞,直到互斥锁被释放,如果多个线程同时竞争一个互斥锁,有且只有一个线程可以获得互斥锁。

1、pthread_mutex (C语言);
2、NSLockNSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK

NSLockCocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lockunlock方法外,NSLock还提供了tryLocklockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

NSLock *lock = [NSLock new];
[lock lock];
//需要执行的代码
[lock unlock];

3、NSCondition:一种最基本的条件锁。手动控制线程waitsignalNSCondition封装了一个互斥锁和条件变量。互斥锁保证线程安全,条件变量保证执行顺序。

[condition lock]:一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock外等待,只到unlock ,才可访问;
[condition unlock]:与lock 同时使用;
[condition wait]:让当前线程处于等待状态;
[condition signal]:CPU发信号告诉线程不用在等待,可以继续执行。

NSCondition *lock = [NSCondition new];
[lock lock];
//需要执行的代码
[lock unlock];

4、NSConditionLockNSConditionLock借助 NSCondition 来实现,本质是生产者-消费者模型。

NSConditionLock *lock = [NSConditionLock new];
[lock lock];
//需要执行的代码
[lock unlock];

5、GCD信号量:详见本文 5.2.7.2、Dispatch Semaphore 线程安全(为线程加锁)

6.2、递归锁

递归锁是互斥锁的变种。它允许一个线程在已经拥有一个锁,并且没有释放的前提下再次获得锁。当该线程释放锁时也需要一个一个释放。

1、pthread_mutex(recursive)pthread_mutex(c语言)锁的一种,属于递归锁。一般一个线程只能申请一把锁,但是,如果是递归锁,则可以申请很多把锁,只要上锁和解锁的操作数量就不会报错。
2、NSRecursiveLock:递归锁,pthread_mutex(recursive)的封装,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

6.3、自旋锁

OSSpinLock >> 自旋锁与互斥锁有点类似,但不同的是其他线程不会被自旋锁阻塞,而是而是在进程中空转,就是执行一个空的循环。一般用于自旋锁被持有时间较短的情况。

自旋锁的实现原理比较简单,就是死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。如果a线程在临界区的执行时间过长,则b线程会消耗大量的cpu时间,不太划算。所以,自旋锁用在临界区执行时间比较短的环境性能会很高。

6.4、部分说明:

自旋锁和互斥锁

相同点:都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。

不同点:

互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。

自旋锁的效率高于互斥锁。

两种锁的加锁原理:

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换(主动出让时间片,线程休眠,等待下一次唤醒),cpu的抢占,信号的发送等开销。

自旋锁:线程一直是running(加锁——>解锁),死循环(忙等 do-while)检测锁的标志位,机制不复杂。

锁的具体关键字:

  1. @synchronized 关键字加锁
  2. NSLock 对象锁
  3. NSCondition
  4. NSConditionLock 条件锁
  5. NSRecursiveLock 递归锁
  6. pthread_mutex 互斥锁(C语言)
  7. dispatch_semaphore 信号量实现加锁(GCD)
  8. OSSpinLock 自旋锁

1、使用@synchronized关键字:在Objective-C中,我们会经常使用@synchronized关键字来修饰变量,确保变量的线程安全,它能自动为修饰的变量创建互斥锁或解锁。@synchronized:一个对象层面的锁,锁住了整个对象,底层使用了互斥递归锁来实现。@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁

2、synchronized block[_lock lock][_lock unlock] 效果相同。你可以把它当成是锁住 self,仿佛 self 就是个 NSLock。锁在左括号 { 后面的任何代码运行之前被获取到,在右括号 } 后面的任何代码运行之前被释放掉,再也不用担心忘记调用 unlock 了!

参考链接:https://www.jianshu.com/p/938d68ed832c

7、http协议和https协议

7.1、http和https的区别

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。
即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 它是一个URI scheme(抽象标识符体系),句法类同http:体系。用于安全的HTTP数据传输。

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

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

7.2、https流程

1、客户端向服务器发起请求。
2、服务器响应到请求,同时把服务器的证书发给客户端。
3、客户端接收到证书,然后和客户端中的证书对比,如果证书不一致或者无效,那么断开连接。如果通过,那么进行第四部。
4、用户产生一个随机密钥,然后经服务器证书中的公钥进行加密,传给服务端。
5、服务端拿到加密数据和加密密钥,用服务器的私钥解开密钥,得到对称密钥key。
6、服务端和客户端互相通讯指定这个密钥为加密密钥。握手结束
7、客户端和服务端开始通讯,通讯数据由对称密钥加密。

7.3、ATS

1)iOS9中新增App Transport Security(简称ATS)特性, 让原来请求时候用到的HTTP,全部都转向TLS1.2协议进行传输。
2)这意味着所有的HTTP协议都强制使用了HTTPS协议进行传输。
3)如果我们在iOS9下直接进行HTTP请求是会报错。系统会告诉我们不能直接使用HTTP进行请求,需要在Info.plist中控制ATS的配置。
"NSAppTransportSecurity"是ATS配置的根节点,配置了节点表示告诉系统要走自定义的ATS设置。
"NSAllowsAritraryLoads"节点控制是否禁用ATS特性,设置YES就是禁用ATS功能。
4)有两种解决方法,一种是修改配置信息继续使用以前的设置。
另一种解决方法是所有的请求都基于基于”TLS 1.2”版本协议。(该方法需要严格遵守官方的规定,如选用的加密算法、证书等)

7.4、AFSecurityPolicy

AFSecurityPolicy,内部有三个重要的属性,如下:

AFSSLPinningMode SSLPinningMode; //该属性标明了AFSecurityPolicy是以何种方式来验证
BOOL allowInvalidCertificates; //是否允许不信任的证书通过验证,默认为NO
BOOL validatesDomainName; //是否验证主机名,默认为YES

AFSSLPinningMode枚举类型有三个值:
AFSSLPinningModeNone
AFSSLPinningModePublicKey
AFSSLPinningModeCertificate

AFSSLPinningModeNone代表了AFSecurityPolicy不做更严格的验证,只要是系统信任的证书就可以通过验证,不过,它受到allowInvalidCertificatesvalidatesDomainName的影响;

AFSSLPinningModePublicKey是通过”比较证书当中公钥(PublicKey)部分”来进行验证,通过SecTrustCopyPublicKey方法获取本地证书和服务器证书,然后进行比较,如果有一个相同,则通过验证,此方式主要适用于自建证书搭建的HTTPS服务器和需要较高安全要求的验证;

AFSSLPinningModeCertificate则是直接将本地的证书设置为信任的根证书,然后来进行判断,并且比较本地证书的内容和服务器证书内容是否相同,来进行二次判断,此方式适用于较高安全要求的验证。

如果HTTPS服务器满足ATS默认的条件,而且SSL证书是通过权威的CA机构认证过的,那么什么都不用做。如果上面的条件中有任何一个不成立,那么都只能修改ATS配置。

8、FMDB的线程安全

FMDB使用databaseQueue实现数据库操作线程安全,FMDatabase不能多线程使用一个实例多线程访问数据库,不能使用同一个FMDatabase的实例。否则会发生异常。如果线程使用单独的FMDatabase 实例是允许的,但是同样有可能发生database is locked的问题。这是由于多线程对sqlite的竞争引起的。
FMDatabaseQueue 解决这个问题的思路是:创建一个队列(串行线程队列),然后将放入的队列的block顺序执行,这样避免了多线程同时访问数据库。FMDatabaseQueue要使用单例创建,这样多线程调用时,数据库操作使用一个队列,保证线程安全。
 

9、网络传输的数据安全性

9.1、保证API的调用者是经过授权的App

解决方案:采用设计签名的方式。对每个客户端,Android、iOS、WeChat,分别分配一个AppKeyAppSecret。需要调用API时,将AppKey加入请求参数列表,并将AppSecret和所有参数一起,根据某种签名算法生成一个签名字符串,然后调用API时把该签名字符串也一起带上。服务端收到请求之后,根据请求中的AppKey查询相应的AppSecret,按照同样的签名算法,也生成一个签名字符串,当服务端生成的签名和请求带过来的签名一致的时候,那就表示这个请求的调用者是经过自己授权的,证明这个请求是安全的。而且,每个端都有一个Key,也方便不同端的标识和统计。为了防止AppSecret被别人获取,这个AppSecret一般写死在代码里面。另外,签名算法也需要有一定的复杂度,不能轻易被别人破解,最好是采用自己规定的一套签名算法,而不是采用外部公开的签名算法。另外,在参数列表中再加入一个时间戳,还可以防止部分重放攻击。

9.2、保证数据传输的安全

主要就是采用HTTPS了。HTTPS因为添加了SSL安全协议,自动对请求数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,主要就是防止中间人攻击。苹果从iOS9开始,默认就采用HTTPS了。为了安全考虑,建议对SSL证书进行强校验,包括签名CA是否合法、域名是否匹配、是不是自签名证书、证书是否过期等。

9.3、采用密文传输

  • MD5加密

加盐(Salt):在明文的固定位置插入随机串,然后再进行MD5
先加密,后乱序:先对明文进行MD5,然后对加密得到的MD5串的字符进行乱序

  • 钥匙串(KeyChain)加密

用原生的Security.framework 就可以实现钥匙串的访问、读写。但是只能在真机上进行。 通常我们使用KeychainItemWrapper来完成钥匙串的加密。

步骤:创建钥匙串对象>>存储加密对象>>存入到钥匙串里面>>获取钥匙串的数据

  • 公钥加密

公钥加密也叫非对称加密,iOS中用的最多的是RSA,iOS使用RSA加密, 只需要公钥。

公钥(public key): 用于加密数据. 用于公开, 一般存放在数据提供方, 例如iOS客户端.
私钥(private key): 用于解密数据. 必须保密, 私钥泄露会造成安全问题. 私钥解密的字符串需要由JAVA后台提供;

iOS中的Security.framework提供了对RSA算法的支持.这种方式需要对密匙对进行处理, 根据public key生成证书, 通过private key生成p12格式的密匙.

10、KVC and KVO

KVC(key-value-coding)键值编码,是一种间接操作对象属性的一种机制,可以给属性设置值。通过setValue:forKey:valueForKey:,实现对属性的存取和访问。

KVO(key-value-observing)键值观察,是一种使用观察者模式来观察属性的变化以便通知注册的观察者。通过注册observing对象addObserver:forKeyPath:options:context:和观察者类必须重写方法 observeValueForKeyPath:ofObject:change:context:

KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:didChangeValueForKey:。在存取数值的前后分别调用 2 个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey:被调用,通知系统该keyPath 的属性值已经变更,之后, observeValueForKey:ofObject:change:context:也会被调用。

11、ViewController 的生命周期

当一个视图控制器被创建,并在屏幕上显示的时候。 代码的执行顺序
1、 alloc 创建对象,分配空间
2、init (initWithNibName) 初始化对象,初始化数据
3、loadView 从nib载入视图 ,通常这一步不需要去干涉。除非你没有使用xib文件创建视图
4、viewDidLoad 载入完成,可以进行自定义数据以及动态创建其他控件
5、viewWillAppear 视图将出现在屏幕之前,马上这个视图就会被展现在屏幕上了
6、viewDidAppear 视图已在屏幕上渲染完成

当一个视图被移除屏幕并且销毁的时候的执行顺序,这个顺序差不多和上面的相反
1、viewWillDisappear 视图将被从屏幕上移除之前执行
2、viewDidDisappear 视图已经被从屏幕上移除,用户看不到这个视图了
3、dealloc 视图被销毁,此处需要对你在initviewDidLoad中创建的对象进行释放

ViewControlleralloc,loadView, viewDidLoad,viewWillAppear,viewDidUnload,deallocinit分别是在什么时候调用的?在自定义ViewController的时候这几个函数里面应该做什么工作?

alloc //申请内存时调用
loadView //加载视图时调用
ViewDidLoad //视图已经加载后调用
ViewWillAppear //视图将要出现时调用
ViewDidUnload //视图已经加载但没有加载出来调用
dealloc //销毁该视图时调用
init //视图初始化时调用

12、定时器Timer

iOS中的Timer可以通过三种方式来实现:NSTimerdispatchCADisplayLink,其执行的精确度依次提高。下面介绍一下各自的使用方式。

12.1、NSTimer

NSTimer是OC以面向对象方式封装的Timer对象,从其类文档中可以看到它的两种创建方式:timerscheduledTimer

timerWithTimeInterval创建的timer, 在fire后唤醒;
scheduledTimerWithTimeInterval创建的timer, 创建后立即唤醒;
timerinvalidate后停止。

UIScrollView滑动会暂停计时,添加到NSDefaultRunLoopModetimerUIScrollView滑动时会暂停,若不想被UIScrollView滑动影响,需要将 timer 添加再到 UITrackingRunLoopMode 或直接添加到NSRunLoopCommonModes 中。

12.2、dispatch

利用多线程GCD创建的Timer,精确度更高,也可以通过参数设置Timer首次执行时间。

CADisplayLink是和iOS界面刷新效率同步执行,可以在1s内执行60次,执行效率最高。如果屏幕滑动时卡顿,可以用它来检测屏幕屏幕刷新频率。当然,不能在其执行方法中加载大量任务,否则手机内存会急剧增高。

12.4、总结

NSTimer 使用简单方便,但是应用条件有限,CADisplayLink 刷新频率与屏幕帧数相同,用于绘制动画,GCD定时器 精度高,可控性强,使用稍复杂。

13、APP 架构分层

一个App的核心就是数据,那么,从App对数据处理的角色划分出发,最简单的划分就是:数据管理、数据加工、数据展示。相应的也就有了三层架构:数据层、业务层、展示层。数据层是三层中的最底层,往下,它接入API;往上,它向业务层交付数据。业务层夹在三层中间,属于数据的加工厂,将数据层提供上来的数据加工成展示层需要展示的数据。展示层处于三层中的最上层,主要就是将从业务层取得的数据展示到界面上。

https://blog.csdn.net/skykingf/article/details/50971392

14、NSURLSession

NSURLSession在iOS7时就推出了,为了取代NSURLConnection,在iOS9时NSURLConnection被废弃了,包括SDWebImage和AFNetworking3也全面使用NSURLSession作为基础的网络请求类了。

Foundation框架提供了三种NSURLSession的运行模式,即三种NSURLSessionConfiguration会话配置,defaultSessionConfiguration默认Session运行模式,使用该配置默认使用磁盘缓存网络请求相关数据如cookie等信息。ephemeralSessionConfiguration临时Session运行模式,不缓存网络请求的相关数据到磁盘,只会放到内存中使用。backgroundSessionConfiguration后台Session运行模式,如果需要实现在后台继续下载或上传文件时需要使用该会话配置,需要配置一个唯一的字符串作为区分。同时,NSURLSessionConfiguration还可以配置一些其他信息,如缓存策略、超时时间、是否允许蜂窝网络访问等信息。

图片

NSURLSessionTask类似抽象类不提供网络请求的功能,具体实现由其子类实现,例如:NSURLSessionDataTask用来获取一些简短的数据,如发起GET/POST请求,NSURLSessionDownloadTask用于下载文件,它提供了很多功能,默认支持将文件直接下载至磁盘沙盒中,就可以避免占用过多内存的问题,NSURLSessionUploadTask用于上传文件,NSURLSessionStreamTask提供了以流的形式读写TCP/IP流的功能,可以实现异步读写的功能。

NSURLSession相关的类也提供了丰富的代理来监听具体请求的状态,相关代理协议的类图如下所示:

图片

15、浅拷贝、深拷贝

15.1、定义及实现拷贝

浅拷贝:就是对内存地址的复制,让目标对象指针和源对象指针指向同一片内存空间。当内存销毁时,指向该内存的其他指针需重新指向,否则将成为野指针。

深拷贝:就是拷贝地址中的内容,让目标对象产生新的内存区域,且将源内存区域中的内容复制到目标内存区域中。深拷贝就是产生一个新的对象,将源对象的所有内容拷贝到新的对象中,新对象和源对象各自指向自己的内存区域,相互之间不受影响。

在开发过程中,大体上会区分为对象和容器两个概念,对象的copy是浅拷贝,mutablecopy是深拷贝。容器(内包含对象)的拷贝,无论是copy,还是mutablecopy都是浅拷贝,要想实现对象的深拷贝,必须自己提供拷贝方法。

  • 非容器不可变对象:NSString

1、对于非容器不可变对象的copy为浅拷贝,mutableCopy为深拷贝;
2、浅拷贝获得的对象地址和原对象地址一致, 返回的对象为不可变对象;
3、深拷贝返回新的内存地址,返回对象为可变对象;

  • 非容器可变对象: NSMutableString

1、对于非容器可变对象的copy为深拷贝;
2、mutableCopy为深拷贝;
3、并且copy和mutableCopy返回对象都为可变对象;

  • 容器类不可变对象: NSArray

容器类不可变对象mutableCopy和copy都返回一个新的容器,但容器内的元素仍然是浅拷贝;

  • 容器类可变对象: NSMutableArray

容器类可变对象mutableCopy和copy都返回一个新的容器,但容器内的元素仍然是浅拷贝;

  • 自定义类对象的深浅拷贝

在OC中不是所有的类都支持拷贝,只有遵循<NSCopying>才支持copy,只有遵循才支持mutableCopy。如果没有遵循,拷贝时会直接Crash。

#import <Foundation/Foundation.h>

@interface Person : NSObject <NSCopying, NSMutableCopying>

@property (nonatomic, copy) NSString *name;

@end


#import "Person.h"

@implementation Person

- (id)copyWithZone:(NSZone *)zone {
    Person *person = [Person allocWithZone:zone];
    person.name = self.name;
    return person;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Person *person = [Person allocWithZone:zone];
    person.name = self.name;
    return person;
}

@end
  • 实现容器对象的完全拷贝(通过归档解档的方式)
// 归档
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray];
// 解档>>获取新的容器,容器内为深拷贝
NSMutableArray *newMutableArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];

15.2、property中的copy属性

@property (nonatomic,copy) NSString *name;

如果用strong修饰,那么外部的值变化了,里面的值也会变化,这是因为指向的是同一个内存地址;如果用copy修饰,那么外部的值变化了,里面的值也不会变化,因为对对象的内存做了深度拷贝,复制了一份内存,指针的指向已经变化了。

16、数据存储

16.1、关于沙盒机制。

和Android系统不同的是,iOS系统使用的是特有的数据安全策略:沙盒机制。所谓沙盒机制是指:系统会为每个APP分配一块独立的存储空间,用于存储图像,图标,声音,映像,属性列表,文本等文件,并且在默认情况下每个APP只能访问自己的空间。

Documents, Library, tmp:

Documents: 用于保存应用运行时生成的需要持久化、非常大的或者需要频繁更新的数据,iTunes会自动备份该目录。

NSArray *directory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docPath = [directory lastObject];

Libaray: 用于存储程序的默认设置和其他状态信息,iTunes会自动备份该目录。Libaray/下主要有两个文件夹:Libaray/CachesLibaray/Preferences

NSArray *directory = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSString *libraryPath = [directory lastObject];

Libaray/Caches:存放缓存文件,iTunes不会备份此目录,此目录下文件不会在应用退出删除,一般存放体积比较大,不是很重要的资源。

NSArray *directory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
NSString *cachesPath = [directory lastObject];

Libaray/Preferences:保存应用的所有偏好设置,ios的Settings(设置)应用会在该目录中查找应用的设置信息,iTunes会自动备份该目录。

NSArray *directory = NSSearchPathForDirectoriesInDomains(NSPreferencesDirectory,NSUserDomainMask,YES);
NSString *preferencesPath = [directory lastObject];

tmp: 保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除,应用没有运行时,系统也可能会自动清理该目录下的文件,iTunes不会同步该目录,iPhone重启时该目录下的文件会丢失。

NSString *tmpPath = NSTemporaryDirectory();

16.2、常见的存储方式

常见的有四种存储方式:用NSUserDefaults存储配置信息、用NSKeyedArchive归档的形式来保存对象数据、文件沙盒存储 、Core Data、sqlit数据库存储 。

16.2.1、用NSUserDefaults存储配置信息

NSUserDefaults用来存储设备和应用的配置、属性、用户的信息,它通过一个工厂方法返回默认的实例对象。它实际上是存储于文件沙盒中的一个.plist文件。该文件的可以存储的数据类型包括:NSDataNSStringNSNumberNSDateNSArrayNSDictionary。如果要存储其他类型,则需要转换为前面的类型,才能用NSUserDefaults存储。

使用方式:

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// 存储
[userDefaults setObject:@"Jeanne" forKey:@"name"];
// 获取
NSString *userName = [userDefaults objectForKey:@"name"];

16.2.2、文件沙盒存储

主要存储非机密数据,大的数据,如数据库、资源文件:视频、音频、图片等。

存储(以图片为例):

// 获取文件夹路径
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,  NSUserDomainMask,YES);
NSString *docPath = [documentPaths objectAtIndex:0];
// 生成文件路径
NSString *fileName = @"20180808173223.jpg";
NSString *filePath =[docPath stringByAppendingPathComponent:fileName];
// 写入文件
[imageData writeToFile:filePath atomically:YES];

获取(以图片为例):

// filePath获取见上文
NSData *imageData = [NSData dataWithContentsOfFile:filePath options:0 error:NULL];
// 转image >> 略

16.2.3、NSKeyedArchiver归档存储

在带健的档案中,会为每个归档对象提供一个名称,即健(key)。根据这个key可以从归档中检索该对象。这样,就可以按照任意顺序将对象写入归档并进行检索。另外,如果向类中添加了新的实例变量或删除了实例变量,程序也可以进行处理。

NSKeyedArchiver存储在硬盘上的数据是二进制格式:

图片

注意:默认情况下,只能对NSDate, NSNumber, NSString, NSArray, or NSDictionary来进行归档。如果需要归档对象,需要对自己定义的对象通过NSCoding协议进行“编码/解码”。

NSCoding协议的方法:

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;

首先,创建自定义对象Person,并在Person.h中申明实现NSCoding协议。

@interface Person : NSObject<NSCopying,NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic ,copy) NSString *address;
@property (nonatomic, copy) NSString *telephone;

在Person.m中,实现NSCoding协议的编码/解码方法:

#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeObject:_address forKey:@"address"];
    [aCoder encodeObject:_telephone forKey:@"telephone"];
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    _name = [aDecoder decodeObjectForKey:@"name"];
    _address = [aDecoder decodeObjectForKey:@"address"];
    _telephone = [aDecoder decodeInt32ForKey:@"telephone"];
    return self;
 }

这样,我们就能够归档自己定义的类对象。

// 归档
[NSKeyedArchiver archiveRootObject:personObj toFile:archiverPath];
// 解档
Person *personObj = [NSKeyedUnarchiver unarchiveObjectWithFile:archiverPath];

归档需要注意的是:
1、同一个对象属性,编码/解码的key要相同!
2、每一种基本数据类型,都有一个相应的编码/解码方法。
如:encodeObject方法与decodeObjectForKey方法,是成对出现的。
3、如果一个自定义的类A,作为另一个自定义类B的一个属性存在;那么,如果要对B进行归档,那么,B要实现NSCoding协议。并且,A也要实现NSCoding协议。

16.2.4、Core Data

https://blog.csdn.net/willluckysmile/article/details/76464249

17、OC的反射机制

17.1、反射机制的概念

对于任意一个类,都能够知道这个类的都有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;

这种动态获取的信息以及动态调用对象的方法的功能成为OC的反射机制。

17.2、如何利用反射机制

利用 NSClassFormString: 方法类使用字符串获得类
利用 isMemberOfClass:判断是否是某一个类
利用 isKindOfClass: 判断是否是某一个类的子类
利用 conforesToprotocol:判断对象是否遵守某一个方法
利用 respondsToSelector:判断是否实现了某一个方法
利用 performSelector 或者 objc_msgSend 间接调用方法

后记:知识点

  • 1、描述应用程序的启动顺序

    1、程序的入口:进入main函数, 设置AppDelegate称为函数的代理
    2、程序完成加载:[AppDelegate application:didFinishLaunchingWithOptions:]
    3、创建window窗口
    4、程序被激活:[AppDelegate applicationDidBecomeActive:]
    5、当点击home键时,
    程序取消激活状态:[AppDelegate applicationWillResignActive:]
    程序进入后台:[AppDelegate applicationDidEnterBackground:]
    6、点击进入工程
    程序进入前台:[AppDelegate applicationWillEnterForeground:]
    程序被激活:[AppDelegate applicationDidBecomeActive:]

  • 2、类变量的@protected ,@private,@public,@package,声明各有什么含义?

@private:作用范围只能在自身类
@protected:作用范围在自身类和继承自己的子类(默认)
@public:作用范围最大,可以在任何地方被访问。
@package:这个类型最常用于框架类的实例变量,同一包内能用,跨包就不能访问

  • 3、UIImage初始化一张图片有几种方法?简述各自的优缺点。

imageNamed:系统会先检查系统缓存中是否有该名字的Image,如果有的话,则直接返回,如果没有,则先加载图像到缓存,然后再返回。
initWithContentsOfFile:系统不会检查系统缓存,而直接从文件系统中加载并返回。
imageWithCGImage:scale:orientation:当scale=1的时候图像为原始大小,orientation制定绘制图像的方向

  • 4、什么是安全释放?

    置nil 再释放

  • 5、单例的好处是什么?

    节省内存

  • 6、打点是怎么做的?埋点?用户行为统计,用户画像

    借助第三方SDK:友盟、神策、GrowingIO
    APP内埋点:定义事件(事件ID)、用户信息(用户ID)

  • 7、如何组件化解耦 或者 组件化原理

    实现代码的高内聚低耦合,方便多人多团队开发!

猜你喜欢

转载自blog.csdn.net/liuq0725/article/details/81238321