谈谈那些你不知道的iOS

作者: 程序员_秃头怪 | 来源:发表于2019-02-20 18:54 被阅读6次

    一、谈谈多线程的应用

    通常耗时的操作都放在子线程处理,然后到主线程更新UI,如

    1、我们要从数据库提取数据还要将数据分组后显示,那么就会开个子线程来处理,处理完成后才去刷新UI显示。

    2、拍照后,会在子线程处理图片,完成后才回到主线程来显示图片。拍照出来的图片太大了,因此要做处理。

    3、音频、视频处理会在子线程来操作

    4、文件较大时,文件操作会在子线程中处理

    5、做客户端与服务端数据同步时,会在后台闲时自动同步

    2.1 线程之间是如何通信的?

    通过主线程和子线程切换的时候传递参数performSelecter:onThread:withObject:waitUntilDone:

    **( 其实作为一个开发者有一个学习的氛围跟一个交流圈子特别重要,这是我的一个iOS交流群:923910776,不管是小白还是大牛都欢迎入驻,大家一起交流成长! ) **

    3. 1网络图片处理问题怎么解决图片重复下载问题?(SDWebImage大概实现原理)

    1、这个就需要用到字典,以图片的下载地址url为key,下载操作为value,所有的图片大概分成三类:已经下载好的,正在下载的和将要下载的;

    2、当一张图片将要进行下载操作的时候,先判断缓存中是否有相同的图片,如果有的话就返回,没有的话就根据url的md5加密值去沙盒中找,有的话就拿出来用,没有的话再去以图片的url为key去字典中找有没有正在进行的任务,最后去判断等待的下载操作任务里面的字典有无相同key,如果没有,就自己开启任务,记录一下,文件保存的名称是url的md5值

    这里建立了两个字典 :

    1.iconCache:保存缓存的图片

    2.blockOperation 用来保存下载任务

    每当进入或退出程序时,会进行图片文件的管理:超过一星期的文件会被清除,如果设置了最大缓存,超过这个缓存就会删除最旧的文件,直到当前缓存文件为最大缓存文件的一半大小;

    一般app中大部分缓存都是图片的情况下,可以直接调用clear方法进行清除缓存,getSize()方法获取当前缓存大小。

    4.1 多线程安全的几种解决方法?

    1> 只有在主线程刷新访问UI

    2> 如果要防止资源抢夺,需要用synchronize进行加锁保护

    3> 如果是异步操作要保证线程安全等问题,尽量使用GCD(有些函数默认就是安全的)

    4> 单例为什么用static dispatch_once?使用dispatch_once可以简化代码并且彻底保证线程安全,开发者无需担心加锁或同步。此外,dispatch_once更高效,它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁。

    5. 1原子属性

    原子属性采用的是"多读单写"机制的多线程策略;"多读单写"缩小了锁范围,比互斥锁的性能好

    规定只在主线程更新UI,就是因为如果在多线程中更新,就需要给UI对象加锁,防止资源抢占写入错误,但是这样会降低UI交互的性能,所以ios设计让所有UI对象都是非线程安全的(不加锁)

    6.1 代理的作用、block

    代理又叫委托,是一种设计模式(可以理解为java中回调监听机制),代理是对象与对象之间的通信交互,代理解除了对象之间的耦合性

    改变或传递控制链,允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针,可以减少框架复杂度

    代理的属性常是 weak 的原因:防止循环引用,以致对象无法得到正确的释放(具体原因会在下文第十一条阐述)

    block底层是根据函数指针和结构体结合实现的,block本身就是结构体,更加简洁,不需要定义繁琐的协议方法,但通信事件比较多的话,建议使用Delegate

    block就是一个数据类型,存放一段代码,编译的时候不会执行,只有用到的时候才会去执行里面的代码。声明的时候使用copy是因为要从栈区拷贝到堆区,在栈区会受到作用域的限制,超出所在的函数就会被销毁,就没办法进行传值回调等一系列操作了。应注意循环引用,__weak来修饰。如果一个变量是在block外部创建,需要在block内部修改,那么需要使用__block修饰这个变量(__block可以在ARC和MRC情况下使用,可以修饰对象和基本数据类型,__weak只能在ARC下使用,只能修饰对象,不能修饰基本数据类型)

    最常用的是使用block作为参数传值,不同情况下回调不同的代码(如成功回调失败回调)

    7. 1NSThread、NSOperation、GCD(注意哦,这个很重要的)

    iOS有三种多线程编程的技术,这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。

    NSThread :NSThread 轻量级最低,相对简单,但是需要手动管理所有的线程活动,如生命周期、线程同步、睡眠等

    NSOperation : 自带线程周期管理,操作上可更注重自己逻辑,但是面向对象的抽象类,只能实现它或者使用它定义好的两个子类: NSInvocationOperation 和 NSBlockOperation。

    GCD: 最高效,避开并发陷阱.

    所以一般简单而安全的选择NSOperation实现多线程即可。而处理大量并发数据,又追求性能效率的选择GCD。

    8. 1谈谈你对Run time的运行机制的理解。

    runtime是一套比较底层的纯C语言API,属于一个C语言库,包含了很多底层的C语言的API平时编写的OC代码,在程序运行过程中,其实都是转成了runtime的C语言代码,runtime是OC的幕后工作者,底层语言

    1、RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。

    2、对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。

    3、OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。

    4、只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用

    如何处理事件

    1、界面刷新: 当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。 苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    2、手势识别: 如果上一步的 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    3、网络请求:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnection对CFNetwork进行面向对象的封装。当网络开始传输时,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自动退出

    9.1Scoket连接和HTTP连接的区别:

    HTTP协议是基于TCP连接的,是应用层协议,主要解决如何包装数据。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

    HTTP连接:短连接,客户端向服务器发送一次请求,服务器响应后连接断开,节省资源。服务器不能主动给客户端响应(除非采用HTTP长连接技术),iPhone主要使用类NSURLConnection。

    Socket连接:长连接,客户端跟服务器端直接使用Socket进行连接,没有规定连接后断开,因此客户端和服务器段保持连接通道,双方可以主动发送数据,一般多用于游戏.Socket默认连接超时时间是30秒,默认大小是8K(理解为一个数据包大小)。

    10. 1谈一谈内存管理

    iOS的内存管理分为 MRC 和 ARC,管理的是堆区动态产生的对象,基本数据类型就不是内存管理的范围

    内存管理的核心概念是引用计数器:当对象被alloc、copy、new的时候,引用计数器+1,当被release的时候引用计数器—1,为0的时候就会被系统回收,调用dealloc方法

    说道内存管理,就必须说说@property的内存管理参数:

    assign --> 针对于基本数据类型的简单赋值操作

    retain --> release 一次旧对象 retain 一次新对象 (适用于OC对象类型)

    copy --> release 一次旧对象 拷贝一个新对象出来(一般修饰字符串和block)

    weak--> 表示一种非拥有关系,设置该属性时既不释放新值,也不保留旧值,和assign类似,但是目标对象释放时,属性值也会自动清空

    如何避免内存泄露 --> 使用Analyze进行代码的静态分析

    当然使用block的时候最应该注意下循环引用,使用Leaks检测内存泄露,显示绿色的勾告知内存处理的不错,实际上内存得不到释放。一般我的方法是在控制器声明周期的viewDidAppear和dealloc方法里面打印日志[[self class] description],如果没有打印出来,就说明没有被释放。使用__weak __typeof(self) weakSelf = self;解决。有一次我是直接使用成员变量,而不是属性,_age,我以为这样没有使用self就可以了,但是后来测试发现还是造成循环引用了,因为_age是控制器的成员变量,也就是强引用了控制器,也要改成弱引用__block __weak __typeof(_currentModel) weakModel = _currentModel;

    11.1 为什么用 weak 修饰的变量会自动置为 nil?

    runtime 对注册类,会进行布局,将 weak 修饰的对象放到一个hash表中,key值是该对象的内存地址,value是该对象

    当该对象retainCount为0时,执行dealloc,根据该地址去weak的hash表中查询到该对象,从而设置为nil(向nil发送消息是安全的)

    11.2向一个nil对象发送消息会发生什么?

    12. 1@synthesize和@dynamic区别是什么

    这两个关键字都是@property对应的词

    @synthesize 语义是如果没有手动实现setter和getter方法,那么编译器会自动帮你加上这两个方法

    @dynamic告诉编译器,属性的setter和getter由用户自己实现,不自动生成(readOnly只实现getter即可),但是如果没有自己实现,编译的时候不会报错,运行的时候就会报错,这就是所谓的动态绑定

    13. 1谈谈时间响应链的一般顺序

    第一步:事件的产生:首先找到最合适的从UIApplication到keyWindow依次找到view,由上到下

    发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈,因为队列先进先出,先产生的事件优先处理才合理

    UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先把事件发送给应用程序的主窗口(keywindow)

    主窗口会在视图层级结构中找到一个最合适的视图来处理触摸事件(hitTest:方法遍历当前view的所有子类,返回最合适的view)

    找到合适的视图控件后,就会用视图控件的touches 方法来做具体的事件处理

    由于是从父视图到子视图,因此如果父view不接收事件,自视图也无法接收事件

    如果确定父控件是最合适的view,那么父控件的子控件的hitTest:withEvent: 方法也会被调用

    想让谁成为最合适的view就重写自己的父控件的hitTest:withEvent: 方法,返回指定的控件

    第二步:事件的响应:由下到上

    先看inital view能否处理这个事件,如果不能依次往上传递(如self或superView的userInterface/enable属性为NO,透明度小于0.1等原因),一直传递到该视图的VC/window/application ,如果application还不能处理,就将该事件抛弃

    在事件的响应过程中,如果某控件实现了 touches 方法,则这个事件由该控件来接收,如果调用了[super touches] 方法,就会将事件顺着响应者链条往上传递,传递给上一个响应者

    1、下拉和下拉的原理

    14.1 post和get方式的区别

    GET请求的数据会负载URL之后,即把数据放在HTTP协议头中,以?区分URL和传输数据,参数之间以&相连,英文字母/数字,原样发送,如果是空格,转化为+,如果是中文,把字符串用BASE64加密;POST就是把提交的数据放在HTTP包的包体中

    GET一般用于提交少量数据(最多提交1k,浏览器限制),POST用于提交大量数据(理论上无限制,收服务器限制)

    GET 无副作用,POST 有副作用

    GET提交的数据可以在浏览器历史记录中看到,安全性不好,别人可以拿到账号密码,POST不会

    Get是向服务器发索取数据的一种请求,而POST是向服务器发提交数据的一种请求,只是发送机制不同

    GET不可以设置书签,POST可以设置书签

    POST支持更多编码类型且不对数据类型限制

    什么情况下用POST:

    请求的结果具有持续性副作用,如数据库添加新的数据行

    若使用get方法,则表单上手机的数据可能让URL过长

    要传送的数据不是采用7位的ASCII编码

    什么情况下用GET:

    请求是为了查找资源,HTML表单数据仅用来帮助搜索

    请求结果无持续副作用性的副作用

    手机的数据及HTML表单内的输入字段名称的总长不超过1024个字符

    15.1 POST和PUT区别

    POST请求的url表示处理该封闭实体的资源,该资源可能是个数据接收过程、某种协议的网关、或者接收注解的独立实体。

    PUT请求中的url表示请求中封闭的实体-用户代理知道url的目标,并且服务器无法将请求应用到其他资源。如果服务器希望该请求应用到另一个url,就必须发送一个301响应;用户代理可通过自己的判断来决定是否转发该请求。

    POST是用来提交数据的。提交的数据放在HTTP请求的正文里,目的在于提交数据并用于服务器端的存储,而不允许用户过多的更改相应数据(主要是相对于在url 修改要麻烦很多)。

    PUT操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同

    POST操作既不是安全的,也不是幂等的,比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建出了若干的资源。

    安全和幂等的意义在于:当操作没有达到预期的目标时,我们可以不停的重试,而不会对资源产生副作用。从这个意义上说,POST操作往往是有害的,但很多时候我们还是不得不使用它。

    还有一点需要注意的就是,创建操作可以使用POST,也可以使用PUT,区别在于POST 是作用在一个集合资源之上的,而PUT操作是作用在集合的一个具体资源之上的,再通俗点说,如果URL可以在客户端确定,那么就使用PUT,如果是在服务端确定,那么就使用POST,比如说很多资源使用数据库自增主键作为标识信息,而创建的资源的标识信息到底是什么只能由服务端提供,这个时候就必须使用POST。

    16.1. 深复制和浅复制

    非集合类对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制

    非集合类对mutable对象进行copy和mutableCopy都是内容复制

    在集合类对象中,对immutable对象进行copy,是指针复制,mutableCopy是内容复制

    在集合类对象中,对mutable对象进行copy和mutableCopy都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制

    copy出来的对象都是不可变的,mutableCopy出来的对象都是可变的

    NSString *str = @"string"; str = @"newString"; 打印对象地址,发现是发生变化的,需要把@"newStirng"当做一个新的对象,将这段对象的内存地址赋值给str

    17. 1关于项目中动画的使用

    序列帧动画:self.imageView.animationImages = array;

    [UIView animateWithDuration] + CGAffinetransform

    核心动画CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position.y"]; anim.fromValue toValue repeatCount [btn.layer addAnimation]

    关键帧动画CAKeyframeAnimation,anim.values = array,添加到layer上

    组动画CAAnimationGroup,将以上动画组合起来

    转场动画:CATransition,设置duration和type,然后添加到layer上。利用UIView 的类方法实现转场动画

    [UIView transitionWithView: duration: options: animations:^{ } completion:nil];

    UIDynamicAnimator仿真者 、 UISnapBehavior吸附行为,设置damping来调节震动幅度 、 UIPushBehavior推动行为 、 UICollisionBehavior碰撞边缘检测行为 、 UIAttachmentBehavior附着行为 、 UIGravityBehavior重力行为

    POPSpringAnimation

    springBounciness[0,20]越大振幅越大。

    springSpeed速度

    18. 1优化tableViewCell高度

    一种是针对所有 Cell 具有固定高度的情况,通过:self.tableView.rowHeight = 88;

    指定了一个所有 cell 都是 88 高度的 UITableView,对于定高需求的表格,强烈建议使用这种(而非下面的)方式保证不必要的高度计算和调用。

    另一种方式就是实现 UITableViewDelegate 中的:heightForRowAtIndexPath:需要注意的是,实现了这个方法后,rowHeight 的设置将无效。所以,这个方法适用于具有多种 cell 高度的 UITableView。

    iOS7之后出了了estimatedRowHeight,面对不同高度的cell,只要给一个预估的值就可以了,先给一个预估值,然后边滑动边计算,但是缺点就是

    设置估算高度以后,tableView的contentSize.height是根据cell高度预估值和cell的个数来计算的,导致导航条处于很不稳定的状态,因为contentSize.height会逐渐由预估高度变为实际高度,很多情况下肉眼是可以看到导航条跳跃的

    如果是设计不好的上拉加载或下拉刷新,有可能使表格滑动跳跃

    估算高度设计初衷是好的,让加载速度更快,但是损失了流畅性,与其损失流畅性,我宁愿让用户加载界面的时候多等那零点几秒

    iOS8 WWDC 中推出了 self-sizing cell 的概念,旨在让 cell 自己负责自己的高度计算,使用 frame layout 和 auto layout 都可以享受到:

    self.tableView.estimatedRowHeight = 213;

    self.tableView.rowHeight = UITableViewAutomaticDimension;

    如果不加上估算高度的设置,自动算高就失效了

    这个自动算高在 push 到下一个页面或者转屏时会出现高度特别诡异的情况,不过现在的版本修复了。

    相同的代码在 iOS7 和 iOS8 上滑动顺畅程度完全不同,iOS8 莫名奇妙的卡。很大一部分原因是 iOS8 上的算高机制大不相同,从 WWDC 也倒是能找到点解释,cell 被认为随时都可能改变高度(如从设置中调整动态字体大小),所以每次滑动出来后都要重新计算高度。

    dequeueReusableCellWithIdentifier:forIndexPath: 相比不带 “forIndexPath” 的版本会多调用一次高度计算

    iOS7 计算高度后有”缓存“机制,不会重复计算;而 iOS8 不论何时都会重新计算 cell 高度

    使用 UITableView+FDTemplateLayoutCell(百度知道负责人孙源) 无疑是解决算高问题的最佳实践之一,既有 iOS8 self-sizing 功能简单的 API,又可以达到 iOS7 流畅的滑动效果,还保持了最低支持 iOS6

    FDTemplateLayoutCell 的高度预缓存是一个优化功能,利用RunLoop空闲时间执行预缓存任务计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。

    当用户正在滑动 UIScrollView 时,RunLoop 将切换到 UITrackingRunLoopMode 接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他 Mode (除 NSRunLoopCommonModes 这个组合 Mode)下的事件将全部暂停执行,来保证滑动事件的优先处理,这也是 iOS 滑动顺畅的重要原因

    注册 RunLoopObserver 可以观测当前 RunLoop 的运行状态,并在状态机切换时收到通知:

    RunLoop开始

    RunLoop即将处理Timer

    RunLoop即将处理Source

    RunLoop即将进入休眠状态

    RunLoop即将从休眠状态被事件唤醒

    RunLoop退出

    分解成多个RunLoop Source任务,假设列表有 20 个 cell,加载后展示了前 5 个,那么开启估算后 table view 只计算了这 5 个的高度,此时剩下 15 个就是“预缓存”的任务,而我们并不希望这 15 个计算任务在同一个 RunLoop 迭代中同步执行,这样会卡顿 UI,所以应该把它们分别分解到 15 个 RunLoop 迭代中执行,这时就需要手动向 RunLoop 中添加 Source 任务(由应用发起和处理的是 Source 0 任务)

    19.1 为什么AFN显示图片不如SDWebImage流畅?同样是从网络上下载图片而不是从缓存取图片?

    因为SDWebImage有一个decoder

    UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像

    所以每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求

    为了提高效率通过SDWebImageDecoder将包装在Data的资源解压,然后画在另外一张图片上,这样新的图片就不再需要重复解压了

    这是典型的空间换时间的做法

    20.1 我是怎样用两个imageView实现了无线轮播!

    建立一个scrollView,设置contentsize为3*kWidth,contentOffSet为kWidth

    接下来使用代理方法scrollViewDidScroll来监听scrollview的滚动,定义一个枚举变量来记录滚动的方向

    使用KVO来监听direction属性值的改变-->[self addObserver:self forKeyPath:@"direction" options:NSKeyValueObservingOptionNew context:nil];

    通过observeValueForKeyPath判断滚动的方向,当偏移量大于x,表示左移,则将otherImageView加在右边,偏移量小于x,表示右移,则将otherImageView加在左边。同时判断设置对应的索引,图片

    通过代理方法scrollViewDidEndDecelerating来监听滚动结束,结束后,scrollview的偏移量为0或者2x,我们通过代码再次将scrollview的偏移量设置为x,并将currImageView的图片修改为otherImageView的图片,那么我们看到的还是currImageView,只不过展示的是下一张图片,如图,又变成了最初的效果

    ,然后设置自动轮播,添加计时器,利用setContentOffset方法里面setContentOffset:animated:方法执行完毕后不会调用scrollview的scrollViewDidEndDecelerating方法,但是会调用scrollViewDidEndScrollingAnimation方法,因此我们要在该方法中调用pauseScroll(即监听减速结束后由otherImageView切换到currImageView的方法)

    添加计时器:self.timer = [NSTimertimerWithTimeInterval:self.timetarget:self selector:@selector(nextPage) userInfo:nil repeats:YES];

    在scrollViewWillBeginDragging中停止计时器

    在scrollViewDidEndDragging中开启计时器

    判断外界传入的是图片还是路径,如果是图片,直接加入图片数组中,如果是路径,先添加一个占位图片,然后根据路径去下载图片

    监听图片被点击

    定义一个block属性暴露给外界void(^imageClickBlock)(NSInteger index)

    (不会block的可以用代理,或者看这里)

    设置currImageView的userInteractionEnabled为YES

    给currImageView添加一个点击的手势

    在手势方法里调用block,并传入图片索引

    NSTimer的两种形式

    scheduledTimerWithTimeInterval 是创建一个定时器,并加入到当前运行循环[NSRunLoop currentRunLoop]中

    其他两个([NSTimer timerWithTimeInterval:3 target:self selector:@selector(doSomeThing1) userInfo:nil repeats:YES]; [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:5] interval:3 target:self selector:@selector(doSomeThing2) userInfo:nil repeats:YES];)只是创建定时器,并未添加到当前运行循环中,所以如果是其他两种方式创建的定时器则需要手动添加到currentRunLoop中

    NSTimer是普通的定时器,如果系统繁忙,刷新可能会被延迟。但是CADisplaylink实时刷新,跟着屏幕的刷新频率实时刷新,60次/s,与屏幕刷新频率相同

    21. tableView的优化

    iOS平台因为UIKit本身的特性,需要将所有的UI操作都放在主线程执行,所以有时候就习惯将一些线程安全性不确定的逻辑,以及它线程结束后的汇总工作等等放到了主线程,所以主线程包含大量计算、IO、绘制都有可能造成卡顿。

    可以通过监控runLoop监控监控卡顿,调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿.

    使用到CFRunLoopObserverRef,通过它可以实时获得这些状态值的变化

    监控后另外再开启一个线程,实时计算这两个状态区域之间的耗时是否到达某个阀值,便能揪出这些性能杀手.

    监控到了卡顿现场,当然下一步便是记录此时的函数调用信息,此处可以使用一个第三方Crash收集组件PLCrashReporter,它不仅可以收集Crash信息也可用于实时获取各线程的调用堆栈

    当检测到卡顿时,抓取堆栈信息,然后在客户端做一些过滤处理,便可以上报到服务器,通过收集一定量的卡顿数据后经过分析便能准确定位需要优化的逻辑

    设置正确的 reuseidentifer 以重用 cell

    尽量将 View 设置为不透明,包括 cell 本身(backgroundcolor默认是透明的),图层混合靠GPU去渲染,如果透明度设置为100%,那么GPU就会忽略下面所有的layer,节约了很多不必要的运算。模拟器上点击“Debug”菜单,然后选择“color Blended Layers”,会把所有区域分成绿色和红色,绿色的好,红色的性能差(经过混合渲染的),当然也有一些图片虽然是不透明的,但是也会显示红色,如果检查代码没错的话,一般就是图片自身的性质问题了,直接联系美工或后台解决就好了。除非必须要用GPU加载的,其他最好要用CPU加载,因为CPU一般不会百分百加载,可以通过CoreGraphics画出圆角

    有时候美工失误,图片大小给错了,引起不必要的图片缩放(可以找美工去改,当然也可以异步去裁剪图片然后缓存下来),还是使用Instrument的Color Misaligned Images,黄色表示图片需要缩放,紫色表示没有像素对齐。当然一般情况下图片格式不会给错,有些图片格式是GPU不支持的,就还要劳烦CPU去进行格式转换。还有可以通过Color Offscreen-Rendered Yellow来检测离屏渲染(就是把渲染结果临时保存,等到用的时候再取出,这样相对于普通渲染更消耗内存,使用maskToBounds、设置shadow,重写drawRect方法都会导致离屏渲染)

    避免渐变,cornerRadius在默认情况下,这个属性只会影响视图的背景颜色和 border,但是不会离屏绘制,不影响性能。不用clipsToBounds(过多调用GPU去离屏渲染),而是让后台加载图片并处理圆角,并将处理过的图片赋值给UIImageView。UIImageView 的圆角通过直接截取图片实现,圆角路径直接用贝塞尔曲线UIBezierPath绘制(人为指定路径之后就不会触发离屏渲染),UIGraphicsBeginImageContextWithOptions。UIView的圆角可以使用CoreGraphics画出圆角矩形,核心是CGContextAddArcToPoint 函数。它中间的四个参数表示曲线的起点和终点坐标,最后一个参数表示半径。调用了四次函数后,就可以画出圆角矩形。最后再从当前的绘图上下文中获取图片并返回,最后把这个图片插入到视图层级的底部。

    “Flash updated Regions”用于标记发生重绘的区域

    如果 row 的高度不相同,那么将其缓存下来

    如果 cell 显示的内容来自网络,那么确保这些内容是通过异步下载

    使用 shadowPath 来设置阴影,图层最好不要使用阴影,阴影会导致离屏渲染(在进入屏幕渲染之前,还看不到的时候会再渲染一次,尽量不要产生离屏渲染)

    减少 subview 的数量,不要去添加或移除view,要就显示,不要就隐藏

    在 cellForRowAtIndexPath 中尽量做更少的操作,最好是在别的地方算好,这个方法里只做数据的显示,如果需要做一些处理,那么最好做一次之后将结果储存起来.

    使用适当的数据结构来保存需要的信息,不同的结构会带来不同的操作代价

    使用,rowHeight , sectionFooterHeight 和 sectionHeaderHeight 来设置一个恒定高度 , 而不是从代理(delegate)中获取

    cell做数据绑定的时候,最好在willDisPlayCell里面进行,其他操作在cellForRowAtIndexPath,因为前者是第一页有多少条就执行多少次,后者是第一次加载有多少个cell就执行多少次,而且调用后者的时候cell还没显示

    读取文件,写入文件,最好是放到子线程,或先读取好,在让tableView去显示

    tableView滚动的时候,不要去做动画(微信的聊天界面做的就很好,在滚动的时候,动态图就不让他动,滚动停止的时候才动,不然可能会有点影响流畅度)。在滚动的时候加载图片,停止拖拽后在减速过程中不加载图片,减速停止后加载可见范围内图片

    (有一句话叫三人行必有我师,其实作为一个开发者有一个学习的氛围跟一个交流圈子特别重要,这是我的一个iOS交流群659170228,不管是小白还是大牛都欢迎入驻,大家一起交流成长!话糙理不糙,互相学习,共同进步!一起加油吧!)

    小编给大家推荐一个iOS技术交流群:923910776!群内提供数据结构与算法、底层进阶、swift、逆向、整合面试题等免费资料!

    相关文章

      网友评论

        本文标题:谈谈那些你不知道的iOS

        本文链接:https://www.haomeiwen.com/subject/uxyhyqtx.html