美文网首页面试IOS面试专题
iOS底层原理总结 -- iOS面试题

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

作者: 小李小李一路有你 | 来源:发表于2019-11-25 16:11 被阅读0次

    总结一些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)

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

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

    相关文章

      网友评论

        本文标题:iOS底层原理总结 -- iOS面试题

        本文链接:https://www.haomeiwen.com/subject/xjljwctx.html