GCD

本篇内容
1. 死锁
2. 异步与同步概念
3. 串行与并发概念(重点)
4. dispatch_group
5. dispatch_barrier_async
6. 信号量
6. dispatch_source(重点)

简介

在单核的CPU里,采用分时的执行,来回在不同线程之间进行切换。在多核的CPU里可以同时多个线程,也可以切换线程。这样就实现多线程。

多线程带来的问题
数据竞争:多个线程更新相同的资源
死锁:线程之间相互等待
消耗大量内存:太多线程会消耗大量内存

死锁

在GCD中,以同步分发的方式非常容易出现死锁。死锁的本质资源的相互等待。异步调用不会产生死锁。

  dispatch_queue_t queueA;
    dispatch_sync(queueA, ^{
        dispatch_sync(queueA, ^{
            [self foo];
        });
    });
    /*
    一旦进入第二个dispatch_sync就会死锁
     它们两个在同一个线程里执行外面的正在执行,第二个会等待外面的执行完,而第二个永远都不会执行完。
     */

队列

同步队列只是在执行任务时,顺序的从对列里去任务。在一个任务没有完时不会执行下一个任务。

并发队列是执行任务时,在一个任务没有执行完,可以去拿另一个任务去找线程执行。

异步

异步操作有开启线程的权利。
同步操作没有开启线程的权利。

不同执行方式在不同的队列里执行

同步异步在并发串行队列里的四种组合
同步与异步决定是否开启线程
串行与并发决定拿任务方式
任务时需要线程去执行的。也就时需要考虑,有线程没任务,有任务没线程去执行的情况。

  • 同步执行 串行队列 因为是同步,不会从线程池里拿线程执行,会在当前线程里执行下一个任务(要在当前任务执行完后)。即若果在主线程里同步执行任务。会出现死锁
 /*
     死锁
     */
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
- (void)gcdDemo1 {
    //1.创建串行队列
    dispatch_queue_t q = dispatch_queue_create("demo1_seral", DISPATCH_QUEUE_SERIAL);
    
    //2.同步执行任务
     for (int i =0; i< 20; i++) {
         dispatch_sync(q, ^{
        NSLog(@"demo1 %@",[NSThread currentThread]);
         });
     }
}
  • 同步执行 并发队列 因为是同步,不会开启线程,因为是并发队列,拿任务可以并发,但没有线程执行,所有还是一个一个执行
 //1.创建串行队列
    dispatch_queue_t q = dispatch_queue_create("demo4_seral", DISPATCH_QUEUE_CONCURRENT);
    //2.同步执行任务
    for (int i =0; i< 20; i++) {
        dispatch_sync(q, ^{
            NSLog(@"demo3+%@, %@",@(i),[NSThread currentThread]);
        });
    }
    NSLog(@"come here");
  • 异步执行 串行队列 因为是异步,可以去线程池里拿线程执行。当前是串行队列,要等一个任务执行完后才去拿下一个任务。所以还是一个一个执行
- (void)gcdDemo2 {
    //1.创建串行队列
    dispatch_queue_t q = dispatch_queue_create("demo2_seral", DISPATCH_QUEUE_SERIAL);
    
    //2.同步执行任务
    for (int i =0; i< 20; i++) {
        dispatch_async(q, ^{
            NSLog(@"demo2+%@, %@",@(i),[NSThread currentThread]);
        });
    }
    NSLog(@"come here");
}
  • 异步执行 并发队列 因为是异步,可以去线程池中拿线程执行。当前是并发队列。当前任务没有执行完可以去拿下一个任务执行,因为是并发,可以从线程池里拿新的线程去执行这个任务。这时就真正实现了幷发执行。
- (void)gcdDemo3 {
    //1.创建串行队列
    dispatch_queue_t q = dispatch_queue_create("demo3_seral", DISPATCH_QUEUE_CONCURRENT);
    
    //2.同步执行任务
    for (int i =0; i< 20; i++) {
        dispatch_async(q, ^{
            NSLog(@"demo3+%@, %@",@(i),[NSThread currentThread]);
        });
    }
    NSLog(@"come here");
}
  • 应用(如果将这个例子理解了,就真正理解的同步异步执行与串行并发对列) 指定一个同步任务,让所有异步任务等待同步任务执行完后才执行。 解决方法: 在一个并发队列里先执行加入这个同步任务,在加入后面两个异步任务。因为是同步任务在前,不会有开启多余的线程去执行后面的任务。当第一个执行完,执行到第二个时候,是异步的,可以开启线程执行其他的,因为是并发队列,第三个任务有线程可以用来执行。
- (void)gcdDemo6 {
    
    dispatch_queue_t loginQueue = dispatch_queue_create("rao-login-queue", DISPATCH_QUEUE_CONCURRENT);
    void (^task)(void) = ^(){
        dispatch_sync(loginQueue, ^{
            NSLog(@"用户登录了%@",[NSThread currentThread]);
        });
        
        dispatch_async(loginQueue, ^{
            NSLog(@"用户支付了%@",[NSThread currentThread]);
        });
        
        dispatch_async(loginQueue, ^{
            NSLog(@"用户下载了%@",[NSThread currentThread]);
        });
    };
    dispatch_async(loginQueue, task);
}

dispatch group

使用场景:在一个对列里并发执行完后,想执行一个操作,就可以用dispatch_group

dispatch_queue_t queue = dispatch_queue_create("rao-queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        NSLog(@"down load1%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"down load2%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
         NSLog(@"down load3%@",[NSThread currentThread]);
    });
    
    //当前面的所有
    dispatch_group_notify(group, queue, ^{
        NSLog(@"所有的都执行完%@",[NSThread currentThread]);
    });
    
    //在主队列里进行更新
    dispatch_group_notify(group,dispatch_get_main_queue(), ^{
        NSLog(@"所有的都执行完%@",[NSThread currentThread]);
    });

dispatch_barrier_async

dispatch_barrier_async等待之前追加的任务执行完后,就会执行这个任务,并且不会执行下一个任务,要等这个任务执行完后,才会并发执行下一个任务。

- (void)viewDidLoad {
    [super viewDidLoad];    
    dispatch_queue_t queue = dispatch_queue_create("label", DISPATCH_QUEUE_CONCURRENT);
    static NSInteger readCount = 0;
    void(^read)(void) = ^() {
        NSLog(@"这是read%@",@(readCount));
      
    };
    void(^write)(void) = ^(){
        readCount++;
        NSLog(@"这是write%@",@(readCount));
        
    };
    
    dispatch_async(queue, read);
    dispatch_async(queue, read);
    dispatch_async(queue, read);
    dispatch_async(queue, read);
    
    dispatch_barrier_async(queue, write);
    
    dispatch_async(queue, read);
    dispatch_async(queue, read);
    
}

信号量

NSMutableArray是线程不安全的,当有多个线程同时对数组进行操作的时候可能导致崩溃或数据错误。这里采用的就时信号量,所谓信号量,可以理解成一个数,占有空间时+1,走开始-1;

- (void)viewDidLoad {
    [super viewDidLoad];
    //并发写入数据
    dispatch_queue_t queue  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);//创建信号量
    NSMutableArray *array = [[NSMutableArray alloc]init];
    for (int i = 0 ; i<1000; i++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//减少信号量
            [array addObject:@(i)];
        });
        dispatch_semaphore_signal(semaphore);//提高信号量
    
}

锁的部分参考深入理解iOS开发中的锁
1513585102364922
本图来自ibireme
- OSSpinLock
自旋锁的实现原理比较简单,就是死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。如果a线程在临界区的执行时间过长,则b线程会消耗大量的cpu时间,不太划算。所以,自旋锁用在临界区执行时间比较短的环境性能会很高。

  • dispatch_semaphore
    dispatch_semaphore实现的原理和自旋锁有点不一样。首先会先将信号量减一,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作。

  • pthread_mutex
    pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切换。

  • NSLock
    NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。

  • NSCondition
    NSCondition封装了一个互斥锁和条件变量。互斥锁保证线程安全,条件变量保证执行顺序。

  • NSRecursiveLock
    递归锁,pthread_mutex(recursive)的封装。

  • @synchronized:
    一个对象层面的锁,锁住了整个对象,底层使用了互斥递归锁来实现。

事件源

Runloop里我们说过source_t的概念,其实与队列的源是相同的。Runloop提供对源的监控。队列也可以实现对源的监控。且都可以创建自定义源。

理解:dispatch都是主动添加任务到队列中,然而当系统事件发生时,我们希望做一定的工作当监听到系统事件后就会触发一个任务,并自动将其加入队列执行,这里与之前手动添加任务的模式不同,一旦将Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中。(这个概念在监听系统事件时做一定的操作时是很有用处的!!!哈哈)

监听事件类型
Dispatch Source一共可以监听六类事件,分为11个类型,我们来看看都是什么:

 DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据,下文中会有详细的演示。     
 DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
DISPATCH_SOURCE_TYPE_READ:读文件事件。
DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。  
  • timer_source 示例
 dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
    if(time){
        dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 1, 1);
        dispatch_source_set_event_handler(timer, ^{
            
        });
        dispatch_resume(timer);
    }
    
  • 监听度文件读事件
dispatch_source_t processContentsOfFile(const char *fileName) {
    //prepare the file for reading
    int fd = open(fileName,O_RDONLY);
    if(fd == -1){
        return NULL;
    }
    
    fcntl(fd,F_SETFL,O_NONBLOCK);
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
    if(!readSource){
        close(fd);
        return NULL;
    }
    
    dispatch_source_set_event_handler(readSource, ^{
        
        
    });
    return nil;
}

取消dispatch_source

当设置了Dispatch source对象将一直保持有效状态,除非手动调用dispatch_source_cancel函数来取消它。但取消了dispatch source对象后,将不能再接收到新的事件

暂停与恢复dispatch_source

可以通过使用dispatch_suspend和 dispatch_resume函数来暂停和恢复事件传递给dispatch source对象

2018/9/9 posted in  iOS基础概念

iOS如何将像素显示到屏幕


绘制像素到屏幕

参考文章绘制像素到屏幕

参考文章iOS视图,动画渲染机制

本文将总结
1. 位图数据如何存储的?
2. 像素绘制到屏幕上需要经历的流程?
3. CPU的工作是什么?
4. GPU的工作是什么?
5. 离屏渲染的取舍?
6. 绘制与动画的关系?
7. 渲染性能优化的总结

一.像素

显示在屏幕上的是什么?
当像素映射到屏幕的时候,每一个像素均由三个颜色组件构成:红,绿,蓝,透明度。三个独立的颜色单元会根据给定的颜色显示到一个像素上。例如在iPhone5的显示屏上是1136 *640个像素。

1.1 像素在内存里的默认布局

  A   R   G   B   A   R   G   B   A   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

1.2 二维数据

在实际使用像素数据时,有时会使用二位数据。就时颜色组件红,绿,蓝,alpha,每一个组件是一个数组。这样可以实现很好的对数据进行压缩或者其他处理。

二. 软件构成

Xnip2018-09-09_08-32-56

Display(显示屏)
GPU
GPU Driver
OpenGL
CoreGraphics
CoreAnimation
CoreImage

GPU是一个专门为图形并发计算而量身定做的处理单元。
CoreAnimation使用CoreGraphics来做渲染。CoreGraphics在CPU里进行运算出数据,形成位图包装成纹理交给OpenGl进行操作GPU进行渲染。

2.1 工作大致流程

GPU需要将每一个frame的纹理合成在一起。每一个纹理会占用VRAM,
CPU开始程序时,会让CPU从bundle加载一张PNG的图片并且解压它,这所有的事情都在CPU上。在显示文本时,会促进CoreText和CoreGrapic生成一个位图(coreText排版器最终也是要绘制到图片上下文),一旦准备好,它将会被作为一个纹理上传到GPU并显示出来。但滚动或者在屏幕上移动文本时,不管怎样,同样的纹理会能够复用,CPU只需要简单告诉GPU新的位置就可以,所有GPU可以重用存在的纹理,CPU并不需要重新渲染文本,并且位图也不需要重新上传到GPU。

三. 纹理合成

在简化的理解中,纹理相当于CoreAnimatio里的CALayer,纹理可以有位图内容,其实CALayer也是有位图内容的,对于每一个纹理,所有的纹理都以某种方式叠加在彼此的顶部。当两个纹理覆盖在一起时候,GPU要为所有像素做合成操作。

  • 不透明的合成
    在不透明时,即opaque=yes时,不用合成,直接取上面的纹理。这样就减少了合成的时间。(这也就是为什么在做性能优化时,减少层次关系,减少不必要的透明)

  • mask合成
    一个图层可以有一个和它相关联的 mask(蒙板),mask 是一个拥有 alpha 值的位图,当像素要和它下面包含的像素合并之前都会把 mask 应用到图层的像素上去。当你要设置一个图层的圆角半径时,你可以有效的在图层上面设置一个 mask。但是也可以指定任意一个蒙板。比如,一个字母 A 形状的 mask。最终只有在 mask 中显示出来的(即图层中的部分)才会被渲染出来。
    所谓的mask就是一mask的不透明区域,显示本身的内容。实际上mask也是一种合成。

3.1 离屏渲染

  • 帧缓冲区
    屏幕缓存区,在屏幕上

  • 屏幕外缓冲区
    屏幕外缓冲区

  • 哪些情况会默认会强制进行离屏渲染?
    CoreAnimation为了应用mask会强制进行屏幕外渲染。
    CoreAnimation设置圆角半径会进行屏幕外渲染
    CoreAnimation设置阴影也会出现屏幕外渲染
    设置层为光栅化layer.shouldRasterize = yes
    (特别说明下,rasterize是图层的光栅化,会造成离屏渲染)

  • 离屏渲染的性能取舍?
    一般情况下需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区(屏幕上)比先创建屏幕外缓冲区,然后将屏幕外缓冲区内容般到帧缓冲区要廉价很多。但有时候需要渲染树很复杂,可以强制离屏渲染那些图层,这样就可以缓存合成的结果。性能就会有所提升。(当使用离屏渲染时,GPU第一次会混合所有图层到一个基于新的纹理的位图缓存上,然后使用这个纹理来绘制到屏幕上)。当对这个纹理进行移动,变形等操作时,可以使用这个位图缓存。这样这部分的合成将减少很多工作。其实这就是做动画的流畅的原因。所以要不要离屏渲染,要看有取舍,一般对于静态的不经常变更的可以使用离屏,增加缓存。对于经常变更的就最好不要使用离屏。这样会增加建立屏幕外缓冲区的时间,以及屏幕内与屏幕外的切换时间。

3.2离屏渲染检测

Instrument的CoreAnimationg工具,Color Offscreen-Rendered Yellow,是检测离屏渲染。Color Hits Green and Misses Red 选项,绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。

四. CoreAnimation与CoreGraphics与OpenGLES

CoreAnimation利用CoreGraphics绘制,CoreGraphics利用OpenGLES实现绘制。。

OpenGLES做的就是将纹理合并,做些另外的操作,比如mask,阴影等。OpenGLES对这些有层次关系的纹理进行合成,而这些具体的操作是通过GPU来实现的(也就是通过OpenGLES来操作GPU,OpenGLES只是编程接口)。

CoreAnimation重要的任务是判断出哪些图层需要被重新绘制,绘制完成后会有生成bitmap,CoreAnimion里的图层有backsore,就是一这个bitmap;这个bitmap可以是读取的图片数据,也可以是利用CoreGrapics绘制的。无论是给的图片还是自己通过CrorCrapics绘制的最终,提交把这个位图数据交个生成的纹理(这个纹理与这个Layer相对应)。

五. CPU瓶颈与GPU瓶颈优化

要在1/60里完成渲染工作,CPU与GPU的总时间不能操作这个时间。否则就会出现掉帧。
在出现性能瓶颈时, 我们采用Instrument里的,OpenGL ES Driver instrument 进行查看。

六. CoreGraphics

CPU实现的是位图绘制,这个绘制过程是用CoreGraphics完成的,也就是通过CPU进行计算而来。我们自己画的线条,长方形,通过CPU计算,最终将数据形成与CGContext里。
当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用CGContextRef 绘制

  • UIKit 版本的代码为何不传入一个上下文参数到方法中?
    这是因为当使用 UIKit 或者 AppKit 时,上下文是唯一的。UIkit 维护着一个上下文堆栈,UIKit 方法总是绘制到最顶层的上下文中。UIGraphicsGetCurrentContext() 来得到最顶层的上下文。你可以使用 UIGraphicsPushContext() 和 UIGraphicsPopContext() 在 UIKit 的堆栈中推进或取出上下文。

  • 自己创建一个位图上下文
    自己创建的CGContext,那么绘制的数据在这个自己创建的CGContext里,可以用这个CGGcontext形成位图或者图片。(这里可以自己绘制然后生成图片,也可以实现异步绘制,哈哈)

6.1 drawRect原理

当你调用 -setNeedsDisplay,UIKit 将会在这个视图的图层上调用 -setNeedsDisplay。这为图层设置了一个标识,标记为 dirty,但还显示原来的内容。它实际上没做任何工作,所以多次调用 -setNeedsDisplay并不会造成性能损失。当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用 CGContextRef 绘制
当你使用 UIKit 的绘制方法,例如: UIRectFill() 或者 -[UIBezierPath fill] 代替你的 -drawRect: 方法,他们将会使用这个上下文。使用方法是,UIKit 将后备存储的 CGContextRef 推进他的 graphics context stack,也就是说,它会将那个上下文设置为当前的。因此 UIGraphicsGetCurrent() 将会返回那个对应的上下文。既然 UIKit 使用 UIGraphicsGetCurrent() 绘制方法,绘图将会进入到图层的后备存储。如果你想直接使用 Core Graphics 方法,你可以自己调用 UIGraphicsGetCurrent() 得到相同的上下文,并且将这个上下文传给 Core Graphics 方法。

总结下:检测是否需要重绘,需要重绘,若果实现了drawRect就会生成一个相应大小的后备存储。然后调用drawRect里的代码进行绘制。将数据写入上下文,幷形成新的后备存储。幷将新的后备存储交给GPU渲染。(这里如果不需要绘制,就不要重写drawRect方法了,这样就不会生成后没有必要的备存储对象了,以免造成性能的浪费),幷切每次出现重绘时,都会执行drawRect方法。造成时间和内存浪费。可以采用自己创建位图上下文,生成图片后,赋值给视图。这样可以避免不断绘制。

6.2 异步绘制

将一些耗时的工作,避过图片的获取,图片的解码等工作放到子线程中去做,形成图片后,在放到主线程里将图片放进去。

UIImageView *view; // assume we have this
NSOperationQueue *renderQueue; // assume we have this
CGSize size = view.bounds.size;
[renderQueue addOperationWithBlock:^(){
UIImage *image = [renderer renderInImageOfSize:size];
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
view.image = image;
}];
}];

6.3 图片解码

你需要知道在 GPU 内,一个 CALayer 在某种方式上和一个纹理类似。图层有一个后备存储,这便是被用来绘制到屏幕上的位图。
在给CALayer设置图片时,CoreAnimation会将其查看这个图片时否已经解码,没解码的化话进行解码(这里就是一个优化点,可以将解码的工作全部放到子线程里进行,哈哈)。
imageNamed:从 bundle 里加载会立马解压。一般的情况是在赋值给 UIImageView 的 image 或者 layer 的 contents 或者画到一个 core graphic context 里才会解压。

6.4 可变尺寸的图片

使用较小的图片好处
解码快
占用内存小

七. 完整的绘制与动画流程

动画在APP内部的4个阶段

  1. 布局:
    在这个阶段,程序设置 View/Layer 的层级信息,设置 layer 的属性,如 frame,background color 等等。

  2. 创建 backing image:在这个阶段程序会创建 layer 的 backing image,无论是通过 setContents 将一个 image 传給 layer,还是通过 drawRect:或 drawLayer:inContext:来画出来的。所以 drawRect:等函数是在这个阶段被调用的。

  3. 准备:在这个阶段,Core Animation 框架准备要渲染的 layer 的各种属性数据,以及要做的动画的参数,准备传递給 render server。同时在这个阶段也会解压要渲染的 image。(除了用 imageNamed:方法从 bundle 加载的 image 会立刻解压之外,其他的比如直接从硬盘读入,或者从网络上下载的 image 不会立刻解压,只有在真正要渲染的时候才会解压)。

  4. 提交:在这个阶段,Core Animation 打包 layer 的信息以及需要做的动画的参数,通过 IPC(inter-Process Communication)传递給 render server。

动画在APP外部的2个阶段

当这些数据到达 render server 后,会被反序列化成 render tree。然后 render server 会做下面的两件事:

  • 根据 layer 的各种属性(如果是动画的,会计算动画 layer 的属性的中间值),用 OpenGL 准备渲染。

  • 渲染这些可视的 layer 到屏幕。

如果做动画的话,最后的两个步骤会一直重复知道动画结束。

八. 渲染性能优化的总结

  • 隐藏的绘制
    UILabel将text画入backing image。也就是将文字搞成相对应的图片(文字最终都会是图片)。如果改了一个包含 text 的 view 的 frame 的话,text 会被重新绘制。

  • Rasterize
    当使用layer的shouldRasterize的时候,layer会被强制绘制到一个offscreen image上,并且会被缓存起来。这种方法可以在比较复杂的不会变化的图层上。

  • 离屏绘制
    使用 Rounded corner, layer masks, drop shadows 的效果可以使用 stretchable images。比如实现 rounded corner,可以将一个圆形的图片赋值于 layer 的 content 的属性。并且设置好 contentsCenter 和 contentScale 属性。

  • Blending
    如果一个 layer 被另一个 layer 完全遮盖,GPU 会做优化不渲染被遮盖的 layer,但是计算一个 layer 是否被另一个 layer 完全遮盖是很耗 cpu 的。将几个半透明的 layer 的 color 融合在一起也是很消耗的。

  • opaque
    减少透明,减少合成时间

  • drawRect
    没有必要不要在drawRect里实现。可以采用异步绘制。

  • 图片解码
    图片解码可以异步进行,不要在设置图片的时候解码

2018/9/7 posted in  iOS性能优化

利用Cocoapods构建自己的私有与公有库

一:Spec文件

在我们第一次安装pods时,都会主动的去拉取公有的repo,其实无论是共有的还是私有的下载后都在用户的/Users/用户名/.cocoapods/repos 目录下。每一个第三库都有一个spec文件,或者你自己的私有库也应该有这个spec文件。这些spec文件就在相应的repo里。
我将我自己创建一个私有库的spec文件贴出来

#
# Be sure to run `pod lib lint KBRCategory.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#

Pod::Spec.new do |s|
  s.name             = 'KBRCategory'
  s.version          = '0.1.2'
  s.summary          = 'KBRCategory.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?
#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!

  s.description      = "a description KBRCategory"

  s.homepage         = 'https://gitee.com/BlueLegend'
  # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'raojb@knowbox.cn' => 'raojb@knowbox.cn' }
  s.source           = { :git => 'https://gitee.com/BlueLegend/KBRCategory.git', :tag => s.version.to_s }
  #这里改成了不以tag版本来拉取,以最新的commit
    # s.source           = { :git => 'https://gitee.com/BlueLegend/KBRCategory.git'}
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '8.0'

  s.source_files = 'KBRCategory/Classes/**/*'
  
  # s.resource_bundles = {
  #   'KBRCategory' => ['KBRCategory/Assets/*.png']
  # }

  # s.public_header_files = 'Pod/Classes/**/*.h'
  # s.frameworks = 'UIKit', 'MapKit'
  s.dependency 'AFNetworking'
end

里面无非就是你要提交到Cocoapods上的名字、版本号,简介、主页、License、作者信息、最低平台信息、从哪个Git上下载、需要引入的framework、那些文件需要被引入,那些文件是资源文件以及是否需要ARC的模式。

  • license
    用MIT就可以了,一定要正确填写不然在验证的时候验证通过

  • source
    是库的地址

  • s.source_files
    是要将哪些文件放入进去

  • s.subspec 添加子模块
    其实可以添加子模块,所谓的子模块,就是在pods出来后有子文件夹

s.subspec 'Security' do |ss|  
  ss.source_files = 'AFNetworking/AFSecurityPolicy.{h,m}'
  ss.public_header_files = 'AFNetworking/AFSecurityPolicy.h'
  ss.frameworks = 'Security'
end  

二:创建公有库

  • 向Cocoapods方面注册一个账号 这个命令会收到一份邮件,让你进行会话授权。具体机制没有研究过,当你切换电脑时,需要重新进行会话授权 pod trunk register raojb@knowbox.cn 'rjb' --description='My own computer'
  • 创建spec文件
    pod spec create 'KBCategory'
    在写好代码后,在你项目的根目录下运行,会在该项目目录下生成一个spec文件。修改这个spec文件的里的相关信息。
    如果是项目已经建立好了,需要创建时,可以在根目录下执行
    pod spec create 'KBCategory'也会生成一个spec文件

  • 验证Podspec文件
    pod lib lint Name.podspec
    这一步是最坑人的地方。会有各种让你通不过的理由。比如,找不到文件,找不到Lience等。
    我遇到了,spec文件与s.source_files文件路径设置的不一致问题。
    开源协议文件也要生成。路径也要设置正确。

        - ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code.
    

    遇到这样的错误,是编译没有通过的情况。如果自己在工程里编译通过,看是不是依赖库没有设置正确。别忘了依赖系统库也要填写。总之基本没有一次性验证通过的,具体问题具体查吧。

    例如
    s.framework = "UIKit"
    最终你出现passed validation就表示验证通过。验证通过表示,你给定的环境,单独编译这部分你提供的目录下的代码能自行编译通过。

  • 上传
    可上传项目到Cocoapods官方的仓库里。
    pod trunk push 项目名.podspec

    上传成功后,可以在命令行里pod search "库名字",若果没有应该是有缓存,删除这个文件
    ~/Library/Caches/CocoaPods/search_index.json
    重新pod search就可以了。

三:创建私有库

其实创建私有库的核心过程还是跟公有库是差不多的。不管是私有库还是公有库,关注点都在于Podspec文件的书写。其实我们讲到pod trunk push 项目名.podspec这条命令,其实是默认我们的Podspec文件提交到Cocoapod的仓库(Specs),然后我们之后的pod install或者pod update都是从这个仓库中提取Podspec文件,然后根据文件里面的信息去取对应的源代码。大家可以上去找找自己的开源的Podespec文件转换成json的文件

  • 建立自己的私有仓库
    pod repo add '仓库名' '仓库地址'建立好后,可以在你的cd ~/.cocoapods/repos目录下查看,是否有你的私有仓库。

  • 写代码->写Podspec文件了->检查项目和Podspec文件->打tag
    这些工作与创建公有库一样

  • 提交podspec文件到仓库
    公有库是pod trunk push 项目名.podspec
    私有库是pod repo push '私有仓库名' 项目名.podspec
    其实就都是讲podspec文件提交到相应的仓库里

特别说明

四:使用私有库

使用公有库与使用私有库的方式一样。都是在podfile文件里有一个source。这个source就是标识所要使用的源(spec文件仓库)。

#公用的私有远程索引库源
source  'git@gitee.com:BlueLegend/BlueLegendPrivateSpec.git'
#github远程索引库源
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target ‘testdown’ do
    #pod 'lottie-ios'
    pod 'Masonry'
    pod 'SDWebImage', '~> 4.3.0'
    pod 'YYModel', '~> 1.0.4'
    pod 'MJRefresh', '~> 3.1.15.3'
    pod 'KBRCategory'
end
2018/9/5 posted in  iOS工具深入

Cocoapods入门

一:安装

  1. 安装cocoapods

    sudo gem install cocoapods

  1. 安装指定版本的cocoapods
    在协同开发时,podfile文件会加入Git版本库,为了防止pod版本不一样,导致其他开发人员Pod时执行错误,所以协同开发时最好安装一样的版本号。

    sudo gem install cocoapods -v 1.4

二:初次使用

  1. 更新cocoapods的repo
    如果是新电脑或者没有下载过repo执行pod repo update。这个耗时会很长,这个文件现在达到2G了。改repo下载完后在/Users/用户目录/.cocoapods(这个文件夹的作用将在后面说明)

  2. 生成podfile文件
    在指定的xcode工程目录下执行pod init就会有一个Podfile文件生成,或者干脆从其他工程里拷贝一份过来也行。
    编辑如

    platform:ios,'7.1'
    pod 'SDWebImage', '~>3.7'
    pod 'AFNetworking' 
    
  3. 查找相关库
    有时为了查看要使用一个第三方库的指定版本号,pod search AFNetworking进行类似的查询,将会列出历史版本。

  4. pod install 与pod update

    • pod install
      在podfile.lock不存在时,会根据podfile文件生成这个文件,并安装相应的库。存在时会根据podfile.lock文件下载与安装相应的库。

    • pod update
      会根据podfile文件更新库,并更新podfile.lock文件。也就是pod update时,会检测podfile文件里有的库配置是没有指定版本号的就会取最新的版本号,重新下载与安装。

    • --no-repo-update
      其主要的作用是用于在执行pod install和pod update两条命令是而执行的pod repo update的操作。pod repo update操作时间比较长,这也就是为什么平时我们加--no-repo-update的原因

三:podfile文件深入

  1. pod '框架名' 参数
    参数需要特别说明下

    • 指定版本号(这个版本号是spec文件)
      ’>3.7' 大于3.7版本
      '>=3.7' 大于等于3.7版本
      '<3.7' 小于3.7版本
      '~>3.7' 大于等于3.7并且小于3.8版本(这个参数有意思,经常使用的也是这个参数)

    • 指定path,branch,tag,commit
      :branch => 'branch名'
      :tag => 'tag名'
      :commit => '提交号'
      :path => '~/Documents/AFNetworking'

  2. platform
    platform :ios, '7.0'。说希望采用iOS7.0的进行编译
    最好进行指定,不指定的化,会采用默认的,因为有些库指定了最低的ios版本所以,不指定的化可能编译不过

  3. target
    如果不指定的化,就默认是全部的target。有些时候我向在一个target里有,在其他target没有,这是就可以采用下面指定target指定需要的库

    target 'TRapidCalculation' do
    //写你要的库
    end
    target 'TRapidCalculation_auto' do
    //写你要的库
    end
    
  4. use_frameworks!
    这个指明编译成动态库,而不是静态库,特别是在使用Swift库的过程中,特别需要使用这句,swift里。不过他会把所有项目的编译动态库,这一点有点不好。不过在使用Swift库的过程中就没办法了。

  5. source
    Cocoapods从哪些仓库(装有Spec文件的repo)中获得框架的源代码。(至于什么是Spec我将在后面建立私有库里做说明)我们使用公开的第三库时都是使用的source 'https://github.com/CocoaPods/Specs.git'这个源。在做私有库时,可以引用自己源如我的source 'git@gitee.com:BlueLegend/BlueLegendPrivateSpec.git'

  6. podfile文件示例

#公用的私有远程索引库源
source  'git@gitee.com:BlueLegend/BlueLegendPrivateSpec.git'
#github远程索引库源
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target ‘CoreConcept’ do
    pod 'lottie-ios'
    pod 'Masonry'
    pod 'SDWebImage', '~> 4.3.0'
    pod 'YYModel', '~> 1.0.4'
    pod 'YYCache', '~> 1.0.4'
    pod 'YYText', '~> 1.0.7'
    pod 'BlocksKit', '~> 2.2.5'
    pod 'MJRefresh', '~> 3.1.15.3'
    pod 'KBRCategory'
    pod 'HyphenateLite'
end

四:podfile.lock文件深入

Podfile.lock 文件主要包含三个块:
PODS 用来记录每个pod的版本号
DEPENDENCIES 依赖的其他库
SPEC CHECKSUMS 每个库对应的podspec.json文件的checksum(SHA-1算法)。
通过这些信息可以确保多人协作的时候,大家使用的是相同版本的第三方库。

当团队中的某个人执行完pod install命令后,生成的Podfile.lock文件就记录下了当时最新Pods依赖库的版本,这时团队中的其它人check下来这份包含Podfile.lock文件的工程以后,再去执行pod install命令时,获取下来的Pods依赖库的版本就和最开始用户获取到的版本一致。如果没有Podfile.lock文件,后续所有用户执行pod install命令都会获取最新版本的SBJson,这就有可能造成同一个团队使用的依赖库版本不一致,这对团队协作来说绝对是个灾难!

五:Manifest.lock文件

Manifest.lock文件格式与podfile.lock文件是一样的。主要用来对比远程与本地安装文件是否一样。
Manifest.lock是你的本地的清单,在你没做任何操作时时与podfile.lock文件内容一样的。但当你git pull 从其他开发人员那里拿到了podfile.lock文件,其他开发人员可能修改了podfile.lock文件,就造成安装清单不一致。需要你pod install重新安装,Manifest.lock这个文件,重新保持了两个文件的一致性。所以我们在工程文件里编译脚本里有这两个文件的比较。

diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
    # print error to STDERR
    echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
    exit 1
fi
# This output is used by Xcode 'outputs' to avoid re-running this script phase.
echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"

还是给个截图解释下
/bin/sh 是指定脚本解释器(这里是shell脚本解释器)
inputFile 就是Podfile.lock文件与Mainifest.lock

Xnip2018-09-05_14-26-04

2018/9/4 posted in  iOS工具深入

Runtime系列二 消息机制

前面讨论了Runtime中对类和对象的处理以及对成员变量与属性的处理。本文将讨论Runtime里的消息机制。

一:方法调用的流程

在Objective-C中,消息直到运行时才绑定到指定的方法实现上。编译器会将消息表达式转换成一个消息函数的调用

 RuntimePerson *p = objc_msgSend([RuntimePerson class], @selector(alloc));
    p = objc_msgSend(p, @selector(init));
    [p eat];
    objc_msgSend(p, sel_registerName("eatFoot:"),@"汉堡");
//相应的底层实现(这是上面代码经过编译后)
Class pClass = objc_getClass("RuntimePerson");
RuntimePerson *pp  = objc_msgSend(pClass,sel_registerName("alloc"));
pp = objc_msgSend(pp, sel_registerName("init"));  
[pp eat];

objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)

二 :消息的转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,就会启动所谓”消息转发(message forwarding)“机制
消息转发机制基本上分为三个步骤:
动态方法解析
备用接收者
完整转发

1. 动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

```
+ (BOOL)resolveInstanceMethod:(SEL)sel{
class_addMethod(self, sel, (IMP)hh, "v@:@");

return [super resolveInstanceMethod:sel];
}

void hh(id obj,SEL sel,NSString *objc){
NSLog(@"我来了%@,%@,%@",obj,sel,objc);
}
```

2. 备用接收者

如果在上一步无法处理消息,则Runtime会继续调以下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象

3. 完整转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

还有一个很重要的问题,我们必须重写以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
            signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_helper];
    }
}
2018/9/1 posted in  iOS基础概念

rebase

在命令行里git rebase --help 可以查看英文版本的解释
下面将对这个英文的文档做输出
假定下面历史记录
Xnip2018-08-27_19-14-08

当前在topic分支上
执行
git rebase master 或者 git rebase master topic
将得到如下结果
Xnip2018-08-27_19-14-18

NOTE:git rebase master topic 形式是git checkout topic ,git rebase master两条命令的相继执行。

如果upstream分支(在这里举例的就是master分支,因为topic从master分支开分支而来)已经包含一个改变的提交,这个提交将会跳过。(A,A`是相同的改变,不同的提交信息)
Xnip2018-08-27_19-14-28

执行 git rebase master
将会得到
Xnip2018-08-27_19-14-36

下面将介绍如何使用rebase --onto将一个分支的base迁移到另一个分支。
假设现在是如下历史记录
Xnip2018-08-27_19-14-48

我们要得到如下结果
Xnip2018-08-27_19-14-58

执行 git rebase --onto master next topic

2018/8/27 posted in  Git工具

详解Git里的Rebase操作

前言:
用了一段时间的git,基础命令已经使用的比较熟练,在回顾时,发现平时使用git rebase比价少。找了些文章,都是支离破碎的。最终自己从命令行的文档里找到了平易见人的解释。

下面跟着文章的实践思路一步步走完,会有异想不到的收获

在命令行里git rebase --help 可以查看英文版本的解释
下面将对这个英文的文档做理解输出

假定下面历史记录
Xnip2018-08-27_19-14-08

当前在topic分支上
执行
git rebase master
或者
git rebase master topic

将得到如下结果
Xnip2018-08-27_19-14-18

NOTE:git rebase master topic 形式是git checkout topic ,git rebase master两条命令的相继执行。

如果upstream分支(在这里举例的就是master分支,因为topic从master分支开分支而来)已经包含一个改变的提交,这个提交将会跳过。(A,A`是相同的改变,不同的提交信息)
Xnip2018-08-27_19-14-28

执行 git rebase master

将会得到
Xnip2018-08-27_19-14-36

下面将介绍如何使用rebase --onto将一个分支的base迁移到另一个分支。
假设现在是如下历史记录
Xnip2018-08-27_19-14-48

我们要得到如下结果
Xnip2018-08-27_19-14-58

执行 git rebase --onto master next topic

另一种使用
Xnip2018-08-28_09-55-15
执行 git rebase --onto master topicA topicB

(我对这条的理解,checkout到topicB,取topicA到topcB多出来的变更,以master为新的基础)
Xnip2018-08-27_19-15-17

另一种使用
假设当前历史记录
Xnip2018-08-27_19-15-25
执行 git rebase --onto topicA~5 topicA~3 topicA
得到如下结果
Xnip2018-08-27_19-15-33

在rebase操作时,如果有冲突,git rebase将停止让你解决冲突,解决后git add ,在执行git rebase --continue,直到没有冲突为止。想取消rebase可以执行git rebase --abort;

从上面的总结,可以看出,rebase,其内在含义就是变基础(起点)。有两个参数,一个是”找到变基的提交“。另一个是指“定变基的到哪”。

2018/8/26 posted in  Git工具

深入理解Git

本篇文章是对使用Git的使用的概述。然后对几个忽略的概念做了一下重新理解。

一:Git 文件管理

  • 工作目录
    纯净的工作文件

  • 索引
    git add命令,将对象添加到对象库中,维持一个新的目录树,这个目录树在工作区没有再次改变时是与工作区相同的目录树。新添加的修改与对象库里的目录树是不同的。而这些新添加的就是待提交到对象库里的修改。

  • 对象库
    包括,提交对象(commit),目录树对象(tree),实际的数据(blob),标签(tag);
    blob对应正真的数据,文件的每个一版本。
    tree对应目录。树对象下可以有其他数,最终都会有blob的指向
    commit对应提交。对应当前目录树的一个完整快照

    从总体上来理解,就是工作目录,索引,对象库,都有一个目录树。从编辑,git add ,git commit,的三个过程,就是目录树的同步工作。当然对象库里的commit是某个目录树的引用即快照。这就快照就是目录树,而这个目录树维持着blob的数据。而commit就记录着作者日志等信息。而commit实际上是链表,将一个个commit串联起来,构成一个分支开发线。

二:Git分支操作

远程追踪分支(追踪远程分支),本地追踪分支(追踪"远程追踪分支"),本地分支(谁也不追踪,自己玩,不具有pull功能)

  • 分支管理
    git show-branch
    git branch -d "branchname"
    git branch
    git branch -r
    git branch -a
    git branch -d
    git branch -D
    git branch testbranch //从当前commit创建分支
    git branch testbranch2 commitID //从指定的commit创建分支

  • 分支合并
    HEAD 当前引用
    FETCH_HEAD 远程跟踪分支的最新
    ORIGIN_HEAD 本分支合并前的commit号或者是reset前的commit号
    MERGE_HEAD 合并时别人的分支的commit号(在有冲突时,哈哈)

所有对对分支的操作都应该在本地操作。

三:Git提交操作

  • git reset git reset 精髓是--soft ,--mixed,--hard,三者对于head指向,索引树,工作目录的影响。

    git reset --soft 提交 会将HEAD引用指向给定提交,相当于是将后面对commit的提交的修改重新放入了索引里;(commit树与索引树不同)

    git reset --mixed 提交 会将HEAD指向给定提交,索引内容改变以符合给定提交的树。相当于是将后这个提交的后面的带面放到工作区;(commit树与索引树相同,工作树不同)

    git reset --hard 提交 会将HEAD指向给定提交。并且三个树同步。即此种情况,会导致新修改丢失。(工作区树,索引区,commit树

    特别说明:
    git reset --mixed ,让你有机会重新编辑文件(系统默认)
    git reset --soft,让你有机会重新修改提交日志
    giet reset --hard,全部删除了.没机会了

  • git revert
    指定反转一个提交,并形成新的这个提交记录

  • git cherry-pick(重点,难点)
    将其他分支的commit指定合并到当前分支并形成一个新的提交

  • git commit --amend
    修改最新提交,主要是对此提交新添加内容,并在修改了最新的提交

  • git checkout
    检出分支内容,即沿着当前分支树路径取出内容

  • git rebase(这个概念理解的不够透彻)

  • git rebase master topic
    将topic可达到master的提交添加到master的最新的后面。(有时也有说成,将你的补丁变基到master分支的头)

  • git rebase --onto master maint^ feature
    将从maint到feature的路径的提交,迁移到master后面(onto表示把一条分支上的开发线整个移植到完全不同的分支上)。在此期间需要使用git rebase --continue 继续下一个提交,也可git rebase --abort进行编辑中止

  • git rebase -i ( -- interactive)
    合并提交,或者改变提交顺序,或者删除,编辑,即将两个提交合并成一个提交(注意不是合并分支),

特别注意:这些操作可以专门针对某个文件进行操作。直接在commit后面添加 文件名就可以了

四:Git的Diff操作

diff的操作在实际的工作中是很有用的。

  • git diff
    显示工作目录和索引差异

  • git diff commit
    显示工作目录与commit差异

  • git diff --cached commit
    显示索引中的变更和给定提交中的变更差异

  • git diff commit1 commit2
    显示两个commit差异

差异都是用各个树来进行比较,然后通过比较程序,进行差异

五:远程版本库

重点解释了“本地分支”,“本地追踪分支”,“远程追踪分支”区别

  1. 远程分支几个易混淆的概念
    远程版本库:为版本库提供友好的名字,里面的分支就时远程分支。
    本地版本库:本地库
    本地分支:本地分支(没有设置track)
    本地追踪分支:设置了track远程分支的分支(一般也就我们本地开发的分支)
    远程追踪分支:当fectch拉取远程分支时,实际下载的文件都在远程追踪个分支里。当执行pull时,这个分支与本地的分支(这个本地分支的upstream要是这个远程追踪分支)执行merge,用来追踪远程版本库中分支的变化。

    config文件里如下是记录本地各个分支HEAD的commit号与远程的各个分支Head的commit号。 建立这个refspec预示你要通过从原始版本库中抓取变更来持续更新本地版本库。下面就是这个映射关系

    配置信息里的定义:
    remote.origin.fetch定义的是远程的别名,
    以及与**本地追踪分支的关联**(refs/heads/\*),
    **远程追踪分支的关联**关系(refs/remotes/origin/*);
    remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
    
  2. git remote 命令创建,删除,操作和查看远程版本库。
    引入的所有远程版本库都记录在.git/config文件中。可以通过一个本地库添加多个远程仓库。

    git remote add origin https://github.com/xxx(仓库地址)
    git remote update origin
    git remote show origin
    git remote rm origin
    git remote prune 删除”远程版本库已经删除的分支“的远程追踪分支
    
  3. 建立本地分支与远程追踪分支的关联(即设立本地追踪分支)

    [remote "origin"]
    url = git@gitee.com:rjb_555/BookerReading.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    [branch "master"]
    remote = origin
    merge = refs/heads/master
    [branch "develop"]
    remote = origin
    merge = refs/heads/develop
    

    [remote "origin"] 定义是远程仓库地址,远程仓库别名,本地追踪分支(refs/heads/),远程追踪分支(refs/remotes/origin/)的关联关系;

    git branch --track test origin/dev
    或者
    git checkout -b mypu --track origin/testbranch

  4. Git fetch,pull,push 的实质

    git fetch (拉取远程版本库到远程追踪分支)
    git pull (git fectch,git merge origin/master)
    git push (变更发送到远程版本库,在同步到origin/master)

    指定远程版本库的指定分支进行推送
    git push origin master
    这里要理解git push本质"将变更打包“传输”在解包“放到指定的库中。所以git push 只是变更。所以可以将其推到指定的远程仓库。

    在非快进的push时,会遭到(non-fast forward)拒绝。 在此时因为远程的已经有人提交了。你的分支在当前远程分支之后,但有共同的提交记录。push -f 强制覆盖(覆盖掉别人的记录)。但此种情况一般都是先Pull,合并别人的提交。
    删除与创建远程分支
    git branch testbranch
    git push origin testbranch(直接在远程创建了一根分支)
    git push origin :foo

    原理(git push origin 源:目标)
    git push origin testbranch (简写)(如果远程没偶testbranch ,会将本地的分支推送到远程的版本,即相当于在远程新建分支)
    git push origin testbranch:testbranch
    git push origin raotest:testbranch(将内容推到指定的分支)
    Git push origin :testbranch (将空分支推发哦指定分支,即删除远程分支)

六:多个仓库可以共用一个对象库吗?(很重要,初学时的疑惑点)

经过实践是可以的。比如,当前库是A,在分支master上,我现在在当前目录添加一个远程的仓库B,幷Pull下来。这时pull下来的对象与库A共用一个对象库。所谓的共用一个对象仓库,就时存储真实数据的地方。当时一般情况我们不这么弄,因为如果都放入一个对象库里,会导致上库变大,上传时变慢。

Xnip2018-08-26_14-47-26

当如果是这样一个使用场景(fork公司的参考到你自己的远程库,然后通过发pull request的工作方式):
公司的库A,你fork公式的库B,你在你本地添加了这两个的远程库,都pull下来,因为你们使用的是很多相同的文件,所以,不会造成很多重复的存储,因为文件都是按内容hash的。如果当远程同步不能工作时(将A的内容同步到B),可以先pull下A,然后本地合并到B,在通过将B库push 到你自己的远程,然后通过发push request方式请求公司的库的合并。当然你有权限合公司库,可以直接push到公司的库。

七:解释fast-forward,non-fast-forward

我们举例说明:
开发一直在master分支进行,但忽然有一个新的想法,于是新建了一个develop的分支,并在其上进行一系列提交,完成时,回到 master分支,此时,master分支在创建develop分支之后并未产生任何新的commit。此时的合并就叫fast forward。
反之,是non-fast-forwad
哈哈,在知道了,命令行,长长有fast-forward的日志信息!

八:解释git rebase ,git cherry-pick

者两命令,不常用,但有时能解决关键问题

  • git cherry-pick
    git cherry-pick 通常用于将一个分支的特定提交引入一个不同的分支中。
    举例说明
    master分支已经有很多提交。master其中一个提交时修复一个bug时,这个bug在另一个分支也存在,需要将这个提交也放到dev分支。在dev会形成一个新的提交。

    git checkout dev
    git cherry-pick master~2

    一句话总结,就时将一个分支的提交拷贝到另一个分支的最后面,幷形成一个新的提交(这句话很重要)

  • git rebase

rebase是个很重要的概念,需要比较综合的能力才能理解的比较好。我将在这篇文章里做详细的解释详解Git里的Rebase操作

九:提交范围(很重要)

很多命令都可以对提交范围执行某些操作。那么怎么表达这些提交范围呢?
"..."表示一个范围,“开始...结束"
"maser~2"表示master分支往后数第二个提交。(表示的是一个提交点)

示例
1. master~5 ... master~2,表示master的倒数第5个提交到倒数第2个提交之间的提交
2. topic...master,表示在master分支而不在topic分支生的提交

终于写完了这篇总结,以前看书《Git版本控制管理》JonLoeLiger 著 因为没有实践的前提,所以理解的不深。用了将近一年后,重新回过来看时,很做概念就很清晰了。

十:多人协作开发时,造成起点分叉的原因(还待研究)

2018/8/26 posted in  Git工具

iOS保持界面流畅的技巧(性能优化)

参考iOS 性能优化总结
参考微信读书 iOS 性能优化总结
参考iOS实时卡顿监控
参考移动端IM实践:iOS版微信界面卡顿监测方案
参考iOS保持界面流畅的技巧

本文是对上面几篇文章的总结。并与我自己的知识体系相融合,以达到一个自相融洽的整体。

卡顿原理

VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知App,APP主线程开始在CPU中计算显示内容,比如视图的创建,布局计算,图片解码,文本绘制等。随后CPU会将计算好的内容提交到GPU,由GPU进行变换,合成,渲染。随后GPU会把渲染结果提交到帧缓冲区区,等待下一次VSync信号到来时显示到屏幕上。
由于垂直同步机制,若果在一个VSync时间内,CPU或者GPU没有完成内容的提交,则这个没有完成的提交不会显示到屏幕,在其提交后会到缓冲区,等待下一次机会在显示。那么这个下一次有可能是正常时间提交修改,则会将缓存区的内容覆盖。导致被覆盖的永远没机会显示了。就是掉帧了。
那么与runloop有什么关系呢?
Runloop的时间概念比1/60更小,也就是Runloop处理事物的时间远远比1/60要小。绝大部分时间是睡眠的。所以它们两个本身是没有关系的。绝大部分时候是主线程早就处理完毕所有显示数据,并提交到了渲染系统,渲染系统也完成了合成,也就是提交到了缓存区。只需要等待时钟的到来,将这个缓冲区的内容显示到屏幕上。

FPS的真正含义是,1s的时间真正有多少帧显示到了屏幕上。

卡顿监控思路

  1. 主线程卡顿监控
    通过子线程监测主线程的runloop,判断两个状态区域之间的耗时是否达到一定阈值。这两个状态就是kCFRunLoopBeforeSources与kCFRunLoopAfterWaiting

  2. 借助FPS监控
    CADisplayLink 可以将其看做一个定时器。定时器都与机器的硬件有关系。而这个定时器是有屏幕“垂直时钟”驱动。也就是与垂直时钟同步,1/60时间跳动一次。因为CADisplayLink的回调也要在线程里执行,将其加入到主线程的Runloop里。runloop的TimerSource就会触发runloop的“叫醒”,执行该执行的内容。若果不将CADisplayLink加入,当然就不会定固定时间“叫醒”。

CADislayLink的timestampe的两次差为1s之内的tick次数,即使FPS。

线程卡顿监控方案一的实现

iOS实时卡顿检测
这一方案的思路,就是从引起卡顿的本质来优化。引起卡顿,在kCFRunLoopBeforeSources通知后执行主队列里的代码,执行block的代码等。或则在kCFRunLoopAfterWaiting被唤起后,也会执行队列里的代码,block代码。

#import <CrashReporter/CrashReporter.h>

@interface PerformanceMonitor ()
{
    int timeoutCount;
    CFRunLoopObserverRef observer;
    
    @public
    dispatch_semaphore_t semaphore;
    CFRunLoopActivity activity;
}
@end

@implementation PerformanceMonitor

+ (instancetype)sharedInstance
{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    
    moniotr->activity = activity;
    
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);//没当状态发生变化的时候+1;这是在主线程执行
}

- (void)stop
{
    if (!observer)
        return;
    
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    observer = NULL;
}

- (void)start
{
    if (observer)
        return;
    
    // 信号
    semaphore = dispatch_semaphore_create(0);
    
    // 注册RunLoop状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    //在子线程监控时长
    //在子线程不断的监控信号量,如果在50 ms里还没有超时,也就是在50ms状态没有发生该边的话。就说明在两个状态的时间间隔里有执行时长超过50ms的
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (!observer)
                {
                    timeoutCount = 0;
                    semaphore = 0;
                    activity = 0;
                    return;
                }
                
                //若果超时的是kCFRunLoopBeforeSources 或者kCFRunLoopAfterWaiting 就更能说明问题
                //如果5此都是这样,说明有问题。几下主线程的调用堆栈。
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount < 5)
                        continue;
                    
                    PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
                                                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                    PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                    
                    NSData *data = [crashReporter generateLiveReport];
                    PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                    NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                                              withTextFormat:PLCrashReportTextFormatiOS];
                    
                    NSLog(@"------------\n%@\n------------", report);
                }
            }
            timeoutCount = 0;
        }
    });
}

@end
2018/8/25 posted in  iOS性能优化

组件化系列(一) 组件化原理及方案


本系列文章是对大神的博客文章的研读与总结。我们公司从18年初开始组件化的开发的构建。为了理解组件开发的原理,找了相关资料做了对比分析。以下是我参考的文章
模块化与解耦
Casa的iOS应用架构谈 组件化方案
蘑菇街的开源
Casa的组件化方案开源

一:模块化与解耦

模块化与解耦

1.为什么模块化

因为在实际的开发中,项目业务较多,一个APP会有多个小组进行开发,比如我们公司的有数学组,语文组,英语组,商业组,等,出现的问题大多数情况下一个开发人员只关心我这个组的代码。这样在编译时实际上是编译整个项目,编译效率低。每个小组在同一个工程里增,删,改文件,xcode的工程文件会经常发生冲突(我们用git进行版本控制),合并代码时很痛苦。在整个项目查找自己忘记类名了的功能时,犹如大海捞针。有些基础模块核心模块需要专人维护,对基础的开发人员不开发,需要隔离基础库,也需要进行解耦。

这些理由已经足以说明进行模块化组件化的迫切了。

2. 模块设计原则

  1. 越底层的模块,应该越稳定,越抽象,越具有高度复用度。
  2. 不要让稳定的模块依赖不稳定的模块,减少依赖
  3. 提升模块的复用度,自完备性有时候要优于代码复用
  4. 每个模块只做好一件事情,不要让Common出现
  5. 按照你架构的层数从上到下依赖,不要出现下层模块依赖上层模块的现象,业务模块之间也尽量不要耦合

3. 模块解耦手段

要实现模块之间真正的解耦才算真正的模块化。
解耦目标:

在基于模块设计原则上让模块之间没有循环依赖,让业务模块之间解除依赖

公共模块可以通过架构设计来避免耦合业务。但业务模块之间还是会有耦合的。
业务模块之间的比如页面跳转,数据传递,怎么实现解耦不同业务模块之间的代码调用呢?

1. 面向接口调用(很妙)

直接引用有依赖的情况

//A 模块
- (void)getSomeDataFromB {
    B.getSomeData();
}
//B 模块
- (void)getSomeData {
    return self.data;
}
//接口
@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;
}

这样就可以实现了既满足了模块之间的调用,也实现了解耦
优点

接口类似代码,可以非常灵活的定义函数和回调

缺点

1. 接口定义文件需要放在一个模块里以供依赖,但是这个模块不贡献代码。还好。
2. 使用麻烦,每个调用都需要定义一个service的接口,幷实现。

2. 面向自定义协议的调用

面向接口的调用的缺点导致幷不能满足所有的需求,也解耦的不彻底(对接口的依赖)。
终极手段就是通过定义一套自定义协议来实现模块间的通信,可以采用现成的协议如URL协议,简单,易于上手,这也是很多人采用url作为协议原因。可统一实现本地于远程的页面跳转,并且实现业务间的解耦。
要实现真正的解耦,采用注册机制。

3.利用运行时的反射机制

Object-C的反射机制是通过一个字符串找到一个类的类对象,即NSClassFromString();
要实现真正的解耦,可以采用通过反射机制获取类,在创建对象,实现跳转或者通信。这样就不用依赖”要跳转的类“了。

总结
以上的解耦方式是从模块化与解耦得到的启发。这些解耦方式无论是在平时开发中,还是要搭建组件化的框架都可以使用。具体的组件化框架我们将在下面对前人的组件化探索做下分析

二:模块拆分

  1. 基础库组件
    第三方库如AFNetWorking,SDWebImage等,还有一些工具也要从主库重剥离出来,形成自己的私有基础仓库。

  2. 服务组件

  3. 业务组件

https://blog.csdn.net/xinzhou201/article/details/51000807

  1. 拆分中遇到的问题 主工程与壳工程的pods版本的管理问题

三:蘑菇街组件化

蘑菇街的方案一

原理采用的是面向自定义协议的方式实现解耦。自定义协议采用的是现成的url协议,url协议成熟,方便,利用URL可以在这里做key值,方便的参数解析,与应用之间的调用相吻合。
嫌在这里看代码代码麻烦的话,可以下载我自己写的原理 Demo组件化原理分析

//Mediator3.h文件
typedef void(^componetBlock)(id param);
typedef id(^objectComponetBlock)(id param);

/*
 原理就是将url与Block进行映射
 url起到两个作用:一是作为key值与block进行映射,二是可以直接接参数就想普通url后面跟参数一样。
 
 特别说明:
 url传递参数受到一定的限制。比如对本地来说,需要传非常规的参数时,就办不到。
 这里我自己的Demo直接将参数放在了函数后面省去对url进行解析的操作。实际的开发中将这个参数从url里解析出来。如果要传递非常规的参数。也可以直接在后面添加有一个param。
 */

@interface Mediator3 : NSObject

+ (instancetype)shareInstance;

//指定相应的url的执行操作
//例如,只是简单的打开一个页面
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block;
//打开某个页面
- (void)openURL:(NSString *)url withParam:(id)param;


//指定相应rul的执行操作,并给一个返回值
//例如打开一个页面,或者在一个组件里面取值后返回来
- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block;
//取得某个组件操作后的值
- (id)objectForURL:(NSString *)url withParam:(id)param;
//Mediator3.m文件
@interface Mediator3()
@property (nonatomic, strong)NSMutableDictionary *cache;
@end

@implementation Mediator3

- (instancetype)init {
    if(self = [super init]){
        _cache = [[NSMutableDictionary alloc]init];
    }
    return self;
}

+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static Mediator3 *mediator = nil;
    dispatch_once(&onceToken, ^{
        
        mediator = [[self alloc]init];
    });
    return mediator;
}

- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componetBlock block = [self.cache objectForKey:url];
    if(block){
        block(param);
    }
}

- (void)registerURLPattern:(NSString *)urlPattern toObjectHandler:(objectComponetBlock)block {
    [self.cache setObject:block forKey:urlPattern];
}

- (id)objectForURL:(NSString *)url withParam:(id)param{
    objectComponetBlock block = [self.cache objectForKey:url];
    if(block){
        return block(param);
    }
    return nil;
}

@end
//BookDetailViewController.m
@implementation BookDetailViewController

+ (void)load {
    //实现跳转功能
    [[Mediator3 shareInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(id param) {
        NSDictionary *paramDict = (NSDictionary *)param;
        BookDetailViewController *bookDetailVC = [[BookDetailViewController alloc]initWithBookId:paramDict[@"bookId"]];
        UINavigationController *nav = (UINavigationController *) [UIApplication sharedApplication].keyWindow.rootViewController;
        [nav pushViewController:bookDetailVC animated:YES];
    }];
    
    
    //只是取值的操作
    [[Mediator3 shareInstance]registerURLPattern:@"weread://bookCount" toObjectHandler:^id(id param) {
        //执行一定的操作后
        return @"5";
    }];
    
//ReadingViewController.m
- (void)viewDidLoad {
 [super viewDidLoad];
 //注册url与block的映射,实现调转的操作
    [[Mediator3 shareInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId":@"2"}];
    
    //注册url与block映射,实现只是取值的操作
    NSString *bookCount =  [[Mediator3 shareInstance] objectForURL:@"weread://bookCount" withParam:@""];
    NSLog(@"这是从某个组件取回来的值%@",bookCount);
    }

蘑菇街的方案二

实现原理:
面向接口调用,即新开了一个对象叫做ModuleManager,提供了一个registerClass:forProtocol:的方法,注册后,@protocol和Class进行配对。因此ModuleManager中就有了一个字典来记录这个配对。
当有涉及非常规参数的调用时,业务方就不会去使用[MGJRouter openURL:@"mgj://detail?id=404"]的方案了,转而采用ModuleManager的classForProtocol:方法。业务传入一个@protocol给ModuleManager,然后ModuleManager通过之前注册过的字典查找到对应的Class返回给业务方,然后业务方再自己执行alloc和init方法得到一个符合刚才传入@protocol的对象,然后再执行相应的逻辑。

这里的protocol统样起到两个作用,一是key值,另一个是起到定义调用接口的作用,可以定义任意类型的参数。

缺点
1. 被调用方与调用方,虽然不相互依赖,但都得依赖这个协议。这实际上是一种不彻底的解耦。

  1. 同url注册形式一样,都得维持注册表。

四:casa组件化

基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务

实现原理
[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]向CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。

Xnip2018-09-04_16-42-52

下面是工程实践
下面是casa开源的实现的头文件。.m文件请自行下载

//casa开源的CTMeditor.h
@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;

@end

//CTMeditor.m
略
采用运行时构建可执行的NSInvocation。在内部都给其添加了前缀
 NSString * targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
 NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
//Target_B.h
@interface Target_B : NSObject

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)dict;

@end

//Target_B.m
#import "Target_B.h"
#import "DemoModuleBDetailViewController.h"
@implementation Target_B

- (UIViewController *)Action_DemoModuleBDetailViewController:(NSDictionary *)param {
    DemoModuleBDetailViewController *bDetailVC = [[DemoModuleBDetailViewController alloc]init];
    return bDetailVC;
}
@end
//ViewController.m
//这是调用方
#import "CTMediator+CTMediatorModuleAActions.h"
#import "CTMediator+CTMediatorModuleBActions.h"
- (void)viewDidLoad {
//直接调用
  if (indexPath.row == 6) {
        [[CTMediator sharedInstance] performTarget:@"InvalidTarget" action:@"InvalidAction" params:nil shouldCacheTarget:NO];
    }
    //通过分类调用
    if(indexPath.row == 7){
        UIViewController *vc =  [[CTMediator sharedInstance]CTMediator_viewControllerForModuleBDetail:@{}];
        [self.navigationController pushViewController:vc animated:YES];
    }
}
  1. 从.h文件里可以看到,外部的调用将远程与本地分开,内部实现时远程利用了本地(通过解析url,将url转换成了本地的调用)。

  2. Mediator分别对每一个模块有个一个分类,提供对外部的调用的列表。这些分类被需要调用的模块所依赖。也就是只需要依赖Mediator就可以了,是单向依赖。

  3. 为了更好的实现组件对外接口的管理。此种方案专门针对每个模块有一个Target_A类似的对外服务接口的实现。

  4. 用户调用都是通过对Mediator的分类,对固定的模块的类的名字的反射,来对Target_A的调用,当然就调用到了A的服务。

  5. 此种方式为了使代码方便管理,会为每个模块提供Target和一个对Mediator的分类。

  6. Mediator与其分类可以是单独一个repo,方便其他组件依赖。也就是其他组件只依赖于这个中间件。解耦与组件化就完成了。

五:总结

组件化就是在与解耦,解耦的方式大致就是上面提到的三种方法(也可能有其他办法,但至少现在我看到的最好实践就这三)。然后是基于各个原理的工程化实践。从工程实践来看casa的Mediator+target-action更胜一筹。思路清晰,调用统一,没有注册机制的维护,模块的服务的实现(Target)在同一个地方,不用耦合到真正的模块里。
多说一句,滴滴组件化,页面间的跳转采用openURL,页面在+(void)load方法里进行注册,ONERoute内部保存一份URL与Class的对应表。当调用openURL时,会查找到相应的类,然后生成相应的实例对象。
最后强烈建议大家认真读Casa的iOS应用架构谈 组件化方案

2018/8/25 posted in  iOS性能优化