Objective-C
进程和线程
-
进程:进程是资源(CPU、内存等)分配的基本单位。本来的应用程序都是直接访问我们的操作系统,后来所有的应用程序都以进程的方式运行在比操作系统更低的级别,每个进程都有自己独立的地址空间,使得进程之间的地址空间相互隔离。CPU由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到CPU,但是,如果运行时间超出了一定的时间,操作系统会暂停该进程,将CPU资源分配给其他等待运行的进程。这种CPU的分配方式即所谓的抢占式,操作系统可以强制剥夺CPU资源并且分配给它认为目前最需要的进程。如果操作系统分配给每个进程的时间都很短,即CPU在多个进程间快速的切换,从而造成了很多进程都在同时运行的假象。
-
线程: 线程是程序执行的最小单元。各个线程之间共享程序(进程)的内存空间。操作系统会让多线程程序轮流执行,每次仅执行一小段时间(通常是几十到几百毫秒),这样每个线程就“看起来”在同时执行。这样的一个不断在处理器上切换不同的线程的行为称之为“线程调度,在线程调度中,线程通常拥有至少三种状态:运行、就绪、等待。
线程又分为内核线程和用户线程。用户态多线程的实现方式又分为: 1. 一对一模型:用户线程和内核线程一一对应,线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其他线程不会受到影响。此外,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。许多操作系统内核调度时,上下文切换的开销较大,导致用户线程执行效率下降。 2. 多对一模型:多个用户线程映射到一个内核线程上。多处理器系统上,处理器的增多对多对一模型的线程性能不会有明显的帮助。但同时,多对一模型得到的好处是高效的上下文切换和几乎无限制的线程数量。 3. 多对多模型:多个用户线程映射到少数但不止一个内核线程上。在多对多模型中,一个用户阻塞并不会使得所有的用户线程阻塞,因为此时还有别的线程可以被调度来执行。另外,多对多模型对用户线程数量也没什么限制,在多处理器系统上,多对多模型的线程也能得到一定的性能提升,不过提升的幅度不如一对一模型高(一对一模型是真正的并发)。 iOS中主线程的栈空间大小为1M,所有二级线程的栈空间默认分配512KB。
区别:
- 单元:进程是资源分配的最小单位,线程是程序执行的最小单元
- 开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 内存分配方面:进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。相对于多进程应用,多线程在数据共享方面效率要高很多。
- 通讯:进程间通讯通过TCP/IP的端口来实现。线程的通信就比较简单,有一大块共享的内存,只要大家的指针是同一个就可以看到各自的内存。
进程与线程的关系
进程的颗粒度太大,每次的执行都要进行进程上下文的切换。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。这里a,b,c的执行是共享了A进程的上下文,CPU在执行的时候仅仅切换线程的上下文,而没有进行进程上下文切换的。进程的上下文切换的时间开销是远远大于线程上下文时间的开销。这样就让CPU的有效使用率得到提高。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。线程主要共享的是进程的地址空间。
ps:一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4字节;64位平台下的指针为64位,即8字节。
iOS 内存管理机制
分类和扩展有什么区别?
- 类扩展: extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的extension,所以对于系统一些类,如nsstring,就无法添加类扩展 能为某个类附加额外的属性,成员变量,方法声明 一般的类扩展写到.m文件中 @interface ViewController()//这就是类扩展的写法,类扩展写在.m文件中 这里写类扩展的私有属性和私有方法的声明,并在@implementation中将扩展方法实现 @end 以上便是类扩展的写法,在.m中添加额外的属性和私有方法 一般的私有属性写到类扩展
- 分类:分类的小括号中必须有名字 区别
- 分类只能扩充方法,不能扩展属性和成员变量
- 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是私有的。
- 类扩展中声明的方法没被实现,无法编译通过,但是分类中的方法没被实现可以编译通过。这是因为类扩展是在编译阶段被添加到类中,分类是在运行时添加到类中。
- 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
- 我们通常使用类扩展来隐藏我们的实现,或者增加私有实例变量。当然,这些私有实例变量仍然可以通过KVC访问。
你知道有哪些情况会导致app崩溃,分别可以用什么方法拦截并化解?
导致app崩溃的原因有很多,比如向某个对象发送其无法响应的方法,数组越界,集合类中添加nil对象,string访问越界,KVO不合理的移除关联key(KVO导致的崩溃不仅仅这一种原因)等。而崩溃非常影响用户体验,所以笔者认为一名高级 iOS 开发应该具备避免这些崩溃的能力,起码至少也要知道这些容易导致崩溃的场景。如可以利用runtime黑魔法—交换方法可以减少程序中的崩溃
对象等同性
思考下面输出什么?
NSString *aString = @"iPhone 8";
NSString *bString = [NSString stringWithFormat:@"iPhone %i", 8];
NSLog(@"%d", [aString isEqual:bString]);
NSLog(@"%d", [aString isEqualToString:bString]);
NSLog(@"%d", aString == bString);
答案是110 ==操作符只是比较了两个指针所指对象的地址是否相同,而不是指针所指的对象的值,所以最后一个为0
属性
关于ARC下,不显示指定属性关键字时,默认关键字:
- 基本数据类型:atomic readwrite assign
- 普通OC对象: atomic readwrite strong
单例
+(instance)shareManager{
static Manager *shareManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
shareManager = [[Manager alloc]init];
});
return shareManager;
}
dispatch_once 主要是根据 onceToken 的值来决定怎么去执行代码。
- 当 onceToken = 0 时,线程执行 dispatch_once 的 block 中代码;
- 当 onceToken = -1 时,线程跳过 dispatch_once 的 block 中代码不执行;
- 当 onceToken 为其他值时,线程被阻塞,等待 onceToken 值改变。
-
当线程调用 shareInstance,此时 onceToken = 0,调用 block 中的代码,此时 onceToken 的值变为 140734537148864。
-
当其他线程再调用 shareInstance 方法时,onceToken 的值已经是 140734537148864 了,线程阻塞。
-
当 block 线程执行完 block 之后,onceToken 变为 -1,其他线程不再阻塞,跳过 block。
-
下次再调用 shareInstance 时,block 已经为 -1,直接跳过 block。
单例应用
- 设置单例类访问应用的配置信息
- 用户的个人信息登录后用的NSUserDefaults存储,对登录类进一步采用单例封装方便全局访问
绘图动画UI框架
1、UIKit:最常用的视图框架,封装度最高,都是OC对象。
2、CoreAnimation:核心动画,提供强大的2D和3D动画效果。
3、CoreGraphics:绘图,纯C的API,使用Quartz2D做引擎。
4、CoreText:创建文本布局,优化字体处理,并访问字体度量和字形数据。
5、CoreImage:给图片提供各种滤镜处理,比如高斯模糊、锐化等
6、OpenGL-ES / Metal:OpenGL-ES是一种用于创建实时3D图像的编程接口,是图形硬件的软件接口,基本上是一种底层渲染API。苹果自研的Metal功能与OpenGL类似,早期也是使用OpenGL-ES,自研Metal后弃用OpenGL-ES。
frame和transform
- frame是一个复合属性,由center、bounds和transform共同计算而来。
- transform改变,frame会受到影响,但是center和bounds不会受到影响。也就是你使用transform来缩放,bounds是不会变的。那么由center和bounds计算得到的frame是永远保持transform为identity时的状态。这也是为什么把transform设为identity后,view会回归到最初状态。
渲染
CoreGraphics
与 Core Graphics 中其他部分一样,Quartz 的绘制是在图形环境(graphics context)中进行的,通常简称为环境(context)。每个视图都有相关的环境。你需要先获取当前环境,通过它可以调用各类 Quartz 绘图函数,由环境负责将图形渲染在视图上。你可以认为环境是一种画布。系统提供了默认的环境来将显示内容呈现在屏幕上。然而你也可以创建一一个自已的环境,来绘制不想立刻显现在屏幕上的内容,以待后续使用。我们现在研究如何使用默认的环境,可以在 draw(_ rect:)方法中使用 let context = UIGraphicsGetCurrentContext() 来获取它。
- 1 CGContextRef 上下文->画板
- 2 画图的内容 -> 设置画图的内容
- 3 把内容添加到上下文(画板)上
- 4 把内容画到画板上
如果对视图实现了 -drawRect: 方法,或者 CALayerDelegate 的 -drawLayer:inContext: 方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
用Core Graphics实现一个简单的绘图应用
#import "DrawingView.h"
@interface DrawingView ()
@property (nonatomic, strong) UIBezierPath *path;
@end
@implementation DrawingView
- (void)awakeFromNib
{
//create a mutable path
self.path = [[UIBezierPath alloc] init];
self.path.lineJoinStyle = kCGLineJoinRound;
self.path.lineCapStyle = kCGLineCapRound;
self.path.lineWidth = 5;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];
//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];
//add a new line segment to our path
[self.path addLineToPoint:point];
//redraw the view
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
//draw path
[[UIColor clearColor] setFill];
[[UIColor redColor] setStroke];
[self.path stroke];
}
@end
这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径( UIBezierPath ),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。看来我们需要一个更好的方法了。
Core Animation为这些图形类型的绘制提供了专门的类,并给他们提供硬件支持。 CAShapeLayer 可以绘制多边形,直线和曲线。 CATextLayer 可以绘制文本。 CAGradientLayer 用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。
如果稍微将之前的代码变动一下,用 CAShapeLayer 替代Core Graphics,性能就会得到提高(见清单13.2).虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。
用 CAShapeLayer 重新实现绘图应用
#import "DrawingView.h"
@interface DrawingView ()
@property (nonatomic, strong) UIBezierPath *path;
@end
@implementation DrawingView
+ (Class)layerClass {
//this makes our view create a CAShapeLayer
//instead of a CALayer for its backing layer
return [CAShapeLayer class];
}
- (void)awakeFromNib {
//create a mutable path
self.path = [[UIBezierPath alloc] init];
//configure the layer
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineWidth = 5;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//get the starting point
CGPoint point = [[touches anyObject] locationInView:self];
//move the path drawing cursor to the starting point
[self.path moveToPoint:point];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//get the current point
CGPoint point = [[touches anyObject] locationInView:self];
//add a new line segment to our path
[self.path addLineToPoint:point];
//update the layer with a copy of the path
((CAShapeLayer *)self.layer).path = self.path.CGPath;
}
@end
CoreGraphics API
CGContextRef context = UIGraphicsGetCurrentContext(); // 获取上下文
CGContextMoveToPoint // 开始画线
CGContextAddLineToPoint // 画直线
CGContextAddEllipseInRect // 画一椭圆
CGContextSetLineCap // 设置线条终点形状
CGContextSetLineDash // 画虚线
CGContextAddRect // 画一方框
CGContextStrokeRect // 指定矩形
CGContextStrokeRectWithWidth // 指定矩形线宽度
CGContextStrokeLineSegments // 一些直线
CGContextAddArc // 画曲线 前两点为中心 中间俩点为起始弧度 最后一数据为0则顺时针画 1则逆时针
CGContextAddArcToPoint //先画俩条线从point 到 第1点, 从第1点到第2点的线 切割里面的圆
CGContextSetShadowWithColor // 设置阴影
CGContextSetRGBFillColor // 只填充颜色
CGContextSetRGBStrokeColor // 画笔颜色设置
CGContextSetFillColorSpace // 颜色空间填充
CGConextSetStrokeColorSpace // 颜色空间画笔设置
CGContextFillRect // 补充当前填充颜色的rect
CGContextSetAlaha // 透明度
CGContextTranslateCTM // 改变画布位置
CGContextSetLineWidth // 设置线的宽度
CGContextAddRects // 画多个线
CGContextAddQuadCurveToPoint // 画曲线
CGContextStrokePath // 开始绘制图片
CGContextDrawPath // 设置绘制模式
CGContextClosePath // 封闭当前线路
CGAffineTransformRotate // 旋转
CGAffineTransformScale // 缩放
CGAffineTransformTranslate // 平移
Core Animation
CALayer
比如CALayer下的CAShapeLayer、CATextLayer、CATransformLayer、CAGradientLayer、CAReplicatorLayer、CAScrollLayer、CATiledLayer、CAEmitterLayer、CAEAGLLayer
使用CAShapeLayer
QuartzCore里面的类以CA开头,包含CAAnimation(动画),CADisplayLink(定时器),CAShapeLayer(图层),CAGradientLayer(梯度,颜色渐变)等
-
CAShapeLayer继承自CALayer,可使用CALayer的所有属性
-
CAShapeLayer需要和贝塞尔曲线配合使用才有意义。贝塞尔曲线可以为其提供形状,而单独使用CAShapeLayer是没有任何意义的。
-
使用CAShapeLayer与贝塞尔曲线可以实现不在view的DrawRect方法中画出一些想要的图形
每个CAShapeLayer对象都代表着将要被渲染到屏幕上的一个任意的形状(shape),具体的形状(shape)有一个神奇的属性 path(CGPathRef类型)用这个属性配合上 UIBezierPath 这个类就可以达到超神的效果。
CAShapeLayer和drawRect比较:
- CAShapeLayer:属于CoreAnimation框架,通过GPU来渲染图形,节省性能,高效使用内存。
- drawRect:属于Core Graphics框架,占用大量CPU,耗费性能。
CATextLayer
它以图层的形式包含了 UILabel 几乎所有的绘制特性,并且额外提供了一些新的特性。CATextLayer 使用了Core text
CATransformLayer
Core Animation图层很容易就可以让你在 2D 环境下做出这样的层级体系下的变换,但是 3D 情况下就不太可能,因为所有的图层都把他的孩子都平面化到一个场景中 CATransformLayer 解决了这个问题,CATransformLayer 不同于普通的 CALayer ,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。
CAGradientLayer
CAGradientLayer 是用来生成两种或更多颜色平滑渐变的。用 Core Graphics 复制一个 CAGradientLayer 并将内容绘制到一个普通图层的寄宿图也是有可能的,但是 CAGradientLayer 的真正好处在于绘制使用了硬件加速。
CAReplicatorLayer
CAReplicatorLayer 的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。
CAScrollLayer
如果一个数据或文本的长列表,我们能够随意滑动,我们应该会想到使用 UITableView 或 UIScrollView。但是对于对于单独的图层操作,什么会等价于 UITableView 或 UIScrollView 呢?
这个时候就需要 CAScrollLayer 了。 CAScrollLayer 有一个 -scrollToPoint: 方法,它自动适应 bounds 的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。前面提到过,Core Animation 并不处理用户输入,所以 CAScrollLayer 并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何 iOS 指定行为例如滑动反弹。
CATiledLayer
能高效绘制在 iOS 上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为 OpenGL纹理,同时 OpenGL 有一个最大的纹理尺寸(通常是 2048*2048,或 4096*4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为 Core Animation 强制用 CPU 处理图片而不是更快的 GPU。
CATiledLayer 为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。
CAEmitterLayer
CAEmitterLayer 是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。
CAEAGLLayer
当 iOS 要处理高性能图形绘制,必要时就是 OpenGL。在 iOS 5 中,苹果引入了一个新的框架叫做 GLKit,它去掉了一些设置 OpenGL 的复杂性,提供了一个叫做 CLKView 的 UIView 的子类,帮你处理大部分的设置和绘制工作。前提是各种各样的 OpenGL 绘图缓冲的底层可配置项仍然需要你用 CAEAGLLayer 完成,它是 CALayer 的一个子类,用来显示任意的 OpenGL 图形。CAEAGLLayer 的存储绑定到 OpenGL ES renderBuffer对象。
KVC普通对象的setter过程
- 比如本文中的name,顺序就是
setName->_setName->setIsName。当找到这三种setter中任意一个时,则进行赋值 - 判断
+ (BOOL)accessInstanceVariablesDirectly函数是否返回YES,如果返回YES,则按照_key->_iskey->key->iskey的顺序搜索成员,找到任意一个则进行赋值。如果返回NO,系统会执行该对象的setValue:forUndefinedKey:函数,默认抛出异常
成员变量与属性的联系
本质上,一个属性一定对应一个成员变量,但是属性又不仅仅是一个成员变量,属性还会根据自己对应的属性特性的定义来对这个成员变量进行一系列的封装:提供 Getter/Setter 方法、内存管理策略、线程安全机制等等。
MVVM
MVVM是把MVC中的Controller改变成了ViewModel。View的变化会自动更新到ViewModel。ViewModel的变化也会自动同步到View上显示,通过数据来显示视图层。
MVVM把小的业务逻辑封装出来,变成可测试的代码,从而使程序更健壮
MVVM优点:
- 低耦合:View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,由于ViewModel中抽离出来了部分逻辑,使得视图(View)可以独立于Model变化和修改。
- 可重用性: 可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
- 独立开发: 开发人员可以专注于业务逻辑和数据的开发,设计人员可以专注于页面的设计。
锁
公平锁
其实我们平时使用的锁(除了os_unfair_lock)基本都是公平锁,这一类锁有着FIFO的特性即:
多个线程情况下排队,先到先获得锁。
如果进入等待的顺序为12345,则最后等待结束被执行的顺序也是12345。
非公平锁
当锁被释放后,所有线程竞争锁,抢到的线程就会获得锁。
在iOS 上的非公平锁为os_unfair_lock。
基本的锁就包括三类:自旋锁、互斥锁、读写锁
互斥锁
互斥锁充当资源周围的保护屏障,如果多个线程竞争同一个互斥锁,每次只允许一个线程访问。如果一个互斥锁正在使用中,另一个线程试图获取它,该线程就会阻塞,进入睡眠状态,直到该互斥锁被它的原始持有者释放再被唤醒。(NSLock、pthread_mutex、synchronized)
自旋锁
与互斥锁不同的是,如果一个自旋锁正在使用中,另一个线程试图获取它时,该线程不会进入休眠状态,而是反复轮询其锁条件,直到该条件变为真。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。(OSSpinLock、os_unfair_lock、)iOS10之后抛弃自旋锁,改用互斥锁。
读写锁
-
读写锁实际是一种特殊的自旋锁,一个读写锁同时只能有一个写者或者多个读者,但不能既有读者又有写者,如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立刻获得读写锁(pthread_rwlock)。知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。iOS实现读写锁 - 掘金 (juejin.cn)
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写的操作,又有读的操作
atomic 的底层实现,老版本是自旋锁,新版本是互斥锁。
atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。
- 递归锁就是同一个线程可以加锁N次而不会引发死锁。递归锁是特殊的互斥锁,即是带有递归性质的互斥锁(NSRecursiveLock、pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可、)
- 条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。(NSCondition、NSConditionLock)
- 信号量是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。是一种高级的互斥锁。 简单的性能测试
下图是我针对iOS中的锁自己测试得出的,图中数字代表每次加解锁需要消耗的时间,单位为ns。代码在这里LockPerformance,代码参考自YY大神的不再安全的 OSSpinLock,基本跟YY大神的图差不多??,YY大神的单位是μs,应该是1000次的,或者写错了吧~
- 1、OSSpinLock(自旋锁) 优先级反转问题(
iOS10之后被os_unfair_lock互斥锁替代) 优先级反转概念:比如两个线程 A 和 B,优先级 A < B。当 A 获取锁访问共享资源时,B 尝试获取锁,那么 B 就会进入忙等状态,忙等时间越长对 CPU 资源的占用越大;而由于 A 的优先级低于 B,A 无法与高优先级的线程争夺 CPU 资源,从而导致任务迟迟完成不了。解决优先级反转的方法有“优先级天花板”和“优先级继承”,它们的核心操作都是提升当前正在访问共享资源的线程的优先级。
OSSpinLock 由于这个问题导致很多开源库都放弃使用了,有兴趣可以看看一篇文章:不再安全的 OSSpinLock。
- 2、避免死锁
很常见的场景是,同一线程重复获取锁导致的死锁,这种情况可以使用递归锁来处理,
pthread_mutex_t使用pthread_mutex_init_recursive()方法初始化就能拥有递归锁的特性。pthread_mutex_t递归方法实现中的pthread_mutex结构体字段options中会维护一个lockcount,来记录加锁的次数,从而实现递归锁。
使用 pthread_mutex_trylock() 等尝试获取锁的方法能有效的避免死锁的情况,在 YYCache 源码中有一段处理就比较精致:
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
...
finish = YES;
...
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
这段代码除了避免潜在的死锁情况外,还做了一个 10ms 的挂起操作然后循环尝试,而不是直接让线程空转浪费过多的 CPU 资源。虽然挂起线程“浪费了”互斥锁的空转期,增加了唤醒线程的资源消耗,降低了锁的性能,但是考虑到 YYCache 此处的业务是修剪内存,并非是对锁性能要求很高的业务,并且修剪的任务量可能比较大,出现线程竞争的几率较大,所以这里放弃线程空转直接挂起线程是一个不错的处理方式。
- 3、NSLock (互斥锁) 不是递归锁,不能递归调用
- 4、NSCondition(互斥锁) 不是递归锁,不能递归调用
- 5、NSRecursiveLock(互斥锁、递归锁)
只能单线程递归,不能多线程递归调用
- 6、@synchronized(互斥锁、递归锁) 可以多线程递归
消息查找流程(objc_msgSend流程)
- 快速查找:检查class的缓存中是否有该方法的实现,如果有,调用。类的结构(isa、surperCls、cache、bits)cache(_buckets、_mask、_flags、_occupied) -> 首地址偏移16字节获取cache,高16位存mask,低48位存buckets -> 通过SEL &mask得到方法下标index,从buckets里面取对应index的bucket,判断sel和bucket(imp,sel)的sel是否相同,不同循环查找(从后往前),找到了调用并缓存
- 慢速查找(C语言实现):lookUpImpOrForward -> 二分查找 -> 本类找不到找父类 ,递归查找,一直找到NSObject,NSObject->superCls = nil,停止递归循环 -> 动态方法决议,找到了调用并缓存
- 动态方法决议:resolveInstanceMethod、resolveClassMethod 询问当前类能够通过动态添加方法处理这个未知的selector
- 快速消息转发:forwardingTargetForSelector 查看是否存在其他对象能处理这条消息,称为备援接收者,能处理则交给备援接收者处理,处理不了,则进入完整消息转发流程
- 完整消息转发:methodSignatureForSelector、forwardInvocation runtime 系统会把和消息有关的所有信息都放进 NSInvocation 对象中,再给接收者一次机会,处理未知的 selector
消息转发机制:
- 若对象无法响应某个选择子,则进入消息转发流程。
- 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”
- 第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时运行期系统会请求接收者以其他手段来处理与消息相关方法的调用。为分两小步: 1):请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。 2):若没有“备援接收者”,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
应用启动流程
我把APP的生命流程分为三大部分:
- APP的启动流程(pre-main)
- APP的初始化流程(main)
- APP的运行时生命周期
APP的启动流程(pre-main)
1.iOS系统首先会加载解析该APP的 Info.plist 文件,因为 Info.plist 文件中包含了支持APP加载运行所需要的众多 Key、value配置信息,例如APP的运行条件(Required device capabilities),是否全屏,APP启动图信息等。
2.创建沙盒(iOS8后,每次启动APP都会生成一个新的沙盒路径)
3.根据 Info.plist 的配置检查相应权限状态
4.加载 Mach-O 文件读取 dyld(动态链接加载器) 路径并运行 dyld 动态连接器(内核加载了主程序,dyld 只会负责动态库的加载)
4.1 首先dyld会寻找合适的CPU运行环境
4.2 然后加载程序运行所需的依赖库和我们自己写的.h.m文件编译成的.o可执行文件,并对这些库进行链接。
4.3 加载所有方法(runtime就是在这个时候被初始化并完成OC的内存布局)
4.4 加载C函数
4.5 加载category的扩展(此时runtime会对所有类结构进行初始化)
4.6 加载C++静态函数,加载OC+load
4.7 最后dyld返回main函数地址,main函数被调用
使用dyld2启动应用的过程如图:
-
加载
dyld到App进程 -
加载动态库(包括所依赖的所有动态库)
-
Rebase & Bind这里先来讲讲为什么要
Rebase?有两种主要的技术来保证应用的安全:
ASLR和Code Sign。-
ASLR的全称是Address space layout randomization,翻译过来就是“地址空间布局随机化”。App被启动的时候,程序会被影射到逻辑的地址空间,这个逻辑的地址空间有一个起始地址,而ASLR技术使得这个起始地址是随机的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函数的地址。 -
Code Sign相信大多数开发者都知晓,这里要提一点的是,在进行Code sign的时候,加密哈希不是针对于整个文件,而是针对于每一个Page的。这就保证了在dyld进行加载的时候,可以对每一个page进行独立的验证。
mach-o中有很多符号,有指向当前mach-o的,也有指向其他dylib的,比如printf。那么,在运行时,代码如何准确的找到printf的地址呢?mach-o中采用了 PIC技术 ,全称是Position Independ code(位置独立代码)。当你的程序要调用printf的时候,会先在__DATA段中建立一个指针指向printf,在通过这个指针实现间接调用。dyld这时候需要做一些fix-up工作,即帮助应用程序找到这些符号的实际地址。主要包括两部分-
Rebase修正内部(指向当前mach-o文件)的指针指向:由于ASLR(地址空间布局随机化),会对所有指向进程内的符号地址进行调整,符号的地址需要进程被加载后才能确定, 所以被放到__DATA区, 方便修改, 修改的过程称为rebase。 -
Bind 修正外部指针指向: 真正的地址就会被写进对应符号上的过程就是
bind
-
-
初始化
Objective C Runtime -
其它的初始化代码
dyld2 是纯粹的 in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。
dyld3 则是部分 out-of-process,部分 in-process。图中,虚线之上的部分是 out-of-process 的,在App下载安装和版本更新的时候会去执行,out-of-process 会做如下事情:
- 分析
Mach-o Headers(保存[Mach-O]的一些基本信息,包括运行平台、文件类型、LoadCommands指令的个数、指令总大小,dyld标记Flags等等。) - 分析依赖的动态库
- 查找需要
Rebase & Bind之类的符号 - 把上述结果写入缓存 这样,在应用启动的时候,就可以直接从缓存中读取数据,加快加载速度。
APP的初始化流程
-
main函数 -
执行
UIApplicationMain函数2.1. 创建
UIApplication对象2.2. 创建
UIApplication的delegate对象2.3. 创建
MainRunloop2.4.
delegate对象开始处理(监听)系统事件(没有storyboard) -
根据
Info.plist获得最主要storyboard的文件名,加载最主要的storyboard(有storyboard) -
程序启动完毕的时候, 就会调用代理的
application:didFinishLaunchingWithOptions:方法: 4.1. 在application:didFinishLaunchingWithOptions:中创建UIWindow4.2. 创建和设置
UIWindow的rootViewController4.3. 最终显示第一个窗口
main.m文件说明
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
//main函数是整个程序的入口
int main(int argc, char * argv[]) {
//参数argc说明:命令行总的参数个数。
//参数argv说明:是参数的数组,argv中第一个参数为app的路径+全名。
printf("argc = %d\n", argc);
char *argChar = argv[0];
printf("index = %i ,argv = %s\n", 0, argChar);
@autoreleasepool {
//UIApplicationMain函数说明
//第一个参数argc:参数是main函数C语言中传入的,保持与main函数相同。
//第二个参数argv:同argc参数一样
//第三个参数nil:该参数为principalClassName (主要类名)
// 如果principalClassName是nil,那么它的值将从Info.plist去获取,如果Info.plist没有,则默认为UIApplication。
// principalClass这个类除了管理整个程序的生命周期之外什么都不做,它只负责监听事件然后交给delegateClass去做。
//第四个参数NSStringFromClass([AppDelegate class]):委托代理类的类名,UIApplication创建的delegate对象的类名
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplication代理方法说明:
//app启动完毕后就会调用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//打印方法名
//本地通知的Key,UIApplicationDidFinishLaunchingNotification
NSLog(@"--- %s ---",__func__);
}
//比如当有电话进来或短信进来或锁屏等情况下,这时应用程序挂起进入非活动状态。
//也就是手机界面还是显示着你当前的应用程序的窗口,只不过被别的任务强制占用了,也可能是即将进入后台状态(因为要先进入非活动状态然后才会进入后台状态)
- (void)applicationWillResignActive:(UIApplication *)application
{
NSLog(@"--- %s ---",__func__);
}
//指当前窗口不是你的App,大多数程序进入这个后台会在这个状态上停留一会,时间到之后会进入挂起状态(Suspended)。
//如果你程序特殊处理后可以长期处于后台状态也可以运行。
//Suspended (挂起): 程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。
//当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存
- (void)applicationDidEnterBackground:(UIApplication *)application
{
/本地通知的Key,/UIApplicationDidEnterBackgroundNotification
NSLog(@"--- %s ---",__func__);
//当用户按下home键后,程序进入后台运行状态,如果内存不足被系统关闭或者用户手动杀掉程序,都不会调用applicationWillTerminate函数。
//在程序进入后台时,添加一beginBackgroundTaskWithExpirationHandler(后台运行通知函数),程序进入后台10分钟内,程序还在运行,并可以响应一些消息
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"程序关闭");
}];
}
//app程序程序从后台回到前台就会调用
- (void)applicationWillEnterForeground:(UIApplication *)application
{
//本地通知的Key,UIApplicationWillEnterForegroundNotification
NSLog(@"--- %s ---",__func__);
}
//app程序获取焦点就会调用
- (void)applicationDidBecomeActive:(UIApplication *)application
{
//本地通知的Key,UIApplicationDidBecomeActiveNotification
NSLog(@"--- %s ---",__func__);
}
// 内存警告,可能要终止程序,清除不需要再使用的内存
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
NSLog(@"--- %s ---",__func__);
}
// 程序即将退出调用
- (void)applicationWillTerminate:(UIApplication *)application
{
//UIApplicationWillTerminateNotification
NSLog(@"--- %s ---",__func__);
}
APP初始化的 UIApplication 调用顺序为:
application:didFinishLaunchingWithOptions:applicationDidBecomeActive:
二进制重排
Intel 自从1995年的 Pentium Pro CPU 开始采用了 36位 的物理地址,也就是可以访问高达 64GB 的物理内存。原先的 32位 地址线只能访问最多 4GB 的物理内存。但是自从扩展到 36位 地址线之后,Intel 修改了页映射的方式,使得新的映射方式可以访问更多的物理内存。Intel 把这个地址扩展方式叫做 PAE(Physical Address Extension)。
当然扩展的物理地址空间,对于普通应用程序来说正常情况下感觉不到它的存在,因为这主要是操作系统的事,在应用程序里,只有32位的虚拟地址空间。那么应用程序该如何使用这些大于常规的内存空间呢?一个很常见的方法就是操作系统提供一个窗口映射的方法,把这些额外的内存映射到进程地址空间中来。应用程序可以根据需要来选择申请和映射,比如一个应用程序中 0x10000000~0x20000000 这一段 256MB 的虚拟地址空间用来做窗口,程序可以从高于 4GB 的物理空间中申请多个大小为 256MB 的物理空间,编号为 A、B、C 等,然后根据需要将这个窗口映射到不同的物理空间块,用到 A 时将 0x10000000~0x20000000 映射到 A,用到 B、C 时再映射过去,如此重复操作即可。
虚拟内存:
之前的应用都是直接用物理内存加载,物理内存加载满了会出现闪退的情况,因为软件发展比硬件快,物理内存不能够加载全部的应用,所以出现了覆盖载入和页映射两种方式。
覆盖载入
覆盖载入的方法把挖掘内存潜力的任务交给了程序员,程序员在编写程序的时候必须手工将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块何时应该驻留内存而何时应该被替换掉。
由于跨模块间的调用都需要经过覆盖管理器,以确保所有被调用到的模块都能够正确地驻留在内存,而且一旦模块没有在内存中,还需要从磁盘或其他存储器读取相应的模块,所以覆盖装入的速度肯定比较慢,不过这也是一种折中的方案,是典型的利用时间换取空间的方法。
页映射
页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。与覆盖载入的原理相似,页映射也不是一下子就把程序的所有数据和指令都装入内存,而是将内存和所有磁盘中的数据和指令按照 “页(Page)” 为单位划分成若干个页,以后所有的装载和操作的单位就是页。
假设程序所有的指令和数据总和为 32KB,那么程序总共被分为8个页。我们将它们编号为 P0~P7。很明显,16KB 的内存无法同时将 P0,这时装载管理器(我们假设装载过程由一个叫装载管理器的家伙来控制,就像覆盖管理器一样)发现程序的 P0 不在内存中,于是将内存 F0 分配给 P0,并且将 P0 的内容装入 F0 ,运行一段时间以后,程序需要用到 P5,于是装载管理器将 P5 装入 F1;就这样,当程序用到 P3 和 P6 的时候,它们分别被装载到 F2 和 F3,它们的映射管理如图所示
很明显,如果这时候程序只需要 P0、P3、P5、P6这4个页,那么程序就能一直运行下去。但是问题很明显,如果这时候程序需要访问 P4,那么装载管理器必须做出抉择,它必须放弃目前正在使用的 4个内存页中的其中一个来装载 P4。至于选择哪个页,我们有很多中算法可以选择,比如可以选择 F0,因为它是第一个被分配掉的内存页(这个算法我们可以称之为 FIFO,先进先出算法);假设装载管理器发现 F2 很少被访问到,那么我们可以选择 F2(这种算法可以称之为 LUR,最少使用算法)。假设我们放弃 F0,那么这时候 F0 就装入了 P4。程序接着按照这样的方式运行。
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映射文件。
内存缺页中断
进程的虚拟内存通过进程映射表翻译内存地址转化成物理内存,虚拟内存不是以字节进行管理的,而是以页进行管理,假设有5页的数据,并不是全部加载入虚拟内存,而是用多少,加载多少,分页加载。项目启动时符号(方法、函数、block)会加载到对应的虚拟内存页当中,比如第1个符号加载入第1页,第2个符号加载入第5页,第3个符号加载入第2页,第4个符号加载入第1页,就会产生3次内存缺页中断。
Clang插桩二进制重排
通过Clang插桩的方式获得项目启动时的符号表,将应用的启动符号顺序按照插桩获得这个符号表进行排列,就能够有效的减少内存缺页中断的次数。
启动优化
注意: 这个类负责所有的 didFinishLaunchingWithOptions 延迟事件的加载. 以后引入第三方需要在 didFinishLaunchingWithOptions 里初始化或者我们自己的类需要在 didFinishLaunchingWithOptions 初始化的时候, 要考虑尽量少的启动时间带来好的用户体验, 所以应该根据需要减少 didFinishLaunchingWithOptions 里耗时的操作.
- 第一类: 比如日志 / 统计等需要第一时间启动的, 仍然放在 didFinishLaunchingWithOptions 中.
- 第二类: 比如用户数据需要在广告显示完成以后使用, 所以需要伴随广告页启动, 只需要将启动代码放到 startupEventsOnADTimeWithAppDelegate 方法里.
- 第三类: 比如直播和分享等业务, 肯定是用户能看到真正的主界面以后才需要启动, 所以推迟到主界面加载完成以后启动, 只需要将代码放到 startupEventsOnDidAppearAppContent 方法里.
load()和initialize()区别
调用方式
load是根据函数地址直接调用initialize是通过objc_msgSend调用
调用时刻
load是runtime加载类、分类的时候调用(只会调用一次)initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)
+load 和 +initialize 方法是我们写 Objective-C 代码时常用的两个方法,不过貌似在 Swift 4.x 后,这两个方法在 Swift 类中不那么好使,会报如下编译错误:
Method 'load()' defines Objective-C class method 'load', which is not permitted by Swift
Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift
RAC机制 响应式编程 机制 也是 KVO
delloc做了什么?
delloc 先判断是否是TaggedPointer,如果是的话直接返回。因为TaggedPointer它的内存并不存储在堆中,也不需要 malloc 和 free。 1. object_cxxDestruct(c++析构方法)释放成员变量 2. _object_remove_assocations 移除关联对象 3. 清空引用计数表并清除弱引用表,将weak指针置为nil 4.最后free释放内存
alloc做了什么?
alloc 实例创建的内存分配 cls->instanseSize 计算需要开辟的内存空间 calloc 申请内存,返回地址指针 obj->initInstanseIsa 关联到相应的类
init做了什么?
init 初始化 init负责初始化对象,就是一个工厂方法,返回自身, 这意味着此时此对象处于可用状态, 即对象的实例变量可以被赋予合理有效值. 返回的可能是nil,所以重写init方法的时候,需要判断一下 if (self = [super init])
两阶段创建模式
两阶段创建使程序员能够在控制如何为对象分配内存的同时,为初始化实例提供灵活性。很少会创建新的分配方法,但是,几乎每个类都要创建一个或多个新的初始化方法。
应用生命周期
ViewController 生命周期
1.init
init函数并不会每次创建对象都调用,只有在这个类第一次创建对象时才会调用,做一些类的准备工作,再次创建这个类的对象,initalize方法将不会被调用,对于这个类的子类,如果实现了initialize方法,在这个子类第一次创建对象时会调用自己的initalize方法,之后不会调用,如果没有实现,那么它的父类将替它再次调用一下自己的initialize方法,以后创建也都不会再调用。因此,如果我们有一些和这个相关的全局变量,可以在这里进行初始化。
2. initCoder
initCoder方法和init方法相似,只是被调用的环境不一样,如果用代码进行初始化,会调用init,从nib文件或者归档进行初始化,会调用initCoder。
3. loadView
loadView方法是开始加载视图的起始方法,除非手动调用,否则在ViewController的生命周期中没特殊情况只会被调用一次。
不要重写 loadView
4. viewDidLoad
viewDidLoad方法是我们最常用的方法的,类中成员对象和变量的初始化我们都会放在这个方法中,在类创建后,无论视图的展现或消失,这个方法在视图控制器的生命周期中会且仅会被调用一次。
将 viewDidLoad 作为最后的检查点,查看来自数据源的数据是否可用。如果可用,则更 新 UI 元素。
5. viewWillAppear(和 viewDidLoad 的差值小于与 viewDidAppar 的差值)
视图将要展现时会调用。
如果每次都需要展示最新的信息,那么就使用 viewWillAppear: 更新 UI 元素。
6. viewWillLayoutSubviews
在viewWillAppear后调用,将要对子视图进行布局。
7. viewDidLayoutSubviews
已经布局完成子视图。
8. viewDidAppar (和 viewWillAppear 有约300毫秒的差值)
视图完成显示时调用。
9. viewWillDisappear
视图将要消失时调用。
使用 viewWillDisappear: 来暂停或停止动画。同样,不要做其他多余的操作。
10. viewDidDisappear (和 viewWillDisappear 有约300毫秒的差值)
视图已经消失时调用。
使用 viewDidDisappear:销毁内存中的复杂数据结构
11. dealloc
当视图控制器即将被销毁时调用一次,我们用这个方法解除对屏幕通知的注册。
响应者链
事件首先会传递给 UIApplication 对象,接下来会传递给应用程序的UIWindow。UIWindow会选择一个初始响应器来处理事件。初始响应器会按照下面的方式选择。
-
对于触摸事件,
UIWindow对象会确定用户触摸的视图,然后将事件交给注册了这个视图的手势识别器或者注册了更高层级视图的手势识别器。只要存在能处理事件的识别器,就不会继续找了。如果没有的话,被触摸视图就是初始响应器,事件也会传递给它。 -
对于用户摇晃设备产生的或者来自远程遥控设备的事件,将会传递给第一响应器。
如果初始响应器不处理事件,它会将事件传递给它的父视图(如果存在的话),或者传递给视图控制器(如果该视图是视图控制器的视图)。如果视图控制器不处理事件,它将沿着响应器链的视图层级继续传给父视图控制器(如果存在的话)。
如果在整个视图层级中都没有能处理事件的视图或控制器,事件就会被传递给应用程序的窗口。如果窗口不能处理事件,而应用委托是 UIResponder 的子类(如果通过草果公司的应用模板来创建项目,那么通常如此),UIApplication 对象就会将其传递给应用程序委托。最后,如果应用委托不是 UIResponder 的子类,或者不处理这个事件,那么这个事件将会被丢弃。
如果某个对象截获了无法处理的事件,就需要手动将该对象继续向下传递。方法就是在下一个响应者上调用相同的方法。来看看代码清单中的代码。
func respondToF ictionalEvent(event: UIEvent) {
if shouldHandleEvent(event) {
handleEvent(event)
} else {
nextResponder().respondToFictionalEvent(event)
}
}
附:UIWindow 有一个类型为 "UIWindowLevel" 的属性,该属性定义了 UIWindow 的层级,系统定义的 UIWindowLevel 一共有 3 种取值:(UIWindowLevel 的值越高,在视图的层级就越高)
UIKIT EXTERN const UIWindowLevel UIWindowLevelNormal; 0.000000
UIKIT EXTERN const UIWindowLevel UIWindowLevelAlert; 2000.000000
UIKIT EXTERN const UIWindowLevel UIWindowLevelStatusBar; 1000.000000
我们可以看一下 UIWindowLevel 的值
NSLog(@"UIWindowLevelNormal=%f\nUIWindowLevelAlert=%f\nUIWindowLevelStatusBar=%f",UIWindowLevelNormal,UIWindowLevelAlert,UIWindowLevelStatusBar);
输出为:
UIWindowLevelNormal=0.000000
UIWindowLevelAlert=2000.000000
UIWindowLevelStatusBar=1000.000000
SDWebImage图片缓存策略
根据url从内存缓存中查找图片,找到了显示,没找到去硬盘缓存中查找图片,找到了加载图片到内存缓存并显示图片,没找到的话下载图片,图片解码显示图片,将图片加载到内存和硬盘缓存 可以设置缓存过期日期和最大缓存容量,缓存过期日期默认是7天,最大缓存容量的话原则上是无限的,但是最好是50M,根据项目需求来定
GCD有哪些方法api
dispatch_sync
dispatch_async
dispatch_once
dispatch_queue_create
dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal
dispatch_group_create
dispatch_group_enter
dispatch_group_leave
dispatch_set_target_queue
dispatch_barrier_sync
dispatch_barrier_async
dispatch_after
dispatch_apply
dispatch_source_t
函数与队列
iOS多线程原理和线程生命周期是什么样
- 对于
单核CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作,iOS中的多线程同时执行的本质是CPU在多个任务直接进行快速的切换,由于CPU调度线程的时间足够快就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片。多核CPU具有真正意义上的并发!
- 新建状态:用
new关键字建立一个线程后,该线程对象就处于新建状态。处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。 - 就绪状态:处于就绪状态线程具备了运行条件,但还没分配到
CPU,处于线程就绪队列,等待系统为其分CPU。 当系统选定一个等待执行的线程后,它就会从就绪状态进入运行状态,该动作称为“CPU调度”。 - 运行状态:在运行状态的线程执行自己的
run方法中代码直到等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。 - 阻塞状态:处于运行状态的线程在某些情况下,如执行了
sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待被系统选中后从原来停止的位置开始继续执行。 - 死亡状态:死亡状态是线程生命周期中的最后一个阶段。
线程死亡的原因有三个:
- 是正常运行的线程完成了它的全部工作;
- 线程被强制性地终止,如通过
exit方法来终止一个线程【不推荐使用】; - 线程抛出未捕获的异常。
数据库存储
FMDB
- 增:
insert [into] <表名> [列名] values <列值> - 删:
delete from <表名> [where <删除条件>] - 改:
update <表名> set <列名=更新值> [where <更新条件>] - 查:
select <列名> from <表名> [where <查询条件表达试>] [order by <排序的列名>[asc或desc]]
FMDatabaseQueue
基于 FMDatabaseQueue 为同步串行队列来保证数据库访问的安全性。
CoreData
CoreData 是一种在iOS 3系统中,也是苹果自己推出的数据存储框架,采用了一种ORM(对象关系映射)的存储关系。CoreData一个比较大的优势在于在使用 CoreData 过程中不需要我们编写 SQL 语句,也就是将 OC对象 存储于数据库,也可以将数据库数据转为 OC对象(数据库数据与 OC对象 相互转换)。
CoreData中的几个类
(1)NSManagedObjectContext
意思是托管对象上下文,数据库的大多数操作是在这个类操作
(2)NSManagedObjectModel
意思是托管对象模型,其中一个托管对象模型关联到一个模型文件,里面存储着数据库的数据结构。
(3)NSPersistentStoreCoordinator
意思是持久化存储协调器,主要负责协调上下文与存储的区域的关系。
(4)NSManagedObject
意思是托管对象类,其中CoreData里面的托管对象都会继承此类。
CoreData使用
1. 我们在创建项目的时候,勾选Use Core Data
如果利用项目刚建时,勾选Use Core Data,这样在目录中就会出现,后缀名为.xcdatamodeld。
打开AppDelegate发现类中多了以下内容
AppDelegate.h
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (readonly, strong) NSPersistentContainer *persistentContainer;
- (void)saveContext;
@end
AppDelegate.m
#pragma mark - Core Data stack
@synthesize persistentContainer = _persistentContainer;
- (NSPersistentContainer *)persistentContainer {
// The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
@synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"CoreData"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}];
}
}
return _persistentContainer;
}
#pragma mark - Core Data Saving support
- (void)saveContext {
NSManagedObjectContext *context = self.persistentContainer.viewContext;
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}
2. 创建实体、属性
我们可以点开 CoreData.xcdatamodeld 文件 ,我们可以看到实体和关系。如下图:
2.1 我们创建实体后就可以添加属性了
创建后可以清楚的看到模型文件左侧的列表,有三个
Entities、Fetch Requests 以及 Configurations 三个选项,意思分别是:实体,请求模版以及配置信息。
添加完一个实体后,你会发现一个实体是对应着三个内容,分别是 Attributes、Relationships和 Fetched Properties,意思分别是:属性、关联关系以及获取操作。
2.2 Attributes 实体属性类型
-
Undefined:也就是默认值,如果参与编译会报错
-
Integer 16:代表整数,范围是-32768 ~ 32767
-
Integer 32:代表整数,范围是-2147483648 ~ 2147483647
-
Integer 64:代表整数,范围是–9223372036854775808 ~ 9223372036854775807,还是很大的,较少用
-
Double:代表小数
-
Float:代表小数
-
String:代表字符串,NSString表示
-
Boolean:代表布尔值,使用NSNumber表示
-
Date:代表日期时期
-
Binary Data:代表二进制,是用NSData表示
-
Transformable:代表Objective对象,要遵守NSCoding协议
2.3 Relationships 关联关系
点击加号,可以添加关联关系,在inverse这个属性代表两个实体在Relationships设置关联关系后之后,是否可以从一个实体中找到另一个实体,这样使两个实体具有双向的关联关系。
2.4 Editor Style
大家通过点击下面红色按钮,style按钮可以看出实体和属性的关系,以及可以看出实体之间的对应的关系。
3. CoreData的基本使用
在讲述操作之前,我们首先讲述NSManagedObjectContext,苹果推荐使用initWithConcurrencyType方式创建,在创建时,指定当前是什么类型的并发队列,参数也是一个枚举值。
NSManagedObjectContext枚举值参数有三个类型:
(1)NSConfinementConcurrencyType:此类型在iOS9之后被苹果弃用,所以不建议用这个API。
(2)NSPrivateQueueConcurrencyType:代表私有并发队列的类型,操作也是在子线程中完成的。
(3)NSMainQueueConcurrencyType:代表主并发队列类型,如果在操作过程中,需要涉及到UI操作,则应该使用这个参数初始化上下文完成操作。
下面我们一个company的模型文件-主队列并发类型的NSManagedObjectContext
// 创建上下文对象,并发队列设置为主队列
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
// 创建托管对象模型,并使用Company.momd路径当做初始化参数
NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath];
// 创建持久化存储调度器
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 创建并关联SQLite数据库文件,如果已经存在则不会重复创建
NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil];
// 上下文对象设置属性为持久化存储器
context.persistentStoreCoordinator = coordinator;
3.1 插入操作
CoreData 通过 NSEntityDescription 的 insert 进行插入操作,这样就会生成并返回一个托管对象,并将这个对象插入到上下文中。下面以一个Employee为例:
NSManagedObjectContext 将操作的数据放到了缓存层中,只有调用了 NSManagedObjectContext 的 save 后,才会对数据库进行真正的操作,否则对象仅仅存在内存中,这样就很好地避免了数据库的频繁访问。
// 开始创建托管对象,并指明好创建的托管对象所属实体名
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context];
emp.name = @"lxz";
emp.height = @1.7;
emp.brithday = [NSDate date];
// 通过这样上下文保存对象,并在保存前判断是否有了最新的更改
NSError *error = nil;
if (context.hasChanges) {
[context save:&error];
}
// 错误处理
if (error) {
NSLog(@"CoreData Insert Data Error : %@", error);
}
3.2 删除操作
CoreData 首先通过获取需要删除的托管对象,遍历所需要获取的对象数组,逐个删除,最后调用 NSManagedObjectContext 的 save 方法。
// 获取数据的请求对象,指明对实体进行删除操作,以Employee为例
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 通过创建谓词对象,然后过滤掉符合要求的对象,也就是要删除的对象
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
request.predicate = predicate;
// 通过执行获取操作,找到要删除的对象即可
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
// 开始真正操作,一一遍历,遍历符合删除要求的对象数组,执行删除操作
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[context deleteObject:obj];
}];
// 最后保存数据,保存上下文。
if (context.hasChanges) {
[context save:nil];
}
// 错误处理
if (error) {
NSLog(@"CoreData Delete Data Error : %@", error);
}
3.3 修改操作
获取数据的请求对象,指明对实体进行修改操作
// 获取数据的请求对象,指明对实体进行修改操作,以Employee为例
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 创建谓词对象,设置过滤条件,找到要修改的对象
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
request.predicate = predicate;
// 通过执行获取请求,获取到符合要求的托管对象,修改即可
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.height = @3.f;
}];
// 通过调用save将上面的修改进行存储
if (context.hasChanges) {
[context save:nil];
}
// 错误处理
if (error) {
NSLog(@"CoreData Update Data Error : %@", error);
}
3.4 查找操作
查找操作是是有许多条件限制,根据条件查找出相应的数据,下面以查找所有元素的一个例子说明一下
// 获取数据的请求对象,指明对实体进行查询操作,以Employee为例
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 执行获取操作,获取所有Employee托管对象
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"Employee Name : %@, Height : %@, Brithday : %@", obj.name, obj.height, obj.brithday);
}];
// 错误处理
if (error) {
NSLog(@"CoreData Ergodic Data Error : %@", error);
}
组件化
组件化方案
1. url-block(蘑菇街)
蘑菇街通过 MGJRouter 实现中间层,通过 MGJRouter 进行组件间的消息转发,从名字上来说更像是路由器。实现方式大致是,在提供服务的组件中提前注册 block,然后在调用方组件中通过 URL 调用 block。
`MGJRouter` 是一个单例对象,在其内部维护着一个 `“URL -> block”` 格式的注册表,通过这个注册表来保存服务方注册的 `block`,以及使调用方可以通过 `URL` 映射出 `block`,并通过 `MGJRouter` 对服务方发起调用。
在程序开始运行时,需要将所有服务方的接口类实例化,以完成这个注册工作,使 MGJRouter 中所有服务方的 block 可以正常提供服务。在这个服务注册完成后,就可以被调用方调起并提供服务。
- 硬编码问题,每个组件参数调用都需要查找对应。蘑菇街为此开发了一个 `web页面`,这个 `web页面` 统一来管理所有的 `URL` 和参数。
- 需要在内存中维护 `url-block` 的表,组件多了可能会有内存问题。
- `url` 的参数传递受到限制,只能传递常规的字符串参数,无法传递非常规参数,如`UIImage、NSData`等类型。
- 没有区分本地调用和远程调用的情况,尤其是远程调用,会因为url参数受限,导致一些功能受限。
- 组件本身依赖了中间件,且分散注册使的耦合较多。
2. Protocol(阿里BeeHive)
面向接口调用,我们知道只要直接引用代码,就会有依赖,比如:
// A 模块
- (void)getSomeDataFromB {
B.getSomeData();
}
// B 模块
- (void)getSomeData {
return self.data;
}
那么我们可以实现一个 getSomeDataFromB 的接口,让 A 只依赖这个接口,而 B 来实现这个接口,这样就实现了 A 与 B 的解耦。
// 接口
@protocol BService <NSObject>
- (void)getSomeData;
@end
// A 模块, 只依赖接口
- (void)getSomeDataFromB {
id b = findService(@protocol(BService));
b.getSomeData;
}
// B 模块,实现BService接口
@interface B : NSObject <BService>
- (void)getSomeData {
return self.data;
}
@end
这样就可以实现了即满足了模块之间调用,也实现了解耦。
优点:
- 接口类似代码,可以非常灵活的定义函数和回调等。
- 解决了硬编码的问题。
缺点:
-
接口定义文件需要放在一个模块以供依赖,但是这个模块不回贡献代码,所以还好。
-
使用较为麻烦,每各调用都需要定义一个
service,并实现, 对于一些具有普适性规律的场景不太合适,比如页面统一跳转。
3. Target_Action(CTMediator)
就是对每个服务方组件创建一个 CTMediator 的 Category,并将对服务方的 performTarget 调用放在对应的 Category 中,这些 Category 都属于 CTMediator中间件,从而实现了感官上的接口分离。
实现细节
-
对于服务方的组件来说,每个组件都提供一个或多个
Target类,在Target类中声明Action方法。 -
Target类是当前组件对外提供的一个“服务类”,Target将当前组件中所有的服务都定义在里面,CTMediator通过runtime主动发现服务。 -
在
Target中的所有Action方法,都只有一个字典参数,所以可以传递的参数很灵活,这也是casatwy提出的去Model化的概念。 -
在
Action的方法实现中,对传进来的字典参数进行解析,再调用组件内部的类和方法。- 侵入最小,但硬编码较多。
runtime编译阶段不检查,运行时才检查对应类或者方法是否存在,对开发要求较高。
Swift
swift & OC的区别
- swift注重安全,OC注重灵活
- swift注重值类型,OC注重指针和引用
- swift是静态类型语言,OC是动态类型语言
- swift中的泛型类型更加方便和通用,而非OC中只能为集合类型添加泛型
- swift中各种方便快捷的高阶函数(函数式编程) (Swift的标准数组支持三个高阶函数:map,filter和reduce,以及map的扩展flatMap)
- swift中独有的元组类型(tuples),把多个值组合成复合值。元组内的值可以是任何类型,并不要求是相同类型的。
方法列表:
-
swift: 方法列表
V-Table是存储在metadata中 -
OC: OC中的方法存储在
objc_class结构体class_rw_t的methodList中
实例对象
OC中的实例对象本质是结构体,是通过底层的 objc_object 模板创建,类是继承自 objc_class
Swift中的实例对象本质也是结构体,类型是 HeapObject,需要两个参数:metadata、refCounts,其中 metadata 类型是 HeapMetadata,是一个指针类型,占8字节,refCounts 也占8个字节,比OC多了一个 refCounts
引用计数
OC中的ARC维护的是散列表,isa_t的结构位域中有两个成员与引用计数有关分别是
- extra_rc 存放的是可能是对象部分或全部引用计数值减1
- has_sidetable_rc 为一个标志位,值为1时代表
extra_rc的8位内存已经不能存放下对象的retainCount, 需要把一部分retainCount存放在sidetable中。
struct SideTable {
spinlock_t slock; //操作内部数据的锁,保证线程安全
RefcountMap refcnts;//哈希表[伪装的对象指针 : 64位的retainCoint信息值]
weak_table_t weak_table;//存放对象弱引用指针的结构体
}
extra_c 溢出的时候是把一半值减掉后存进对应对象指针的 SideTable 的成员变量 RefcountMap refcnts 中
Swift 中的 ARC 是对象内部有一个 refCounts 属性
面向协议(面向接口)与面向对象的区别
面向对象和面向协议的的最明显区别是对抽象数据的使用方式,面向对象采用的是继承,而面向协议采用的是遵守协议。在面向协议设计中,Apple建议我们更多的使用 值类型 (struct)而非 引用类型 (class)。
有一个很好的例子说明了面向协议比面向对象更符合某些业务需求。其中有飞机、汽车、自行车三种交通工具(均继承自父类交通工具);老虎、马三种动物(均继承父类自动物);在古代马其实也是一种交通工具,但是父类是动物,如果马也有交通工具的功能,则:如果采用面向对象编程,则需要既要继承动物,还要继承交通工具,但是父类交通工具有些功能马是不需要的。
由此可见继承,作为代码复用的一种方式,耦合性还是太强。事物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。以后慢慢会发现面向对象很多时候其实不能很好地对事物进行抽象。
如果采用面向协议编程,马只需要实现出行协议就可以拥有交通工具的功能了。面向协议就是这样的抽离方式,更好的职责划分,更加具象化,职责更加单一。很明显面向协议的目的是为了降低代码的耦合性。
总结
面向协议相对于面向对象来说更具有可伸缩性和可重用性,并且在编程的过程中更加模块化,通过协议以及协议扩展替代一个庞大的基类,这在大规模系统编程中会有很大的便捷之处。
swift构造过程
你要通过定义构造器来实现构造过程,它就像用来创建特定类型新实例的特殊方法。与 Objective-C 中的构造器不同,Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化
函数派发机制
静态派发:
静态派发是三种派发方式中最快的。CPU 直接拿到函数地址并进行调用。编译器优化时,也常常将函数进行内联,将其转换为静态派发方式,提升执行速度。C++ 默认使用静态派发;在 Swift 中给函数加上final关键字,也会变成静态派发。扩展也属于静态派发
函数表派发:
函数所在的类会维护一个“函数表”(虚函数表),存取了每个函数实现的指针。 每个类的 vtable 在编译时就会被构建,所以与静态派发相比多出了两个读取的工作:1. 读取该类的 vtable 2. 读取函数的指针
消息派发:
消息机制是调用函数最动态的方式。由于 Swfit 使用的依旧是 Objective-C 的运行时系统,消息派发其实也就是 Objective-C 的 Message Passing(消息传递)。
解包
-
强制解包
optionValue!,当有十足的把握确定optionValue != nil时,我们可以使用强制解包let y = optionValue! + 1强制解包 容易造成奔溃,不建议使用
-
强制解包的代替方案:??
let y = optionValue ?? 100 -
可选绑定
if let y = optionValue { print('optionValue 有值') } else { print('optionValue 为空') } -
可选值链
struct Order{ let orderNumber:Int let person: Person? } struct Person{ let name: String let address: Address? } struct Address { let streetNmae: String let city: String let state: String? } //显示解包,有可能会奔溃 let state1 = order.person!.address!.state! //可选链 let state2 = order.person?.address?.state -
分支上的可选值
- switch case 分支
// number 是可选值 switch number{ case 0?: print("number 是 0") case (1..<1000)?: print("number 是 1000 以内的数") case .Some(let x): print(" number 是 \(x)") case .None: print(" number 为 空") }- if let
在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;
if let unwrapedHeight = height { return unwrapedHeight // 100 } return nil-
guard let
而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况
guard let unwrapedHeight = height else { return nil } return unwrapedHeight // 100
闭包()
class SomeClass {
let someProperty: SomeType = {
// 在这个闭包中给 someProperty 创建一个默认值
// someValue 必须和 SomeType 类型相同
return someValue
}()
}
注意闭包结尾的花括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。(还有懒加载)
swift 中 Class 和 Struct 的区别
引用类型:将一个对象赋值给另一个对象时,系统不会对此对象进行拷贝,而会将指向这个对象的指针赋值给另一个对象,当修改其中一个对象的值时,另一个对象的值会随之改变。 值类型:将一个对象赋值给另一个对象时,会对此对象进行拷贝,复制出一份副本给另一个对象,在修改其中一个对象的值时,不影响另外一个对象。
- 类属于引用类型,结构体属于值类型
- 类允许被继承,结构体不允许被继承
- 类中的每一个成员变量都必须被初始化,否则编译器会报错,而结构体不需要,编译器会自动帮我们生成init函数,给变量赋一个默认值
指针
swift指针分为两类:
-
typed pointer指定数据类型指针,即UnsafePointer<T>,其中T表示泛型 -
raw pointer未指定数据类型的指针(原生指针) ,即UnsafeRawPointerswift与OC指针对比如下:
| Swift | OC | 说明 |
|---|---|---|
| unsafePointer | const T * | 指针及所指向的内容都不可变 |
| unsafeMutablePointer | T * | 指针及其所指向的内存内容均可变 |
| unsafeRawPointer | const void * | 指针指向未知类型 |
| unsafeMutableRawPointer | void * | 指针指向未知类型 |
Swift 内存管理之 weak 与 unowned
weak
weak var delegate: SomeDelegate?
lazy var someClosure: () -> Void = { [weak self] in
guard let self = self else { retrun }
self.balabala
}
当我们赋值给一个被标记 weak 的变量时,它的引用计数不会被改变。而且当这个弱引用变量所引用的对象被释放时,这个变量将被自动设为 nil。这也是弱引用必须被声明为 Optional 的原因。
unowned
和 weak 相同,unowned 也可以在不增加引用计数的前提下,引用某个类实例。
unowned let someInstance: SomeClass
lazy var someClosure: () -> Void = { [unowned self] in
self.balabala
}
在使用 unowned 时,我们不需要将变量声明为 Optional。
需要注意的是。对于被 unowned 标记的变量,即使它的原来引用已经被释放,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不是 Optional ,也不会被指向 nil。所以,当我们试图访问这样的 unowned 引用时,程序就会发生错误。
逆向
ptrace
debugserver通过ptrace函数调试app ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器里面的数据。ptrace可以用来实现断点调试和系统调用跟踪。
要做到反调试,只需参数1为 PT_DENY_ATTACH, 参数2为自己
#import "MyPtrace.h"
//app反调试防护
ptrace(PT_DENY_ATTACH, 0, 0, 0);
这样你的 app 就不可以用 Xcode 调试了
fishhook
rebinding 结构体用来确定你要 HOOK 的函数和要交换的函数地址。
struct rebinding {
constchar *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};
注意 replaced:在使用该结构体时,由于函数内部要修改外部指针变量所保存的值,所以这里是指针的指针(二级指针)。
rebind_symbols 和 rebind_symbols_image 函数用来 HOOK 的两个方法。只不过后者是指定某个镜像文件的时候使用。所以一般我们直接使用前者。
镜像文件:比如 NSLog 函数是在 Fundation 框架中,那么 Fundation 在内存中就是一个镜像文件。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
int rebind_symbols_image(void *header,intptr_t slide,struct rebinding rebindings[],size_t rebindings_nel);
HOOK一下NSLog
我们新建一个 SingleView 的项目。在 ViewDidLoad 中对系统的 NSLog 函数进行 HOOK 。
//函数指针
staticvoid(*sys_nslog)(NSString* format,...);
//定义一个新的函数。HOOK成功后NSLog调用时,会来到这里
void myNSLog(NSString* format,...){
format = [format stringByAppendingString:@"\n上钩了!\n\n"];
sys_nslog(format);//调用系统的NSLog,HOOK成功后sys_nslog指针保存的是Fundation中NSLog的地址
}
- (void)viewDidLoad {
[super viewDidLoad];
//准备rebinding结构体
struct rebinding nslog;
nslog.name = "NSLog";
//需要HOOK的函数名称
nslog.replacement = myNSLog;
//新函数的地址
nslog.replaced = (void *)&sys_nslog;
//原始函数指针
//准备数组,将一个或多个 rebinding 结构体放进去。
struct rebinding rebs[1] = {nslog};
/**
arg1: 存放rebinding 结构体的数组
arg2: 数组的长度
*/
rebind_symbols(rebs, 1);
}
-(void)touchesBegan:(NSSet<UITouch*> *)touches withEvent:(UIEvent*)event{
NSLog(@"点击了屏幕!");
}
音视频
- 音视频AVFoundation
- 捕捉会话:AVCaptureSession
- 捕捉设备:AVCaptureDevice
- 捕捉设备输入:AVCaptureDeviceInput
- 捕捉设备输出:AVCaptureOutput抽象类
AVCaptureStillImageOutput、 AVCaputureMovieFileOutput、 AVCaputureAudioDataOutput、 AVCaputureVideoDataOutput
- 捕捉连接:AVCaptureConnection
- 捕捉预览:AVCaptureVideoPreviewLayer
Session
在i0S中,会经常使用到 session的方式.比如我们使用任何硬件设备都要使用对应的session,麦克风就要使用 AudioSession,使用 Camera就要使用 AVCaptureSession,使用编码则需要使用 VTCompressionSession.解码时,要使用 VTDecompressionSessionRef.
CVPixelBuffer -> CMBlockBuffer
在AVFoundation 回调方法中,它有提供我们的数据其实就是CVPixelBuffer.只不过当时使用的是引用类型CVImageBufferRef,其实就是CVPixelBuffer的另外一个定义,Camera返回的CVImageBuffer 中存储的数据是一个CVPixelBuffer,而经过VideoToolBox编码输出的CMSampleBuffer 中存储的数据是一个CMBlockBuffer的引用.
解码思路:解析数据
既然NALU,一个接一个实时解码!首先,你要对数据解析!分析NALU数据.前面4个字节是起始位!标识一个NALU的开始!从第5位才开始来获取!从第五位才是NALU数据类型.要获取到第5位数据,转化十进制,然后根据表格判断它数据类型(第5个字节是表示数据类型,转为10进制后,7是sps, 8是pps, 5是IDR(I帧)信息)!判断好数据类型,才能将NALU送入解码器.SPS/PPS获取就可以,是不需求解码的!
PCM
PCM(脉冲编码调制)就是把模拟信号转化为数字信号得到PCM数据 1. 采样 2. 量化 3. 编码
AudioToolbox
在AAC编码的场景下,源格式就是采集的PCM数据,目的格式就是AAC
网络
TCP和UDP的区别,他们应运的业务场景是?
若通信数据完整性需让位与通信实时性,则应该选用 TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)
TCP粘包
出现以上的拆包和粘包原因主要有以下四个
粘包原因:
-
要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
-
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
拆包原因:
-
要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
-
待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
解决方法有三种,具体如下:
-
发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
-
发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
-
可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
Socket
HTTP协议对应于应用层,TCP协议 对应于传输层,IP协议 对应于网络层,HTTP协议 是基于 TCP连接的,三者本质上没有可比性。 TCP/IP 是传输层协议,主要解决数据如何在网络中传输;而 HTTP 是应用层协议,主要解决如何包装数据。Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是它的一组接口。
WEB 使用 HTTP 作传输层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议将它发送到网络上。Socket 是对 TCP/IP协议 的封装,Socket 本身并不是协议,而是一个调用接口(API),通过 Socket,我们才能使用 TCP/IP协议。
Socket连接与HTTP连接的不同
通常情况下 Socket连接 就是 TCP连接,因此 Socket连接 一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际应用中,客户端到服务器之间的通信防火墙默认会关闭,长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
弱网优化
一、TCP/IP协议栈参数调优
1. 控制传输包大小
控制传输包的大小在1400字节以下。概括的说,我们设定1400这个阈值,目的是减少往复,提高效能。因为TCP/IP网络中也有类似高速限高的规定,如果在超限时想要继续顺畅传输,要么做IP分片要么把应用数据拆分为多个数据报文(意指因为应用层客户端或服务器向对端发送的请求或响应数据太大时,TCP/IP协议栈控制机制自动将其拆分为若干独立数据报文发送的情况,后面为简化讨论,都以IP分片这个分支为代表,相关过程分析和结论归纳对二者均适用)。而一旦一个数据报文发生了IP分片,便会在数据链路层引入多次的传输和确认,加上报文的拆分和拼接开销,令得整个数据包的发送时延大大增加,并且,IP分片机制中,任何一个分片出现丢失时还会带来整个IP数据报文从最初的发起端重传的消耗。有点枯燥了,更深入的理解,请参见:《海量之道系列文章之弱联网优化 (二)》。
我们可以得出如下结论,TCP/IP数据报文大小超过物理网络层的限制时,会引发IP分片,从而增加时空开销。
什么是MSS?
TCP MSS(TCP Maximum Segment Size,TCP最大报文段长度,后面均简称MSS)表示TCP/IP协议栈一次可以传往另一端的最大TCP数据长度,注意这个长度是指TCP报文中的有效“数据”(即应用层发出的业务数据)部分,它不包括TCP报文包头部分,我们可以把它理解为卡车能装运生猪的最大数量或重量。它是TCP选项中最经常出现,也是最早出现的选项,占4字节空间。
MSS是在建立TCP链接的三次握手过程中协商的,每一方都会在SYN或SYN/ACK数据报文中通告其期望接收数据报文的MSS(MSS也只能出现在SYN或SYN/ACK数据报中),说是协商,其实也没太多回旋的余地,原因一会讲。如果协商过程中一方不接受另一方的MSS值,则TCP/IP协议栈会选择使用默认值:536字节。
因此,设定合理的MSS至关重要,对于以太网MSS值建议是1400字节。
2. 放大TCP拥塞窗口
把TCP拥塞窗口(cwnd)初始值设为10,这也是目前Linux Kernel中TCP/IP协议栈的缺省值。TCP是个传输控制协议,体现控制的两个关键机制分别是基于滑动窗口的端到端之间的流量控制和基于RTT/RTO测算的端到网络之间的拥塞控制。
拥塞控制目标是在拥塞发生时能及时发现并通过减少数据报文进入网络的速率和数量,达到防止网络拥塞的目的,这种机制可以确保网络大部分时间是可用的。拥塞控制的前提在于能发现有网络拥塞的迹象,TCP/IP协议栈的算法是通过分组丢失来判断网络上某处可能有拥塞情况发生,评判的具体指标为分组发送超时和收到对端对某个分组的重复ACK。在有线网络时代,丢包发生确实能比较确定的表明网络中某个交换设备故障或因为网络端口流量过大,路由设备转发处理不及时造成本地缓存溢出而丢弃数据报文,但在移动网络中,丢包的情况就变得非常复杂,其它因素影响和干扰造成丢包的概率远远大于中间路由交换设备的故障或过载。比如短时间的信号干扰、进入一个信号屏蔽的区域、从空闲基站切换到繁忙基站或者移动网络类型切换等等。网络中增加了这么多不确定的影响因素,这在TCP拥塞控制算法最初设计时,是无法预见的,同时,我们也确信未来会有更完善的解决方案。这是题外话,如有兴趣可以找些资料深入研究(详见:《TCP/IP详解 - 第21章·TCP的超时与重传》、《通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理》、《海量之道系列文章之弱联网优化 (三)》)。
拥塞控制是TCP/IP协议栈最经典的和最复杂的设计之一,互联网自我牺牲的利他精神表露无遗,设计者认为,在拥塞发生时,我们应该减少数据报文进入网络的速率和数量,主动让出道路,令网络能尽快调整恢复至正常水平。
3. 调大SOCKET读写缓冲区
把SOCKET的读缓冲区(亦可称为发送缓冲区)和写缓冲区(亦可称为接收缓冲区)大小设置为64KB。
这两个缓冲区跟我们的TCP/IP协议栈到底有怎么样的关联呢。我们回忆一下TCP数据报格式及首部中的各字段里面有个16位窗口大小(见下图),还有我们前面提到的流量控制机制和滑动窗口的概念,大幕徐徐拉开,主角纷纷粉墨登场。在正式详细介绍之前,按照传统,我们还是先站在猪场老板的角度看一下,读缓冲区就好比买家用来囤货的临时猪圈,如果货到了买家使用部门来不及处理,就先在这里临时囤着,写缓冲区就好比养猪场根据订单装好车准备发货,如果买家说我现在可以收货便可速度发出,有点明白了吧。
4. 调大RTO(Retransmission TimeOut)初始值
将RTO(Retransmission TimeOut)初始值设为3s。
TCP为每一个报文段都设定了一个定时器,称为重传定时器(RTO),当RTO超时且该报文段还没有收到接收端的ACK确认,此时TCP就会对该报文段进行重传。当TCP链路发生超时时,意味着很可能某个报文段在网络路由路径的某处丢失了,也因此判断此时网络出现拥塞的可能性变得很大,TCP会积极反应,马上启动拥塞控制机制。
5. 禁用TCP快速回收
TCP快速回收是一种链接资源快速回收和重用的机制,当TCP链接进入到TIME_WAIT状态时,通常需要等待2MSL的时长,但是一旦启用TCP快速回收,则只需等待一个重传时间(RTO)后就能够快速的释放这个链接,以被重新使用。
6. HTTP协议:打开SOCKET的TCP_NODELAY选项
TCP/IP协议栈为了提升传输效率,避免大量小的数据报文在网络中流窜造成拥塞,设计了一套相互协同的机制,那就是Nagle's Algorithm和TCP Delayed Acknoledgement。
Nagle算法(Nagle's Algorithm)是以发明人John Nagle的名字来命名。John Nagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应用程序一次产生1个字节的数据(典型的如telnet、XWindows等应用),而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易使网络中有太多微小分组而导致过载。
因为传输1个字节有效数据的微小分组却需花费40个字节的额外开销(即IP包头20字节 + TCP包头20字节),这种有效载荷利用率极其低下的情况被统称为愚蠢窗口症候群(Silly Window Syndrome),前面我们在谈MSS时也提到过,如果为一头猪开个大卡车跑一趟,也够愚钝的。对于轻负载广域网或者局域网来说,尚可接受,但是对于重负载的广域网而言,就极有可能引起网络拥塞导致瘫痪。
现代TCP/IP 协议栈默认几乎都启用了这两个功能。
我们在移动APP的设计实现中,请求大部分都很轻(数据大小不超过MSS),为了避免上述分析的问题,建议开启SOCKET的TCP_NODELAY选项,同时,我们在编程时对写数据尤其要注意,一个有效指令做到一次完整写入(后面会讲协议合并,是多个指令一次完整写入的设计思想),这样服务器会马上有响应数据返回,顺便也就捎上ACK了。
二、接入调度
1. 就快接入
在客户端接入服务器调度策略的演化过程中,我们最早采用了“就近接入”的策略,在距离客户端更近的地方部署服务器或使用CDN,期望通过减少RTT来提高网络交互响应性能。这个策略在国内的落地执行还需要加一个前缀:“分省分运营商”,这就给广大负责IDC建设的同学带来了巨大的精神和肉体折磨。
“就快接入”在“就近接入”策略的基础上改善提升,它利用客户端测速和报告机制,通过后台大数据分析,形成与客户端接入IP按就快原则匹配接入服务器的经验调度策略库,令客户端总能优先选择到最快的服务器接入点。
有关就快接入的更详细方案,请参见:《海量之道系列文章之弱联网优化(五)》一文的“3.1.2节”。
2. 去DNS的IP直连
DNS不但需要1个RTT的时间消耗,而且移动网络下的DNS还存在很多其它问题:
- 部分DNS承载全网用户40%以上的查询请求,负载重,一旦故障,影响巨大,这样的案例在PC互联网也有很多,Google一下即可感受触目惊心的效果;
- 山寨、水货、刷ROM等移动设备的LOCAL DNS设置错误;
- 终端DNS解析滥用,导致解析成功率低;
- 某些运营商DNS有域名劫持问题,实际上有线ISP也存在类似问题。域名劫持对安全危害极大,产品设计时要注意服务端返回数据的安全校验(如果协议已经建立在安全通道上时则不用考虑,安全通道可以基于HTTPS或者私有安全体系)。对于劫持的判断需要客户端报告实际拉取服务数据的目标地址IP等信息;
- DNS污染、老化、脆弱。
综上就是在前述就快接入小节中,接入调度FSM会优先使用动态服务器列表的原因。
有关移动端网络的DNS问题,详见:《全面了解移动端DNS域名劫持等杂症:技术原理、问题根源、解决方案等》。
3. 网络可达性探测
在连接建立过程中如果出现连接失败的现象,而终端系统提供的网络状态接口反馈网络可用时,我们需要做网络可达性探测(即向预埋的URL或者IP地址发起连接尝试),以区别网络异常和接入服务异常的情况,为定位问题,优化后台接入调度做数据支持。
探测数据可以异步报告到服务器,至少应该包含以下字段:
- 探测事件ID,要求全局唯一不重复;
- 探测发生时间;
- 探测发生时网络类型和其它网络信息(比如WIFI时的SSID等);
- 本地调度的接入服务器集合类型;
- 本地调度的接入服务器IP(如使用域名接入,可忽略);
- 探测的目标URL或IP地址
- 本次探测的耗时。
三、链路管理
链路就是运肥猪的高速路,就快接入是选路,链路管理就是如何高效的使用这条路。下面是一些实践总结:
1. 链路复用
我们在开篇讨论无线网络为什么慢的时候,提到了链接建立时三次握手的成本,在无线网络高时延、频抖动、窄带宽的环境下,用户使用趋于碎片化、高频度,且请求响应又一次性往返居多、较频繁发起等特征,建链成本显得尤其显著。
因此,我们建议在链路创建后可以保持一段时间,比如HTTP短链接可以通过HTTP Keep-Alive,私有协议可以通过心跳等方式来保持链路。
具体要点建议如下:
- 链路复用时,如果服务端按就快策略机制下发了新的接入动态服务器列表,则应该按照接入调度FSM的状态变迁,在本次交互数据完成后,重建与新的接入服务器的IP链路,有三个切换方案和时机可选择:
- a. 关闭原有链接,暂停网络通讯,同时开始建立与新接入服务器的TCP链路,成功后恢复与服务器的网络交互;
- b. 关闭原有链接,暂停网络通讯,待有网络交互需求时开始建立与新接入服务器的IP链路;
- c. 原有链接继续工作,并同时开始建立与新接入服务器的TCP链路,成功后新的请求切换到新建链路上,这个方式或可称为预建链接,原链接在空闲时关闭。
- 链路复用时区分轻重数据通道,对于业务逻辑等相关的信令类轻数据通道建议复用,对于富媒体拉取等重数据通道就不必了;
- 链路复用时,如与协议合并(后面会讨论)结合使用,效果更佳。
2. 区分网络类型的超时管理
在不同的网络类型时,我们的链路超时管理要做精细化的区别对待。链路管理中共有三类超时,分别是连接超时、IO超时和任务超时。
我们有一些经验建议,提出来共同探讨:
- 连接超时:2G/3G/4G下5 ~ 10秒,WIFI下5秒(给TCP三次握手留下1次超时重传的机会,可以研究一下《TCP/IP详解 卷一:协议》中TC P的超时与重传部分);
- IO超时:2G/3G/4G下15 ~ 20秒(无线网络不稳定,给抖动留下必要的恢复和超时重传时间),WIFI下15秒(1个MSL);
- 任务超时:根据业务特征不同而差异化处理,总的原则是前端面向用户交互界 面的任务超时要短一些(尽量控制在30秒内并有及时的反馈),后台任务可以长一些,轻数据可以短一些,重数据可以长一些;
- 超时总是伴随着重试,我们要谨慎小心的重试,后面会讨论。
超时时间宜短不宜长,在一个合理的时间内令当前链路因超时失效,从而驱动调度FSM状态的快速变迁,效率要比痴痴的等待高得多,同时,在用户侧也能得到一个较好的正反馈。
各类超时参数最好能做到云端可配可控。
3. 优质网络下的并发链路
当我们在4G、WIFI(要区分是WIFI路由器还是手机热点)等网络条件较优时,对于请求队列积压任务较多或者有重数据(富媒体等下载类数据)请求时,可以考虑并发多个链路并行执行。
对于单一重数据任务的多链接并发协同而言,需要服务器支持断点续传,客户端支持任务协同调度;
4. 轻重链路分离
轻重链路分离,也可以说是信令和数据分离,目的是隔离网络通讯的过程,避免重数据通讯延迟而阻塞了轻数据的交互。在用户角度看来就是信息在异步加载,控制指令响应反馈及时。
移动端大部分都是HTTP短链接模式工作,轻重数据的目标URL本身就不同,比较天然的可以达到分离的要求,但是还是要特别做出强调,是因为实践中有些轻数据协议设计里面还会携带类似头像、验证码等的实体数据。
5. 长链接
长链接对于提升应用网络交互的及时性大有裨益,一方面用户使用时,节省了三次握手的时间等待,响应快捷;另一方面服务器具备了实时推送能力,不但可以及时提示用户重要信息,而且能通过推拉结合的异步方案,更好的提升用户体验。
长链接的维护包括链接管理、链接超时管理、任务队列管理等部分,设计实施复杂度相对高一些,尤其是在移动网络环境下。为了保持链路还需要做心跳机制(从另外一个角度看,这也是针对简单信息一个不错的PULL/PUSH时机,,但需注意数据传输要够轻,比如控制在0.5KB以内),而心跳机制是引入长链接方案复杂度的一个重要方面,移动网络链路环境复杂,国内网关五花八门,链路超时配置各有千秋,心跳时长选择学问比较大,不但要区分网络类型,还得区分不同运营商甚至不同省市,历史上曾经实践了2分钟的心跳间隔,最近比较多的产品实践选择4.5分钟的心跳间隔。而且长链接除了给移动网络尤其是空中信道带来负担外,移动设备自身的电量和流量也会有较大的消耗,同时还带来后端带宽和服务器投入增加。
所以,除了一些粘性和活跃度很高、对信息到达实时性要求很高的通讯类APP外,建议谨慎使用长链接,或可以考虑采用下面的方式:
- 退化长链接:即用户在前台使用时,保持一个长链接链路,活跃时通过用户使用驱动网络IO保持链路可用;静默时通过设置HTTP Keep-Alive方式,亦或通过私有协议心跳方式来保持链路。一旦应用切换后台,且在5~10分钟内没有网络交互任务则自行关闭链路,这样在用户交互体验和资源消耗方面取得一个平衡点;
- 定时拉取/询问:对于一些有PUSH需求的APP,我们可以采用一个云端可配置间隔时长的定时拉取/询问方案。有三个重点,一是定时的间隔云端可以配置,下发更新到客户端后下次生效;二是拉取/询问时,如果下发的指令有要求进一步PULL时,可以复用已建立的链路,即前述退化长链接的模式;三是定时拉取/询问时机在客户端要做时间上的均匀离散处理,避免大的并发查询带来带宽和负载的巨大毛刺;
- 如果可能,优先使用OS内置的PUSH通道:比如iOS的APNS、Andriod的 GCM(Google这个以工程师文化著称的公司,在做OS级基础设施建设时,却表现出了很差的前瞻性和系统思考的能力,GCM的前身C2DM都没怎么普及使用就被替换了,这也意味着Android各种版本PUSH能力不 一致的问题。但无论怎么说,OS级的基础设施无论在性能、稳定性还是在效率上都会优于APP层自己实现的方案),实施推拉结合的方案。特别要提到的一点是,中国特色无所不在,国内运营商曾经封过APNS的PUSH端口2195,也会干扰GCM的端口5528,更别提这些底层服务的长链接会被运营商干扰。对于Android平台,还存在系统服务被各种定制修改的问题。别担心,办法总比问题多,保持清醒。
优化请求性能
可以通过压缩数据及管道化请求以最大化地提升应用的性能,不过最快的请求实际上是没有发出的请求。通过仔细考虑应用需求以及服务器的行为,可以将数据保留在缓存中,只有当服务器上的数据发生变化时才刷新,从而避免发出这些请求。
设计模式
数据结构 - 数据结构与算法
二叉树
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树 。
根节点:没有父节点的结点就是根节点(入度为0)
二叉查找树
二叉查找树(BST:Binary Search Tree)是一种特殊的二叉树,它改善了二叉树节点查找的效率。二叉查找树有以下性质:对于任意一个节点 n,
- 其左子树(left subtree)下的每个后代节点(descendant node)的值都小于节点 n 的值;
- 其右子树(right subtree)下的每个后代节点的值都大于节点 n 的值。
红黑树
- 每个结点不是红色就是黑色
- 不可能有连在一起的红色结点
- 根节点都是黑色
root - 每个红色结点的两个子节点都是黑色。叶子结点都是黑色:出度为0。满足了性质就可以近似的平衡了,不一定要红黑,可以为其他的。
左旋
将右子树的左子树链接到父亲节点的右孩子结点,父亲节点作为ptr结点的左孩子结点便完成了旋转。
右旋
右单旋是左单旋的镜像旋转。 即将右子树的左子树连接到父亲结点的左子树,父亲结点作为ptr结点的右孩子结点便完成了旋转。 当前节点ptr,与父亲节点和当前节点的左孩子结点位于一条直线上时,使用右单旋进行平衡。
GitHub
怎么去找开源项目
特殊的查找资源小技巧
常用前缀后缀
- 找百科大全 awesome xxx
- 找例子 xxx sample
- 找空项目架子 xxx starter / xxx boilerplate
- 找教程 xxx tutorial