图像的显示过程:CPU计算好数据,并由GPU渲染为帧缓存,然后视频控制器读取帧缓存,并显示到屏幕。帧缓存有前帧缓存/后帧缓存,GPU计算帧缓存交替存储在前帧和后帧缓存,提高效率。视频显示器发出的垂直同步信号VSync 按照固定时间扫描帧缓存,一秒60次,并显示到屏幕上。如果垂直同步信号到来之前,还没有计算好帧缓存,就会掉针卡顿。卡顿就是主线程执行了比较耗时的操作,尽量减少CPU、GPU资源消耗。按照60FPS的刷帧率,每隔16ms就会有一次垂直同步信号VSync。1秒==1000毫秒。一秒60次,1次就是16毫秒。
图解码后的大小:像素宽 x 像素长 x 4个字节。
卡顿优化:
卡顿针对CPU做优化:因为CPU负责对象的创建/销毁,尽量用轻量级的对象:用不到事件处理的地方,可以使用CALayer取代UIView;UIView的frame、bounds、transform等属性不要频繁调整,减少不必要的修改,提前计算好布局,在有需要时一次性调整;autolayout比frame消耗更多的CPU资源;尽量把耗时的操作放到子线程:文本处理-尺寸计算、绘制,boundingRectWithSize:options:attributes:context:、drawWithRect:options:attributes:context:,并且控制线程的最大并发数;图片处理:解码、绘制(Core Graphics);图片格式转换;图片的size最好刚好跟UIImageView的size保持一致,减少伸缩带来的cpu消耗; CPU:对象 -> View -> Frame -> Text -> 图片。
卡顿案例: 发送进度的优化,存储到DB,耗时:回调有排序等操作,子线程。进度内存缓存。
卡顿针对GPU做优化:GPU负责纹理的渲染。GPU能处理的最大纹理尺寸是 4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸;减少视图数量和层级;减少透明的视图,不透明的视图就设置opaque为YES;避免短时间内大量图片的显示,尽可能将多张图合成一张进行显示;尽量避免出现离屏渲染; GPU: 纹理 -> View/不透明 -> 图片 -> 离屏渲染: 圆角->阴影->遮罩->光栅
触发离屏渲染的操作:圆角同时设置layer.masksToBounds = YES、layer.cornerRadius大于0会触发离屏渲染,可以使用CoreGraphics绘制裁剪圆角,或者使用圆角图片;阴影layer.shadow,如果设置了layer.shadowPath 就不会产生离屏渲染;遮罩layer.mask;光栅化layer.shouldRasterize=YES;
OpenGL有两种渲染方式:在当前屏幕缓冲区(前帧缓存/后帧缓存)进行渲染(On-Screen Rendering);离屏渲染(Off-Screen Rendering),在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作;在当前屏幕渲染不够用,所以需要离屏渲染。离屏渲染消耗性能的原因:需要创建新的缓冲区;离屏渲染的过程需要多次切换上下文环境,先从当前屏幕切换到离屏,等离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕;
卡顿检测:给主线程Runloop添加Observer,通过监听Runloop 状态切换时的耗时,达到监控卡顿的目的。
如何监听帧率卡顿:在子线程开启一个Runloop,监听主线程的事物执行,在kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting之间的时间,连续超过两次或者更多次也行,就是一次卡顿。 微信的卡顿检测Matrix?,2秒确认为一次卡顿。 滴滴的卡顿检测??
卡顿实战: 网络请求线程 -> 预排版线程CellLayout 提前子线程计算并缓存:布局、层级、渲染信息、模型数据-高度/富文本 -> 预解码 & 预渲染 -> 按需加载:TableView在滚动结束的时候,加载。-> 异步渲染
耗电优化: 降低CPU、GPU功耗;少用定时器;优化I/O操作:不要频繁写入小数据,批量一次性写入。读写大量重要数据时,考虑用dispatch_io,,基于GCD的异步操作文件I/O,对磁盘访问进行了优化。数据量比较大建议使用数据库(SQLite);定位优化,快速定位使用CLLocationManager的requestLocation方法,定位完成后,会自动让定位硬件断电;不是导航应用,不要实时更新位置,定位完毕就关掉定位服务;降低定位精准度,不要使用最高精度kCLLocationAccuracyBest;需要后台定位时,设置暂停自动更新pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新; 耗电优化:CPU/GPU -> 定时器 -> IO -> 定位:快速定位CLLocationManager -> 不要实时更新位置 -> 精准度 -> 后台定位暂停自动更新
网络优化:
减少不必要的数据传输,去除冗余字段;压缩网络数据gzip;使用体积较小的数据格式传输json、Protocolbuffer;多次请求的结果如果相同使用缓存;网络不可用时,不要尝试执行网络请求;让用户可以取消长时间运行、速度很慢的网络操作,设置合适的超时时间;批量传输,比如,数据请求每页20条30条。视频流下载时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载邮件,一次下载多封,不要一封一封地下载;使用断点下载,否则网络不稳定时可能多次传输相同的内容;使用分片上传;
网络优化:数据压缩 -> 缓存 -> 网络不可用 -> 超时时间 -> 打包下载
断点续传-断点下载:HTTP请求头的Range实现,需要比较本地文件和服务器文件的大小,AFN默认超时时间60秒。 分片上传,首先服务器会生成一个uploadId给到客户端,客户端请求每次带上这个uploadId标识唯一资源,会校验md5,每次分片是并发上传,单个文件不是并发;
APP的启动过程,分三大阶段:dyld -> runtime -> main 。
App启动,首先由程序dyld(dynamic link editor,Apple 的动态链接器)装载Mach-O文件包括:装载App的可执行文件,同时会递归加载所有依赖的动态库。
然后通知Runtime处理:Runtime 会调用map_images 对可执行文件内容进行解析和处理,然后在load_images 中调用call_load_methods,call_load_methods会调用所有Class 和 Category 的 +load方法,并且注册Objc类、初始化类对象等;之后调用C++静态初始化器和__attribute__((constructor))修饰的函数,这样就完成了运行环境的搭建,把可执行文件、动态库中所有的符号Class、Protocol、Selector、IMP等都加载到内存中被runtime管理。最后,dyld会调用main函数:UIApplicationMain 函数,AppDelegate的application: didFinishLaunchingWithOptions:方法。
dyld阶段:减少动态库包括:把动态库变为静态库、合并一些动态库、定期清理不必要的动态库,苹果建议动态库6个,减少binding时间。减少Objc类,分类的数量,减少Selector数量,定期清理不必要的类、分类。swift尽量使用结构体。
swift 类class 和结构体struct区别: 1、class是引用类型。struct是值类型。值类型在传递和赋值时进行复制,而引用类型则只会使用一个指向。 2、内存中,class是在堆heap上,而结构体struct是在栈stack上进行存储和操作。相比栈上的操作,堆上的操作更加复杂耗时,所以苹果推荐使用struct。
class 独有的功能: Class可以继承,子类可以使用父类的特性和方法; 类型转换可以在runtime的时候检查和解释一个实例的类型; 可以用deinit来释放资源; 一个类可以被多次引用。
Swift结构体为什么会启动更快??swift的struct值类型,不是引用类型,没有引用计数器,写时copy,所以性能会比class优化。 Struct和class的区别:1、内存分配。内存分配有两种:栈内存分配和堆内存分配。栈内存就是通过移动栈针来分配和销毁内存。堆内存需要查找合适大小的内存块类分配内存,把内存块重新插入堆来销毁内存,相对比较复杂,开销比较大。2、引用计数。堆内存安全可释放,需要根据引用计数来判断。首先引用计数加减是间接调用retain和release,另外,引用计数支持多线程,需要保证多线程安全。 3、方法派发。struct 是静态派发。编译器将函数地址直接编码在汇编中,调用的时候直接根据地址跳转到实现,编译器会进行内联等优化。 动态派发:运行时查找函数表,阻止了编译器进行内联优化。
无用代码分析可以使用:clang plugin、fui、AppCode。原理???
runtime阶段:用+initialize加dispatch_once取代所有的__attribute__((constructor))修饰的函数、C++静态构造器、Objc的+load方法。
Main阶段:在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在 finishLaunching 方法中,多个tab 只加载第一个tab的数据,按需加载。
APP启动App Delegate加载顺序:didFinishLaunchingWithOptions: -> 获取焦点didBecomeActive: -> 进入后台didEnterBackground: -> 即将失去焦点willResignActive: -> 即将回到前台 willEnterForeground: -> 内存警告,可能要终止程序 didReceiveMemory -> 即将退出 willTerminate: 。
程序的状态:Not running 未运行,程序没启动;Inactive 未激活,程序在前台运行,但没有事件可处理;Active 激活,在前台,在接收并处理事件;Backgroud 后台,且能执行代码;Suspended 挂起,在后台不能执行代码。系统把程序变成挂起状态不会发出通知。当系统内存较低时,会把挂起的程序清除掉,为前台程序提供更多的内存;
添加环境变量打印APP的启动时间:Edit scheme -> Run -> Arguments ,DYLD_PRINT_STATISTICS _DETAIL设置为1。 statistics 统计。400毫秒以内正常。
启动优化具体方案实施:方案、工具、监控:debug/线上、监控方案。都优化了哪些业务,推迟了哪些业务,异步了哪些业务。
Method originalMethod = class_getInstanceMethod(Class, SEL) class_getInstanceMethod 方法:如果这个类中没有实现selector这个方法,返回的是父类的Method。
method_exchangeImplementations(originalMethod, swizzleMethod) method_exchangeImplementations是直接交换IMP。如果自己没有实现,父类实现了,会交换失败,调用的还是之前的方法。
IMP previousIMP = class_replaceMethod(Class, SEL original, IMP swizzled, const * types) class_replaceMethod(cls, newSelector, previousIMP, method_getTypeEncoding(original)) 如果自己没有实现,父类实现了,class_replaceMethod 会调用 class_addMethod 方法,而 class_addMethod 会把父类的方法实现添加到当前类中,最终交换成功。
安全策略: 请求header增加sign签名校验;关键逻辑进行代码混淆;https本地证书校验。
加固:为了增加应用的安全性,防止应用被破解、盗版、二次打包、注入、反编译等
常见的加固方式有:数据加密,包括字符串、网络数据、敏感数据等;应用加壳,二进制加密,苹果的iPad就是这种;代码混淆,类名、方法名、代码逻辑等。
iOS可以通过class-dump、Hopper、IDA等获取类名、方法名以及分析程序的执行逻辑,代码混淆加大破解难度。
代码混淆: iOS混淆方案:源码的混淆,包括类名、方法名、协议名等;LLVM中间代码IR的混淆,容易产生BUG,自己编写Pass。
源码混淆:使用宏定义,混淆自定义方法,不能混淆系统方法,不能混淆init开头的等初始化方法,混淆属性时需要额外注意set方法,如果xib、storyboard中有了混淆的内容,需要手动修正。可以考虑把要混淆的符号都加上前缀,跟系统自带的符号进行区分。混淆过多可能会导致App Store拒绝上架,需要说明用途。
混淆字符串加密:加大破解、逆向难度,考虑对字符串进行加密。字符串的加密技术有很多,可以根据自己的需要自行制定算法。字符串,宏定义替换,存储对应替换。
Https证书校验: 证书链:最上层root,对应是CA,用来颁发证书;最下层end-user,对应每个用户使用的证书;中间层为intermediates,二级CA,这一层可以继续划分为多层,用来帮助root给end-user颁发证书,root只需要向intermediates颁发证书。 只有当整个证书链上的证书都有效的时候,才会认定当前证书合法。
每个证书中包含的信息:颁发机构,有效期,签名等。 签名:摘要的一个用途。 数字签名:对摘要加密,使用私钥加密。
上层证书公钥可以解密下层证书的签名。 一般根证书CA证书内置在操作系统中。 Charles抓包原理就是在手机操作系统中安装了Charles的根证书,并且用户信任了这个根证书,然后网络请求的时候,就偷换了自己的子证书,抓取网络数据。对于客户端来说它是服务端,对于服务端来说它是客户端。
解决:客户端使用SSl锚点校验,自己的证书是CA颁发的,如果使用Charles的公钥是无法解密锚点证书,代表验证失败,就可以认为此次网络请求是不安全的。
抓包的主要原理就是:中间人替换了原本的证书;防抓包就可以通过针对证书的校验来实现。
Https 链接建立过程: 1、客户端向服务器发送请求:TLS版本号,支持的加密算法,随机数C。 2、服务器返回非对称加密的公钥-证书,确定的加密算法,随机数S给客户端。 3、客户端验证服务器返回的证书。 4、证书验证通过,客户端根据服务器返回的证书、随机数S、随机数C生成一个会话密钥,对称加密。 5、客户端用服务器返回的公钥(证书)对话密钥进行非对称加密后传输给服务器。 6、服务器通过私钥解密得到会话密钥。 7、客户端和服务器互相传输加密的握手消息来验证安全通道是否已完成。
响应者链 responder chain:响应者链条从用户最接近的view,initial view开始查找nextResponder -> superView -> UIViewController的top view -> UIViewController -> Super ViewController 的top view -> UIWindow -> UIWindow的contentView指向UIApplication响应者链的终点,UIApplication的nextResponder指向nil。 响应链是由距离用户最近的view向系统传递。
Hit-Test View查找:按照触摸区域检查subview,首先在ViewController的topView -> topView的点击区域的subView -> 检查subView的点击区域的subView,直到找到距离用户最近的view为止,这个view就是Hit-Test View。
UIResponder处理用户事件、接收事件、传递事件,可以处理触摸事件touchesBegan、按压事件(3D Touch)pressesBegan、远程控制事件remoteControlReceivedWithEvent、硬件运动事件motionBegan。
UIControl也是通过hitTest的方式查找第一响应者的。UIControl及其子类,都是由UIApplication直接派发事件的,不通过Responder Chain派发。
图片中view等级 [ViewA addSubview:ViewB]; [ViewA addSubview:ViewC]; [ViewB addSubview:ViewD]; [ViewB addSubview:ViewE];
按照添加 addSubview: 顺序的反顺序进行查找合适的View的。
先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
UITableView的优化:Cell重用;提前计算行高和布局进行缓存;使用局部刷新,不要全部刷新reloadData;GPU渲染,少用或不用透明图层,防止离屏渲染;在初始化的时候添加子view,通过hidden控制显示隐藏;避免Cell重新布局;Cell显示web内容,使用异步加载,缓存请求结果;网络图片异步请求并缓存;显示缩略图,大图查看再显示大图,缩略图可以是服务器直接返回,或客户端处理并缓存;图片数量很多,滚动速度很快时,图片延迟加载,避免频繁请求服务器数据;按需加载Cell,cell滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后几行加载;复杂界面,朋友圈图文混排,需要异步绘制,draw方法,利用GCD异步绘制,或者直接重写drawRect方法,drawRect本身是异步绘制。绘制cell建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是CPU。CALayer使用的是Core Animation,CPU、GPU通吃,由系统决定使用哪个。View的绘制使用的是自下向上的一层一层的绘制,然后渲染Layer处理的是Texure,利用GPU的Texture Cache和独立的浮点数计算单元加速纹理的处理。GPU不喜欢透明,不要绘制透明的。layer的回调里面一定只作绘图,不做计算。cell重用的时候,内部绘制的内容并不会被自动清除,因此需要调用setNeedsDisplay或setNeedsDisplayInRect:方法;
metal渲染原理:
- 顶点着色器:构建顶点信息
- 图元装配:顶点连接起来,变成形状
- 几何着色器:将三维形态用二维表示
- 光栅化:就是把图像映射成最终需要点亮屏幕的像素
- 片段着色器
- 测试与融合
AutoLayout的原理与性能 autolayout基于一个engine,我们需要声明横纵轴上的数量关系,等于列出一条条的等式,即可能是某个元素,距离左边右边分别是100,就有x+200=屏幕宽度,这个引擎就是帮我们算出每个视图最终实际上的位置即大小,然后最终还是回到是用frame去设置位置。在iOS12后,这个得到优化,二者性能能做到差不多。但是由于刚刚说的,是一个类似函数等于,因此如果层级多了,或者元素多的时候,这个等于计算可能就会花更多的时间。
Constraints,Layout,Display三者关系。
update
,setNeeds
,IfNeeded
分别
- updateConstraints,setNeedsUpdateConstraints,updateConstraintsIfNeeded
- layoutSubView,setNeedsLayout,layoutIfNeed
- draw(),setNeedsDisplay, ----------
区别:
update
方法复写可以自己提供额外到操作,即当限制更新了,布局更新了,绘制有内容了,自己可以在这个时机后面做事情。例如布局更新了,你需要再之前基础上另外做新的限制,即可以。setNeeds
:即只是设置需要。不会立刻马上调!。不过这个更新不会立刻做,只是打上脏标记,等到runloop进行下一个绘制流程,才会调用实际update
方法。ifNeeded
:不需要等待到下个,判断是否需要,如果需要马上调!
第一次显示图像不如再次显示图像流畅的问题:可以预加载图像并缓存,减少解压和重采样的CPU时间消耗,可以预先在一个bitmap context 画出图像再缓存这个图像。 UIGraphicsBeginImageContextWithOptions、UIGraphicsGetImageFromCurrentImageContext、UIGraphicsEndImageContext,scale设置为0自动设置正确的比例。
SDWebImage:设计架构,充分体现了软件设计中的依赖倒置原则,上层模块更倾向于依赖抽象接口。业务层 UIView 只关心是否创建成功一个任务,所以 UIView 分类依赖 SDWebImageManager,把任务交给 SDWebImageManager,任务开启后返回一个 operation。任务缓存:使用NSMapTable,默认使用当前类的类名作为key,对缓存获取、移除使用了 @synchronized(self)。
SDWebImageManager 依赖 SDImageCache、SDImageLoader抽象接口。上层模块依赖抽象接口。 下层具体实现模块 SDImageCache、SDImageDownloader 也依赖 SDImageCache、SDImageLoader抽象接口。 解决了:业务(UIView分类)和具体的功能类(下层具体实现模块 SDImageCache、SDImageDownloader)之间的耦合。
缓存 SDImageCache 实现,也是依赖抽象接口 SDImageCache,下层具体实现 SDMemoryCache、SDDiskCache依赖接口,实现了接口。
内存缓存有两份:NSCache 内部、NSMapTable。用空间来换取缓存的命中率,避免下载这个耗时的操作。weakCache 不影响存储的对象的生命周期。
NSMapTable 、NSMutableDictionary、NSCache区别: NSMutableDictionary 的key 遵循 NSCopying协议。
NSMapTable 可以指定 key、value的内存管理方式。 NSMapTable 是工厂类,真实类是 NSConcreteMapTable: NSMapTable。线程不安全。 底层是一个数组,数组元素是 GSIMapBucket 结构体,GSIMapBucket 结构是一个链表,链表节点是 GSIMapNode。bucket 的 index 是使用 key hash值计算而来 :buckets + hash % bucketCount。从数组中查找bucket。
NSCache :线程安全的。key是 strong 的,不会对key进行copy。key释放时机:对象本身释放,调用removeObject;程序进入后台会释放;缓存大小达到某个阈值(缓存内存大小或缓存时间)会释放,并不是很严格的,有缓存淘汰策略。当系统内存警告时,会自动删减缓存。 NSCache 底层实现:使用功能传入的 key 生成 NSCacheKey 对象,NSCacheKey 内部实现了 isEqual、hash 两个方法,传入的key在内部做比较用。 value 是 NSCacheEntry 对象,NSCacheEntry 是一个双向链表。
NSHashTable:可以持有弱引用,而且在对象被销毁的时候能正确的将其移除。
weak指针的实现原理
当一个对象要释放的时候,会自动调用dealloc,接下来的调用轨迹是
dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance(里面调用clearDeallocating)
free
其实就是将那些弱引用存到一个hash表里面,当对象要销毁的时候,取出当前对象对应的弱引用表,将弱引用表里面的那些弱引用给清除掉。
ARC都帮我们做了什么?
LLVM编译器和Runtime系统相互协作的一个结果.
首先利用LLVM编译器帮我们自动生成内存管理相关的代码(比如retain, release),然后在程序运行过程中帮我们处理弱引用这种操作。
OC对象的内存管理:
在iOS中使用引用计数来管理OC对象,一个新创建的OC对象的引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
调用retain会让对象的引用计数+1, 调用release会让对象的引用计数-1
当调用copy, new, alloc, mutableCopy方法的时候,当不需要这个对象的时候,需要指向release或者autorelease操作来释放它
想要拥有一个对象,就让它的引用计数+1,不想再拥有的时候,就让他的引用计数-1\