1.1 谈一谈GCD和NSOperation的区别?
首先二者都是多线程相关的概念,当然在使用中也是根据不同情境进行不同的选择;
· GCD是将任务添加到队列中(串行/并发/主队列),并且制定任务执行的函数(同步/异步),其性能最好,底层是C语言的API,也更轻量级。iOS4.0以后推出的,针对多核处理器的并发技术,只能设置某一个队列的优先级,其高级功能有一次性执行dispatch_once,延迟操作dispatch_after,调度组等等;
· NSOperation把操作(异步)添加到队列中(全局的并发队列),是OC框架,更加面向对象,是对GCD的封装,iOS2.0推出,苹果推出GCD之后,对NSOperation的底层全部重写,可以随时取消已经设定准备要执行的任务,已经执行的除外,可以设置队列中每一个操作的优先级,其高级功能可以设置最大操作并发数,继续/暂停/全部取消,可以快队列设置操作的依赖关系。
1.2 谈谈多线程的应用
通常耗时的操作都放在子线程处理,然后到主线程更新UI,如
· 我们要从数据库提取数据还要将数据分组后显示,那么就会开个子线程来处理,处理完成后才去刷新UI显示。
· 拍照后,会在子线程处理图片,完成后才回到主线程来显示图片。拍照出来的图片太大了,因此要做处理。
音频、视频处理会在子线程来操作
· 文件较大时,文件操作会在子线程中处理
做客户端与服务端数据同步时,会在后台闲时自动同步
2.线程之间是如何通信的?
通过主线程和子线程切换的时候传递参数performSelecter:onThread:withObject:waitUntilDone:
3.网络图片处理问题怎么解决图片重复下载问题?(SDWebImage大概实现原理)
这个就需要用到字典,以图片的下载地址url为key,下载操作为value,所有的图片大概分成三类:已经下载好的,正在下载的和将要下载的;
当一张图片将要进行下载操作的时候,先判断缓存中是否有相同的图片,如果有的话就返回,没有的话就根据url的md5加密值去沙盒中找,有的话就拿出来用,没有的话再去以图片的url为key去字典中找有没有正在进行的任务,最后去判断等待的下载操作任务里面的字典有无相同key,如果没有,就自己开启任务,记录一下,文件保存的名称是url的md5值
这里建立了两个字典 :
1.iconCache:保存缓存的图片
2.blockOperation 用来保存下载任务
每当进入或退出程序时,会进行图片文件的管理:超过一星期的文件会被清除,如果设置了最大缓存,超过这个缓存就会删除最旧的文件,直到当前缓存文件为最大缓存文件的一半大小;
一般app中大部分缓存都是图片的情况下,可以直接调用clear方法进行清除缓存,getSize()方法获取当前缓存大小。
4.多线程安全的几种解决方法?
· 1> 只有在主线程刷新访问UI
· 2> 如果要防止资源抢夺,需要用synchronize进行加锁保护
· 3> 如果是异步操作要保证线程安全等问题,尽量使用GCD(有些函数默认就是安全的)
· 4> 单例为什么用static dispatch_once?使用dispatch_once可以简化代码并且彻底保证线程安全,开发者无需担心加锁或同步。此外,dispatch_once更高效,它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁。
5.原子属性
· 原子属性采用的是”多读单写”机制的多线程策略;”多读单写”缩小了锁范围,比互斥锁的性能好
· 规定只在主线程更新UI,就是因为如果在多线程中更新,就需要给UI对象加锁,防止资源抢占写入错误,但是这样会降低UI交互的性能,所以ios设计让所有UI对象都是非线程安全的(不加锁)
6.代理的作用、block
· 代理又叫委托,是一种设计模式(可以理解为java中回调监听机制),代理是对象与对象之间的通信交互,代理解除了对象之间的耦合性
· 改变或传递控制链,允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针,可以减少框架复杂度
代理的属性常是assign的原因:防止循环引用,以致对象无法得到正确的释放
· block底层是根据函数指针和结构体结合实现的,block本身就是结构体,更加简洁,不需要定义繁琐的协议方法,但通信事件比较多的话,建议使用Delegate
· block就是一个数据类型,存放一段代码,编译的时候不会执行,只有用到的时候才会去执行里面的代码。声明的时候使用copy是因为要从栈区拷贝到堆区,在栈区会受到作用域的限制,超出所在的函数就会被销毁,就没办法进行传值回调等一系列操作了。应注意循环引用,weak来修饰。如果一个变量是在block外部创建,需要在block内部修改,那么需要使用block修饰这个变量(block可以在ARC和MRC情况下使用,可以修饰对象和基本数据类型,weak只能在ARC下使用,只能修饰对象,不能修饰基本数据类型)
· 最常用的是使用block作为参数传值,不同情况下回调不同的代码(如成功回调失败回调)
7.谈谈你对runTime运行时机制的了解(注意哦,这个很重要的)
runtime是一套比较底层的纯C语言API,属于一个C语言库,包含了很多底层的C语言的API
· 平时编写的OC代码,在程序运行过程中,其实都是转成了· runtime的C语言代码,runtime是OC的幕后工作者,底层语言,例如:
·· OC–> [[WPFPerson alloc] init]
·· runtime–>objc_msgSend(objc_msgSend(“WPFPerson”, “alloc”), “init”)
利用runtime可以实现一些非常底层的操作(用OC不好实现)
· 在程序运行过程中,动态创建一个类(比如KVO底层实现:检测isa指针,发现是新建了一个类,当然Xcode7.0以前的版本才可以监听到isa指针)
· 遍历一个类的所有成员变量、方法,访问私有变量(先通过runtime的class_getInstanceVariable获取成员变量,再通过class_getIvar获取它的值)
· 在程序运行过程中,动态为某个类添加属性\方法,修改属性值\方法,比如产品经理需要跟踪记录APP中按钮的点击次数和频率等数据,可以通过集成按钮或者类别实现,但是带来的问题比如别人不一定去实例化你写的子类,或者其他类别也实现了点击方法导致不确定会调用哪一个,runtime可以这样解决:在按钮的分类里面,重写load方法,利用dispatch_once保证只执行一次,新建监控按钮点击的方法,先用class_addMethod方法,判断其返回的bool值,如果添加成功,就用class_replaceMethod将原来的方法移除,如果添加失败,就用method_exchangeImplementations方法进行替换
· 拦截并替换方法,比如由于某种原因,我们要改变这个方法的实现,但是又不能动它的源码(比如一些开源库出现问题的时候,这时候runtime就可以出场了)–>先增加一个tool类,然后写一个我们自己实现的方法-change,通过runtime的class_getInstanceMethod获取两个方法,在用class_replaceMethod方法进行替换。防止数组越界的方法:数组越界的时候报错的方法是add_object,做一个逻辑判断,越界的时候通过class_replaceMethod交换掉add_object(相当于重写了这个方法)
相关应用
· NSCoding(归档和解档),如果一个模型有很多个属性,那么需要对每个属性都实现一遍encodeObject和decodeObjectForKey方法,十分麻烦,但是如果使用class_copyIvarList获取所有属性,然后循环遍历,使用[ivarName substringFromIndex:1]去掉成员变量下划线
· 字典转模型:像几个出名的开源库JSONModel、MJExtension等都是通过这种方式实现的(利用runtime的class_copyIvarList获取属性数组,遍历模型对象的所有成员属性,根据属性名找到字典中key值进行赋值,当然这种方法只能解决NSString、NSNumber等,如果含有NSArray或NSDictionary,还要进行第二步转换,如果是字典数组,需要遍历数组中的字典,利用objectWithDict方法将字典转化为模型,在将模型放到数组中,最后把这个模型数组赋值给之前的字典数组)
· Method Swizzling:OC中调用方法事实上就是向对象发送消息,而查找消息的唯一依据就是selector的名字,因此可以使用runtime运行时机制动态交换方法。在+load方法里面调换,因为method swizzling的影响范围是全局的,所以应该放在最保险的地方来处理,+load方法能够保证能在类初始化的时候一定能被调用,可以保证统一性,如果是在使用的时候才去调用,可能达不到全局处理的效果;使用dispatch_once保证只交换一次。[objc_getClass(“__NSArrayM”) swizzleSelector:@selector(addObject:)
withSwizzledSelector:@selector(hyb_safeAddObject:)];使用场景:addObject方法添加的值为nil的时候会崩溃。调用objectAtIndex:时出现崩溃提示empty数组问题
8.谈谈你对Run Loop的理解
· RunLoop是多线程的一个很重要的机制,就是一个线程一次只能执行一个任务,执行完任务后就会退出线程。主线程会通过do-while死循环让程序持续等待下一个任务不退出。通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。非主线程通常来说就是为了执行某个任务而创建的,执行完就会归还资源,因此默认不开启RunLoop
· 实质上,对于子线程的runloop是默认不存在的,因为苹果采用了懒加载的方式,如果没有手动调用[NSRunLoop currentRunLoop]的话,就不会去查询当前线程的RunLoop,也不会创建、加载
· 当然如果子线程处理完某个任务后不退出,需要继续等待接受事件,需要启动的时候也可以手动启动,比如说添加定时器的时候就要手动开始RunLoop
如何处理事件
· 界面刷新: 当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。 苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
· 手势识别: 如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
· 网络请求:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装,NSURLConnection是iOS7中新增的接口。当网络开始传输时,NSURLConnection创建了两个新线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket线程是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接受底层socket的事件,并添加到上层的Delegate
应用
· 滑动与图片刷新:当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片。(在viewDidLoad中调用self.imageView performSelector@selector(setImage) withObject:…afterDelay:…inModes@[NSDefayltRunLoopMode])
· 常驻子线程,保持子线程一直处理事件 为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出