iOS面试题总结-未完待续

iOS面试总结

1. 网络

  1. HTTP协议(HyperText Transfer Protocol)的请求和响应

    • 请求: 请求头, 请求行, 请求体
      • 请求行: 指定请求方法, 请求路径 协议版本等信息
      • 请求头: 描述客户端环境, 例如: host要请求的主机地址, UserAgent客户端类型, Accept 可接受数据类型, Accept_language可接受语言
      • 请求体: 客户端要发送的具体数据, 例如上传时的上传数据
    • 响应: 状态行,响应头, 响应数据
      • 状态行: 包含http协议版本, 状态码
      • 响应头: 服务器描述, 数据类型描述例如 Sever, Content-Type
      • 响应数据: 请求所获得的后台数据
  2. HTTP的特点

    • 无状态: 服务器不会保留客户端状态, 不会记忆上次的状态, 不受前面请求的影响, 客户端每次请求独立, 每次请求需带上自己的状态

    • 持久链接: 每个链接可以处理多个事务, 特殊强调http1.0及其之前版本都是非持久链接(每个链接只能处理一个事务)

      持久链接如何结束?

      • 如果有Content-length字段, 可以判断接受数据是否完成,
      • 如果是分块传输, 响应头中不包含Content-length, 服务器传输完成万发送一个空数据块, 当客户端收到空数据块时算作数据接受完成
  3. HTTP的请求方式

    GET、POST、PUT、DELETE、HEAD、OPTIONS等

  4. GET和和POST的区别:

    • get请求参数拼接在url后面, post的参数拼接在请求体里面,相对安全(在http抓包情况下依然不安全)

    • get参数长度有限制2048, post无限制

    • get获取资源是安全的(不会修改服务器资源), 幂等的(多次执行结果相同), 可缓存的(可以直接有CDN缓存, 减轻服务器负担), post相反

  5. HTTP常用状态码含义 参考文档点击 HTTP状态码总结

    • 1xx 表示消息

      • 100 初始的请求已经接受,客户应当继续发送请求的其余部分, 例如post请求(两段)的第一段请求收到的就是100
      • 101 服务器将遵从客户的请求转换到另外一种协议
    • 2xx 表示成功

      • 200: OK, 完全正常
    • 3xx 表示重定向

    • 4xx 表示请求错误

      • 400 请求出现语法错误
      • 401 访问收到密码保护的页面
      • 403 服务器拒绝执行,
      • 404 服务器资源未找到
      • 405 请求方法(get, post等)对指定资源不适用,
    • 5xx 表示服务器错误

      • 500 服务器遇到意料不到的错误, 不能完成客户端请求
  6. TCP和UDP的特点, 参考链接 简单理解TCP/IP协议

    • TCP协议(Transport Control Protocol,传输控制协议): 是一种面向连接、可靠的、基于字节流的传输层协议,采用了确认机制、超时重传机制,还会对接收到的TCP报文段进行重新排列整理。(TCP报头含20字节定长、选项和填充<选项和填充小于等于40字节>)(TCP是一种面向连接的传输层协议。它可以保证两端通信主机之间的通信可达。TCP能够正确处理传输过程中丢包、传输顺序乱掉等异常情况。)

      TCP能保证可靠性、稳定性, 适用于可靠性较高的服务

    • UDP协议:(User Datagram Protocol,用户数据报协议)是一种不可靠无连接的传输层协议,不考虑流控制、错误控制,没有重传机制,不会对分组进行顺序检查和排序。

      UDP控制选项少,无须建立连接,从而使得数据传输过程中的延迟小、数据传输效率高, 适用于实时性要求高的程序

  7. 网络七层模型

    应用层, 表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层

    • 应用层: 网络应用如: http ftp, pop, smtp等应用协议
    • 表示层: 定义数据格式及加密, 例如,FTP允许你选择以二进制或ASCII格式传输。如果选择二进制,那么发送方和接收方不改变文件的内容。如果选择ASCII格式,发送方将把文本从发送方的字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等。对应网络协议: Telnet, Rlogin, SNMP, Copher等
    • 会话层: 它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而使表示层看到的数据是连续的,在某些情况下,如果表示层收到了所有的数据,则用数据代表表示层。示例:RPC,SQL等
    • 传输层: 这层的功能包括是否选择差错恢复协议还是无差错恢复协议,及在同一主机上对不同应用的数据流的输入进行复用,还包括对收到的顺序不对的数据包的重新排序功能。示例:TCP,UDP,SPX。
    • 网络层: 这层对端到端的包传输进行定义,它定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。示例:IP,IPX, UUCP等。
    • 数据链路层: 它定义了在单个链路上如何传输数据。这些协议与被讨论的各种介质有关。示例:ATM,FDDI等。
    • 物理层: OSI的物理层规范是有关传输介质的特性,这些规范通常也参考了其他组织制定的标准。连接头、帧、帧的使用、电流、编码及光调制等都属于各种物理层规范中的内容。物理层常用多个规范完成对所有细节的定义。示例:Rj45,IEEE 802.3 等。
  8. Socket

    客户端: socket()->connect()->write()/read()->close()

    服务端: socket()->bind()->listen()->accept()->read()/write()->close()

  9. 数据解析:

    • JSON: 使用NSJSONSerialization解析
    • XML: SAX解析, DOM解析
      • SAX: NSXMLParser 实现代理方法解析, 特点从上往下依次解析
      • DOM: 特点一次性读取解析
  10. 网络安全

    • Base64加密, 可逆加密

    • MD5/SHA1/SHA256等 不可逆加密

    • 对称加密和非对称加密

      • 对称加密: 加解密使用同一个秘钥, 代表: DES, AES等, 有点效率高, 缺点, 秘钥交换时安全不能保障

      • 非对称加密: 加解密双方使用不同的秘钥, 加密使用公钥加密, 解密是用私钥解密, 公钥是公开的 代表: RSA, ECC, DSA(数字签名用)

  11. HTTPS

    • HTTPS中的S含义

      S代表的是SSL/TLS协议, 即: Secure Sockets layer(安全套接层)和Transport Layer Security(安全传输层)

    • HTTPS的建立流程

      HTTPS为了兼顾安全与效率,同时采用了对称加密算法和非对称加密算法, 通信过程主要会设计三个秘钥: 服务器端的公钥和私钥, 用来进行非对称加密, 客户端随机生成的随机秘钥, 用来进行对称加密

      1. 客户端访问HTTPS链接, 告诉服务器客户端支持的加密算法列表, 和随机数C
      2. 服务器选择一种对称算法(如AES), 一种非对称算法, 一种MAC算法发送给客户端, 同时把数字证书和随机数S发给客户端
      3. 客户端验证服务器的数字证书, 客户端生成前主秘钥, 并使用服务器的公钥加密发送个服务器, 并使用前主秘钥/随机数C/随机数S, 生成会话秘钥
      4. 服务器解密得到前主秘钥, 通过前主秘钥/随机数C/随机数S生成会话秘钥
      5. 数据加密传输
  12. NSURLSession

    • NSURLSession的优势

      • 支持后台上传下载
      • 可以暂停, 停止, 重启网络任务
      • 可以对缓存策略,session类型、任务类型(比如上传、下载等任务)进行单独的配置
      • 支持block回调使用方便
      • 支持IPV6网络
    • NSURLsession的使用

      NSURLsession 是一个管理类, 可以通过NSURLSessionConfiguration进行配置

      URLSessionTask是任务的父类, 包含两个子类URLSessionDataTask和URLSessionDownloadTask, 其中URLSessionDataTask有一个子类URLSessionUploadTask, URLSessionDataTask用来处理一般网络请求

      URLSessionDownloadTask处理下载任务, URLSessionUploadTask上传任务

2. 多线程

  1. iOS开发常用的多线程方式

    • pthread: C语言实现, 可以跨平台, 线程生命周期需要手动管理
    • NSThread: OC实现, 线程生命周期需要手动管理
    • GCD: 苹果对多核性能优化, C语言, 线程生命周期自动管理
    • NSOperation: 对GCD封装, 线程生命周期自动管理

  2. NSThread简单介绍

    • 创建线程对象: 显示创建(alloc init)和隐式创建(performSelector…)

    • 线程状态:新建, 就绪(start), 阻塞(sleep), 运行, 死亡(exit)

    • 常用属性: name(当前线程名字), threadPriority(线程优先级), isMainThread等

  3. GCD和NSOperation对比

    • GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
    • GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
    • NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
    • NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceled)
  4. 什么情况下会出现死锁

    • 在主线程中将同步任务添加到主队列会导致死锁

      - (void)viewDidLoad {
          [super viewDidLoad];
          
          dispatch_sync(dispatch_get_main_queue(), ^{
             
              NSLog(@"deallock");
          });
          // Do any additional setup after loading the view, typically from a nib.
      }
      
    • 在一个串行队列任务中将 同步任务添加到当前的队列中

      dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
      
      dispatch_async(serialQueue, ^{
      	dispatch_sync(serialQueue, ^{
      		NSLog(@"deadlock");
        });
      });
      
  5. GCD中的线程栅栏

    dispatch_barrier_async/dispatch_barrier_sync 分别表示同步栅栏和异步栅栏

    栅栏的作用是可以将任务分块执行, 条件是任务必须在同一个队列上, 否则无效

    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
        
        for (NSInteger i = 0; i < 10; i++) {
            
            dispatch_async(concurrentQueue, ^{
                NSLog(@"%zd",i);
            });
        }
        
        dispatch_barrier_async(concurrentQueue, ^{
            NSLog(@"barrier");
        });
        NSLog(@"哈哈哈");
        for (NSInteger i = 10; i < 20; i++) {
            
            dispatch_async(concurrentQueue, ^{
                
                NSLog(@"%zd",i);
            });
        }
     执行结果: 
     哈哈哈
     0-9 乱序打印
     barrier
     10-19乱序打印
     
    

    由于采用的是异步栅栏, 所以栅栏后的任务会"哈哈哈"会提起执行, 如果将栅栏换成同步栅栏则 "哈哈哈"一定是在"barrier"之后执行

    通过线程栅栏可以实现多度单写, 即允许多个地方同时读取数据, 但是在写入数据时只允许一个地方写入

    - (id)readDataForKey:(NSString *)key {
        __block id result;
        
        dispatch_sync(_concurrentQueue, ^{
            result = [self valueForKey:key];
        });
        
        return result;
    }
    
    - (void)writeData:(id)data forKey:(NSString *)key {
        dispatch_barrier_async(_concurrentQueue, ^{
            [self setValue:data forKey:key];
        });
    }
    
    
  6. GCD线程组的使用

    需求多个网络请求完成之后一并刷新UI

    - (void)testGCDGroup {
        __block NSInteger number = 0;
        
        dispatch_group_t group = dispatch_group_create();
        
        //A耗时操作
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(3);
            number += 2222;
            NSLog(@"A:%zd", number);
        });
        
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self sendRequestWithCompletion:^(id response) {
                number += [response integerValue];
                NSLog(@"B:%zd", number);
            }];
        });
        
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self sendRequestWithCompletion:^(id response) {
                number += [response integerValue];
                NSLog(@"C:%zd", number);
            }];
        });
        
        //        //B网络请求
        //        dispatch_group_enter(group);
        //        [self sendRequestWithCompletion:^(id response) {
        //            number += [response integerValue];
        //            NSLog(@"B:%zd", number);
        //            dispatch_group_leave(group);
        //
        //        }];
        //
        //        //C网络请求
        //        dispatch_group_enter(group);
        //        [self sendRequestWithCompletion:^(id response) {
        //            number += [response integerValue];
        //            NSLog(@"C:%zd", number);
        //            dispatch_group_leave(group);
        //        }];
        //
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"finish: %zd", number);
        });
        
    }
    
    - (void)sendRequestWithCompletion:(void (^)(id response))completion {
        //模拟一个网络请求
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(queue, ^{
            sleep(2);
            dispatch_async(dispatch_get_main_queue(), ^{
                if (completion) completion(@1111);
            });
        });
    }
    
    调用结果:
    B:1111
    C:2222
    A:4444
    finish: 4444
    
    

    使用方式如代码所示, 先创建线程组, 然后使用dispatch_group_async, 将任务绑定到线程组, 最后使用dispatch_group_notify接受线程组任务完成的回调. 其中也可以使用dispatch_group_enter(group)这种方式将任务添加到线程组, 不过任务结束之后要配合使用 dispatch_group_leave(group);

  7. Dispatch Semaphore 信号量

    Dispatch Semaphore 提供了三个函数

    • dispatch_semaphore_create: 创建一个Semaphore并初始化总量
    • dispatch_semaphore_signal: 发送信号, 让信号总量加1
    • dispatch_semaphore_wait: 可以是信号总量减1, 信号总量为0是进入等待

    Dispatch Semaphore 在实际开发中主要用于:

    • 保持线程同步,将异步执行任务转换为同步执行任务

      - (void)testSemaphore {
          dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
          
          __block NSInteger number = 0;
          
          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
              number = 100;
              NSLog(@"执行异步任务");
              dispatch_semaphore_signal(semaphore);
          });
          
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
          NSLog(@"semaphore---end,number = %zd",number);
      }
      //打印结果如下: 
      //执行异步任务
      //semaphore---end,number = 100
      
      

      创建信号量初始化总量为0, 由于是异步执行,所以直接执行到dispatch_semaphore_wait, 如果信号量为0则一直等待阻断线程, 当异步任务执行完之后将信号量加1, 此时信号量等待函数终结, 继续执行后面的任务

    • 保证线程安全,为线程加锁

      - (void)testSemaphore2 {
          _semaphore = dispatch_semaphore_create(1);
       
          for (NSInteger i = 0; i < 100; i++) {
              
              dispatch_async(dispatch_get_global_queue(0, 0), ^{
                  
                  [self asyncTask];
              });
          }
      }
      
      - (void)asyncTask {
          dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
          _count++;
          sleep(1);
          NSLog(@"执行任务:%zd",_count);
          dispatch_semaphore_signal(_semaphore);
      }
      
      ///执行结果发现, 是顺序从1-100执行
      

      原因: 在子线程执行并发任务时, 由于第一次执行任务将信号量减1, 信号总量变为0, 当第二个任务进来时需要由于信号总量为0所以进入等待状态, 任务一执行完之后将信号量加1, 任务二开始执行, 并立即将信号总量减1变为0, 任务三继续等待, 依次类推, 实现了线程加锁目的

  8. 使用dispatch_once实现单例

    //手写单例
    - (id)sharedInstance {
    		static id instance = nil;
    		static dispatch_once_t onceToken;
    		dispatch_once(&onceToken, ^{
    				instance = [[self alloc] init];
    		})
    		return instance;
    }
    
  9. NSOperationQueue/NSOperation简介

    • 优势

      • 可以添加任务依赖,方便控制执行顺序

      • 可以设定操作执行的优先级

      • 可以设置最大并发量

      • 任务执行状态控制:isReady,isExecuting,isFinished,isCancelled

        如果只是重写NSOperation的main方法,由底层控制变更任务执行及完成状态,以及任务退出
        如果重写了NSOperation的start方法,自行控制任务状态
        系统通过KVO的方式移除isFinished==YES的NSOperation

    • 基本操作

      NSOperation是抽象类, 主要使用两个子类: NSBlockOperation和NSInvocationOperation

      可以直接添加Block任务NSOperation到队列中执行, 默认在当前线程执行, NSBlockOperation添加多任务时会自动开启新线程执行

      也可以将NSOperation添加到NSOperationQueue中执行操作, 当maxConcurrentOperationCount = 1时为串行队列, 大于1时为并发队列

    • 示例代码

      /**
       * 设置 MaxConcurrentOperationCount(最大并发操作数)
       */
      - (void)setMaxConcurrentOperationCount {
      
          // 1.创建队列
          NSOperationQueue *queue = [[NSOperationQueue alloc] init];
      
          // 2.设置最大并发操作数
          queue.maxConcurrentOperationCount = 1; // 串行队列
      // queue.maxConcurrentOperationCount = 2; // 并发队列
      // queue.maxConcurrentOperationCount = 8; // 并发队列
      
          // 3.添加操作
          [queue addOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
          [queue addOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
          [queue addOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
          [queue addOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
      }
      

      执行结果为顺序执行1,1,2,2,3,3,4,4

      如果将maxConcurrentOperationCount改为2, 则并并发执行, 这里就不再验证

    • NSOperation的操作依赖设置

      /**
       * 操作依赖, 操作2依赖操作1
       * 使用方法:addDependency:
       */
      - (void)addDependency {
      
          // 1.创建队列
          NSOperationQueue *queue = [[NSOperationQueue alloc] init];
      
          // 2.创建操作
          NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
          NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
              }
          }];
      
          // 3.添加依赖
          [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
      
          // 4.添加操作到队列中
          [queue addOperation:op1];
          [queue addOperation:op2];
      }
      
    • NSOperation 提供了queuePriority属性, 新建操作的默认优先级是Normal

      // 优先级的取值
      typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
          NSOperationQueuePriorityVeryLow = -8L,
          NSOperationQueuePriorityLow = -4L,
          NSOperationQueuePriorityNormal = 0,
          NSOperationQueuePriorityHigh = 4,
          NSOperationQueuePriorityVeryHigh = 8
      };
      
    • 线程间通信示例

      /**
       * 线程间通信
       */
      - (void)communication {
      
          // 1.创建队列
          NSOperationQueue *queue = [[NSOperationQueue alloc]init];
      
          // 2.添加操作
          [queue addOperationWithBlock:^{
              // 异步进行耗时操作
              for (int i = 0; i < 2; i++) {
                  [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                  NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
              }
      
              // 回到主线程
              [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                  // 进行一些 UI 刷新等操作
                  for (int i = 0; i < 2; i++) {
                      [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                      NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
                  }
              }];
          }];
      }
      
  10. RunLoop

    • 什么是RunLoop

      RunLoop是通过内部维护的事件循环来对事件进行管理的一个对象

    • 为什么main函数不会退出

      UIApplicationMain函数内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码

    • RunLoop的数据结构

      NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
      RunLoop 相关的主要涉及五个类:

      • CFRunLoop:RunLoop对象

      • CFRunLoopMode:运行模式

        1. kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
        2. UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
        3. UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
        4. GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
        5. kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案

        苹果对外开放的主要有kCFRunLoopDefaultModekCFRunLoopCommonModes

        一个比较常见的问题:滑动tableView时,定时器还会生效吗?
        默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。 怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
        所以我们需要把Timer同时添加到UITrackingRunLoopModekCFRunLoopDefaultMode上。
        那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
      • CFRunLoopSource:输入源/事件源

      • CFRunLoopTimer:定时源

      • CFRunLoopObserver:观察者

    • RunLoop常见问题

      - (void)test {
      	NSLog(@"1");
      	dispatch_async(dispatch_get_global_queue(0, 0), ^{
              
              NSLog(@"2");
              
              [[NSRunLoop currentRunLoop] run];
              
              [self performSelector:@selector(test) withObject:nil afterDelay:10];
        
              NSLog(@"3");
      	});
      	NSLog(@"4");
      }
      
      - (void)test {
      	NSLog(@"5");
      }
      

      输出顺序? 答案是: 1423, 5不会执行, 原因是runloop开启时应该有任务执行mode中应该有item才行, 否则会退出, 所以应调整成如下代码

      dispatch_async(dispatch_get_global_queue(0, 0), ^{
              
              NSLog(@"2");
              //先添加任务
               [self performSelector:@selector(test) withObject:nil afterDelay:10];
               //再开启循环
              [[NSRunLoop currentRunLoop] run];
              NSLog(@"3");
      	});
      
    • 如何创建一个常驻线程

         @autoreleasepool {
              
              NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
              
              [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
              
              [runLoop run];
              
          }
      
    • 怎样保证子线程数据更新回到主线程更新UI时不影响滑动操作

      答: 将更新UI的任务添加到主线程的NSDefaultRunLoopMode 上执行即可, 这样主线程将会在用户停止滑动之后由UITrackingRunLoopMode切换到NSDefaultRunLoopMode之后更新UI

      [self performSelectorOnMainThread:@selector(reloadData) 
      											 withObject:nil waitUntilDone:NO
                                  modes:@[NSDefaultRunLoopMode]];
      
  11. 自旋锁与互斥锁

    • 自旋锁

      当任务被另一个线程锁定是, 尝试执行的线程会进入等待(不会休眠), 等上一个线程解除锁定时, 立即执行下一个线程任务,

      优点: 因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁

      缺点: 自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

      常用自旋锁: atomic, dispatch_semaphore_t

    • 互斥锁

      当上一个线程锁定时, 下一个尝试执行的线程会进入休眠, 等上一个线程解除锁定, 下一个线程自动唤醒然后执行任务

      常用互斥锁: NSLock, NSCondition, @ synchronized

3. UI

  1. UIView与CALayer的关系

    UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
    CALayer负责显示内容contents

  2. 事件传递与响应者链

    • 事件传递过程

      • 触摸事件发生后, 系统会将事件加入到UIApplication管理的事件队列

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

      • rootViewController再把事件发送给他的根View,然后会在View的层次结构中找到一个最合适的视图来处 理触摸事件, 找到合适的视图控件后,就会调用视图控件的touches方法来做具体的事件处理, 注意如果

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

            touchesBegan
            touchesMoved
            touchesEnded
        
    • 响应者链, 找到最合适处理事件的view

      • 判断当前view是否能够接收触摸事件
      • 判断当前的点,是否在当前view中
      • 如果点在当前的view中 遍历当前view的所有子view,遍历的过程是从后往前遍历的 如果找到子控件,返回子控件 如果没有子控件满足条件,返回当前view
      //    hitTest:withEvent:方法的处理流程如下:
      //    • hitTest:withEvent:会忽略隐藏、不和用户交互的、透明度小于0.01的视图
      //    • 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
      //    • 若返回NO,则hitTest:withEvent:返回nil;
      //    • 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有
      //    子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图
      //      返回非空对象或者全部子视图遍历完毕;
      //    • 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
      //    • 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
      
      
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
      //1 判断当前view,是否可以接收事件
      	if (!self.userInteractionEnabled || self.alpha <= 0.01 || self.hidden) {
      			return nil; 
        }
      //2 判断点击的点,是否在当前view中
      	if (![self pointInside:point withEvent:event]) {
      			return nil; 
        }
      //3 从后往前遍历当前view的子view,是否是最合适的view,如果不是返回self 
        NSInteger count = self.subviews.count;
      	for (NSInteger i =count-1;i>=0 ; i--) {
      			UIView *childView = self.subviews[i]; //把当前坐标系的点,转换成子view坐标系的点
      			CGPoint subPoint = [self convertPoint:point toView:childView]; //寻找最合适的view
      		//        //判断点是否在子控件中
      			if ([self pointInside:subPoint withEvent:event]) {
         			 	UIView *fitView = [childView hitTest:subPoint withEvent:event]; 
         				if (fitView) {
      						return fitView; 
                }
         		}
      	}
      	return self;
      }
      
  3. 图像显示原理

    • CPU工作

      • Layout: 布局
      • Display: 绘制
      • Prepare: 图片解码
      • Commit: 提交位图
    • GPU工作

      顶点着色,图元装配,cell光栅化,片段着色,片段处理

  4. 滚动视图优化:

    • CPU角度

      • 对象创建销毁,调整, 对于开销大的对象进行优化处理
      • 预排版: 布局计算, 文本计算, 采用缓存高度等
      • 预渲染: 文本图片异步绘制等
    • GPU角度

      • 考虑是有有不必要的CPU渲染

      • 是否有太多的离屏渲染

      • 是否有图层的混合操作(透明度尽量都不要使用)

      • view的层次结构是否合理

      • cell光栅化处理

        // 光栅化
        layer.shouldRasterize = true
        layer.rasterizationScale = UIScreen.mainScreen().scale
        
        // 异步绘制
        layer.drawsAsynchronously = true
        
  5. 什么是离屏渲染(Off-Screen Rendering)

    • 与之对应的就是On-Screen Rendering(在屏渲染), GPU的渲染操作主要用于显示当前屏幕缓冲去进行的操作, 而离屏渲染指的是GPU在当前屏幕缓冲区外新开辟缓冲区进行渲染操作

    • 离屏渲染什么时候会触发?

      答案: 圆角, 蒙版, 阴影, 等

4. OC语言特性

  1. 分类category

    • 分类作用: 声明私有方法, 给已存在类扩展方法(实例方法, 类方法, 协议, 属性), 属性需要借助运行时的关联对象, 不能直接添加属性

    • 分类特点: 运行时决议

  2. 扩展

    声明私有属性,声明方法(没什么意义),声明私有成员变量

  3. 代理

    代理是一种设计模式,以@protocol形式体现,一般是一对一传递。
    一般以weak关键词以规避循环引用。

  4. 通知

    使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。

  5. KVO

    • 实现原理 :

      KVO的实现依赖于Objective-C强大的runtime,KVO的底层实现是监听setter方法。当观察某对象A时,KVO动态机制会动态创建一个A类的子类NSKVONotifying_A,并为这个新的子类重写父类的属性的setter方法, 方法内容如下

      [super setAge:age]; `
      [self willChangeValueForKey:@"age"];
      [self didChangeValueForKey:@"age"];
      ///其中后面两个方法会调用
      -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
        
      
    • 作用:

      能够监听某个对象属性值的改变

  6. KVC

    通过Key值直接访问对象属性,或者给属性赋值的操作, 常用操作valueforkey: / setValue: forKey:

    底层执行机制: (属性name举例子)

    • 程序优先调用属性setName: 方法,代码通过setter方法完成设置。
    • 如果没有属性name, 会按照_name,_iskey,key,iskey的顺序搜索成员变量
    • 如果以上都没有会调用setValue:forUndefinedKey: 方法抛出错误

5 RunTime

RunTime是OC语言底层运行原理, OC代码最终都要通过runtime去调用,

  1. 常见的runtime应用如下

    • 交换方法

      通过交换方法可以实现拦截系统方法, 添加自己的需求

      //常用方法
      class_getClassMethod 获取类方法
      class_getInstanceMethod 获取实例方法
      method_exchangeImplementations 方法交换
      
      在load方法中执行狡猾
      
    • 关联对象(给分类添加属性)

      const char* name = "rylsj";
      - (void) setNick:(NSString *)nick {
          objc_setAssociatedObject(self, &name, nick, OBJC_ASSOCIATION_COPY_NONATOMIC);
      }
      - (NSString*) nick {
          return objc_getAssociatedObject(self, &name);
      }
      
    • 字典转模型

      • 获取属性列表
    • 动态添加方法

      - (void) rylsj_AddMethod {
        //"v@:@": v表示void, @表示id, :表示 SEL
          class_addMethod([self.persion class], @selector(run:), (IMP)runMethod, "v@:@");
      }
      
      void runMethod(id self, SEL _cmd, NSString* rylsj) {
          NSLog(@"%@", rylsj);
      }
      
      //调用
       if ([self.persion respondsToSelector:@selector(run:)]) {
              [self.persion performSelector:@selector(run:) withObject:@"66 rylsj"];
       } else {
              NSLog(@"方法没有实现!!");
       }
      
  2. OC 消息转发机制 (文中代码有错误, 可以c)

    1. 首先根据receiver对象的isa指针获取它对应的class

    2. 优先在class的cache查找message方法,如果找不到,再到
      methodLists查找

    3. 如果没有在class找到,再到super_class查找

    4. 一旦找到message这个方法,再依据receiver 中的self 指针找到当前的对象,调用当前对象的具体实现的方法(IMP),然后传递参数,调用实现方法。

    5. 当对象收到无法解读的消息后, 就会启动"消息转发(Message forwarding)"机制, 程序员可以通过消息转发告诉对象应该如何处理位置消息, 过程如下:

      1. 系统会调用resolveInstanceMethod方法, 如果是类方法则会调用resolveClassMethod, 在这个方法中我们可以增加调用方法的实现, 我们可以通过一个Person类来实践一下

        @interface Person : NSObject
        - (void)run;
        @end
        @implementation Person
        ///要添加的方法
        void run (id self, SEL _cmd) {
        		NSLog(@"人跑");
        }
        //在这个方法中添加自己的方法实现
        + (BOOL)resolveInstanceMethod:(SEL)sel {
            if(sel == @selector(run)){
              //关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方				法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表				  返回值为void,@表示self,:表示_cmd。 
                class_addMethod(self, sel, (IMP)run, "v@:");
                return YES;
            }
            return [super resolveInstanceMethod:sel];
        }
         
        @end
          
          
        @implementation ViewController
        - (void)viewDidLoad {
            [super viewDidLoad];
          	Person *person = [Person new];
            [person run];
          
        }
        @end
          
        //调用结果打印: "人跑"
          
        

        如果未实现以上方法, 则会进入步骤二

      2. forwardingTargetForSelector: 调用

        在这个方法中可以返回你需要转发消息的对象, 这里我们可以新建一个对象Car来演示, 在Person中不是现方案一中的resolveInstanceMethod方法 改成实现forwardingTargetForSelector方法 如下:

        @interface Car : NSObject
        - (void)run;
        @end
          
        @implementation Car
        - (void)run {
            NSLog(@"车跑");
        }
        @end
          
        @implementation Person
          
        - (id)forwardingTargetForSelector:(SEL)aSelector {
          	return [Car new];
        }
          
        //执行结果: 车跑
        
        
        

        通过forwardingTargetForSelector把消息转发给我们认为合适的对象去执行, 如果此步骤未实现, 则会进入下一步

      3. 通过forwardInvocation函数 设置我们自己生成的函数签名和对象

        //生成签名
        - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
            NSString *sel = NSStringFromSelector(selector);
            if([sel isEqualToString:@"run"]) {
                return [NSMethodSignature signatureWithObjCTypes:"v@:"];
            }
            return [super methodSignatureForSelector:selector];
        }
        
        //设置
        - (void)forwardInvocation:(NSInvocation *)invocation {
            SEL selector = [invocation selector];
            //新建需要转发消息的对象
            Car *car = [[Car alloc] init];
            if([car respondsToSelector:selector]){
                [invocation invokeWithTarget:car];
            }
        }
        
        //执行结果: 车跑
        
        

        以上是当前类未实现方法的三种不就措施, 调用顺序从前往后, 如果实现了第一种, 后面就不会执行以此类推, 如果都没实现程序就会crash

6. 内存

  1. 内存布局, 从代码区到栈区地址一次从低到高

    • 栈区(stack): 方法调用,局部变量等,是连续的,高地址往低地址扩展, 系统自动管理
    • 堆区(heap): 通过alloc等分配的对象,是离散的,低地址往高地址扩展,需要我们手动控制
    • 未初始化数据(bss): 未初始化的全局变量等
    • 已初始化数据(data): 已初始化的全局变量等
    • 代码段(text): 程序代码 (常量区)
  2. static关键字的作用

    • 用于修饰存储类型使之成为静态存储类型

      在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。
      在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。
      
    • 用于修饰链接属性使之成为内部链接属性

      静态函数只能在声明它的源文件中使用。
      
  3. const关键字

    • 声明常变量,使得指定的变量不能被修改。

      const int a = 5;/*a的值一直为5,不能被改变*/
      const int b; b = 10;/*b的值被赋值为10后,不能被改变*/
      const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/
      int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/
      const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/
      
    • 修饰函数形参,使得形参在函数内不能被修改,表示输入参数

      int fun(const int a);int fun(const char *str);
      
    • 修饰函数返回值,使得函数的返回值不能被修改。

      const char *getstr(void);使用:const *str= getstr();
      
      const int getint(void);  使用:const int a =getint();
      
  4. iOS 得内存管理机制?

    采用的是引用计数的方式进行内存管理, MRC下需要用户手动管理引用计数, ARC下, 系统禁用retain,release,retainCount,autorelease等关键字

  5. 自动释放池

    在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool, AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。

    实现原理:

    编译器会将 @autoreleasepool {} 改写为:

    void * ctx = objc_autoreleasePoolPush;
        {}
    objc_autoreleasePoolPop(ctx);
    
    • objc_autoreleasePoolPush:

      把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置,
      AutoreleasePool的多层嵌套,即每次objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。

    • objc_autoreleasePoolPop:

      根据传入的哨兵对象找到对应位置。给上次push操作之后添加的对象依次发送release消息。回退next指针到正确的位置。

  6. 循环引用场景

    • 代理(delegate)引起的相互循环引用

      解决方案: 声明地阿里是,使用weak关键字修饰

    • NSTimer引起的循环引用

      解决方案: 在合适的时候进行invalidate 并将指针指向nil

    • Block引起的循环引用

      对block中使用的self进行弱化 __weak typeof(self) weakSelf = self;

      注意: 并不是所有block都会造成循环引用, 只有被强引用了的block才会产生循环引用

  7. Block的内存问题

    • Block的三种形式

      • 全局Block(_NSConcreteGlobalBlock), 存储在已初始化的数据区(.data)

        不使用外部变量的是全局Block, 如下:

        NSLog(@"%@",[^{
        		NSLog(@"globalBlock");
        } class]);
        
        //输出: __NSGlobalBlock__
        
      • 栈Block(_NSConcreteStackBlock), 存储在栈区(Stack)

        使用外部变量但是未进行copy操作的Block是栈Block, 如下:

        NSInteger num = 10;
        NSLog(@"%@",[^{
        		NSLog(@"stackBlock:%zd",num);
        } class]);
        
        //输出: __NSStackBlock__
        
      • 堆Block(_NSConcreteMallocBlock), 存储在堆区(Heap)

        对栈Block进行copy操作,就是堆block,而对全局Block进行copy,仍是全局Block, 如下:

        //对堆Block copy操作, 依然是全局Block
        void (^globalBlock)(void) = ^{
        		NSLog(@"globalBlock");
        };
        NSLog(@"%@",[globalBlock class]);
        //输出: __NSGlobalBlock__
        
        
        
        //对栈Block进行copy,是堆Block
        NSInteger num = 10;
        void (^mallocBlock)(void) = ^{
        		NSLog(@"stackBlock:%zd",num);
        };
        NSLog(@"%@",[mallocBlock class]);
        //输出: __NSMallocBlock__
        
        

        注意: 对栈Block copy之后,并不代表着栈Block就消失了,左边的mallock是堆Block,右边被copy的仍是栈Block

        - (void)testWithBlock:(dispatch_block_t)block {
            block();
            
            dispatch_block_t tempBlock = block;
            
            NSLog(@"%@,%@",[block class],[tempBlock class]);
        }
        //输出: __NSStackBlock__,__NSMallocBlock__
        

7. 数据存储

  1. iOS中常用数据持久化方式

    • NSUserDefaults, 适用于轻量数据存储

      偏好设置会将所有数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。

      NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
      [defaults setObject:@"jack" forKey:@"firstName"];
      [defaults setInteger:10 forKey:@"Age"];
      ///现在可不手动调用同步方法
      [defaults synchronize];
      
    • 文件写入本地(数组, 字典, 字符串等) 如下:

      NSString *filePath = [[self getDocumentPath] stringByAppendingString:@"fileTest.txt"];
      NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:@"[email protected]", @"email", @"[email protected]", @"emailDisplay", nil];
      [dictionary writeToFile:filePath atomically:YES];
      
      
    • 对象归档

      需要对象实现NSCoding协议

      NSString *filePath = [[self getDocumentPath] 
      - (void)encodeWithCoder:(NSCoder *)aCoder {
        [aCoder encodeObject:_name forKey:kAddressCardName];
      }
      - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
        _name = [aDecoder decodeObjectForKey:kAddressCardName];
      }
      //存储
      [NSKeyedArchiver archiveRootObject:obj toFile:filePath];
      
    • 数据库(SQLite, CoreData)

      • SQLite 常用函数

        sqlite3_open() //创建并打开数据库连接
        sqlite3_exec() //执行数据库操作(crud)
        
      • SQLite常用语句

        • DDL(Data Definition Language) 数据库定义语句

          --create创表,
          create table t_student (id integer, name text, age inetger, score real);
          
          --drop删除表
          drop table if exists t_student; 
          
          
        • DML(Data Manipulation Language) 数据库操作语句

          --insert、
          insert into t_student (name, age) values (‘wg’, 10);
          --update、
          update t_student set name = ‘jack’, age = 20;
          --delete
          delete from t_student;
          
          
        • DQL(Data Query Language) 数据库查询语句

          -- select
          -- 查指定字段
          select name, age from t_student;
          -- 查全部
          select * from t_student ;
          -- 查数量
          select count (age) from t_student ;
          
          
          
        • 常用关键字: where,order by,group by和having等

          --where关键字示例
          delete from t_student where age <= 10 or age > 30 ;
          select * from t_student where age > 10 ;  
          -- 排序
          select * from t_student order by age desc 
          -- limit条件限制
          select * from t_student limit 4, 8 ;
          
          
        • 别名

          -- select 字段1 别名 , 字段2 别名 , … from 表名 别名 ; 
          select name myname, age myage from t_student;
          

8. 音视频开发

  1. 音频

    • 音效播放 (AVFoundation), 支持acc , wav 等格式, 特征: 比较短 30秒以内

      //创建音效
       NSString *path = [[NSBundle mainBundle]pathForResource:"music.acc" ofType:nil];
      NSURL *url  = [NSURL fileURLWithPath:path];
      AudioServicesCreateSystemSoundID((__bridge CFURLRef _Nonnull)(url), &soundID);
      //播放音效, 静音无振动
      AudioServicesPlaySystemSound(soundID);
      //静音有震动
      //AudioServicesPlayAlertSound(soundID);
      
      
      
    • 音乐播放 (AVAudioPlayer: 只能播放本地音乐, 不支持网络媒体)

      //1.创建一个音乐对象
      NSString *path = [[NSBundle mainBundle]pathForResource:@"music.mp3" ofType:nil];
      NSURL *url = [NSURL fileURLWithPath:path];
      AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
      
      //2. 准备, 播放
      [audioPlayer prepareToPlay];
          //播放
      [audioPlayer play];
      
    • 音频录制(AVAudioRecorder)

      //保存路径
       NSURL *url = [NSURL fileURLWithPath:[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject]stringByAppendingPathComponent:@"recoder001.wav"]];
      //录音参数设置: AVSampleRateKey采样率 AVNumberOfChannelsKey音频通道数, AVLinearPCMBitDepthKey线性音频深度, 
      NSMutableDictionary *settings = [NSMutableDictionary dictionary];
      // 音频采样率
      settings[AVSampleRateKey] = @(8000.0);
      
      //创建音频录音机
      AVAudioRecorder *recorder = [[AVAudioRecorder alloc]initWithURL:url settings:settings error:&error];
      
      //录音
      [recorder record];
      //停止录音
      [self.recorder stop];
      
      
      
    • 网络媒体流播放(AVPlayer, 见下文视频播放描述)

  2. 视频

    • 几种视频播放

      首先在iOS平台使用播放视频,可用的选项一般有这四个,他们各自的作用和功能如下:

      使用环境 优点 缺点
      MPMoviePlayerController MediaPlayer 简单易用, 自带页面 不可定制
      AVPlayerViewController AVKit 简单易用, 自带页面 不可定制
      AVPlayer AVFoundation 可定制度高,功能强大 不支持流媒体
      IJKPlayer IJKMediaFramework 定制度高,支持流媒体播放 使用稍复杂
    • AVPlayer

      • 播放音频

        NSURL *playUrl = [NSURL URLWithString:@"http://www.xxx.com/music.mp3"];
        self.player = [[AVPlayer alloc] initWithURL:playUrl];
        [self.player play];//播放
        [self.player pause];//暂停
        self.player.rate = 1.5//倍速
        
        
      • 播放视频 需要创建显示层AVPlayerLayer

    • 直播

      • 主播端:

        1. 获取音视频授权

        2. 配置采样参数

          • 音频: 码率和采样率
          • 视频: 分辨率, 帧率, 码率
        3. 采集数据

          • 音频采集: AVAudioSession
          • 视频采集: GPUImageVideoCamera
        4. 编码

          • 音频编码: 软编码(h.264, mpeg等,第三方SDK)/硬编码(AudioToolbox, 原生)
          • 视频编码: 软编码(mp3, acc等, 第三方sdk)/硬编码(VideoToolbox, 原生)
        5. 推流

          • 音视频封装(flv或者TS格式)
          • 协议(RTMP, HLS, FLV)
          • 数据推送
      • 服务器端

        • 数据分发(CDN)
        • 截屏
        • 录制
        • 转码
      • 用户端

        1. 拉流: RTMP, HLS, FLV
        2. 解码: 软解码和硬解码
        3. 播放: IJKPlayer 开源播放器播放
      • 直播常见问题 请参考文章 iOS开发之移动直播技术秒开、直播优化经验、直播问题解析、直播知识解惑

9. iOS开发中的动画

  1. 核心动画(CoreAnimation)

    • 核心动画的分类(CAAnimation类的继承结构)

      CAAnimation有三个子类: CAAnimationGroup(组动画), CAPropertyAnimation(属性动画), CATranstion(转场动画), 其中属性动画又分为: CABasicAnimation(基础动画)和CAKeyframeAnimation(关键帧动画)

  2. 基础动画

    
    

你可能感兴趣的:(iOS之实战,ios,objective-c,swift)