面试总结六部曲
- 1、iOS基础系列(一):底层分析
- 2、iOS基础系列(二):零碎知识点
- 3、iOS基础系列(三):各种优化
- 4、iOS基础系列(四):扩展问题
- 5、iOS基础系列(五):Swift总结
- 6、iOS基础系列(六):Flutter总结
1、UIKit
1、UIViewController
视图的生命历程
- 1、initWithCoder:或initWithNibName:Bundle首先从归档文件中加载UIViewController对象。即使是纯代码,也会把nil作为参数传给后者。
- 2、awakeFromNib作为第一个方法的助手,方法处理一些额外的设置
- 3、loadView创建或加载一个view并把它赋值给UIViewController的view属性
- 4、viewDidLoad此时整个视图层次(view hierarchy)已经放到内存中,可以移除一些视图,修改约束,加载数据等
- 5、viewWillAppear视图加载完成,并即将显示在屏幕上。还没设置动画,可以改变当前屏幕方向或状态栏的风格等
- 6、viewWillLayoutSubviews即将开始子视图位置布局
- 7、viewDidLayoutSubviews用于通知视图的位置布局已经完成
- 8、viewDidAppear视图已经展示在屏幕上,可以对视图做一些关于展示效果方面的修改。
- 9、viewWillDisappear视图即将消失
- 10、viewDidDisappear视图已经消失
- 11、dealloc视图销毁的时候调用
总结
- 只有init系列的方法,如initWithNibName需要自己调用,其他方法如loadView和awakeFromNib则是系统自动调用
- 纯代码写视图布局时需要注意,要手动调用loadView方法,而且不要调用父类的loadView方法。纯代码和用IB的区别仅存在于loadView方法及其之前,编程时需要注意的也就是loadView方法。
- 除了initWithNibName和awakeFromNib方法是处理视图控制器外,其他方法都是处理视图。这两个方法在视图控制器的生命周期里只会调用一次。
loadView
这个方法中,要正式加载View了。首先我们得知道,控制器 view 是通过懒加载的方式进行加载的,即用到的时候再加载。永远不要主动调用这个方法。当我们用到控制器 view 时,就会调用控制器 view 的 get 方法,在 get 方法内部,首先判断 view 是否已经创建,如果已存在,则直接返回存在的 view,如果不存在,则调用控制器的 loadView 方法,在控制器没有被销毁的情况下,loadView 也可能会被执行多次。
当 ViewController 有以下情况时都会在此方法中从 nib 文件加载 view :
- ViewController 是从 storyboard 中实例化的。
- 通过 initWithNibName:bundle: 初始化。
- 在 App Bundle 中有一个 nib 文件名称和本类名相同。
符合以上三点时,也就不需要重写这个方法,否则你无法得到你想要的 nib 中的 view。
如果这个 ViewController 与 nib 无关,你可以在这里手写 ViewController 的 view (这一步大概也可以在 viewDidLoad 里写,实际上我们也更常在 viewDidLoad 里写)。
2、UIControl
UIControl是UIView的子类,当然也是UIResponder的子类。UIControl是诸如UIButton、UISwitch、UITextField等控件的父类,它本身也包含了一些属性和方法,但是不能直接使用UIControl类,它只是定义了子类都需要使用的方法
3、UITableView
1、UITableViewCell的重用机制原理
查看UITableView头文件,会找到NSMutableArrayvisiableCells,和NSMutableDictneryreusableTableCells两个结构。visiableCells内保存当前显示的cells,reusableTableCells保存可重 用的cells
TableView显示之初,reusableTableCells为空,那么tableView dequeueReusableCellWithIdentifier:CellIdentifier返回nil。开始的cell都是通过 [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]来创建,而且cellForRowAtIndexPath只是调用最大显示cell数的 次数
比如:有100条数据,iPhone一屏最多显示10个cell。程序最开始显示TableView的情况是:
- **1、**用[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]创建10次cell,并给cell指定同样的重用标识(当然,可以为不同显示类型的 cell指定不同的标识)。并且10个cell全部都加入到visiableCells数组,reusableTableCells为空。
- **2、**向下拖动tableView,当cell1完全移出屏幕,并且cell11(它也是alloc出来的,原因同上)完全显示出来的时候。cell11加入到 visiableCells,cell1移出visiableCells,cell1加入到reusableTableCells。
- **3、**接着向下拖动tableView,因为reusableTableCells中已经有值,所以,当需要显示新的 cell,cellForRowAtIndexPath再次被调用的时候,tableView dequeueReusableCellWithIdentifier:CellIdentifier,返回cell1。cell1加入到 visiableCells,cell1移出reusableTableCells;cell2移出visiableCells,cell2加入到 reusableTableCells。之后再需要显示的Cell就可以正常重用了。
一些情况
使用过程中,我注意到,并不是只有拖动超出屏幕的时候才会更新reusableTableCells表,还有:
- **1、**reloadData,这种情况比较特殊。一般是部分数据发生变化,需要重新刷新cell显示的内容时调用。在 cellForRowAtIndexPath调用中,所有cell都是重用的。我估计reloadData调用后,把visiableCells中所有 cell移入reusableTableCells,visiableCells清空。cellForRowAtIndexPath调用后,再把 reuse的cell从reusableTableCells取出来,放入到visiableCells。
- **2、**reloadRowsAtIndex,刷新指定的IndexPath。如果调用时reusableTableCells为空,那么 cellForRowAtIndexPath调用后,是新创建cell,新的cell加入到visiableCells。老的cell移出 visiableCells,加入到reusableTableCells。于是,之后的刷新就有cell做reuse了。
2、如何进行 cell 高度的缓存
1、如何进行 cell 高度的缓存?说一下UITableView-FDTemplateLayoutCell的实现原理?
缓存 cell 高度:
- 如果用的 frame ,则给 model 添加一个 cellH 的属性,然后在获取数据时计算好高度赋值给 cellH。
- 如果用的 AutoLayout,创建相应布局等同的 cell,计算好高度然后缓存。
FD 的实现:
fd_heightForCellWithIdentifier: configuration:方法会根据 identifier 以及 configuration block 提供一个和 cell 布局相同的 template layout cell,并将其传入fd_systemFittingHeightForConfiguratedCell:这个私有方法返回计算出的高度。主要使用技术为 runtime 。
3、列表流畅度优化
在view显示的过程中,CPU和GPU都各自承担了不同的任务,CPU和GPU不论哪个阻碍了显示流程,都会造成掉帧现象。所以优化方法也需要分别对CPU和GPU压力进行评估和优化,在CPU和GPU压力之间找到性能最优的平衡点, 无论过度优化哪一方导致另一方压力过大都会造成整体FPS性能的下降。而寻找平衡点的过程则因项目特点不同而不同,并没有一套通用的方法,需要我们用instrument等性能评测工具,根据实际app的性能度量结果去做优化
CPU优化
在滑动过程中CPU占用特点是:
- 滑动时CPU占用率高、空闲时CPU占用率底
- 主线程CPU占用高、子线程CPU占底
1、预加载,空间换时间
为什么要预加载:
- 滑动时CPU占用过高,16.67ms内无法完成内容提交—>导致卡顿
- 滑动时CPU占用率高,但空闲时CPU占用率底—>CPU占用分布特点
- 利用CPU空闲时间预加载,降低滑动时CPU占用峰值—>解决卡顿
预加载内容:
静态资源预加载
- 如何预加载:创建列表前找时机加载。如启动时、viewDidLoad、runloop空闲时等等
- 加载内容:缓存在磁盘的网络数据、图片、其他滑动时需要的耗时的资源
- 注意事项:在预加载带来的滑动性能提升和内存占用增加之间权衡
动态资源预加载
- 如何预加载:
- 在iOS10以后,UITableView和UICollectionView提供了预加载机制,iOS12开始prefeatching做了优化,不再与cell的加载同时并发进行,而是cell加载完成之后串行开始prefeatch,从而优化了流畅度
- iOS10以前,也可以自己实现类似机制,主要利用的机制有:
- UIScrollViewDelegate 提供滑动开始、结束、速度时机回调
- indexPathsForRowsInRect 和layoutAttributesForElementsInRect 提供预加载的indexPath
- 可根据滑动速度动态调整加载的量
- 加载内容:
- Cell的高度、subView的布局计算
- 拉取网络数据
- 网络图片
- 其他耗时的资源
- 注意事项:
- 在预加载带来的滑动性能提升和内存占用增加之间权衡
- 注意数据过期的问题
2、多线程优化
为什么要多线程:
- UIKit 大部分API只能在主线程调用, 特别是一些耗时的操作,如view的创建,布局和渲染默认都是在主线程上完成
- 主线程任务过多,16.67ms内无法完成,导致卡顿
- 将非主线程必须的任务,移到子线程中,减轻主线程负担
- 多核处理器,多线程可以发挥多核并发优势,提高性能
可在子线程中进行的任务
- 1、图片解码
- 2、文本渲染,UILabel和UITextview都是在主线程渲染的,当显示大量文本时,CPU的压力会非常大。特别是对于一些资讯类应用,这部分耗时相当大,对流畅度的影响也十分明显。
- 3、UIView的drawRect, 由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行
- 4、耗时的业务逻辑
3、缓存
缓存的内容可以是
- UIView。 view的创建代价很大,一些可以复用的view可以cache。例如UITableView为我们实现的了cell的复用。
- 图片。 图片涉及磁盘IO和解码,十分耗时,可以考虑缓存。
- 布局。其实不仅仅是cell的高度可以缓存,如果cell里面有大量的文字图片等复杂元素,cell的subView的布局也可以在第一次计算好,用Model的key来缓存。避免频繁多次的调整布局属性。在滑动列表(UITableView和UICollectionView)中强烈不建议使用Autolayout。随着视图数量的增长,Autolayout带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:pilky.me/36/。在WWDC20…
- 数据, 网络拉取的数据或者db中的数据
- 其他创建耗时,可重复利用的资源。 如NSDateFormatter等
GPU优化
- 减少视图数量和层次,可把多个视图预先渲染为一张图片
- 不要让图片和视图超过GPU可渲染的最大尺寸
- 视图不透明
- 防止离屏渲染 OpenGL 中,GPU 屏幕渲染有以下两种方式:
- On-Screen Rendering 意为当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区中进行。
- Off-Screen Rendering 意为离屏渲染,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
- 相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
- 创建新缓冲区要想进行离屏渲染,首先要创建一个新的缓冲区。
- 上下文切换离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
- 所以在图形生成的步骤我们要尽可能的避免离屏渲染
4、UIView
UIView和CALayer关系
首先从继承关系来分析两者:
- UIView : UIResponder,UIView是对视图的抽象类,它主要用来负责数据的存储和操作逻辑的实现
- CALayer : NSObject,CALayer则是对视图在屏幕上的渲染和显示信息的抽象类
UIView 中持有一个 layer 对象,同时这个 layer 对象 delegate,UIView 和 CALayer 协同工作。
平时我们对 UIView 设置 frame、center、bounds 等位置信息,其实都是 UIView 对 CALayer 进一层封装,使得我们可以很方便地设置控件的位置;例如圆角、阴影等属性, UIView 就没有进一步封装,所以我们还是需要去设置 Layer 的属性来实现功能。
绘制相关
CALayer 在屏幕上绘制东西是因为 CALayer 内部有一个 contents (CGImage)的属性,contents 也被称为寄宿图。
平时为自定义 View 添加空间或者在上下文画图都会使用到drawRect,但是如果当我们实现了这个方法的时候,这个时候会生成一张寄宿图,这个寄宿图的尺寸是 layer 的宽 * 高 * contentsScale,其实算出来的是有多少像素。然后每个像素占用 4 个字节,总共消耗的内存大小为:宽 * 高 * contentsScale * 4 字节。
所以在使用 drawRect 方法来实现功能之前,需要看看是否有替代方案,避免产生寄宿图增加程序的内存,使用 CAShapeLayer 来绘制是一个不错的方案。
UIView中frame属性的内部实现
一个视图最终渲染出来的位置和尺寸需要通过设置视图或者层的center、bounds、transform、anchorPoint四个属性来完成
Auto Layout 是怎么进行自动布局的
Auto Layout ,是苹果公司提供的一个基于约束布局,动态计算视图大小和位置的库,并且已经集成到了 Xcode 开发环境里
Auto Layout拥有一套Layout Engine引擎,由它来主导页面的布局。App启动后,主线程的Run Loop会一直处于监听状态,当约束发生变化后会触发Deffered Layout Pass(延迟布局传递),在里面做容错处理(约束丢失等情况)并把view标识为dirty状态,然后Run Loop再次进入监听阶段。当下一次刷新屏幕动作来临(或者是调用layoutIfNeeded)时,Layout Engine 会从上到下调用 layoutSubviews() ,通过 Cassowary算法计算各个子视图的位置,算出来后将子视图的frame从Layout Engine拷贝出来,接下来的过程就跟手写frame是一样的了
view绘制渲染机制和runloop什么关系
- 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
- 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
5、UILabel的绘制
UILabel的绘制
每一项设置都会增加UILabel的渲染代价,具体步骤如下:
- 1、使用字体样式及要被渲染的文本时,计算需要的像素数,这是一个消耗较大的过程,应尽量少做。
- 2、检查要被渲染的宽度。
- 3、检查numberOfLines,计算将要展示的行数。
- 4、sizeToFit是否被调用,调用则计算高度,未调用则检查当前尺寸是否能展示完整内容。
- 5、如果size不够展示,则使用lineBreakMode确定隐藏或截断的位置。
- 6、检查其他配置选项,如纯文本or富文本,富文本的样式,对齐方式,自动收缩等。
- 7、最后使用字体、类型及颜色等渲染最终显示的文本。
ps:有时会发现label的文字变模糊,那么你需要检查一下label的frame是否都为整数
UILabel的成本超出你的想象
在内存使用方面,我们倾向于将lables视为轻量级的。最后,它们只是显示文本。UILabel实际上存储为位图,这很容易消耗兆字节的内存。
如果label是单色的,UILabel将选择kCAContentsFormatGray8Uint的calayercontents格式(每像素1字节),而非单色标签(例如,要显示”?是聚会时间了”,或多色NSAttributedString)将需要使用kCAContentsFormatRGBA8Uint(每像素4字节)。
单色标签最多消耗width * height * contentsScale ^ 2 *(每像素1字节)字节,而非单色标签则消耗4倍的:width * height * contentsScale ^ 2 *(每像素4字节)
例如,在iPhone 11 Pro Max上,大小为414 * 100 points的lable最多可消耗:
- 414 * 100 * 3 ^ 2 * 1 = 372.6kB(单色)
- 414 * 100 * 3 ^ 2 * 4 =〜1.49MB(非单色)
当这些cells进入重用队列时,一种常见的反模式是使UITableView / UICollectionView cell labels填充文本内容。一旦cells被回收,label的文本值很可能会有所不同,因此存储它们很浪费。
要释放潜在的兆字节内存:
- 如果将label的文本设置为隐藏,则将label的文本设置为nil,仅偶尔显示它们。
- 如果label的文本显示在UITableView / UICollectionView cell中,则将label的文本设置为nil,在:
tableView(_:didEndDisplaying:forRowAt:)
collectionView(_:didEndDisplaying:forItemAt:)
复制代码
sizeToFit 和 sizeThatFits的使用区别
- sizeToFit:会计算出最优的 size 而且会改变自己的size
- sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size
6、setNeedsDisplay和layoutIfNeeded
1、layoutSubviews在以下情况下会被调用/被触发
- 1、init初始化不会触发layoutSubviews,但是是用initWithFrame 进行初始化时,当rect的值 非CGRectZero时,也会触发。
- 2、addSubview会触发layoutSubviews
- 3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
- 4、滚动一个UIScrollView会触发layoutSubviews
- 5、旋转Screen会触发父UIView上的layoutSubviews事件
- 6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
- 7、调用setLayoutSubviews,调用LayoutIfNeeds
- 8、当删除子视图的时候是会调用的,添加子视图也会调用这个方法,当子视图的frame发生改变的时候是会调用的
2、layoutIfNeeded
- 如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
- 若需要立即刷新view的frame更改:(同时调用,注意先后顺序)先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
- 在初始化方法init..。、或者view第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]
3、drawRect(重写此方法,执行重绘任务)
drawRect在以下情况下会被调用:
- 1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的。所以不用担心在控制器中,这些View的drawRect就开始画了。这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值)。
- 2、该方法在调用sizeToFit后被调用。所以可以先调用sizeToFit计算出size,然后系统自动调用drawRect:方法。
- 3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
- 4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。以上1,2推荐;而3,4不提倡drawRect方法使用注意点:
- 5、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
- 6、若使用CALayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法3。若要实时画图,不能使用gestureRecognizer,只能使用touchBegan等方法来调用setNeedsDisplay实时刷新屏幕
4、总结
- layoutSubviews对subviews重新布局
- layoutSubviews方法调用先于drawRect
- setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews
- layoutIfNeeded方法如其名,UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded方法应该是这样的
- layoutIfNeeded遍历的不是superview链,应该是subviews链
- drawRect是对receiver的重绘,能获得context
- setNeedDisplay在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘
7、事件响应机制
传递过程
应用如何找到最合适的控件来处理事件?
- 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
- 2.判断触摸点是否在自己身上
- 3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
- 4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
- 5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
- 6、UIViewController没有hitTest:withEvent:方法,所以控制器不参与查找响应视图的过程。但是控制器在响应者链中,如果控制器的View不处理事件,会交给控制器来处理。控制器不处理的话,再交给View的下一级响应者处理。
UIView不能接收触摸事件的三种情况:
- 不允许交互:userInteractionEnabled = NO
- 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
- 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明
hitTest:withEvent:方法
- 什么时候调用?
- 只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
- 作用
- 寻找并返回最合适的view(能够响应事件的那个最合适的view)
响应过程
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
- UIApplication
- UIViewController
- UIView
响应者链的事件传递过程:
- 1、如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
- 2、在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
- 3、如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 4、如果UIApplication也不能处理该事件或消息,则将其丢弃
手势识别器
手势优先级比事件响应链高,当有事件时会先判断是否为手势,如果不满足手势,则走响应链。
实际上判断是否为手势需要一段时间,在这段时间会先走响应链,如果手势被识别,则会取消响应链。
事件冲突
案例一 手势影响响应链的情况
解决办法:
我们可以在控制器中实现UIGestureRecognizerDelegate代理,实现gestureRecognizer:shouldReceiveTouch:方法,然后作如下判断。
// 当返回YES时处理本次手势事件,当返回为NO时不处理本次手势事件
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (touch.view != self.view) {
return NO;
}
return YES;
}
复制代码
这样当判断点击的视图不为view时,就不处理本次手势。响应链也就不会被取消。
案例二 多个手势之间的冲突
当一个页面上存在多个手势时,他们之间可能会出现一些干扰导致较差的用户体验。
情况1:在一个视图上同时加上轻扫手势(swipe)和拖动手势(pan)时,会出现轻扫手势较难触发的问题。(swipe与pan手势冲突)
情况2:有一个很多app都有的小功能,在页面上加一个悬浮的小球,然后可以拖动点击它,如果拖动使用了拖动手势(pan),同时它的父视图上又加了轻扫手势(swipe),比如视图控制器的轻扫返回,那么在快速拖动小球时可能会触发轻扫手势。(pan与swipe手势冲突)
情况3:同上的悬浮小球,如果它自身或者它的父视图加了长按手势(longPress),按住小球时会触发长按而不触发拖动,导致小球无法拖动。(pan与longPress手势冲突)
情况4:假如页面中有一个小视图,小视图上有一个点击手势点击后做一个动画。此外给控制器的view添加点击手势,用于处理输入框缩回键盘。结果是点击小视图只会走它自己的响应方法,不会处理缩回键盘。(pan与longPress手势冲突)
解决办法
对于情况1、情况2、情况3都可以通过设置手势之间的优先关系来解决。
设置手势之间的优先关系有两种方法:
// 第一种:
// 使用requireGestureRecognizerToFail:方法
// 表示当手势2识别失败时才会识别手势1(手势2优先于手势1)
[gestureRecognizer1 requireGestureRecognizerToFail:gestureRecognizer2];
// 第二种:
// 实现手势的代理方法
gestureRecognizer.delegate = self
// 返回YES时表示,gestureRecognizer优先于otherGestureRecognizer
// 返回NO时意义相反
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// 可在这里使用gestureRecognizer和otherGestureRecognizer做一些自己想要的判断
return YES;
}
// 返回YES时表示,otherGestureRecognizer优先于gestureRecognizer
// 返回NO时意义相反
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
// 可在这里使用gestureRecognizer和otherGestureRecognizer做一些自己想要的判断
return YES;
}
// 如果上面的两个方法都实现了,那么只会走gestureRecognizer:shouldRequireFailureOfGestureRecognizer:方法。
复制代码
对于情况4:
可实现如下的手势代理方法,使两个手势的事件都触发
// 当遇到设置代理的手势和其他手势发生冲突时,会走该方法
// 当返回YES时,两个手势的响应事件都会触发
// 当返回NO时,会根据后续的优先级来判断触发哪个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
复制代码
8、keyWindow 和 delegate的window有何区别
其实我们平常写的最多的也就是keyWindow的写法
UIWindow *window = [UIApplication sharedApplication].keyWindow
可是有的地方我们还是要写成这样子的
UIWindow *window = [UIApplication sharedApplication].delegate.window
复制代码
那么,这两者的区别是什么呢?
keyWindow与delegate中的window其实是一样的,keyWindow的存在的意义,其实就是为了说明当前的window接管了这个控制器的view而已,你可以在keyWindow上加载你自己的建立的view了。
之前想要拿到app的窗口,我们通常的写法是:
[UIApplication sharedApplication].keyWindow
这种写法之前一直也觉得是正确的,没什么问题,而且网上大多数的博客或者资料中也是这样写的,但是最近在项目,发现这样写是不安全的,如果应用程序没有跳转,这种写法还算是可行的,但是如果应用程序出现了跳转(分享跳转到其他APP,访问系统相册等),这时返回原APP,你会发现加载原窗口上的视图位置会发生明显偏移,查阅了一些资料,发现如果写成
[[[UIApplication sharedApplication]delegate]window]
就不会出现上述问题,
2、Foundation
1、#import 跟 #include 、@class
- #import指令是Object-C针对#include的改进版本,#import确保引用的文件只会被引用一次,这样你就不会陷入递归包含的问题中
- #import会链入该头文件的全部信息,包括实体变量和方法等;而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑
2、宏和常量的区别
const简介:之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。
- 编译时刻:宏是预编译(编译之前处理),const是编译阶段。
- 编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误
- 宏的好处:宏能定义一些函数,方法。 const不能。
- 宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。
【注意】:很多Blog都说使用宏,会消耗很多内存,我这验证并不会生成很多内存,宏定义的是常量,常量都放在常量区,只会生成一份内存。
3、id 和 instanceType 有什么区别?
instancetype是clang 3.5开始,clang提供的一个关键字,表示某个方法返回的未知类型的Objective-C对象
1、相同点
都可以作为方法的返回类型
2、不同点
①instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;
②instancetype只能作为返回值,不能像id那样作为参数,但是有利于编译器在编译阶段发现问题
4、self和super的区别
- self
- 1.是关键字
- 2.代表当前方法的调用者。如果是类方法:代表当前类;如果是对象方法:代表当前类的对象
- super:编译器指令
[self message]和[super message]的实现
其实不管是self还是super真正调用的对象都是一样的,只是查找方法的位置不一样,self是从当前类结构中开始查找,super是从父类中查找,但方法真正的接受者都是当前类或者当前类的对象
[self message]:
会转化为objc_msgSend(id self,SEL _cmd)这个函数,在当前类结构中找到方法并且调用
[super message]
会转化为id objc_msgSendSuper(struct __rw_objc_super *super, SEL op, …)
,对比[self message]这里除了函数名加了super以外,第一个参数由self变成了一个结构体,下面让我们来解开这个结构体的真面目:
struct __rw_objc_super {
struct objc_object *object; //代表当前类的对象
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
复制代码
这个结构体中有两个参数:object的对象和一个superClass的结构体指针,这里的object相当于上面的self
在执行[super message]时,会做下面的事
- 编译器会先构造一个__rw_objc_super的结构体
- 然后去superClass的方法列表中找方法
- 找到之后由object调用。所以当你用[self Class]和[super Class]打印类的时候,打印的都是同一个类,因为他们只是查找方法的位置不同,但是调用方法的类/对象是一样的.
为什要写self = [super init]?
因为在Xcode中,你输入init然后tab就会帮你补全这个方法,以至于我一直都忽略了为什么在[super init]之后还要赋值给self,然后进行判断,其实这和类簇有关系,我们不能保证init的内存和alloc出来的内存是同一块内存,像NSString在alloc和init之后的对象分别是NSPlaceholderString和__NSCFConstantString*造成[super init]之后的内存被改变,所以在[super init]之后是nil,因此我们不能保证alloc和init的是同一块内存,加上这样的判断是为了提高容错性,如果init成功就返回对象,否则返回nil。
5、推送通知
总体上分为
- 1、本地推送
- 2、远程推送
1、本地推送
- **概念:**由APP本身给应用程序推送消息,不需要服务器的支持
- **常见场景:**记账软件定时提醒记账/番茄工作法中提醒你时间等等
流程
- 直接由应用程序(程序中写入对应代码)给用户发出通知
- 本地通知需要用到一个重要的类:UILocalNotification
- 本地通知的实现步骤:
- (1)、创建本地通知
- (2)、设置本地通知要发出的内容等信息
- 发出时间
- 发出内容
- 播放的音效
- (3)、调度本地通知
2、远程推送
什么是远程通知 ?
- **概念:**由服务器发送消息给用户弹出消息的通知(需要联网)
- 远程推送服务,又称为APNs(Apple Push Notification Services)
- APNs 通知:是指通过向 Apple APNs 服务器发送通知,到达 iOS 设备,由 iOS 系统提供展现的推送。用户可以通过 IOS 系统的 “设置” >> “通知” 进行设置,开启或者关闭某一个 App 的推送能力。
为什么苹果服务器可以推消息给用户?
- 所有的苹果设备,在联网状态下,都会与苹果的服务器建立长连接
- 苹果建立长连接的作用: 时间校准、系统升级提示、查找我的iPhone、远程通知 等等
- 常见疑惑:苹果在推送消息时,如何准确的推送给某一个用户,并且知道是哪一个APP ?
- 在服务器把消息给苹果的APNs服务器时,必须告知苹果DeviceToken
- 什么是DeviceToken?
- DeviceToken是由用户手机的UDID和应用程序的BundleID共同生成的
- 通过DeviceToken可以找到唯一手机中的唯一应用程序
- 如何获得DeviceToken:客户端到苹果的APNs注册即可获得。
步骤
- 1、我们的设备安装了具有推送功能的应用(应用程序要用代码注册消息推动),我们的 iOS设备在有网络的情况下会连接APNs推送服务器,连接过程中,APNS 服务器会验证devicetoken,连接成功后维持一个基于TCP 的长连接;
- 2、Provider(我们自己的应用服务器)收到需要被推送的消息并结合被推送的 iOS设备的devicetoken一起打包发送给APNS服务器;
- 3、APNS服务器将推送信息推送给指定devicetoken的iOS设备;
6、NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程
同步。子线程。
7、Instrument
8、NSCache
- NSCache是苹果官方提供的缓存类,具体使用和NSMutableDictionary类似,在AFN和SDWebImage框架中被使用来管理缓存
- 官方解释NSCache在系统内存很低时,会自动释放对象 ( 但是注意 , 这里还有点文章 , 本文会讲 )
- NSCache是线程安全的,在多线程操作中,不需要对NSCache加锁
- NSCache的Key只是对对象进行Strong引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小 , 其key不需要实现NSCoping协议. ( 这一点不太了解的同学可以类比NSMapTable去学习)
9、LLDB
LLDB
LLDB 是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。
- expression:可简写为e,作用为执行一个表达式
- 1、查询当前堆栈变量的值
- 2、动态修改当前线程堆栈变量的值
- po:po的作用为打印对象,我们可以通过它打印出对象,而不是打印对象的指针
- p即是print,也是expression –的缩写,与po不同,它不会打出对象的详细信息,只会打印出一个$符号,数字,再加上一段地址信息
- bt即是thread backtrace,作用是打印出当前线程的堆栈信息。
- frame select指令我们可以任意的去选择一个作用域去查看
Chisel
Chisel是facebook开源的一个LLDB命令的集合,它里面简化和扩展了LLDB的命令。使用它会更方便的调试我们的程序
- **pviews:**这个命令可以按层级递归打印指定view的所有subView,相当于 UIView 的私有辅助方法 [view recursiveDescription]
- **pvc:**这个命令递归打印出viewController的层级,相当于 UIViewController 的一个私有辅助方法 [UIViewController _printHierarchy] :
10、基本数据类型
数据类型 | 16位编译器 | 32位编译器 | 64位编译器 |
---|---|---|---|
char | 1byte | 1byte | 1byte |
int | 2byte | 4byte | 4byte |
float | 4byte | 4byte | 4byte |
double | 8byte | 8byte | 8byte |
short int | 2byte | 2byte | 2byte |
unsigned int | 2byte | 4byte | 4byte |
long | 4byte | 4byte | 8byte |
unsigned long | 4byte | 4byte | 8byte |
long long | 8byte | 8byte | 8byte |
11、armv7,armv7s,arm64,i386,x86_64
ARM处理器,特点是体积小、低功耗、低成本、高性能,所以几乎所有手机处理器都基于ARM,在嵌入式系统中应用广泛
armv6|armv7|armv7s|arm64都是ARM处理器的指令集,这些指令集都是向下兼容的,例如armv7指令集兼容armv6,只是使用armv6的时候无法发挥出其性能,无法使用armv7的新特性,从而会导致程序执行效率没那么高
模拟器32位处理器测试需要i386架构,(iphone5,iphone5s以下的模拟器)
模拟器64位处理器测试需要x86_64架构,(iphone6以上的模拟器)
真机32位处理器需要armv7,或者armv7s架构,(iphone4真机/armv7, ipnone5,iphone5s真机/armv7s)
真机64位处理器需要arm64架构。(iphone6,iphone6p以上的真机)
12、5种常见的消息传递机制
常见的消息传递方法有5种
- 1、KVO
- 2、通知
- 3、delegation
- 4、block
- 5、target-action
事件总线
事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
这种模式本质是通过 Promise 对象保存异步数据操作,同时 Promise 对象提供统一的异步数据操作事件处理的接口。这样,事件总线的数据订阅和数据发布事件,就可以通过 Promise 对象提供的接口实现出来
事件总线是对发布和订阅设计模式的一种实现,通过发布、订阅可以将组件间一对一和一对多的耦合关系解开。这种设计模式,特别适合数据层通过异步发布数据的方式告知 UI 层订阅者,使得 UI 层和数据层可以不用耦合在一起,在重构数据层或者 UI 层时不影响业务层
13、iOS内存映射mmap详解
文件映射是将文件的磁盘扇区映射到进程的虚拟内存空间的过程。一旦被映射,您的应用程序就会访问这个文件,就好像它完全驻留在内存中一样(不占用内存,使用的是虚拟内存)。当您从映射的文件指针读取数据时,将在适当的数据中的内核页面并将其返回给您的应用程序。
适合的场景
- 您有一个很大的文件,其内容您想要随机访问一个或多个时间。
- 您有一个小文件,它的内容您想要立即读入内存并经常访问。这种技术最适合那些大小不超过几个虚拟内存页的文件。(页是地址空间的最小单位,虚拟页和物理页的大小是一样的,通常为4KB。)
- 您需要在内存中缓存文件的特定部分。文件映射消除了缓存数据的需要,这使得系统磁盘缓存中的其他数据空间更大。
当随机访问一个非常大的文件时,通常最好只映射文件的一小部分。映射大文件的问题是文件会消耗活动内存。如果文件足够大,系统可能会被迫将其他部分的内存分页以加载文件。将多个文件映射到内存中会使这个问题更加复杂。
不适合的场景
- 您希望从开始到结束的顺序从头到尾读取一个文件。
- 这个文件有几百兆字节或者更大。将大文件映射到内存中会快速地填充内存,并可能导致分页,这将抵消首先映射文件的好处。对于大型顺序读取操作,禁用磁盘缓存并将文件读入一个小内存缓冲区。
- 该文件大于可用的连续虚拟内存地址空间。对于64位应用程序来说,这不是什么问题,但是对于32位应用程序来说,这是一个问题。
- 该文件位于可移动驱动器上。
- 该文件位于网络驱动器上。
14、iOS开发中Hash 的身影
哈希表
哈希表(hash table,也叫散列表),是根据键(key)直接访问访问在内存储存位置的数据结构。
哈希表本质是一个数组,数组中的每一个元素成为一个箱子,箱子中存放的是键值对。根据下标index从数组中取value。关键是如何获取index,这就需要一个固定的函数(哈希函数),将key转换成index。不论哈希函数设计的如何完美,都可能出现不同的key经过hash处理后得到相同的hash值,这时候就需要处理哈希冲突。
哈希查找步骤
- 1、使用哈希函数将被查找的键映射(转换)为数组的索引,理想情况下(hash函数设计合理)不同的键映射的数组下标也不同,所有的查找时间复杂度为O(1)。但是实际情况下不是这样的,所以哈希查找的第二步就是处理哈希冲突。
- 2、处理哈希碰撞冲突。处理方法有很多,比如拉链法、线性探测法。
哈希表存储过程
- 1、使用hash函数根据key得到哈希值h
- 2、如果箱子的个数为n,那么值应该存放在底(h%n)个箱子中。h%n的值范围为[0, n-1]。
- 3、如果该箱子非空(已经存放了一个值)即不同的key得到了相同的h产生了哈希冲突,此时需要使用拉链法或者开放定址线性探测法解决冲突。
1、拉链法
简单来说就是 数组 + 链表 。将键通过hash函数映射为大小为M的数组的下标索引,数组的每一个元素指向一个链表,链表中的每一个结点存储着hash出来的索引值为结点下标的键值对。
2、开放定址线性探测发
使用两个大小为N的数组(一个存放keys,另一个存放values)。使用数组中的空位解决碰撞,当碰撞发生时(即一个键的hash值对应数组的下标被两外一个键占用)直接将下标索引加一(index += 1),这样会出现三种结果:
- 1、未命中(数组下标中的值为空,没有占用)。keys[index] = key,values[index] = value。
- 2、命中(数组下标中的值不为空,占用)。keys[index] == key,values[index] == value。
- 3、命中(数组下标中的值不为空,占用)。keys[index] != key,继续index += 1,直到遇到结果1或2停止。
3、Web
1、前端与原生的桥梁:JavaScriptCore
JavaScriptCore,原本是 WebKit 中用来解释执行 JavaScript 代码的核心引擎。
正是因为 JavaScriptCore 的这种桥梁作用,所以出现了很多使用 JavaScriptCore 开发 App 的框架 ,比如 React Native、Weex、小程序、WebView Hybird 等框架
JavaScriptCore框架构成
virtual [ˈvɜːtʃuəl] 虚拟的
Machine[məˈʃiːn]机械,机器;机构;机械般工作的人
JavaScriptCore 框架主要由 JSVirtualMachine 、JSContext、JSValue 类组成
- JSVirturalMachine 的作用,是为 JavaScript 代码的运行提供一个虚拟机环境
- JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript 的数据传递。
- JSValue 是 JavaScript 的值对象,用来记录 JavaScript 的原始值,并提供进行原生值对象转换的接口方法
可以看到,每个 JavaScriptCore 中的 JSVirtualMachine 对应着一个原生线程,同一个 JSVirtualMachine 中可以使用 JSValue 与原生线程通信,遵循的是 JSExport 协议:原生线程可以将类方法和属性提供给 JavaScriptCore 使用,JavaScriptCore 可以将 JSValue 提供给原生线程使用
JavaScriptCore 和原生应用要想交互,首先要有 JSContext。JSContext 直接使用 init 初始化,会默认使用系统创建的 JSVirtualMachine
OC调用JS
- 本质:JS代码中已经定义好变量和方法,通过OC去获取,并且调用
- 步骤:
- 1.创建JS运行环境
- 2.执行JS代码
- 3.获取JS数据(变量,方法)
- 4.使用JS数据,方法
JS调用OC
1、JS调用OC中的block
- 本质:一开始JS中并没有OC的block,所以没法直接调用OC的block,需要把OC的block,在JS中生成方法,然后在通过JS调用。
- 步骤:
- 1.创建JS运行环境
- 2.在JS中生成对应的OC代码
- 3.使用JS调用,在JS环境中生成的block方法,就能调用到OC的block中.
2、JS调用OC中的类
- 本质:一开始JS中并没有OC的类,需要先在JS中生成OC的类,然后在通过JS调用。
- 步骤
-
1.OC类必须遵守JSExport协议,只要遵守JSExport协议,JS才会生成这个类
-
2.但是还不够,类里面有属性和方法,也要在JS中生成
-
3.JSExport本身不自带属性和方法,需要自定义一个协议,继承JSExport,在自己的协议中暴露需要在JS中用到的属性和方法
-
4.这样自己的类只要继承自己的协议就好,JS就会自动生成类,包括自己协议中声明的属性和方法
-
2、UIWebview和JS的交互
H5调用原生方法
1、通过拦截加载的URL
相信iOS的小伙伴应该都知道Delegate这个玩意儿,在UIWebview中就存在UIWebViewDelegate这个玩意儿,拦截URL就是通过代理中的一个方法实现的
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
复制代码
2、系统自带JavaScriptCore库里的,JSContext类。
这个是系统类库里的玩意儿,用起来也是比较简单方便的,这个和上面方法的处理时机不太一样,是在另一个代理方法里面实现的。
- (void)webViewDidFinishLoad:(UIWebView *)webView
复制代码
这个方法就是当WebView在加载完成之后执行的。
还有一点就是通过 JSContext 可以有两种方式去实现交互的功能。
原生调用H5方法
1、UIWebView 执行 JavaScriptString。
原生调用H5就更简单啦,一句话搞定
// webView对象在合适的时机,调用这个方法就行啦。
//入参就是一个JavaScriptString。
//changeString('haahhahah') 他的意思就是调用H5的 changeString 方法,传入参数'haahhahah'。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
复制代码
2、JSContext 执行 JavaScriptString。
// context 这个玩意儿还记得么?就是在页面加载完成时,通过
//JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//代码获取到的
[self.context evaluateScript:@"changeString('haahhahah')"];
复制代码
3、WKWebView和JS的交互
H5调用原生方法
1、通过拦截加载的URL。
这个和UIWebview那边区别不大,就是拦截URL的代理变掉了。通过下面代码就可以发现喽,代理方法里面执行的逻辑和UIWebview一眼的额。
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
//获取url里的相关参数(就是把URL里?后面的玩意儿取出来)
//如下:
//http://127.0.0.1:8080/index.html?message=ocuiwebviewurl拦截&flag=yjkwap://wjj.com&action=helloOC'
//这个玩意儿获取到的 dict = {@"message":@"ocuiwebviewurl拦截", @"flag":@"yjkwap://wjj.com", @"action":@"helloOC"}
NSDictionary *dict = [YJKTool urlPramaDictionaryWithUrlString:request.URL.absoluteString];
//存在约定的规则就进行拦截(这里约定的规则就是flag=yjkwap://wjj.com)
if ([[dict valueForKey:@"flag"] isEqualToString:@"yjkwap://wjj.com"]) {
//执行相关的动作(action代表要执行的动作,就是要调用原生的啥方法)
if ([[dict valueForKey:@"action"] isEqualToString:@"helloOC"]) {
// message 就是H5给我们传递过来的参数
//在这里搞事情
// ... 此处省略十来行代码 ...
// 最后要阻止页面的加载
decisionHandler(WKNavigationActionPolicyCancel);
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
复制代码
2、WKScriptMessageHandler协议
先初始化 WKWebView 对象
//弄一个 WKWebViewConfiguration 对象
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//然后 configuration.userContentController 对象初始化一下哦
configuration.userContentController = [[WKUserContentController alloc] init];
//添加 helloOC 方法给H5小伙伴调用哦
[configuration.userContentController addScriptMessageHandler:self name:@"helloOC"];
//下面就是创建 WKWebView 对象的相关代码,没啥好看的哦
self.wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.wkWebView.navigationDelegate = self;
[self.wkWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://192.168.2.50:8080/index.html"]]];
[self.view addSubview:self.wkWebView];
复制代码
helloOC 方法的具体实现使用下面的代理就好啦
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// message.name 获取到对应的方法名
if ([message.name isEqualToString:@"helloOC"]) {
// message.body H5传过来的参数,具体啥类型要看H5所传参数是啥类型哦
NSDictionary *dict = message.body;
}
}
复制代码
3、WKWebView-协议拦截
#pragma mark - WKNavigationDelegate
//! WKWeView在每次加载请求前会调用此方法来确认是否进行请求跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if ([navigationAction.request.URL.scheme caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) {
[WKWebViewInterceptController showAlertWithTitle:navigationAction.request.URL.host message:navigationAction.request.URL.query cancelHandler:nil];
decisionHandler(WKNavigationActionPolicyCancel);
}
else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
复制代码
实现原理:
- 1、JS与iOS约定好jsToOc协议,用作JS在调用iOS时url的scheme;
- 2、JS在登录成功后加载含有token数据的url:(jsToOc://loginSucceed?js_tokenString);
- 3、iOS的WKWebView在请求跳转前会调用-webView:decidePolicyForNavigationAction:decisionHandler:方法来确认是否允许跳转;
- 4、iOS在此方法内截取jsToOc协议获取JS传过来的数据,用UIAlertController显示出来,并通过decisionHandler不允许此请求跳转。
原生调用H5方法
WKWebView 执行 JavaScriptString。
原生要做的就是下面这句话,H5的做法和 UIWebview 那边一样的。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
复制代码
4、iOS 端 h5 页面秒开优化实践
首先来看,在 iOS 平台加载一个 H5 网页,需要经过哪些步骤:
初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片
由于在 dom 渲染前的用户看到的页面都是白屏,优化思路具体也是去分析在 dom 渲染前每个步骤的耗时,去优化性价比最高的部分。这里面又可以分为前端能做的优化,以及客户端能做的优化,前端这个需要前端那边配合,暂且不在这篇文章中讨论,这边文章主要讨论的是客户端能做的优化思路。总体思路大概也是这样:
- 1、能够缓存的就尽量缓存,用空间换时间。这里可以去拦截的 h5 页面的所有资源请求,包括 html、css/js,图片、数据等,右客户端来接管资源的缓存策略(包括缓存的最大空间占用,缓存的淘汰算法、缓存过期等策略);
- 2、能够预加载的,就提前预加载。可以预先处理一些耗时的操作,如在 App 启动的时候就提前初始化好 webview 等待使用;
- 3、能够并行的的,就并行进行,利用设备的多核能力。如在加载 webview 的时候就可以同时去加载需要的资源;
一般来说,WebView 渲染需要经过下面几个步骤
- 解析 HTML 文件
- 加载 JavaScript 和 CSS 文件
- 解析并执行 JavaScript
- 构建 DOM 结构
- 加载图片等资源
- 页面加载完毕
白屏
目前可以想到最直观的方案就是对 WebView 进行截图,遍历截图的像素点的颜色值,如果非白屏颜色的颜色点超过一定的阈值,就可以认为不是白屏,目前需要考虑的是这个方案的性能问题和检测时机。
iOS 中提供了 WebView 快照的接口获取当前 WebView 渲染的内容,底层采用异步回调的实现方式,API 耗时 10ms 左右,用户基本无感知。
- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));
复制代码
白屏优化
我们通过白屏检测和上报之后的数据分析之后发现,非网络原因导致的详情页的白屏问题大体是 WebView 加载的问题。
在 iOS 中,我们使用的是系统提供的 WKWebView,WKWebView 是运行在一个独立进程中的组件,所以当 WKWebView 上占用内存过大时,WKWebView 所在的 WebContent Process 会被系统 kill 掉,反映在用户体验上就是发生了白屏。
根据网上的做法,我们可以在 WKWebView 提供的回调webViewWebContentProcessDidTerminate函数中通过 reload 方法重新加载当前页面恢复,但是这种情况只适用于通过 loadRequest 加载的请求,在详情页中,由于使用了模板化的 WebView 中,重新 reload 只能重新 reload 模板,并不能正常恢复整个详情页,需要客户端重新加载模板之后再重新注入数据。
当然不管是 iOS 和 Android, WebView 加载的逻辑都比较复杂,有时候怎么重试也无法成功,这个时候我们会直接降级到加载线上的详情页,优先保证用户的体验。
5、iOS WKWebView+UITableView混排
1、基础
详情页展示的是纯文本,我们仅需在 WebViw 代理webView(_, didFinish)方法通过执行document.body.scrollHeightJS代码注入获取文档高度,再将高度反馈给 TableView 进行刷新即可,该高度即为文档渲染的正确高度。
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 获取 HTML 文档高度
webView.evaluateJavaScript("document.body.scrollHeight") { (heightValue, error) in
guard let height = heightValue as? CGFloat else { return }
self.heightAction?(height)
}
}
复制代码
2、观察者模式
当详情页展示的是少量的图文混排后,因为图片的加载是一件耗时操作的事情,我们通常将其设定为懒加载模式,当页面内的资源加载完毕后我们再获取其高度,才为正确的文档高度。所以在webView(_, didFinish)里获取其高度也不是准确的,该代理方法是 WebView 载入 HTML 文档完成后的回调,并不等于该 HTML 完全渲染完后的回调。
contentSize 方法:根据以上判断,我们需要捕捉 HTML 渲染变化的信号。而 HTML 渲染动作直接影响到的是 WebViewscrollView.contentSize属性,每当该值发生变化代表的是当前 HTML文档 已渲染到的位置。
3、JS 监听
在 HTML DOM 中 Event 有个函数onload是用于一张页面或一幅图像完成加载时所执行的,我们需要监听所有的img标签 或body标签,然后在这个方法里发个消息给 WebKit 然后进行拦截即可。
接下来我们要做两件事:
- 1、在 WebView 里注册一个方法,用以接收 HTML DOM 的事件;
- 2、在 HTML 里补充 JS 脚本,用以发送消息给 WebView ;
6、WKWebView默认缓存策略与HTTP缓存协议
- 1、缓存不存在,则直接请求。
- 2、缓存存在,且缓存response头没有指明每次必须校验资源更新(revalidated这个词可能会产生误导,后文说),且缓存没有过期,则系统会直接返回缓存,不会发起请求。
- 3、如果缓存过期了或者要求每次必须校验资源更新,则会发起一个校验资源更新的请求,如果(服务器告诉客户端)资源有更新则使用服务器返回来的新数据,如果资源没有更新则使用本地缓存。
实际上,WKWebView默认缓存策略完全遵循HTTP缓存协议,苹果并没有做额外的事情
4、数据安全及加密
1、对称加密和非对称加密
- 1、对称加密又称公开密钥加密,加密和解密都会用到同一个密钥,如果密钥被攻击者获得,此时加密就失去了意义。常见的对称加密算法有DES、3DES、AES、Blowfish、IDEA、RC5、RC6。
- 2、非对称加密又称共享密钥加密,使用一对非对称的密钥,一把叫做私有密钥,另一把叫做公有密钥;公钥加密只能用私钥来解密,私钥加密只能用公钥来解密。
对称加密:
优点:算法简单,加密解密容易,效率高,执行快。
缺点:相对来说不算特别安全,只有一把钥匙,密文如果被拦截,且密钥也被劫持,那么,信息很容易被破译。
非对称加密:
优点:安全,即使密文被拦截、公钥被获取,但是无法获取到私钥,也就无法破译密文。作为接收方,务必要保管好自己的密钥。
缺点:加密算法及其复杂,安全性依赖算法与密钥,而且加密和解密效率很低
2、数字证书
数字证书有点类似于我们的居民身份证,只是数字证书是基于互联网通信的,用于标记通信双方身份的一种方式。数字证书是由权威机构Certificate Authority发行的,又称之为证书授权,简称为:CA。人们在网上可以根据它来识别对方身份信息。
3、数字签名
数字签名是指将摘要信息使用接收者的公钥进行加密,与密文一起发送给接收者。接收者使用自己的私钥对摘要信息进行解密,然后使用Hash函数对收到的密文产生一个摘要信息,然后将摘要信息与发送着传输过来解密后的摘要信息对比是否一致。如果一致,则表明数据信息没有被篡改。
也就是说,数字签名能够验证收到的信息的完整性,避免中途信息被劫持篡改或丢失。对方可以根据数字签名来判断获取到的数据信息时候是最原始的数据。
数字签名中用私钥加密,公钥解密
4、iOS的签名机制
iOS App 目前有以下几种安装方式:
-
1、AppStore 下载的 App可以在手机上安装。
-
2、开发过程中,可以直接 App 安装进手机进行调试。
-
3、In-House 企业内部分发,可以直接安装企业证书签名后的 APP。
-
4、AD-Hoc 相当于企业分发的限制版,它限制了安装设备的数量。
1、线上 App 签名机制
-
1、由苹果生成一对公私钥,公钥内置与iOS设备中,私钥由苹果保管。
-
2、开发者上传App给苹果审核后,苹果用私钥对App数据进行签名,发布至App Store。
-
3、iOS设备下载App后,用公钥进行验证,若正确,则证明App是由苹果认证过的。
2、通过Xcode安装(真机调试)
当开发 App 的时候,需要频繁的安装 App 到手机上,如果每次都先将 App 包传给苹果服务器,得到授权后才可以安装,那对开发者来说简直是灾难,而事实上,苹果也确实没有这么做,而是可以直接安装在手机上。
不过,苹果依然要求对 App 的安装有控制权,
-
1、 必须经过苹果允许才可以安装;
-
2、权利不能被滥用,非开发状态的 App 不允许安装。
由于不需要提交苹果审核,所以苹果没办法对App进行签名,因此苹果采用了双重签名的机制。Mac电脑有一对公私钥,苹果还是原来的一对公私钥。
步骤
-
1、开发时需要真机测试时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。
-
2、Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。
-
3、编译完一个App后,Mac电脑使用私钥对App进行签名。
-
4、在安装App时,根据当前配置把CER证书一起打包进App。
-
5、iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥时经过苹果认证的。
-
6、再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。
3、通过Ad-Hoc正式打包安装
Xcode打包App生成ipa文件,通过iTunes或者蒲公英等第三方发布平台,安装到手机上。流程步骤基本和真机调试相同,差别在于第4步:
-
1、开发时需要打包测试或发布时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。
-
2、Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。
-
3、编译完一个App后,Mac电脑使用私钥对App进行签名。
-
4、编译签名完之后,要导出ipa文件,导出时,需要选择一个保存的方法(App Store/Ad Hoc/Enterprise/Development),就是选择将上一步生成的CER一起打包进App。
-
5、iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥是经过苹果认证的。
-
6、再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。
4、In-House企业版证书打包
企业版证书签名验证流程和Ad-Hoc差不多。只是企业版不限制设备数,而且需要用户在iOS设备上手动点击信任证书。
5、HTTP
1、基础概念
网络七层协议
- 1、物理层:负责0、1 比特流与电压的高低、光的闪灭之间的互换。
- 2、数据链路层:主要负责互联设备之间传送和识别数据帧。
- 3、网络层:将数据传输到目标地址的过程中,目标地址可以是由多个路由器连接而成的某个地址。因此这一层主要负责地址管理和路由选择
- 4、传输层:主要起到传输作用。只在通信双方节点上进行处理,无需在路由器上处理。可靠传输,即确保数据可靠地传送到目标地址。
- 5、会话层:负责建立、断开连接通信,以及数据分割等数据传输相关的管理。
- 6、表示层:将应用处理的信息转为适合网络传输的格式,或将来自下一层的数据转换为上层能够处理的格式。如文字、声音、图像等数据格式。
- 7、应用层:为用用程序提供服务并规定通信细节。如文件传输、电子邮件、远程登录协议。
为什么已经有了网络七层协议还要提出TCP/IP协议四层协议呢
OSI是一个完整的、完善的宏观模型,他包括了硬件层(物理层),当然也包含了很多上面途中没有列出的协议(比如DNS解析协议等);而TCP/IP(参考)模型,更加侧重的是互联网通信核心(也是就是围绕TCP/IP协议展开的一系列通信协议)的分层,因此它不包括物理层,以及其他一些不想干的协议;其次,之所以说他是参考模型,是因为他本身也是OSI模型中的一部分,因此参考OSI模型对其分层
TCP协议
按层次分,TCP位于传输层,提供可靠的字节流服务
可靠的传输服务
是指能够把数据准确的可靠的传输给对方,这就引出了TCP的三次握手
握手过程中使用了TCP的标志(flag):SYN(synronize 同步)和 ACK(acknowledgement 确认)
发送端首先发送一个带SYN标志的数据包给对方,接受端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息,最后,发送端再回传一个带ACK标志的数据包,代表握手结束
若握手过程中某个阶段中断,TCP协议会再次以相同的顺序发送相同的数据包
UDP协议
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,
UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
它有以下几个特点:
- 1、面向无连接:首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了
- 2、有单播,多播,广播的功能:UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
- 3、UDP是面向报文的:发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
- 4、不可靠性:首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
MAC地址
MAC地址也叫物理地址、硬件地址或链路地址,由网络设备制造商生产时写在硬件内部。这个地址与网络无关,也即无论将带有这个地址的硬件(如网卡、集线器、路由器等)接入到网络的何处,它都有相同的MAC地址,MAC地址一般不可改变,不能由用户自己设定。
IP地址
对于IP地址,相信大家都很熟悉,即指使用TCP/IP协议指定给主机的32位地址。IP地址由用点分隔开的4个8八位组构成,如192.168.0.1就是一个IP地址,这种写法叫点分十进制格式。IP地址由网络地址和主机地址两部分组成,分配给这两部分的位数随地址类(A类、B类、C类等)的不同而不同。网络地址用于路由选择,而主机地址用于在网络或子网内部寻找一个单独的主机。一个IP地址使得将来自源地址的数据通过路由而传送到目的地址变为可能。
IP协议
IP(Internet Protocol)网络协议,位于网络层。IP协议的作用是把各种数据包传送给对方。而要保证确实传送到对方那里,则需要满足各类条件。其中最重要的两个条件就是IP地址和MAC地址。
IP是使用ARP协议凭借MAC地址进行通信的
IP间的通信依赖MAC地址。在网络上,通信的双方在同一局域网(LAN)内的情况是很少的,通常是经过多台计算机和网络设备中转才能连接到对方。而在进行转移的时候,会利用下一站中转设备的MAC地址来搜索下一个中转目标。这时就会采用ARP(Address Resolution Protocol)。ARP是一种以解析地址的协议,根据通信方的IP地址就可以发查处对应的MAC地址。
URL和URI
- 1、URL(Uniform Resoure Locator,统一资源定位符)
- 2、URI(Uniform Resoure Identifier ,统一资源标示符)
URI标记了一个网络资源,仅此而已; URL标记了一个WWW互联网资源(用地址标记),并给出了他的访问地址。
2、HTTP组成
可以从以下三点来了解HTTP协议
- 1、报文
- 2、通讯数据转发
- 3、状态码
3、HTTPS
HTTP的不足主要有三个方面
- 1、通讯使用明文,内容会被窃听
- 2、不验证通讯方的身份,因此有可能遭遇伪装
- 3、无法证明报文的完整行,所以可能被篡改
HTTPS = HTTP + 加密 + 认证
- 1、加密:
HTTPS采用了混合加密机制
。在交换密钥环节使用非对称加密方式,之后的建立通讯交换报文阶段使用对称加密方式
- 2、认证
遗憾的是公开密钥加密还有一个问题,就是无法证明公开密钥本身就是货真价实的公开密钥
为了解决这个问题,可以使用数字证书认证机构和其他相关机关颁布的公开密钥证书。
4、Socket
本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:
- 消息传递(管道、FIFO、消息队列)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 共享内存(匿名的和具名的)
- 远程过程调用(Solaris门和Sun RPC)
网络中进程之间如何通信
我们要理解网络中进程如何通信,得解决两个问题:
- 1、我们要如何标识一台主机,即怎样确定我们将要通信的进程是在那一台主机上运行。
- TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机
- 2、我们要如何标识唯一进程,本地通过pid标识,网络中应该怎样标识?
- 传输层的“协议+端口”可以唯一标识主机中的应用程序(进程),因此,我们利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互
首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
什么是Socket
在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket起源于 Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作 (读/写IO、打开、关闭)
Socket怎么通信
网络中进程间如何通信,即利用三元组【ip地址,协议,端口】可以进行网络间通信了,那我们应该怎么实现了,因此,我们socket应运而生,它就是利用三元组解决网络通信的一个中间件工具,就目前而言,几乎所有的应用程序都是采用socket,如UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰)。
Socket通信的数据传输方式,常用的有两种:
a、SOCK_STREAM:表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
b、SOCK_DGRAM:表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
例如:QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响
socket中TCP的四次握手释放连接详解
建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
- 1、客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
- 2、服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。
- 3、客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
- 4、等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
- 5、客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
- 6、服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态
第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
5、从浏览器输入URL到页面展示出来,中间发生了什么
总体来说,分为如下几个部分
- 1、DNS解析
- 2、建立TCP连接,发送HTTP请求
- 3、服务端处理请求并返回HTTP响应
- 4、浏览器解析渲染页面
- 5、关闭连接
6、DNS
DNS是 Domain Name System 的缩写,也就是 域名解析系统,它的作用非常简单,就是根据域名查出对应的 IP地址
DNS 解析过程
- 1、从”根域名服务器”查到”顶级域名服务器”的NS记录和A记录(IP地址)
- 2、从”顶级域名服务器”查到”次级域名服务器”的NS记录和A记录(IP地址)
- 3、从”次级域名服务器”查出”主机名”的IP地址
DNS劫持
通过上面的讲解,我们都知道了,DNS 完成了一次域名到 IP 的映射查询,当你在访问 www.baidu.com 时,能正确返回给你 百度首页的 ip。
但如果此时 DNS 解析出现了一些问题,当你想要访问 www.baidu.com 时,却返回给你 www.google.com 的ip,这就是我们常说的 DNS 劫持。
你一定见过当你在访问 某个网站时,右下角也突然弹出了一个扎眼的广告弹窗。这就是 HTTP 劫持。
借助别人文章里的例子,它们俩的区别就好比是
- 1、DNS劫持是你想去机场的时候,把你给丢到火车站。
- 2、HTTP劫持是你去机场途中,有人给你塞小广告。
那么 DNS劫持 是如何产生的呢?
下面大概说几种DNS劫持方法:
- 1、本机DNS劫持
攻击者通过某些手段使用户的计算机感染上木马病毒,或者恶意软件之后,恶意修改本地DNS配置,比如修改本地hosts文件,缓存等
- 2、路由DNS劫持
很多用户默认路由器的默认密码,攻击者可以侵入到路由管理员账号中,修改路由器的默认配置。
- 3、攻击DNS服务器
直接攻击DNS服务器,例如对DNS服务器进行DDOS攻击,可以是DNS服务器宕机,出现异常请求,还可以利用某些手段感染dns服务器的缓存,使给用户返回来的是恶意的ip地址。
7、iOS 客户端 HTTPS 防中间人攻击实践
-
第一是通讯内容本身加密,无论是走 http 还是 https,request 和 response 的内容本身都要先做一次加密,这样即使 https 的流量被破解,攻击者还需要再攻破一层加密算法。我们一般使用 AES 256 对内容做加密,这里 AES 密钥的管理也有两种方式,其一是在客户端使用固定的密钥,为了加大破解的难度,我们可以对密钥本身做多次加密处理,使用时再在内存里解密出来真正的密钥。其二是每次会话都使用不同的密钥,原理类似 Forward Secrecy,即使流量被记录,将来被暴力破解,也能极大的增加攻击者破解的时间成本。
-
第二种就是大家所熟知的 ssl pinning。在客户端进行代码层面的证书校验,校验方式也有两种,一是证书本身校验,二是公钥校验。
8、网络层的优化方案
正常一条网络请求需要经过的流程是这样:
- 1、DNS 解析,请求DNS服务器,获取域名对应的 IP 地址。
- 2、与服务端建立连接,包括 tcp 三次握手,安全协议同步流程。
- 3、连接建立完成,发送和接收数据,解码数据。
6、iOS 渲染原理解析
1. 计算机渲染原理
- CPU(Central Processing Unit):现代计算机整个系统的运算核心、控制核心。
- GPU(Graphics Processing Unit):可进行绘图运算工作的专用微处理器,是连接计算机和显示终端的纽带。
GPU 可以拿到上一个阶段传递下来的图元信息,GPU 会对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:
- 顶点着色器(Vertex Shader):这个阶段中会将图元中的顶点信息进行视角转换、添加光照信息、增加纹理等操作。
- 形状装配(Shape Assembly):图元中的三角形、线段、点分别对应三个 Vertex、两个 Vertex、一个 Vertex。这个阶段会将 Vertex 连接成相对应的形状。
- 几何着色器(Geometry Shader):额外添加额外的Vertex,将原始图元转换成新图元,以构建一个不一样的模型。简单来说就是基于通过三角形、线段和点构建更复杂的几何图形
2、屏幕成像与卡顿
在图像渲染流程结束之后,接下来就需要将得到的像素信息显示在物理屏幕上了。GPU 最后一步渲染结束之后像素信息,被存在帧缓冲器(Framebuffer)中,之后视频控制器(Video Controller)会读取帧缓冲器中的信息,经过数模转换传递给显示器(Monitor),进行显示。完整的流程如下图所示:
经过 GPU 处理之后的像素集合,也就是位图,会被帧缓冲器缓存起来,供之后的显示使用。显示器的电子束会从屏幕的左上角开始逐行扫描,屏幕上的每个点的图像信息都从帧缓冲器中的位图进行读取,在屏幕上对应地显示。扫描的流程如下图所示
电子束扫描的过程中,屏幕就能呈现出对应的结果,每次整个屏幕被扫描完一次后,就相当于呈现了一帧完整的图像。屏幕不断地刷新,不停呈现新的帧,就能呈现出连续的影像。而这个屏幕刷新的频率,就是帧率(Frame per Second,FPS)。由于人眼的视觉暂留效应,当屏幕刷新频率足够高时(FPS 通常是 50 到 60 左右),就能让画面看起来是连续而流畅的。对于 iOS 而言,app 应该尽量保证 60 FPS 才是最好的体验
屏幕撕裂 Screen Tearing
CPU+GPU 的渲染流程是一个非常耗时的过程。如果在电子束开始扫描新的一帧时,位图还没有渲染好,而是在扫描到屏幕中间时才渲染完成,被放入帧缓冲器中 —- 那么已扫描的部分就是上一帧的画面,而未扫描的部分则会显示新的一帧图像,这就造成屏幕撕裂
垂直同步 Vsync
垂直同步信号(vertical synchronisation,Vsync)相当于给帧缓冲器加锁:当电子束完成一帧的扫描,将要从头开始扫描时,就会发出一个垂直同步信号。只有当视频控制器接收到 Vsync 之后,才会将帧缓冲器中的位图更新为下一帧,这样就能保证每次显示的都是同一帧的画面,因而避免了屏幕撕裂。
掉帧
启用 Vsync 信号以及双缓冲机制之后,能够解决屏幕撕裂的问题,但是会引入新的问题:掉帧。如果在接收到 Vsync 之时 CPU 和 GPU 还没有渲染好新的位图,视频控制器就不会去替换 frame buffer 中的位图。这时屏幕就会重新扫描呈现出上一帧一模一样的画面。相当于两个周期显示了同样的画面,这就是所谓掉帧的情况
三缓冲 Triple Buffering
我们注意到在发生掉帧的时候,CPU 和 GPU 有一段时间处于闲置状态:当 A 的内容正在被扫描显示在屏幕上,而 B 的内容已经被渲染好,此时 CPU 和 GPU 就处于闲置状态。那么如果我们增加一个帧缓冲器,就可以利用这段时间进行下一步的渲染,并将渲染结果暂存于新增的帧缓冲器中
屏幕卡顿的本质
手机使用卡顿的直接原因,就是掉帧。前文也说过,屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。
这样看来,可以大概总结一下
- 屏幕卡顿的根本原因:CPU 和 GPU 渲染流水线耗时过长,导致掉帧。
- Vsync 与双缓冲的意义:强制同步屏幕刷新,以掉帧为代价解决屏幕撕裂问题。
- 三缓冲的意义:合理使用 CPU、GPU 渲染性能,减少掉帧次数。
3、Offscreen Rendering 离屏渲染
App 通过 CPU 和 GPU 的合作,不停地将内容渲染完成放入 Framebuffer 帧缓冲器中,而显示屏幕不断地从 Framebuffer 中获取内容,显示实时的内容。
与普通情况下 GPU 直接将渲染好的内容放入 Framebuffer 中不同,需要先额外创建离屏渲染缓冲区 Offscreen Buffer,将提前渲染好的内容放入其中,等到合适的时机再将 Offscreen Buffer 中的内容进一步叠加、渲染,完成后将结果切换到 Framebuffer 中。
离屏渲染的效率问题
从上面的流程来看,离屏渲染时由于 App 需要提前对部分内容进行额外的渲染并保存到 Offscreen Buffer,以及需要在必要时刻对 Offscreen Buffer 和 Framebuffer 进行内容切换,所以会需要更长的处理时间(实际上这两步关于 buffer 的切换代价都非常大)。
为什么使用离屏渲染
那么为什么要使用离屏渲染呢?主要是因为下面这两种原因:
- 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态,所以不得不使用离屏渲染。
- 处于效率目的,可以将内容提前渲染保存在 Offscreen Buffer 中,达到复用的目的。
对于第一种情况,也就是不得不使用离屏渲染的情况,一般都是系统自动触发的,比如阴影、圆角等等。
shouldRasterize 光栅化
开启光栅化后,会触发离屏渲染,Render Server 会强制将 CALayer 的渲染位图结果 bitmap 保存下来,这样下次再需要渲染时就可以直接复用,从而提高效率。
而保存的 bitmap 包含 layer 的 subLayer、圆角、阴影、组透明度 group opacity 等,所以如果 layer 的构成包含上述几种元素,结构复杂且需要反复利用,那么就可以考虑打开光栅化。
圆角、阴影、组透明度等会由系统自动触发离屏渲染,那么打开光栅化可以节约第二次及以后的渲染时间。而多层 subLayer 的情况由于不会自动触发离屏渲染,所以相比之下会多花费第一次离屏渲染的时间,但是可以节约后续的重复渲染的开销。
不过使用光栅化的时候需要注意以下几点:
- 如果 layer 不能被复用,则没有必要打开光栅化
- 如果 layer 不是静态,需要被频繁修改,比如处于动画之中,那么开启离屏渲染反而影响效率
- 离屏渲染缓存内容有时间限制,缓存内容 100ms 内如果没有被使用,那么就会被丢弃,无法进行复用
- 离屏渲染缓存空间有限,超过 2.5 倍屏幕像素大小的话也会失效,无法复用
触发离屏渲染原因的总结
总结一下,下面几种情况会触发离屏渲染:
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer (layer.masksToBounds/view.clipsToBounds)
- 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/layer.opacity)
- 添加了投影的 layer (layer.shadow*)
- 采用了光栅化的 layer (layer.shouldRasterize)
- 绘制了文字的 layer (UILabel,CATextLayer,Core Text等)
不过,需要注意的是,重写drawRect:方法并不会触发离屏渲染。前文中我们提到过,重写drawRect:会将 GPU 中的渲染操作转移到 CPU 中完成,并且需要额外开辟内存空间。但根据苹果工程师的说法,这和标准意义上的离屏渲染并不一样,在 Instrument 中开启 Color offscreen rendered yellow 调试时也会发现这并不会被判断为离屏渲染。
7、图片
1、将 UIImage 保存到磁盘,用什么方式最好?
目前来说,保存 UIImage 有三种方式:
- 1.直接用 NSKeyedArchiver 把 UIImage 序列化保存,
- 2.用 UIImagePNGRepresentation() 先把图片转为 PNG 保存,
- 3.用 UIImageJPEGRepresentation() 把图片压缩成 JPEG 保存。
representation[,reprizen’teiʃən]:代表;表现;表示法;陈述
实际上,NSKeyedArchiver 是调用了 UIImagePNGRepresentation 进行序列化的,用它来保存图片是消耗最大的。苹果对 JPEG 有硬编码和硬解码,保存成 JPEG 会大大缩减编码解码时间,也能减小文件体积。所以如果图片不包含透明像素时,UIImageJPEGRepresentation(0.9) 是最佳的图片保存方式,其次是 UIImagePNGRepresentation()。
2、UIImage 缓存是怎么回事?
通过 imageNamed 创建 UIImage 时,系统实际上只是在 Bundle 内查找到文件名,然后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。据我观察,在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。
3、我要是用 imageWithData 能不能避免缓存呢?
不能。通过数据创建 UIImage 时,UIImage 底层是调用 ImageIO 的 CGImageSourceCreateWithData() 方法。该方法有个参数叫 ShouldCache,在 64 位的设备上,这个参数是默认开启的。这个图片也是同样在第一次显示到屏幕时才会被解码,随后解码数据被缓存到 CGImage 内部。与 imageNamed 创建的图片不同,如果这个图片被释放掉,其内部的解码数据也会被立刻释放。
4、怎么能避免缓存呢?
- 手动调用 CGImageSourceCreateWithData() 来创建图片,并把 ShouldCache 和 ShouldCacheImmediately 关掉。这么做会导致每次图片显示到屏幕时,解码方法都会被调用,造成很大的 CPU 占用。
- 把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。这也是常见的网络图片库的做法。
5、我能直接取到图片解码后的数据,而不是通过画布取到吗?
1.CGImageSourceCreateWithData(data) 创建 ImageSource。
2.CGImageSourceCreateImageAtIndex(source) 创建一个未解码的 CGImage。
3.CGImageGetDataProvider(image) 获取这个图片的数据源。
4.CGDataProviderCopyData(provider) 从数据源获取直接解码的数据。
ImageIO 解码发生在最后一步,这样获得的数据是没有经过颜色类型转换的原生数据(比如灰度图像)。
6、如何判断一个文件的图片类型?
通过读取文件或数据的头几个字节然后和对应图片格式标准进行比对。
7、 IOS 图片存放3种方式
Image.xcassets
- 创建.xcassets,以Image Set形式管理图片,添加图片后会生成对应的content.json文件
- 加入@2x和@3x等倍图后,打包后以Assets.car的形式存在,
- 使用[UIImage imageNamed:@”xxx”]方式读取图片,可以使用图片缓存 —— 相当于创建了一个key-value的字典,key为图片名,value为图片对象。创建图片对象后,该对象被加入到NSCache中(解码后的Image Buffer),直到收到内存警告的时候,才会释放不在使用的图片对象。 因此,对于需要在多处显示的图片,其对应的UIImage对象只会被创建一次(不考虑内存警告时的回收),减少内存消耗。
图片直接加入工程中作为Resource
- 读取方式:创建图片Resource文件夹,直接将图片加入到工程中,使用如下方式读取图片
- 特性:在Resource的图片管理方式中, 所有的图片创建都是通过读取文件数据得到的, 读取一次文件数据就会产生一次NSData以及产生一个UIImage。 当图片创建好后销毁对应的NSData,当UIImage的引用计数器变为 0 的时候自动销毁UIImage,这样的话就可以保证图片不会长期地存在在内存中
- 使用场景:由于这种方法的特性, 所以Resource的方法一般用在图片数据很大, 图片一般不需要多次使用的情况,比如说引导页背景(图片全屏)
- 优势:图片不会长期保存在内存当中, 所以不会有很多的内存浪费。同时, 大图一般不会长期使用, 而且大图占用内存一般比小图多了好多倍, 所以在减少大图的内存占用中,Resource做的非常好
使用Bundle文件
- Bundle即资源文件包,将许多图片,XIB,文本文件组织在一起,打包成一个Bundle文件,方便在其他项目中引用包内的资源。
- Bundle文件是静态的,不参与项目的编译,Bundle包中不能包含可执行的文件,它仅仅是作为资源,被解析成为特定的二进制数据。
- 优势:Bundle中文件不参与项目编译,不影响App包的大小(可用于App的瘦身); 使用bundle方式方便对文件进行管理,方便在其他项目中引用包内的资源。
- 使用场景:较大的图片,或者使用频率较低的图片
- 读取方式:使用imageWithContentsOfFile进行读取,如下方法1;也可以对UIImage进行扩展,
8、iOS 中图片的解压缩到渲染过程
简单来说
- 1、加载– iOS 获取压缩的图像并加载到 266KB 的内存(在我们这个例子中)。这一步没啥问题。
- 2、解码– 这时,iOS 获取图像并转换成 GPU 能读取和理解的方式。这里会解压图片,像上面提到那样占用 14MB。
- 3、渲染– 顾名思义,图像数据已经准备好以任意方式渲染。即使只是在一个 60x60pt 的 image view 中
详细的
- 1. 加载图片
- 从磁盘中加载一张图片;
- 然后将生成的UIImage赋值给UIImageView;
- 接着一个隐式的CATransaction捕获到了UIImageView图层树的变化;
- 分配内存缓冲区用于管理文件 IO 和解压缩操作,将文件数据从磁盘读到内存中;
- 2. 图片解码(解压)
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,默认在主线程进行;
- 3. 图片渲染
- Core Animation中CALayer使用解压(解码)的位图数据渲染UIImageView的图层;
- CPU计算好图片的Frame,对图片解压之后,就会交给GPU来做图片渲染渲染流程;
- GPU获取获取图片的坐标,将坐标交给顶点着色器(顶点计算),将图片光栅化(获取图片对应屏幕上的像素点),片元着色器计算(计算每个像素点的最终显示的颜色值);
- 从帧缓存区中渲染到屏幕上;
为什么要解压缩图片
既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。要想弄明白这个问题,我们首先需要知道什么是位图
其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图
事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比
因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。
解压缩原理
未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。
强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
复制代码
9、iOS 图像优化
iOS 实际上是从一幅图像的_尺寸_计算它占用的内存 – 实际的文件大小会比这小很多。这张照片的尺寸是 1718 像素宽和 2048 像素高。假设每个像素会消耗我们 4 个比特:
1718 * 2048 * 4 / 1000000 = 14.07 MB 占用
复制代码
当你加载一张图片时,会执行以下三个步骤:
- 1、加载– iOS 获取压缩的图像并加载到 266KB 的内存(在我们这个例子中)。这一步没啥问题。
- 2、解码– 这时,iOS 获取图像并转换成 GPU 能读取和理解的方式。这里会解压图片,像上面提到那样占用 14MB。
- 3、渲染– 顾名思义,图像数据已经准备好以任意方式渲染。即使只是在一个 60x60pt 的 image view 中
解码阶段是消耗最大的。在这个阶段,iOS 会创建一块缓冲区 – 具体来说是一块图像缓冲区,也就是图像在内存中的表示。这解释了为啥内存占用大小和图像尺寸有关,而不是文件大小。因此也可以理解,为什么在处理图片时,尺寸如此重要。
具体到UIImage,当我们传入从网络或者其它来源读取的图像数据时,它会将数据解码到缓冲区,但不会考虑数据的编码方式(比如 PNG 或者 JPG)。然而,缓冲区实际上会保存到UIImage中。由于渲染不是一瞬间的操作,UIImage会执行一次解码操作,然后一直保留图像缓冲区。
接着往下说 – 任何 iOS 应用中都有一整块的帧缓冲区。它会保存内容的渲染结果,也就是你在屏幕上看到的东西。每个 iOS 设备负责显示的硬件都用这里面单个像素信息逐个点亮物理屏幕上合适的像素点。
处理速度非常重要。为了达到黄油般顺滑的每秒 60 帧滑动,在信息发生变化时(比如给一个 image view 赋值一幅图像),帧缓冲区需要让 UIKit 渲染 app 的 window 以及它里面所有层级的子视图。一旦延迟,就会丢帧。
色彩空间
- UIGraphicsImageRenderer :图形图像渲染器
- UIGraphicsBeginImageContextWithOptions:图形以选项开始图像上下文
- Graphics:英 [ˈɡræfɪks](商业设计或插图中的)图形;图样,图案制图学;制图法;图表算法;(计算机程序中)图表
能确定的是,有一部分内存消耗来源于另一个重要因素 – 色彩空间
在上面的例子中,我们的计算基于以下假设 – 图像使用 sRGB 格式,但大部分 iPhone 不符合这种情况。sRGB 每个像素有 4 个字节,分别表示红、蓝、绿、透明度。
如果你用支持宽色域的设备进行拍摄(比如 iPhone 8+ 或 iPhone X),那么内存消耗将变成两倍,反之亦然。Metal 会用仅有一个 8 位透明通道的 Alpha 8 格式。
这里有很多可以把控和值得思考的地方。这也是为什么你应该用UIGraphicsImageRenderer代替UIGraphicsBeginImageContextWithOptions的原因之一。后者_总是_会使用 sRGB,因此无法使用宽色域,也无法在不需要的时候节省空间。在 iOS 12 中,UIGraphicsImageRenderer会为你做正确的选择。
缩小图片 vs 向下采样
UIImage导致性能问题的根本原因,我们在渲染流程里已经讲过,它会解压_原始图像_到内存中。理想情况下,我们需要一个方法来减少图像缓冲区的尺寸。
庆幸的是,我们可以修改图像尺寸,来减少内存占用
在 WWDC 18 的 Session 219,“Images and Graphics Best Practices“中,苹果工程师 Kyle Sluder 展示了一种有趣的方式,通过kCGImageSourceShouldCacheImmediately标志位来控制解码时机,:
这里 Core Graphics 不会开始图片解码,直到你请求缩略图。另外要注意的是,两个例子都传入了kCGImageSourceCreateThumbnailMaxPixelSize,如果不这样做,就会获得和原图同样尺寸的缩略图。根据文档所示:
10、讲如何将一张内存极大的图片可以像地图一样的加载出来
假如你有一张超级大的图片几十m的那种,你会怎么做呢?
直接使用UIImageview吗?那样内存会瞬间爆掉的。苹果提供了一个类来专门干这个事,CATiledLayer。
Tiled 英 [taɪld] 平铺的;用瓦管排水的
思想很简单,就是把大图切割成很多个小图,然后在CATiledLayer的drawRect方法里决定哪一部分该加载哪一张小图,因为是实时绘制的,即绘制完一张图片就释放掉一张图片,所以内存方面基本没有增长,当你放大或者缩小时,就会触发CATiledLayer的drawRect方法。
关键点:
首先,你需要有一个自定义的CATiledLayer
图片切割,取得指定行列的小图:
8、持久化
1、内存缓存和磁盘缓存
缓存分为内存缓存和磁盘缓存两种,其中内存是指当前程序的运行空间,缓存速度快容量小,是临时存储文件用的,供CPU直接读取,比如说打开一个程序,他是在内存中存储,关闭程序后内存就又回到原来的空闲空间;磁盘是程序的存储空间,缓存容量大速度慢可持久化与内存不同的是磁盘是永久存储东西的,只要里面存放东西,不管运行不运行 ,他都占用空间!磁盘缓存是存在Library/Caches。
内存缓存
说道iOS内存就不得不说内存分区了,iOS内存分为5个区:栈区,堆区,全局区,常量区,代码区
- 栈区stack:这一块区域系统会自己管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量,深递归,函数循环调用都可能导致内存耗尽而运行崩溃。
- 堆区heap:与栈区相对,这一块一般由我们自己管理,比如alloc,free的操作,存储一些自己创建的对象。
- 全局区(静态区static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放
- 常量区:存储常量字符串和const常量
- 代码区:存储代码
磁盘缓存
磁盘缓存其实就是把数据存放到硬盘中。也可把沙盒缓存看做磁盘缓存的一部分。
沙盒缓存
沙盒缓存(SandBox)沙盒机制,是一种安全体系。我们所开发的每一个应用程序在设备上会有一个对应的沙盒文件夹,当前的程序只能在自己的沙盒文件夹中读取文件,不能访问其他应用程序的沙盒。在项目中添加的所有非代码的资源,比如图片、声音、属性列表等都存在自己的沙盒中。此外,在程序运行中动态生成的或者从网络获取的数据,如果要存储,也都是存储到沙盒中。
2、iOS中的几种数据存储方式
- 1、Plist文件
- 2、偏好设置(NSUserDefaults)
- 3、归档(NSCoding NSKeyedArchiver NSKeyedUnarchiver)
- 4、SQLITE数据库
- 5、CoreData
- 6、Keychain:Keychain是iOS所提供的一种安全存储参数的方式,最常用来存储账号,密码,用户信息,银行卡资料等信息,Keychain会以加密的方式存储在设备中
3、FMDB的基本使用
- 1.FMDB基本使用
- FMDB简介
- FMDB的安装方式
- FMDB核心类
- FMDatabase数据库:一个FMDatabase对象就代表一个单独的SQLite数据库
用来执行SQL语句
- FMResultSet查询结果集:使用FMDatabase执行查询后的结果集
- FMDatabaseQueue线程安全数据库操作:用于在多线程中执行多个查询或更新,它是线程安全的
复制代码
4、CoreData
CoreData是一门功能强大的数据持久化技术,位于SQLite数据库之上,它避免了SQL的复杂性,能让我们以更自然的方式与数据库进行交互。CoreData提供数据–OC对象映射关系来实现数据与对象管理,这样无需任何SQL语句就能操作他们。
CoreData数据持久化框架是Cocoa API的一部分,⾸次在iOS5 版本的系统中出现,它允许按照实体-属性-值模型组织数据,并以XML、⼆进制文件或者SQLite数据⽂件的格式持久化数据
5、序列化和反序列化
在iOS中一个自定义对象是无法直接存入到文件中的,必须先转化成二进制流才行。
从对象到二进制数据的过程我们一般称为对象的序列化(Serialization),也称为归档(Archive)。
同理,从二进制数据到对象的过程一般称为反序列化或者反归档。
NSCoder
- 归档(序列化) & 解归档(反序列化)
- 提供简单函数,在 Object 和 二进制数据间进行转换
- 抽象类 具体功能需要子类实现
6、数据库升级数据迁移
理想的情况是:数据库升级,表结构、主键和约束有变化,新的表结构建立之后会自动的从旧的表检索数据,相同的字段进行映射迁移数据,而绝大多数的业务场景下的数据库版本升级是只涉及到字段的增减、修改主键约束,所以下面要实现的方案也是从最基本的、最常用的业务场景去做一个实现,至于更加复杂的场景,可以在此基础上进行扩展,达到符合自己的预期的。
网上搜索了下,并没有数据库升级数据迁移简单完整的解决方案,找到了一些思路
- 清除旧的数据,重建表优点:简单缺点:数据丢失
- 在已有表的基础上对表结构进行修改优点:能够保留数据缺点:规则比较繁琐,要建立一个数据库的字段配置文件,然后读取配置文件,执行SQL修改表结构、约束和主键等等,涉及到跨多个版本的数据库升级就变得繁琐并且麻烦了
- 创建临时表,把旧的数据拷贝到临时表,然后删除旧的数据表并且把临时表设置为数据表。优点:能够保留数据,支持表结构的修改,约束、主键的变更,实现起来比较简单缺点:实现的步骤比较多
7、数据库问题
- 1、数据库中的事务是什么意思
- 事务就是访问并操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。如果其中一个步骤出错就要撤销整个操作,回滚到进入事务之前的状态。
- 2、设计一套数据库方案,实现类似微信的搜索关键词能快速检索出包含该字符串的聊天信息,并展示对应数量(聊天记录的数据量较大)。
- 可以对聊天记录的文本值加上索引。正常情况下数据库搜索都是全量检索的,加上索引之后只会检索满足条件的记录,大大降低检索量