iOS底层原理总结 -- iOS面试题

总结一些iOS的底层面试题。巩固一下iOS的相关基础知识。

如有出入,还望各位大神指出。

OC对象

1. NSObject对象的本质是什么?

  • NSObject对象的本质就是结构体

2. 一个NSObject对象占用多少内存?

  • NSObject对象创建实例对象的时候系统分配了16个内存(通过malloc_size函数可获得)

  • 但是 NSObject只使用了8个字节 使用(class_getinstanceSize可获得)

3. 对象的isa指针指向哪里?

  • instance对象的isa指针指向class对象

  • class对象的isa指针指向 meta-class对象

  • meta-class对象的isa指针基类的meta-class对象

  • isa的优化

    • isa在arm64构架之前 isa的值就类对象的地址值。

    • isa在arm64构架开始的时候 采用了 isa优化的策略, 使用了共用体的技术。将64位的内存地址存储了很多东西,其中33位存储的是isa具体的地址值的。因为共用体中 前三位有存储的东西(),所以在&isa_mask出来的类对象地址值的二进制后面三位永远都是000, 十六进制就是8 或者0结尾的地址值

       union isa_t 
      {
          isa_t() { }
          isa_t(uintptr_t value) : bits(value) { }
          Class cls;
          uintptr_t bits;   ///>  typedef unsigned long 
            struct {
                ///> 0 代表普通指针,存储着Class、MetaClass对象的内存地址
              ///> 1 代表优化过,使用位域存储更多信息
              uintptr_t nonpointer        : 1; 
                ///> 是否有设置过关联对象,如果没有,释放时会更快
              uintptr_t has_assoc         : 1;
                ///> 是否有C++的析构函数(.cxx_destruct),如果没有,释放会更快
              uintptr_t has_cxx_dtor      : 1;
              ///> 存储着Class、MetaClass的内存地址
              uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
              ///> 用于在调试时分辨率是否未完成初始化
              uintptr_t magic             : 6;
              ///> 是否被弱指针指向过? 如果没有,释放会更快
              uintptr_t weakly_referenced : 1;
              ///> 对象是否正在释放
              uintptr_t deallocating      : 1;
                    ///> 引用计数器是否过大?无法存储在isa中
                ///> 如果为1,那么引用计数会存储在一个叫 side table的类属性中   
              uintptr_t has_sidetable_rc  : 1;
                      ///> 里面存储的值是引用计数器减1
              uintptr_t extra_rc          : 19;
          };
         ...
       }
      

4. OC类的信息存储在哪里?

  • meta-class存储:类方法
  • class对象存储: 对象方法,属性,成员变量,协议信息
  • instance存储: 成员变量具体的值

5. 说说你对函数调用的理解吧。

  • 函数调用 实际实际上就是 给对象发送一条消息
  • objc_msgSend(对象, @selectir(对象方法))
  • 寻找顺序(对象方法) instance的isa指针找到类对象 在类对象中寻找方法,若没有向superClass中查找。
  • 寻找顺序(类方法) instance的isa指针找到类对象 --> 类对象的isa找到meta-calss --> 在meta-class对象中寻找类方法,若没有向superClass中查找。

KVO

1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 利用Runtime API 动态生成了一个新的类, 并且instance对象的isa指针指向这个生成的新子类。

  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueForKey函数

    • willChangeValueForKey
    • 父类原来的set方法
    • didChangeValueForKey
    • didChangeValueForKey 内部会触发observerValueForKeyPath方法 实现监听。
  • 轻量级KVO框架:GitHub - facebook/KVOController

KVC

1. 使用KVC会不会调用KVO?

  • 会调用KVO, 因为他的内部使用了:
    • willChangeValueForKey
    • 直接去_方式去更改
    • didChangeValueForKey
    • didChangeValueForKey 内部会触发

2. KVC的赋值和取值过程是怎么样的?原理是什么?

  • 赋值
    • setKey、_setKey 顺序查找方法
      • 有: 传递参数调用防范
      • 没有: 查看accessInstanceVariablesDirectly 方法返回值 yes 继续查找
    • _key、_iskey、key、iskey 顺序查找

Category:

1. Category的使用场合是什么?

2. Category中的属性是否也存在类对象中?如果存在是怎么生成和存在的?如果不存在,它存在的位置在哪里?

  • 一个类永远只有一个类对象
  • 在运行起来之后 最重都会合并在 类对象中去。

3. Category的使用原理是什么?实现过程

  • 原理:底层结构是结构体 categoty_t 创建好分类之后分两个阶段:

    1. 编译阶段:

      将每一个分类都生成所对应的 category_t结构体, 结构体中存放 分类的所属类name、class、对象方法列表、类方法列表、协议列表、属性列表。

    2. Runtime运行时阶段:

      将生成的分类数据合并到原始的类中去,某个类的分类数据会在合并到一个大的数组当中(后参与编译的分类会在数组的前面),分类的方法列表,属性列表,协议列表等都放在二维数组当中,然后重新组织类中的方法,将每一个分类对应的列表的合并到原始类的列表中。(合并前会根据二维数组的数量扩充原始类的列表,然后将分类的列表放入前面)

  • 调用顺序

    • 为什么Category的中的方法会优先调用?

      如上所述, 在扩充数组的时候 会将原始类中拥有的方法列表移动到后面, 将分类的方法列表数据放在前面,所以分类的数据会优先调用

    • 延伸问题 - 如果多个分类中都实现了同一个方法,那么在调用该方法的时候会优先调用哪一个方法?

      在多个分类中拥有相同的方法的时候, 会根据编译的先后顺序 来添加分类方法列表, 后编译的分类方法在最前面,所以要看 Build Phases --> compile Sources中的顺序。 后参加编译的在前面。

4. Category和Extension的区别是什么?

  • Category 在运行的时候才将数据合并到类信息中
  • Extension 在编译的时候就将数据包含在类信息中了 @interface Xxxx() 也叫做匿名分类

5. Category中有load方法吗?load是什么时候调用的?

  • 有load方法
  • load什么时候调用
    • load方法在runtime加载类、分类的时候调用

6. load、initialize方法的区别是什么?它们在Category中的调用顺序?以及出现继承时他们之间的调用过程?当一个类有分类的时候为什么+load能多次调用儿initialize值调用了一次?

  • 调用方式:

    • load 根据函数地址直接调用
    • initialize 是通过 objc_msgSend调用
  • 调用时刻:

    • load是runtime加载类、分类的时候调用(只会调用1次)
    • initialize 是类第一次接收到消息的时候调用objc_msgsend()方法 如alloc、每一个类只会调用1次(但是父类的initialize方法可能会调用多次) 有些子类没有initialize方法所以调用父类的。
  • 调用顺序:

    • load:
      • 先调用类的+load方法
        • 按照编译的先后顺序调用(先编译、先调用)
        • 调用子类的+load方法之前会先调用父类的+load
      • 再调用分类的+load方法
        • 按照编译的先后顺序调用(先编译、先调用)
    • initialize
      • 初始化父类
      • 在初始化子类(可能最终调用的是父类的initialize方法)
  • load方法可以继承 我们在子类没有实现的时候可以调用,但是一般都是类自动去调用,我们不会主动调用,当子类没有实现+load方法的时候不会不会自动调用了就

    <img src="/Users/yuangonmg/Library/Application Support/typora-user-images/image-20191112191237086.png" alt="image-20191112191237086" style="zoom:25%;" />

  • 当一个类有分类的时候为什么+load能多次调用儿initialize值调用了一次?

    • 根据源码看出来,+load 直接通过函数指针指向函数,拿到函数地址,找到函数代码,直接调用 分开来直接调用的 不是通过objc_msgsend调用的
    • 而 initialize是通过消息发送机制,isa找到类对象找到方法调用的 所以只调用一次

7. Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以间接添加。

    • 使用一个全局的字典 (缺点: 每一个属相都需要一套相同的代码)
     ///> DLPerson+Test.h
     @interface DLPerson (Test)
     ///> 如果直接使用 @property 只会生成方法的声名 不会生成成员变量和set、get方法的实现。
     @property (nonatomic, assign) int weigjt;
     @end
     
     
     ///> DLPerson+Test.m
    #import "DLPerson+Test.h"
    @implemention DLPerson (Test)
    NSMutableDictionary weights_;
    + (void)load{
    weights_ = [NSMutableDictionary alloc]init];
    }
    
    - (void)setWeight:(int)weight{
       NSString *key = [NSString stringWithFormat:@"%p",self];
       weights_[key] = @(weight);
    
    }
    - (int)weight{
       NSString *key = [NSString stringWithFormat:@"%p",self];
       return [weights_[key] intValue] 
    }
    
    @end
    
    
  • 使用runtime机制给分类添加属性

```objc
#import<objc/runtime.h>

const void *DLNameKey = &DLNameKey
///> 添加关联对象
void objc_setAssociatedObject(
id object,          ///>  给哪一个对象添加关联对象
const void * key,   ///>   指针(赋值取值的key)  &DLNameKey
id value,           ///>  关联的值
objc_AssociationPolicy policy ///>  关联策略 下方表格
)

eg : objc_setAssociatedObject(self,@selector(name),name,OBJC_ASSOCIATION_COPY_NONATOMIC);


///> 获得关联对象
id objc_getAssociatedObject(
id object,           ///>  哪一个对象的关联对象
const void * key     ///>   指针(赋值取值的key) 
)
eg:
objc_getAssociatedObject(self,@selector(name));
/// _cmd  == @selector(name); 
objc_getAssociatedObject(self,_cmd);

///> 移除所有的关联对象
void objc_removeAssociatedObjects(
id object       ///>
)
  - objc_AssociationPolicy(关联策略)

|objc_AssociationPolicy(关联策略) |对应的修饰符|
|:---|:---|:---|:---|
|OBJC_ASSOCIATION_ASSIGN | assign  |
|OBJC_ASSOCIATION_RETAIN_NONATOMIC |  strong, nonatomic | 
|OBJC_ASSOCIATION_COPY_NONATOMIC |  copy, nonatomic | 
|OBJC_ASSOCIATION_RETAIN |  strong, atomic | 
|OBJC_ASSOCIATION_COPY |  copy, atomic | 


Block

1. block的原理是怎样的?本质是什么

  • block的本质就是一个oc对象 内部也有isa指针, 封装了函数及调用环境的OC对象,

2. 看代码解释原因

  int main(int argc, const char *argv[]){
    @autoreleasepool{
      int age = 10;
      void  (^block)(void) = ^{
          NSLog(@" age is %d ",age);
      };
      age = 20;
      block();
    }
  }
  /*
  输出结果为? 为什么?
  输出结果是: 10
  如果没有修饰符  默认是auto
  为了能访问外部的变量 
  block有一个变量捕获的机制 
  因为他是局部变量 并且没有用static修饰 
  所以它被捕获到block中是 一个值,外部再次改变时 block中的age不会改变。
  */
变量类型 捕获到Block内部 访问方式
局部变量 auto 值传递
局部变量 static 指针传递
全局变量 直接访问
int main(int argc, const char *argv[]){
  @autoreleasepool{
    int age = 10;
    static int height = 10;
    void  (^block)(void) = ^{
        NSLog(@" age is %d, height is %d",age, height);
    };
    age = 20;
    height = 20;
    block();
  }
}
/*
输出结果为? 为什么?
age is 10, height is 20
局部变量用static 修饰之后 捕获到block中的是 height的指针,
因此修改通过指针修改变量之后 外部的变量也被修改了
*/
int age = 10;
static int height = 10;
int main(int argc, const char *argv[]){
  @autoreleasepool{
    
    void  (^block)(void) = ^{
        NSLog(@" age is %d, height is %d",age, height);
    };
    age = 20;
    height = 20;
    block();
  }
}
/*
输出结果为? 为什么?
age is 20, height is 20
 因为 age 和 height是全局变量不需要捕获直接就可以修改
 
 全局变量 对应该就可以访问,
 局部变量 需要跨函数访问,所以需要捕获
因此修改通过指针修改变量之后 外部的变量也被修改了
*/

int main(int argc, const char *argv[]){
  @autoreleasepool{
    
    void  (^block)(void) = ^{
        NSLog(@" self %p",self);
    };
    block();
  }
}
/*
self 会不会被捕获?
因为函数默认会有两个参数 void test(DLPerson *self, SEL _cmd)
所以self 也是一个局部变量

访问@property(nonatmic, copy) NSString *name;
因为他是成员变量, 访问的时候 用self.name 或者 self->_name 访问  所以 block在内部会捕获self。
*/

3. 既然block是一个OC对象,那么block的对象类型是什么?

  • ios内存分为5发区域
    • 堆: 动态分配内存,需要程序员申请,也需要程序员自己管理(alloc、malloc等...)
    • 栈: 放一些局部变量,临时变量 系统自己管理
    • 静态区(全局区): 存放全局的静态对象。(编译时分配,APP结束由系统释放)
    • 常量区: 常量。(编译时分配,APP结束由系统释放)
    • 代码区: 程序区
  • block有三种类型,最终都继承自NSBlock类型

    • superClass
      NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject
    block类型 环境 存放位置
    NSGlobalBlock 没有访问auto变量 静态区
    NSStackBlock 访问了auto变量
    NSMallocBlock NSStackBlock调用了copy
  • NSStackBlock调用了copy 代码实例

    void (^block)(void);
    void (^block1)(void);
    void test2(){
        int age = 10;
        block = ^{
            NSLog("age is %d",age);
          /*
          因为 block访问了 auto变量 
          所以目前block的类型为NSStackBlock类型,
          存放的位置在  栈  上 
          在 main访问是  这个block已经被释放了。
          */
        };
        
       [block1 = ^{
            NSLog("age is %d",age);
            /*  
            因为 block访问了 auto变量 
            并且 进行了copy操作
            所以目前block的类型为 NSMallocBlock 类型 
            存放的位置在  堆  上 
            在 main访问是  这个block是可以被访问的。
          */
        } copy];
    }
    
    int main(int argc, const char *argv[]){
    
        @autoreleasepool{
            test2();
            
            block(); /// 输出的值为 很大不是想要的值
            
            block1();/// 输出的值是10
        }
    }
    
  • 每一种类型的block调用了copy之后结果如下所示

    block的类型 副本源的配置存储域 复制后的区域
    NSGlobalBlock 程序的数据区域 什么都不做
    NSStackBlock 从栈复制到堆
    NSMallocBlock 引用计数器+1

4. 在什么情况下 ARC环境下,编译器会根据情况自动将栈上的block复制到堆上?

  • block作为函数的返回值的时候

  • 将block赋值给__strong指针时

  • block作为 Cocoa API中方法名含有usingBlock的方法参数时

    NSArray *arr = @[];
    /// 遍历数组中包含  usingBlock方法的参数
    [arr enumerateObjectUsingBlock:^(id _Nonnullobj, NSUInteger idx, Bool _Nonnull stop){
    
    }] ;
    
  • block作为GCD属性的建议写法

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    
    });
    
    disPatch_after(disPatch_time(IDSPATCH_TIME_NOW, (int64_t)(delayInSecounds *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    });
    
  • MRC下block属性建议写法

    • @property (copy, nonatomic) void (^block)(void);
  • ARC下block属性建议写法

    • @property (strong, nonatomic) void (^block)(void);
    • @property (copy, nonatomic) void (^block)(void);

5. __weak的作用是什么?有什么使用注意点?

  • __weak 是一个修饰符
  • 当block内部访问的对象类型的auto变量
    • 如果block是在栈上,将不会对auto变量产生强引用
    • 如果Block被拷贝到堆上
      • 会调用block内部的copy函数
      • copy函数会调用源码中的_Block_object_assign函数
      • _Block_object_assign函数会根据修饰 auto 变量的修饰符(__strong、__weak 、__unsafe_unretained)来决定作出相应的操作,形成强引用或者弱引用
    • block从对上移除
      • 会调用block内部的dispose函数
      • dispose函数会调用源码中的 _Block_object_dispose函数
      • _Block_object_dispose函数会自动释放auto变量(release)

6. __block的作用是什么?有什么使用注意点?

  • block为什么不能修改外部变量的值?

    • 因为block也是一个函数调用(可以说他们是两个函数, block调用另一个函数的变量是不能调的)
    • 在C++内部 block是调用的另一个函数 实现。
  • __block修饰之后会将变量包装成一个对象 可以解决block内部无法修改auto变量的问题

  • 包装秤对象之后就可以通过指针修改 外部的变量了

  • 使用注意点: 在MRC环境下不会对指向的对象产生强引用的

7. __block的属性修饰词是什么?为什么?使用block有哪些注意点?

  • 修饰词是copy
  • block 如果没有进行copy操作就不会再堆上, 在堆上才能控制它的生命周期
  • 注意循环引用的问题
  • 在ARC环境下 使用呢strong和copy都可以没有区别 在MRC环境下有区别
  • block是一个对象, 所以block理论上是可以retain/release的. 但是block在创建的时候它的内存是默认是分配在栈(stack)上, 而不是堆(heap)上的. 所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当你在该作用域外调用该block时, 程序就会崩溃.

8. block在修饰NSMutableArray,需不需要添加__block?

  • 不需要

    NSMutableArray *array = [[NSMutableView alloc]init]
    void (^block)(void) = ^{
      array = nil;   ///>  这样操作是需要__block的。  ///> 
        
      ///> 下面这个是不需要 __block修饰的,因为这个只是使用它的指针而不是修改它的值
      [array addObject:@"aa"];
      [array addObject:@"aa"];
    }
    
    
  • 在修改NSMutableArray的数组的时候 并不是在修改这个数据 而是在使用这个指针去

Runtime

22. 讲一下OC的消息机制

  • OC中的方法调用最后都是 objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
  • objc_msgSend底层有三大模块
  • 消息发送(当前类、父类中查找)、动态方法解析、消息转发

23. 消息转发机制流程

​ 在objc_msgSend有三大阶段

  • 消息发送阶段
    • 给当前类发送一条消息,
    • 会先从当前的类中的缓存查找
    • 如果没有去遍历 class_rw_t 方法列表查找
    • 如果没有再去父类的缓存查找
    • 如果没有在去父类的class_rw_t方法列表中查找
    • 循环父类 如果找到调用方法, 并且将方法缓存到 方法调用者的方法缓存中
    • 如果一直没有到下一个阶段 动态解析阶段
  • 动态解析阶段
    • 动态解析会调用-resolveInstanceMethod \ +resolveClassMethod 方法 在方法中手动添加class_addMethod方法的调用。
    • 只会解析一次 会将是否解析过的参数置位YES
    • 然后在次 调用消息发送的阶段
    • 如果我们实现了 方法的添加 则在消息发送阶段可以找到这个方法
    • 调用方法并 将方法缓存到 方法调用者的缓存中
    • 如果没有实现, 在第二次走到动态解析阶段,不会进入动态解析,因为上一次已经解析过了
    • 我们将动态解析过的参数设置为YES,所以会走到下一个阶段 消息转发阶段
  • 消息转发阶段
    • 第一种: 实现了forwardingTargetForSelector方法
      • 调用forwardingTargetForSelector 方法(返回一个类对象), 直接使用我们设置的类去发送消息。
    • 第二种: 没有实现forwardingTargetForSelector
      • 回去调用 methodSignatureForSelector 方法,在这个方法添加方法签名
      • 之后会调用forwardInvocation 方法, 在这个方法中我们 [anInvocation invokeWithTarget:类对象];
      • 或者其他操作都可以 这里没有什么限制。

24. 什么是runtime? 平时项目中有用过吗?

  • OC是一门动态性比较强的语言,允许很多操作推迟到程序运行时才进行
  • OC的动态性是由runtime来支撑实现的,runtime是一套C语言的API,封装了许多动态性相关的函数
  • 平时写的代码 底层都是转换成了 runtime的API进行调用的
  • 具体应用
    • 关联对象,给分类添加属性,set和get的实现
    • 遍历类的成员变量 归档解档、字典转模型
    • 交换方法(系统的交换方法)

26. iskindOfClass 和 isMemberOfClass的区别?

  • isMemberOfClass源码:

      ///  返回的直接是 是否是当前的类, 当 象
    - (BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }
    
    ///  返回的直接是 是否是当前的类, 
    /// 当前元类对象
    + (BOOL)isMemberOfClass:(Class)cls {
        return object_getClass((id)self) == cls;
    } 
    
    
    
  • iskindOfClass源码:

    /// for循环查找 , 会根据当前类和 当前类的父类去逐级查找 ,
    - (BOOL)isKindOfClass:(Class)cls {
      for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
          if (tcls == cls) return YES;
      }
      return NO;
    }
    
    ///   for循环查找 , 会根据当前类和 当前类的额父类去逐级查找 ,
    /// 当前元类对象
     + (BOOL)isKindOfClass:(Class)cls {
      for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
          if (tcls == cls) return YES;
      }
      return NO;
    } 
    
  • 相关面试题:

    //        NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
    //        NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
    //        NSLog(@"%d", [[MJPerson class] isKindOfClass:[MJPerson class]]);
    //        NSLog(@"%d", [[MJPerson class] isMemberOfClass:[MJPerson class]]);
          /// 上面的写法 与 下面的写法 相同
          
          // 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的、继承于NSObject),都返回YES
          NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
          NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
          NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
          NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
    //        输出的结果是什么?
    
    

Runloop

1. 讲讲Runloop片在项目中的应用

Runloop

多线程

1. iOS中常见的多线程方案

技术方案 简介 语言 线程生命周期 使用频率
pthread 1. 一套通用的多线程API
2. 适用于Unix/Linux/Windows等系统
3. 跨平台、可移植
4. 使用难度大
C 程序员管理 几乎不用
NSThread 1. 使用更加面向对象
2. 简单易用,可直接操作线程对象
OC 程序员管理 偶尔使用
GCD 1. 旨在代替NSThread等线程技术
2. 充分利用设备的多核
C 自动管理 经常使用
NSOperation 1. 基于GCD(底层是GCD)
2. 比GCD多了一些简单使用的功能
3. 使用更加面向对象
OC 自动管理 经常使用

2. GCD的常用函数

  • GCD中有2个用来执行任务的函数
    • 用同步的方式执行任务
      • dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
      • queue: 队列
      • block: 任务
    • 用异步的方式执行任务
    • dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

3. GCD的队列

  • GCD的队列可以分为2大类型
    • 并发队列
      • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
      • 并发功能只有在异步(dispatch_async)函数下才有效
    • 串行队列
      • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

4. 组合队列执行表

并发队列 手动创建串行队列 主队列
同步 没有开启新线程
串行执行任务
没有开启新线程
串行执行任务
没有开启新线程
串行执行任务
异步 开启新线程
并行执行任务
开启新线程
串行执行任务
没有开启新线程
串行执行任务

5. GCD的线程锁

- (void)interview01{
    ///> 会发生死锁,
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
    ///      dispatch_sync 需要立马在当前线程 同步执行任务  当前在主线程中
    ///      而主队列需要等 主线程的东西执行完之后才会执行。 所以造成了死锁
}

- (void)interview02{
    ///> 不会发生死锁
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
    //  dispatch_sync 不需要需要立马在当前线程 同步执行任务 所以等待主线程执行结束之后才执行的
}

- (void)interview03{
    ///> 会产生死锁
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_SERIAL); /// 同步;
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue, ^{  //死锁
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");   
}

- (void)interview04{
    ///> 不会产生死锁
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_SERIAL); /// 串行;
    //    dispatch_queue_t queue2 = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_CONCURRENT); /// 并发;
    dispatch_queue_t queue2 = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_SERIAL); /// 串行; 也不会
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue2, ^{  //死锁
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
    //不会产生死锁   因为两个任务不在同一个队列之中, 所以不存在互相等待的问题。
}

- (void)interview05{
    ///> 不会产生死锁
    NSLog(@"任务1  thread:%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("muqueue2", DISPATCH_QUEUE_CONCURRENT); /// 并发;
    dispatch_async(queue, ^{
        NSLog(@"任务2 thread:%@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"任务3  thread:%@",[NSThread currentThread]);
        });
        NSLog(@"任务4  thread:%@",[NSThread currentThread]);
    });
    NSLog(@"任务5  thread:%@",[NSThread currentThread]);
    //不会产生死锁   因为两个任务不在同一个队列之中, 所以不存在互相等待的问题。
}

6. GCD的线程锁-- runloop有关的锁

- (void)test{
    NSLog(@"2");
}

- (void)touchesBegan03{
//    NSThread *thread = [[NSThread alloc]initWithBlock:^{
//        NSLog(@"1");
//
//    }];
//    [thread start];
//    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
//    运行后会崩溃  因为子线程 performSelector方法 没有开启runloop, 当执行test的时候这个线程已经没有了。
    
    NSThread *thread = [[NSThread alloc]initWithBlock:^{
        NSLog(@"1");
        [[NSRunLoop  currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
    /// r添加开启runloop后  在线程中有runloop存在线程就不会死掉, 之后调用performSelect就没有问题了
}

- (void)touchesBegan02{
    /// 创建全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");

        //[self performSelector:@selector(test) withObject:nil];///  打印结果  1  2  3   等价于[self test]
        /// 这句代码点进去发现是在Runloop中的方法
        /// 本质就是向Runloop中添加了一个定时器。  子线程默认是没有启动 Runloop的
        
        [self performSelector:@selector(test) withObject:nil afterDelay:.0]; ///  打印结果  1  3
        NSLog(@"3");
        /// 启动runloop
        [[NSRunLoop  currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });
}
- (void)touchesBegan01{
    /// 创建全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");

//        [self performSelector:@selector(test) withObject:nil];///  打印结果  1  2  3   等价于[self test]
        /// 这句代码点进去发现是在Runloop中的方法
//     本质就是向Runloop中添加了一个定时器。  子线程默认是没有启动 Runloop的

        [self performSelector:@selector(test) withObject:nil afterDelay:.0]; ///  打印结果  1  3
        NSLog(@"3");
    });
}

7. GCD组队列的使用

  • 异步并发执行任务1、任务2
  • 等任务1、任务2都执行完毕后,再回到主线程执行任务3
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("muqueue", DISPATCH_QUEUE_CONCURRENT);// 并发队列
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1   thread  --- %@",[NSThread currentThread]);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2   thread  --- %@",[NSThread currentThread]);
        }
    });
    
    ///> 回到主线程执行 任务3
//    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任务3   thread  --- %@",[NSThread currentThread]);
//        }
//    });
    
    ///> 执行完任务1、2之后再执行任务3、4 
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3   thread  --- %@",[NSThread currentThread]);
        }
    });
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4   thread  --- %@",[NSThread currentThread]);
        }
    });
}

8. 多线程安全隐患的解决方案

  • 隐患造成, 多个线程同时访问一个数据然后对数据进行操作
  • 解决方案:使用线程同步技术,
  • 常见线程同步技术: 加锁
  • iOS线程同步方案:
    • OSSpinLock
    • os_unfair_lock
    • pthread_mutex
    • dispatch_semaphore
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock
    • @synchronized

内存管理

性能优化

1. 什么是CPU和GPU

  • CPU (Central Processing Unit,中央处理器 )
    • 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制 (Core Graphics)
  • GPU (Graphics Processing Unit,图形处理器 )
    • 纹理的渲染


2. 卡顿原因

  • cpu处理后GPU处理 若垂直同步信号早于GPU处理的速度那么会形成掉帧问题


  • 在 iOS中有双缓存机制,有前帧缓存、后帧缓存

2. 卡顿优化 - CPU

  • 尽量使用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer 替代 UIView
    • UIView和CALayer的区别?
      • CALayer是UIView的一个成员
      • CALayer是专门用来显示东西的
      • UIView是用来负责监听点击事件等
  • 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改。
  • 尽量提前计算好布局,在有需要时一次性调整好对应的属性,不要多次修改属性。
  • Autolayout会比直接设置frame消耗更多的CPU资源
  • 图片的size最好跟UIImageView的size保持一致
    • 因为如果超出或者UIImageView会对图片进行伸缩的处理
  • 控制线程的最大并发数量
  • 尽量把一些耗时的操作放到子线程
    • 文本处理(储存计算和绘制)
    • 图片处理(解码、绘制)

3. 卡顿优化 - GPU

  • 尽量减少视图数量和层次
  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,机会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸。
  • 减少透明视图,不透明的就设置opaque为YES
  • 尽量避免离屏渲染

4. 离屏渲染

  • 在OpenGL中,GPU有2中渲染方式

    • On-Screen Rendering: 当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作。
    • Off-Screen Rendering: 离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
  • 离屏渲染消耗性能原因:

    • 需要创建新的缓冲区
    • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,有需要将上下文环境从离屏切换到当前屏幕。
  • 那些操作会出发离屏渲染?

    • 光栅化 layer.shouldRasterize = YES;

    • 遮罩,layer.mask =

    • 圆角,同事设置layer.maskToBounds = YES、layer.cornerRadius大于0

      • 考虑通过CoreGraphics绘制圆角,或者美工直接提供。
      • 第一种方法:通过设置layer的属性
      imageView.layer.masksToBounds = YES;
      [self.view addSubview:imageView];
      
      • 第二种方法:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
      UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
      imageView.image = [UIImage imageNamed:@"1"];
      //开始对imageView进行画图
      UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
      //使用贝塞尔曲线画出一个圆形图
      [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
      [imageView drawRect:imageView.bounds];
      
      imageView.image = UIGraphicsGetImageFromCurrentImageContext();
       //结束画图
      UIGraphicsEndImageContext();
      [self.view addSubview:imageView];
      
      • 第三种方法:使用CAShapeLayer和UIBezierPath设置圆角
      #import "ViewController.h"
      
      @interface ViewController ()
      
      @end
      
      @implementation ViewController
      
      - (void)viewDidLoad {
       [super viewDidLoad];
      UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
      imageView.image = [UIImage imageNamed:@"1"];
      UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
      
      CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
      //设置大小
      maskLayer.frame = imageView.bounds;
      //设置图形样子
      maskLayer.path = maskPath.CGPath;
      imageView.layer.mask = maskLayer;
      [self.view addSubview:imageView];
      }
      
      
      • 推荐三种方式,对内存的消耗最少啊,渲染速度快
    • 阴影, layer.shadowXXX

      • 如果设置了layer.shadowPath就不会产生

4. 卡顿检测

  • 平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作

  • 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

  • 参考代码:GitHub - UIControl/LXDAppFluecyMonitor

5. 耗电优化

  • 耗电来源
    • CPU处理 Processing
    • 网络 Networking
    • 定位 Location
    • 图像 Graphice
  • 尽可能降低CPU、GPU的功耗
  • 少用定时器
  • 优化I/O操作 文件读写
    • 尽量不要频繁的写入小数据,最好批量一次性写入
    • 读写大量重要的数据的时候,考虑使用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
    • 数据量比较大的,建议使用数据库(比如SQList、CoreData)
  • 网络优化
    • 减少,压缩网络数据
    • 多次请求结果相同,尽量使用缓存
    • 使用断点续传,否则网络不稳定时可能多次传输相同的内容
    • 网络不可用时尽量不要尝试执行网络请求
    • 让用户可以取消长时间运行或者网络速度很慢的网络操作,设置合理的超时时间
    • 批量传输,比如:下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块的下载,如果下载广告,一次性多下载一些,然后慢慢展示。如果下载电子邮件,一次下载多封不要,一封一封的下载。
  • 定位优化
    • 如果只需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成之后,会自动让定位硬件断电。
    • 如果不是导航应用,尽量不要实时更新位置,定位完毕之后就关掉定位服务
    • 尽量降低定位的精准度,如果没有需求的话尽量使用低精准度的定位。随软件自身要求
    • 如果需要后台定位,尽量设置pausesLocationUpdatasAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
  • App启动
    • App启动分为两种
      • 冷启动(Cold Launch):从零开始启动App
      • 热启动(Warm Launch):App已经在内存中,在后台存活,再次点击图标启动App
    • App启动时间的优化,主要针对于冷启动
      • 通过添加环境变量可以打印app启动的时间分析(Edit scheme -> Run -> Arguments)
        • DYLD_PRINT_STATISTICS设置为1
        • 如果想要看更详细的内容,那就将DYLD_PRINTP_STATISTICS_DETAILS设置为1
    • 冷起订分为三大阶段
      • dyld(dynamic link editor),Apple的动态连接器,可用来装在Mach-O文件(可执行文件,动态库等)
        • 启动时做的事情
          • 装载App的可执行文件,同时会递归加载所有依赖的动态库
          • 当dyld把所有的可执行文件和动态库都装载完毕之后,会通知runtime进行下一步处理
      • runtime
        • 启动时runtime所做的事情
          • 调用map_images进行可执行文件内容的解析和处理
          • 在load_images中调用call_load_methods,调用所有Class和Catrgory的+load方法
          • 进行各种Objc结构的初始化,(注册Objc类、初始化类对象等等)
          • 调用C++静态初始化器和attribute((constructor))修饰的函数
        • 到此为止可执行文件的动态库和所有的符号(Class、protocols、Selector、IMP...)都已经按格式成功加载到内存中,被runtime所管理
      • main
        • 总结一下
          • APP的启动有dyld主导,将可执行文件加载到内存、顺便加载所有依赖的动态库
          • 并有Runtime负责加载成objc定义的结构
          • 所有初始化工作结束后,dyld就会调用main函数
          • 接下来就是UIApplicationMain函数,AppDelegate的Application:didFinishLaunchingWithOptions:方法


            image
  • 如何优化启动时间
    • dyld
      • 减少动态库、合并一些动态库(定期清理不必要的动态库)
      • 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
      • 减少C++虚函数数量
      • swift尽量使用struct
    • runtime
      • 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、Objc的+load
    • main
      • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
      • 按需求加载

6. 安装包瘦身

  • 安装包(IPA)主要由可执行文件、资源组成

  • 资源(图片、音频、视频等)

  • 可执行文件瘦身

    • 编译器优化

      • Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default设置为YES
      • 去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO,Other C Flags添加-fno-exceptions
    • 利用AppCode(AppCode_下载链接)检测未使用的代码:

      • 菜单栏 -> Code -> inspect Code
    • 编写LLVM插件检测出重复代码、未被调用的代码

    • 生成LinkMap文件,可以查看可执行文件的具体组成

      image

构架设计

1. 设计模式

  • 设计模式(Design Pattern)

    • 是一套被反复使用、代码设计经验的总结
    • 使用设计模式的好处是:可重用代码、让代码更容易被他人理解、保证代码可靠性
    • 一般与编程语言无关,是一套比较成熟的编程思想
  • 设计模式可以分为三大类

    • 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
      • 单例模式、工厂方法模式,等等
    • 结构型模式:把类或对象结合在一起形成一个更大的结构
      • 代理模式、适配器模式、组合模式、装饰模式,等等
    • 行为型模式:类或对象之间如何交互,及划分责任和算法
      • 观察者模式、命令模式、责任链模式,等等

推荐阅读更多精彩内容