iOS:程序员手册

分类(category),类扩展(extension):https://blog.csdn.net/u012946824/article/details/51799664
性能优化:https://www.jianshu.com/p/9c450e512020
组件化解耦:https://blog.csdn.net/GGGHub/article/details/52713642
组件化:https://casatwy.com/iosying-yong-jia-gou-tan-kai-pian.html
UIView渲染机制:https://blog.csdn.net/yangyangzhang1990/article/details/52452707
离屏渲染:https://www.cnblogs.com/fishbay/p/7576176.html
流式页面的性能优化:https://blog.csdn.net/qq_27484549/article/details/48589015
GCD死锁:https://www.jianshu.com/p/014c291e6ee2
面试题:https://www.jianshu.com/p/980eb40d1b21
面试题:https://blog.csdn.net/hanangellove/article/details/45033453
BAT面试:https://www.cnblogs.com/fengmin/p/6101972.html
BAT面试:http://www.cocoachina.com/ios/20180403/22844.html
阿里面试: http://www.cocoachina.com/ios/20171129/21362.html
阿里面试:https://www.jianshu.com/p/b4b1a7ef7525
笔试题:https://blog.csdn.net/weixhe/article/details/45157167

1、Runtime

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

1.1、消息

Objective-C之所以能做到执行时才查找要执行的函数,主要归功于Runtime,在Runtime中有一个objc_msgSend()的方法。

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

当我们写下一行代码[obj doSomething],在编译时,编译器会将我们的代码转化为

objc_msgSend(obj,@selector(doSomething));

objc_msgSend()方法实现了函数查找和匹配,以下是它的原理:

1、通过 obj 的 isa 指针找到它的 Class ;
2、在 Class 的中查找 selector 方法:
2-1、先从 cache 里查找;
2-2、如果 cache 找不到就找类的方法列表 methodLists 中是否有对应的方法;
2-3、如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止;
2-4、如果还找不到,就要开始进入动态方法解析了;
3、依据 selector 方法在 methodLists 中查找相应的函数指针 method_imp;
4、依据函数指针 method_imp 调用响应的函数(imp是指向最终实现程序的内存地址的指针)。

关于isa指针,在这里简单的说一下,在OC中每个实例对象都会有个isa指针,它指向对象的类,其实在类里面也会有isa指针,这个指针是指向该类的元类。

1.2、动态方法解析

链接:https://www.cnblogs.com/ioshe/p/5489086.html

动态方法解析即:你可以动态提供一个方法实现。如果我们使用关键字 @dynamic 在类的实现文件中修饰一个属性,表明我们会为这个属性动态提供存取方法,编译器不会再默认为我们生成这个属性的 setter 和 getter 方法了,需要我们自己提供。

@dynamic propertyName;

这时,我们可以通过分别重载 resolveInstanceMethod:resolveClassMethod: 方法添加实例方法实现和类方法实现。

当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod:resolveClassMethod: 来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作。

PS:动态方法解析会在消息转发机制侵入前执行,动态方法解析器将会首先给予提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,就让 resolveInstanceMethod: 方法返回 NO。

1.3、消息转发

链接:https://www.cnblogs.com/ioshe/p/5489086.html

1.3.1、重定向

消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

1.3.2、转发

当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。这时 forwardInvocation: 方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:someOtherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

唯一参数是个 NSInvocation 类型的对象,该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些处理。也可以将消息转发给其他对象处理,而不抛出错误。

注意:参数 anInvocation 是从哪来的?

forwardInvocation: 消息发送前,Runtime 系统会向对象发送methodSignatureForSelector: 消息,并取到返回的方法签名用于生成 NSInvocation 对象。所以重写 forwardInvocation: 的同时也要重写 methodSignatureForSelector: 方法,否则会抛异常。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过 forwardInvocation: 消息通知该对象。每个对象都继承了 forwardInvocation: 方法。但是, NSObject 中的方法实现只是简单的调用了 doesNotRecognizeSelector:。通过实现自己的 forwardInvocation: 方法,我们可以将消息转发给其他对象。

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。这一切都取决于方法的具体实现。

1.4、Runtime应用

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

1.4.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.4.2、Method Swizzling

待补充,详见链接

1.4.3、消息转发

待补充,详见链接

1.4.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.4.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;
}

存在的问题:

1、没有考虑用户传入的JSON数据的key值和property的名称不一致;
2、没有考虑用户传入的JSON数据有嵌套;
3、没有考虑JSON数据的value值不一定是NSString类型;
4、没有考虑JSON数据并不一定是NSDictionary类型;
5、没有考虑用户自定义了Model属性的setter方法;

class_copyPropertyList与class_copyIvarList区别

class_copyPropertyList返回的仅仅是对象类的属性(@property申明的属性),而class_copyIvarList返回类的所有属性和成员变量(包括在@interface大括号中声明的变量)。

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
https://www.cnblogs.com/jiangzzz/p/5619512.html

2.2、Runloop与线程的关系

1、 RunLoop 的作用就是来管理线程的,当线程的 RunLoop 开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,而不是退出。
2.、只有主线程的 RunLoop 是默认开启的,所以程序在开启后,会一直运行,不会退出。
3、一般来讲,一个线程一次只能执行一个任务,执行完毕后线程就会退出,所以其他线程的 RunLoop 如果需要开启,就手动开启。RunLoop 当第一次调用的时候才会创建,即懒加载模式,比如在子线程中第一次调用 [NSRunLoop currentRunLoop] 的时候才会创建。

2.2.1、RunLoop 实现常驻线程

1、创建线程:

_thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[_thread start];

2、线程方法中开启runloop:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 这里主要是监听某个 port,目的是让这个 Thread 不会回收
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];

3、将方法加入到线程:

[self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

4、实现test方法:

- (void)test {
    // 实现
}

2.3、runloop的mode作用

1、model 主要是用来指定事件在运行循环中的优先级的,分为:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
  • UITrackingRunLoopMode :ScrollView滑动时
  • UIInitializationRunLoopMode :启动时
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

2、苹果公开提供的 Mode有两个:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes)

2.4、autorelease

2.4.1、双向链表结构

autorelease的内存结构是以栈为结点构成的双向链表结构。一个线程的autoreleasepool就是一个指针栈。栈中存放的指针指向加入需要release的对象或者POOL_SENTINEL(哨兵对象,用于分隔autoreleasepool)。栈中指向POOL_SENTINEL的指针就是autoreleasepool的一个标记。当autoreleasepool进行出栈操作,每一个比这个哨兵对象后进栈的对象都会release。这个栈是由一个以page为节点双向链表组成,page根据需求进行增减。autoreleasepool对应的线程存储了指向最新page(也就是最新添加autorelease对象的page)的指针。

2.4.1、autoreleasepool什么时候释放对象

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

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

2.5、runloop的应用

2.5.1、滑动与图片刷新

当 tableView 的 cell 上有需要从网络获取的图片的时候,滚动 tableView ,异步线程会去加载图片,加载完成后主线程就会设置 cell 的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动 tableView 的时候,RunLoop 是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

// 只在NSDefaultRunLoopMode下执行(刷新图片)
[self.imageView performSelector:@selector(setImage:) withObject:@"图片URL" afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

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:用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。

5、atomic 和 nonatomic 有什么区别?

atomicnonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样,对于atomic的属性,系统生成的 getter/setter 会保证 getset 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。即:atomic 会加一个锁来保障线程安全,并且引用计数会 +1,来向调用者保证这个对象会一直存在。假如不这样做,如有另一个线程调 setter,可能会出现线程竞态,导致引用计数降到0,原来那个对象就释放掉了。而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。

atomic

是默认的;
会保证 CPU 能在别的线程来访问这个属性之前,先执行完当前流程;
速度不快,因为要保证操作整体完成;

nonatomic

不是默认的;
更快;
线程不安全;
如有两个线程访问同一个属性,会出现无法预料的结果;

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];

解决这种问题的实现方法:

1、封装一个NSTimer的工具类,暴露出类方法和相应的回调,这样NSTimertarget只会是工具类的self,不是使用类的self,就不会导致强引用。同时在调用invalidate方法之后,要把定时器nil掉,这样就能保证NSTimer的及时释放。

2、增加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];

3.4、APP内存优化

  • 1、合理利用图片加载:

常见的从bundle中加载图片的方式有两种:imageNamedimageWithContentsOfFile

imageNamed的优点是当加载时会缓存图片,imageNamed 用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文件中加载然后缓存并返回这个对象。imageWithContentsOfFile仅从指定的文件中加载图片。

  • 2、使用Autorelease Pool:

场景:需要创建很多临时对象,申请了很多临时内存的时候使用Autorelease Pool。

  • 3、在正确的地方使用 reuseIdentifier

例如:一个 tableView 维持一个队列的数据可重用的 UITableViewCell 对象。这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil

  • 4、 重用和延迟加载(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多viewUIScrollView里边的app更是如此。这里我们用到的技巧就是模仿UITableViewUICollectionView的操作:不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。

  • 5、Cache

一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。

更多详见https://blog.csdn.net/xtyzmnchen/article/details/52473664

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、死锁

死锁::两个(多个)线程都要等待对方完成某个操作才能进行下一步,这时就会发生死锁。

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。

6.5、部分说明:

自旋锁和互斥锁

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

不同点:

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

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

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

两种锁的加锁原理:

互斥锁:线程会从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串的字符进行乱序

  • 公钥加密

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

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

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

  • 钥匙串存储账户密码

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

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

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:也会被调用。

10.1、KVO内部实现原理

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

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

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: 判断是否是某一个类的子类
利用 conformsToProtocol:判断对象是否遵守某一个协议
利用 respondsToSelector:判断是否实现了某一个方法
利用 performSelector 或者 objc_msgSend 间接调用方法

17.3、Class反射

1、通过类名的字符串实例化对象:

Class class = NSClassFromString(@"Person");
Person *person = [[clasee alloc] init];

2、将类名转化为字符串:

Class class = [Person clasee];
NSString *className = NSStringFromClass(classs);

17.4、SEL反射

1、通过方法的字符串实例化方法:

SEL selector = NSSelectorFromString(@"updateName");
[person performSelector:selector withObject:@"张三"];

2、将方法转化为字符串:

NSString *selectorName = NSStringFromSelector(@selector(updateName));

18、设计模式

18.1、MVC

场景:通过数据模型(Model),控制器(Controller)逻辑,视图(View)展示将应用程序进行逻辑划分。
优势:使系统,层次清晰,职责分明,易于维护
实例:model-即数据模型,view-视图展示,controller进行UI展现和数据交互的逻辑控制。

18.2、单例

场景:确保程序运行期某个类,只有一份实例,用于进行资源共享控制。单例写法有好几种,通常的写法是基于线程安全的写法,结合dispatch_once来使用,保证单例对象只会被创建一次。如果不小心销毁了单例,再调用单例生成方法是不会再创建的。
优势:使用简单,延时求值,易于跨模块
实例:[UIApplication sharedApplication]。
注意事项:确保使用者只能通过 getInstance方法才能获得,单例类的唯一实例。objective-c中,重写allocWithZone方法,保证即使用户用 alloc方法直接创建单例类的实例,返回的也只是此单例类的唯一静态变量。

18.3、代理

场景:当一个类的某些功能需要由别的类来实现,但是又不确定具体会是哪个类实现。
优势:解耦合
实例:tableview的 数据源delegate,通过和protocol的配合,完成委托诉求,列表row个数delegate, 自定义的delegate

18.4、观察者

场景:一般为model层对controller和view进行的通知方式,不关心谁去接收,只负责发布信息。对于跨模块的类交互,需要使用通知;对于多对多的关系,使用通知更好实现。
优势:解耦合
实例:Notification通知中心,注册通知中心,任何位置可以发送消息,注册观察者的对象可以接收。

18.5、策略

场景:定义算法族,封装起来,使他们之间可以相互替换。
优势:使算法的变化独立于使用算法的用户
敏捷原则:接口隔离原则;多用组合,少用继承;针对接口编程,而非实现。
实例:排序算法,NSArray的sortedArrayUsingSelector;经典的鸭子会叫,会飞案例。
注意事项
1、剥离类中易于变化的行为,通过组合的方式嵌入抽象基类;
2、变化的行为抽象基类为,所有可变变化的父类;
3、用户类的最终实例,通过注入行为实例的方式,设定易变行为防止了继承行为方式,导致无关行为污染子类。完成了策略封装和可替换性。

18.6、工厂

https://www.jianshu.com/p/847af218b1f0

19、Block

19.1、block简介

Block的本质是带有函数执行上下文环境的结构体,其中包含被调函数指针,函数指针即函数在内存中的地址,通过这个地址可以达到调用函数的目的。Block是NSObject的子类,拥有NSObject的所有属性,所以block对象也有自己的生命周期,生存期间也会被持有和释放。

Block有三种:

NSGlobalBlock:静态区block,这是一种特殊的block,因为不引用外部变量而存在。另外,作为静态区的对象,它的释放是有操作系统控制的。
NSStackBlock:栈区block,位于内存的栈区,一般作为函数的参数出现。
NSMallocBlock:堆区block,位于内存的堆区,一般作为对象的property出现。

如果一个block引用了外部变量是栈block,则其不引用外部变量就成为了静态block。
如果一个block引用了外部变量是堆block,则其不引用外部变量就成为了静态block。

https://www.jianshu.com/p/6568f245deb2
https://www.jianshu.com/p/61ff80d310f4

19.2、block优缺点

block优点:

  block的代码可读性更好,因为block只要实现就可以了,而delegate需要遵守协议并且实现协议里的方法,而两者还不在一个地方。代理使用起来也更麻烦,因为要声明协议、声明代理属性、遵守协议、实现协议里的方法。block不需要声明,也不需要遵守,只需要声明属性和实现就可以了。

  block是一种轻量级的回调,可以直接访问上下文,由于block的代码是内联的,运行效率更高。block就是一个对象,实现了匿名函数的功能。所以我们可以把block当做一个成员变量、属性、参数使用,使用起来非常灵活。像用AFNetworking请求数据和GCD实现多线程,都使用了block回调。

block缺点:

  blcok的运行成本高。block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是引用计数加1,使用完或者block置nil后才销毁。delegate只是保存了一个对象指针(一定要用weak修饰delegate,不然也会循环引用),直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。

  block容易造成循环引用,而且不易察觉。因为为了blcok不被系统回收,所以我们都用copy关键字修饰,实行强引用。block对捕获的变量也都是强引用,所以就会造成循环引用。

19.3、block和delegate的区别

也参考一下19.2。

1、从源头上理解和区别block和delegate

delegate运行成本低,block的运行成本高。block出栈需要将使用的数据从栈内存拷贝到堆内存,如果是对象的话就是引用计数加1,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。

2、从使用场景区别block和delegate

有多个相关方法,假如每个方法都设置一个block, 这样会更麻烦。而 delegate 让多个方法分成一组,只需要设置一次,就可以多次回调。当多于3个方法时就应该优先采用delegate。当1,2个回调时,则使用block。delegate更安全些,比如: 避免循环引用(一定要用week修饰delegate,不然也会循环引用)。使用 block 时稍微不注意就形成循环引用,导致对象释放不了。这种循环引用,一旦出现就比较难检查出来。而 delegate 的方法是分离开的,并不会引用上下文,因此会更安全些。

3、 使用场景

1、优先使用block。
2、如果回调的状态很多,多于三个使用delegate。
3、如果回调的很频繁,次数很多,像UITableview,每次初始化、滑动、点击都会回调,使用delegate。
4、block和代理都各有优缺点,所以我们一定要理解区分使用场景,应用适合的回调方式,也是优化APP的性能的体现。

20、SDWebImage底层实现原理

20.1、缓存机制

二级缓存: 1、内存缓存处理 2.、二级缓存处理
处理过程:

  1. 在显示图片之前,先检查内存缓存中时候有该图片
  2. 如果内存缓存中有图片,那么就直接使用,不下载
  3. 如果内存缓存中无图片,那么再检查是否有磁盘缓存
  4. 如果磁盘缓存中有图片,那么直接使用,还需要保存一份到内存缓存中(方便下一次使用)
  5. 如果磁盘缓存中无图片,那么再去下载,并且把下载完的图片保存到内存缓存和磁盘缓存

具体实现流程:

  1. 首先将placeholderImage进行展示,SDWebImageManager根据URL开始处理图片;
  2. SDImageCache从缓存中查找图片,如果有,sdImageCacheDelegate回调image:didFindImage:forkey:useInfo:到SDWebImageManager,然后到前端展示图片;
  3. 缓存中没有,生成NSInvocationOperation添加到队列中开始在磁盘中查找。如果找到,会将图片添加到内存缓存中(如果空闲缓存不够,会先清理)然后SDImageCacheDelegate回调imageCache:didFindImage:forKey:userInfo:进而回调展示图片;
  4. 如果磁盘中没有,则共享或生成下载器SDWebImageDownLoader开始下载图片,图片下载由NSURLSession来做;
  5. 图片解码处理在一个NSOperationQueue完成,不会拖慢主线程UI。(PS:如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。)
  6. 在主线程notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给SDWebImageDownloader。imageDownloader:didFinishWithImage: 回调给SDWebImageManager告知图片下载完成;
  7. 通知所有的downloadDelegates下载完成,回调给需要的地方展示图片,并将图片保存到SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独NSInvocationOperation完成,避免拖慢主线程。
  8. SDImageCache在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

21、AFNetworking的实现原理

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

  1. NSURLSession:网络通信模块(核心模块) 对应 AFNetworking中的 AFURLSessionManager和对HTTP协议进行特化处理的AFHTTPSessionManager,AFHTTPSessionManager是继承于AFURLSessionmanager的;
  2. Security:网络通讯安全策略模块,对应AFSecurityPolicy;
  3. Reachability:网络状态监听模块,对应AFNetworkReachabilityManager;
  4. Seriaalization:网络通信信息序列化、反序列化模块对应AFURLResponseSerialization;
  5. UIKit:对于iOS UIKit的扩展库;

详见:
https://www.jianshu.com/p/02b25f6d1e1f
https://blog.csdn.net/u011993697/article/details/51284664

22、响应链

响应者对象:继承自UIResponder的对象称之为响应者对象。UIApplicationUIWindowUIViewController和所有继承UIViewUIKit类都直接或间接的继承自UIResponder

UIResponder一般响应以下几种事件:触摸事件(touch handling)、点按事件(press handling)、加速事件和远程控制事件:

// 触摸事件(touch handling)
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches;
- 
// 点按事件(press handling) NS_AVAILABLE_IOS(9_0)
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event;

// 加速事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;

// 远程控制事件 NS_AVAILABLE_IOS(4_0)
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event;

响应者链:由多个响应者组合起来的链条,就叫做响应者链。它表示了每个响应者之间的联系,并且可以使得一个事件可选择多个对象处理。

响应者链

假设触摸了initial view,

  1. 第一响应者是initial view,即initial view首先响应touchesBegan:withEvent:方法,接着传递给橘黄色view;
  2. 橘黄色的view开始响应touchesBegan:withEvent:方法,接着传递给蓝绿色view;
  3. 蓝绿色view响应touchesBegan:withEvent:方法,接着传递给控制器的view;
  4. 控制器view响应touchesBegan:withEvent:方法,控制器传递给了窗口;
  5. 窗口再传递给application;

如果上述响应者都不处理该事件,那么事件被丢弃。

事件的产生和传递

当一个触摸事件产生的时候,我们的程序是如何找到第一响应者的呢?

事件的产生传递

当你点击了屏幕会产生一个触摸事件,消息循环(runloop)会接收到触摸事件放到消息队列里,UIApplication会从消息队列里取事件分发下去,首先传给UIWindowUIWindow会使用hitTest:withEvent:方法找到此次触摸事件初始点所在的视图,找到这个视图之后他就会调用视图的touchesBegan:withEvent:方法来处理事件。

  • hitTest:withEvent:查找过程

hitTestview过程

// 图片中view等级
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];

点击viewE:

  1. A是UIWindow的根视图,首先对A进行hitTest:withEvent:;
  2. 判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
  3. pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES;
  4. 遍历A的子视图B和C,由于从后向前遍历;

因此先查看C,调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法 return nil

再查看B,调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES;

遍历B的子视图D和E,从后向前遍历,先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。

至此,点击事件的第一响应者就找到了。

注意

如果hitTest:withEvent: 找到的第一响应者view没有处理该事件,那么事件会沿着响应者链向上传递->父视图->视图控制器,如果传递到最顶级视图还没处理事件,那么就传递给UIWindow处理,若window对象也不处理->交给UIApplication处理,如果UIApplication对象还不处理,就丢弃该事件。

事件流程

注意:控件不能响应的情况

  1. userInteractionEnabled = NO;
  2. hidden = YES;
  3. 透明度 alpha 小于等于0.01;
  4. 子视图超出了父视图区域;

子视图超出父视图,不响应的原因:因为父视图的pointInside:withEvent:方法返回了NO,就不会遍历子视图了。可以重写pointInside:withEvent:方法解决此问题。

后记:知识点

  • 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制定绘制图像的方向

使用场景:如果要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。然而,在图片反复重用的情况下imageNamed是一个好得多的选择。

  • 4、什么是安全释放?

    置nil 再释放

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

    节省内存

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

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

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

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

  • 8、静态库与动态库的区别

    静态库:以.a 和 .framework为文件后缀名。
    动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。

    静态库与动态库的区别

    静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
    动态库:链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存。
    但是苹果不让使用自己的动态库,否则审核就无法通过。

  • 9、TCP和UDP

    图片

TCP的三次握手:

第一次握手:建立连接时,客户端发送SYN包(SYN=j)到服务器端,并进入SYN_SEND状态,等待服务器端确认;

第二次握手:服务器端收到SYN包,必须确认客户端的SYN(ACK=j+1),同时发送SYN+ACK包,此时服 务器端进入SYN_RECV状态;

第三次握手:客户端收到服务端的SYN+ACK包,向服务端发送确认包ACK(ACK=k+1),此包发送完毕,客户端和服务端进入ESTABLISHED状态,完成三次握手。

三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。

  • 10、1-n的递归算法
- (int)sum:(int)n
{
    if(n == 1) {
        return 1;
    } else {
        return [self sum:n-1] + n;
    }
}
  • 11、IOS与图片内存

在IOS上,图片会被自动缩放到2的N次方大小。比如一张1024*1025的图片,占用的内存与一张1024*2048的图片是一致的。图片占用内存大小的计算的公式是;长*宽*4。这样一张512*512 占用的内存就是 512*512*4 = 1M。其他尺寸以此类推。(ps:IOS上支持的最大尺寸为2048*2048)。

  • 12、Session和Cookie的区别与联系

Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

详见:https://www.cnblogs.com/endlessdream/p/4699273.html

  • 13、weak的实现原理

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

weak 的实现原理可以概括一下三步:

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

参考链接:http://www.cocoachina.com/ios/20170328/18962.html

  • 14、BAD_ACCESS

BAD_ACCESS就是内存泄漏BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。野指针指的是本来指针指向的对象已经释放了,但指向该对象的指针没有置 nil,指针指向随机的未知的内存。程序还以为该指针指向那个对象,导致存在一些潜在的危险访问操作,这些危险访问操作无法被指针指向的未知内存所处理,就会导致BAD_ACCESS错误造成程序崩溃。访问的含义包括多种情况,例如:向野指针发送消息,读写野指针本来指向的对象的成员变量等等。

如何调试BAD_ACCESS错误

参考链接:https://blog.csdn.net/cordova/article/details/71774003

15、ARC和MRC混编

现在的开发过程中居多使用ARC,但是常常使用一些MRC的第三方的库,这时候我们需要使用-fno-objc-arc来进行标示,如果使用MRC混编ARC的时候,需要使用-fno-objc-mrc来标示ARC的文件。

16、instruments

1、使用Allocations来检测内存和堆栈信息
2、使用Leaks检测内存的使用情况,包括内存泄露问题
3、使用Zombies来检测过早释放的僵尸对象,通过它可以检测出在哪里崩溃的。
4、使用Time Profiler来检测CPU内存使用情况

17、如何调试Bug

Bug分为测试中的Bug和线上的Bug:

线上Bug:项目使用了友盟统计,因此会有崩溃日志,通过解析dYSM可以直接定位到大部分bug崩溃之处。解决线上bug需要从主干拉一个新的分支,解决bug并测试通过后,再合并到主干,然后上线。若是多团队开发,可以将fix bug分支与其他团队最近要上线的分支集成,然后集成测试再上线。

测试Bug:根据测试所反馈的bug描述,若语义不清晰,则直接找到提bug人,操作给开发人员看,最好是可以bug复现。解决bug时,若能根据描述直接定位bug出错之处,则好处理;若无法直观定位,则根据bug类型分几种处理方式,比如崩溃的bug可以通过instruments来检测、数据显示错误的bug,则需要阅读代码一步步查看逻辑哪里写错。

17、NSNotification、KVO、Delegate是同步还是异步

都是同步。

18、页面间传值有哪几种方式

属性,通知,代理,协议(返回之前的视图),block,单例

19、iOS中的回调方法方法有哪些

Block 代理 通知 单例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值