面试题

作者: AngeloD | 来源:发表于2021-08-09 11:18 被阅读0次

    面试题

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

      • 实际上分配了16个字节的存储空间给NSObject对象
      • 真正有使用的空间是:一个指针变量所占用的大小(64位:8个字节,32位:4个字节)
      • 结构体:继承遵循内存对齐原则:结构体的最终大小必须是最大成员大小的倍数。如果父类内存对齐后有多余的字节,子类继承后声明的变量可以放到父类多余的字节当中。并且OC底层定义小于16个字节的,都给分配16个字节;InstanceSize:最小对齐单位为isa指针的大小8。malloc_size:最小对齐单位为OC定义的最小字节大小16。
    2. 对象的isa指针指向哪里?

      • instance的isa指向class;
        • 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。
      • class的isa指向meta-class;
        • 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。
      • meta-class的isa指向基类的meta-class;
      • 基类的meta-class的isa指向自己;
    3. 对象的superclass指针指向哪里?

      • class的superclass指向父类的class;
        • 如果没有父类,superclass指针为nil。
      • meta-class的superclass指向父类的meta-class;
      • 基类的meta-class的superclass指向基类的class;
    4. OC的类信息存放在哪里?

    5. Objective-C中的对象,简称OC对象,主要可以分为3种:

      • instance对象(实例对象):通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。
      • class对象(类对象):
        • objectClass1 ~ objectClass5 都是NSObject的class对象(类对象)。
        • 它们是同一个对象,每个类在内存中有且只有一个class对象。
      • meta-class对象(元类对象):
        • 每个类在内存中有且只有一个meta-class对象。
        • meta-class对象和class对象的内存结构是一样的,但用途不一样。
    6. instance对象在内存中存储的信息包括:

      • isa指针;
      • 其他成员变量;
    7. class对象在内存中存储的信息包括:

      • isa指针;
      • superclass指针;
      • 类的属性信息(@property)
      • 类的对象方法信息(instance method)
      • 类的协议信息(protocol)
      • 类的成员变量信息(ivar)
      • ......
    8. meta-class对象在内存中存储的信息包括:

      • isa指针
      • superclas指针
      • 类的类方法信息(class method)
      • ......(其他类似class的信息,是空的)
    9. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

      • 利用Runtime的API动态生成一个子类,并且让instance对象的isa指向这个全新的子类
      • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数:
        • willChangeValueForKey
        • 父类原来的setter实现
        • didChangeValueForKey,这个方法内部又会调用监听器(observer)的监听方法
          • 内部又会调用监听器(observer)的监听方法:observeValueForKeyPath:ofObject:change:context:
    10. _NSSetXXXValueAndNotify的内部实现:

      • 调用willChangeValueForKey
      • 调用原来的setter实现
      • 调用didChangeValueForKey
        • didChangeValueForKey内部会调用observer的observeValueForKeyPath:ofObject:change:context方法
    11. 如何手动触发KVO?

      • 手动调用willChangeValueForKey和didChangeValueForKey;
    12. 通过KVC修改属性会触发KVO吗?

      • 会触发KVO(相当于setValue:forKey:内部手动调用了KVO的_NSSetXXXValueAndNotify方法)
    13. KVC:setValue:forkey:的原理:

      • 按照setKey、_setKey顺序查找方法:
        • 如果找到了传递参数,调用方法。
        • 如果找不到,查看accessInstanceVariableDirectory方法的返回值:
          • 返回NO:调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException
          • 返回YES:按照_key、_isKey、key、isKey顺序查找成员变量,查找到直接赋值,查找不到抛出同上NO的异常。
    14. KVC:valueForKey:的原理:

      • 按照getKey、key、isKey、_key顺序查找方法:
        • 如果找到了,调用方法。
        • 如果找不到,查看accessInstanceVariableDirectory方法的返回值:
          • 返回NO:调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
          • 返回YES:按照_key、_isKey、key、isKey顺序查找成员变量,查找到直接取值,查找不到抛出同上NO的异常。
    15. KVC的赋值和取值过程是怎样的?原理是什么?

      • 赋值过程即上面的setValue:forKey:的原理;
      • 取值过程即上面的valueForKey:的原理;
    16. 什么是Runloop?

      • 运行循环
      • 在程序运行过程中循环做一些事情
    17. runloop的基本作用:

      • 保持程序的持续运行
      • 处理APP中的各种事件(比如触摸事件、定时器事件等)
      • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
    1. runloop内部实现逻辑?

      • sources0:
        • 触摸事件处理;
        • performSelector:onThread:
      • sources1:
        • 基于port的线程间通信;
        • 系统事件捕捉(比如点击事件是sources1捕捉,然后分发给sources0去处理);
      • timers:
        • NSTimer;
        • performSelector:withObject:afterDelay;
      • observers:
        • 用于监听RunLoop的状态;
        • UI刷新(BeforeWaiting);
        • Autorelease pool;
    2. runloop和线程的关系?

      • 每条线程都有唯一的一个与之对应的RunLoop对象
      • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为Value
      • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
      • RunLoop会在线程结束时销毁
    3. RunLoop休眠的实现原理:

      • 休眠:从用户态切换到内核态:
      • 内核态:等待消息;
        • 没有消息就让线程休眠;
        • 有消息就唤醒线程;
      • 唤醒:从内核态切换到用户态,来处理消息;
    4. RunLoop的几种状态?

    1. Timer与RunLoop的关系?

      • Timer是运行在RunLoop里面的;
    2. RunLoop是怎么响应用户操作的,具体流程是什么样的?

      • sources1捕捉事件;
      • 交给sources0去处理;
    3. Core Foundation中关于RunLoop的5个类:

      • CFRunLoopRef
      • CFRunLoopModeRef
      • CFRunLoopSourceRef
      • CFRunLoopTimerRef
      • CFRunLoopObserverRef
    4. CFRunLoopModeRef:

      • CFRunLoopModeRef代表RunLoop的运行模式。
      • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。
      • RunLoop启动时只能选择其中一个Mode,作为currentMode。
      • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。
        • 不同组的Source0/Source1/Timer/Observer能分割开来,互不影响。
      • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
    5. CFRunLoopModeRef:目前一直的Mode有5种:

      • kCFRunLoopDefaultMode:APP的默认Mode,通常主线程是在这个Mode下运行。
      • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
      • kCFRunLoopCommonModes:这是一个占位用的Mode,不是一个真正的Mode。
      • UIInitializationRunLoopMode:在刚启动APP时进入的第一个Mode,启动完成后就不再使用。
      • GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。
    6. Category的实现原理是什么?

      • Category编译之后的底层结构是struct category_t:里面存储着分类的对象方法、类方法、属性、协议信息。
      • 在程序运行的时候,Runtime会将Category的数据,喝杯冰岛类信息中(类对象、元类对象中)
    7. Category和Class Extension的区别是什么?

      • Class Extension在编译的时候,它的数据就已经包含在类信息中。
      • Category是在运行时,才将数据合并到类信息中。
    8. Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?

      • 有load方法;
      • 在Runtime加载类、分类的时候调用;
      • +load方法可以继承,但是一般不会主动去调用load方法,都是让系统自动调用。
    9. 分类的对象方法、类方法也是分别存放在类对象、元类对象的方法列表。类里面的方法是在编译时就放进去,分类是通过runtime动态将分类的方法合并到类对象、元类对象中。

    10. 分类里面添加属性:

      • 只会生成set、get方法的声明;
      • 不会生成set、get方法的具体实现;
      • 不会生成属性的成员变量;
    11. Category的加载处理过程:

      • 通过Runtime加载某个类的所有Category数据。
      • 把所有Category的方法、属性、协议数据,合并到一个大数组中。
        • 后面参与编译的Category数据,会被放在数组的前面。
      • 将合并后的数据(方法、属性、协议),插入到类原来数据的前面。
    12. +load方法:

      • +load方法会在Runtime加载类、分类时调用。(是通过指针直接找到方法调用的,不是通过消息机制调用)
      • 每个类、分类的+load,在程序运行过程中只调用一次。
      • 调用顺序:
        • 先调用类的+load;
          • 按照编译先后顺序调用(先编译,先调用)
          • 调用子类的+load之前会先调用父类的+load;
        • 再调用分类的+load;
          • 按照编译先后顺序调用(先编译,先调用)(不会先调用父类的分类)。
    13. load、initialize方法的区别是什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

      • 调用方式:
        • +load是直接找到对应的方法地址直接调用;
        • +initialize是通过objc_msgSend调用的;
      • 调用时刻:
        • +load是Runtime加载类、分类的时候调用(只会调用一次)
        • +initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
    14. +initialize方法:

      • +initialize方法会在类第一次接收消息时调用;(如果从来没接收过消息,就不会调用)
      • 调用顺序:
        • 先初始化父类的+initialize,
        • 再初始化子类的+initialize;(可能最终调用的是父类的initialize方法,但不代表又初始化了父类,只是调用了父类的方法,初始化的是子类,因为每个类只会被初始化一次)
      • 只会初始化一次;(如果子类没有实现+initialize,会调用父类的+initialize;(所以父类的+initialize可能会被调用多次))
      • +initialize是通过objc_msgSend进行调用的;所以具备以下特点:
        • 如果分类实现了+initialize,就会覆盖类本身的+initialize调用。
        • 如果子类没有实现+initialize,会调用父类的+initialize;(所以父类的+initialize可能会被调用多次)
    15. Category能否添加成员变量?如果可以,如何给Category添加成员变量?

      • 不可以直接给Category添加成员变量;
      • 但是可以通过添加关联对象,间接实现Category有成员变量的效果;
    16. 如何实现给分类添加关联对象?

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

      • block本质上也是一个OC对象,它内部也有个isa指针;
      • block是封装了函数调用以及函数调用环境的OC对象;
      • block的底层结构如图: 截屏2021-07-25 上午11.59.07.png
    18. block的变量捕获(capture)

      • 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制:
        • 局部变量:auto:能够捕获到block内部。访问方式:值传递。
        • 局部变量:static:能够捕获到block内部。访问方式:指针传递。
        • 全局变量:不能捕获到block内部。访问方式:直接访问;
    19. Block的类型:Block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型:

      • NSGlobalBlock (_NSConcreteGlobalBlock)(数据段:全局变量):只要没有访问auto变量的,都是Global。Global调用Copy后,什么也不必做。
      • NSMallocBlock(_NSConcreteMallocBlock)(堆段:alloc出来的内容,动态分配内存,需要程序员申请内存、管理内存,比如free。现在有ARC): NSStackBlock 调用了Copy后,就是Malloc。Malloc调用Copy后,引用计数加1;
      • NSStackBlock(_NSConcreteStackBlock)(栈段:局部变量,离开作用域自动销毁):访问了auto变量,默认就是Stack。(没有ARC的情况下,因为ARC做了事情) NSStackBlock 调用了Copy,会从栈复制到堆,堆上的就变成了Malloc。
    20. Block的Copy:在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

      • block作为函数返回值时;
      • 将block赋值给__strong指针时;
      • block作为Cocoa API中方法名含有usingBlock的方法参数时;
      • block作为GCD的方法参数时;
    21. 当block内部访问了对象类型的auto变量时:

      • 如果block是在栈上,将不会对auto变量产生强引用(不管是ARC、MRC都不会)
      • 如果block被拷贝到堆上:
        • 会调用block内部的Copy函数;
        • Copy函数内部会调用_Block_object_assign函数
        • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,类似于retain(形成强引用、弱引用)
      • 如果block从堆上移除:
        • 会调用block内部的dispose函数;
        • dispose函数内部会调用_Block_object_dispose函数;
        • _Block_object_dispose函数会自动释放引用的auto变量,类似于release;
    22. 被__block修饰的对象类型:__block MJPersion *person = [[MSPersion alloc] init];

      • 当__block变量在栈上时,不会对指向的对象产生强引用;
      • 当__block变量Copy到堆时:
        • 会调用__block变量内部的Copy函数;
        • Copy函数内部会调用_Block_object_assign函数;
        • _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或弱引用;(注意:这里仅限于ARC时会retain,MRC时不会retain)
      • 如果__block变量从堆上移除:
        • 会调用__block变量内部的dispose函数;
        • dispose函数内部会调用_Block_object_dispose函数;
        • _Block_object_dispose函数会自动释放指向的对象(release);
    23. __block的作用是什么?有什么使用注意点?__block修饰符:

      • __block可以用于解决block内部无法修改auto变量值的问题;
      • __block不能修饰全局变量、静态变量(static);
      • 编译器会将__block变量包装成一个对象;
    24. __block的内存管理:

      • 当block在栈上时,并不会对__block变量产生强引用;
      • 当block被Copy到堆时:
        • 会调用block内部的Copy函数;
        • Copy函数内部会调用_Block_object_assign函数;
        • _Block_object_assign函数会对__block变量形成强引用(retain)(__block变量也会从栈上复制到堆上,是堆上的block对堆上的__block变量形成强引用)
      • 当block从堆中移除时:
        • 会调用block内部的dispose函数;
        • dispose函数内部会调用_Block_object_dispose函数;
        • _Block_object_dispose函数会自动释放引用的__block变量(release);
    25. 解决循环引用问题:

      • ARC环境下:
        • __weak解决:不会产生强引用,指向的对象销毁时,会自动让指针置为nil;
        • __unsafe_unretained解决:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变;
        • __block解决:必须要调用block,并且在block内部必须要把引用的对象置为nil;
      • MRC环境下:不支持__weak;
        • __unsafe_unretained解决;
        • __block解决:不用调用block方法,因为MRC下,__block变量不会对对象进行retain操作;
    26. block的属性修饰词为什么是copy?使用block有哪些使用注意?

      • block一旦没有进行Copy操作,就不会在堆上;
      • 使用注意:循环引用问题;
    27. block在修饰NSMutableArray,需不需要添加__block?

      • 不需要;([array addObject:xxx],这个方法是对array里面的内容进行修改,不是修改array本身,所以不需要。如果是修改array本身,则是需要的,比如在block里面执行:array = nil、array = [NSMutableArray alloc]);
    28. isa详解:

      • 位域:
    29. OC中的方法调用,其实都是转换为objc_msgSend函数的调用。objc_msgSend执行流程:

      • 消息发送;
      • 动态方法解析;
      • 消息转发;
    30. [super message]的底层实现:

      • 只是从父类开始查找方法的实现;
      • 消息接收者仍然是子类对象;
    31. 什么是Runtime?

      • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行;
      • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数;
      • 平时编写的OC代码,底层都是转换成了RuntimeAPI进行调用;
    32. Runtime平时项目中有用过么?

      • 利用关联对象(AssociatedObject)给分类添加属性;
      • 遍历类的所有成员变量(修改TextField的占位文字颜色、字典转模型、自动归档解档);
      • 交换方法实现(交换系统的方法);
      • 利用消息转发机制解决方法找不到的异常;
      • weak的底层实现也是依赖于Runtime;
      • ...
    33. 类簇:NSString、NSArray、NSDictionary,真是类型是其他类型;

    34. RunLoop的应用范畴?

      • 定时器(Timer)、PerformSelector;
      • GCD Async Main Queue;
      • 事件响应、手势识别、界面刷新;
      • 网络请求;
      • AutoreleasePool;
    35. RunLoop在实际开发中的应用:

      • 控制线程生命周期(线程保活,比如AFNetworking);
      • 解决NSTimer在滑动时停止工作的问题;
      • 监控应用卡顿;
      • 性能优化;
    36. iOS中的常见多线程方案:

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

      • 同步:dispatch_sync(dispatch_queue_t queue, dispatch_block_t block):
        • queue:队列;
        • block:任务;
      • 异步:dispatch_async(dispatch_queue_t queue, dispatch_block_t block):
    38. GCD的队列可以分为2大类型:

      • 并发队列(Concurrent Dispatch Queue):
        • 可以让多个任务并发(同时)执行(自动开启多线程同时执行任务);
        • 并发功能只有在异步(dispatch_async)函数下才有效;
      • 串行队列(Serial Dispatch Queue)
        • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务);
      • 主队列:也是一种串行队列;
    39. 容易混淆的术语:

      • 同步和异步主要影响:能不能开启新的线程;

        • 同步:在当前线程中执行任务,不具备开启新线程的能力;
        • 异步:在新的线程中执行任务,具备开启新线程的能力;
      • 并发和串行主要影响:任务的执行方式;

        • 并发:多个任务并发(同时)执行;
        • 串行:一个任务执行完毕后,再执行下一个任务;
    40. 各种队列的执行效果:

      • 同步(sync):
        • 并发队列:
          • 没有开启新线程;
          • 串行执行任务;
        • 串行队列:
          • 没有开启新线程;
          • 串行执行任务;
        • 主队列:
          • 没有开启新线程;
          • 串行执行任务;
      • 异步(async):
        • 并发队列:
          • 会开启新线程;
          • 并发执行任务;
        • 手动创建的串行队列:
          • 会开启新线程;
          • 串行执行任务;
        • 主队列:
          • 没有开启新线程;
          • 串行执行任务;
    41. 使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁);

    42. 多线程的安全隐患:

      • 资源共享:
        • 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源;
        • 比如多个线程访问同一个对象、同一个变量、同一个文件;
      • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题;
    43. 多线程安全隐患的解决方案:

      • 使用线程同步技术(同步,就是协同步调,按预定的先后次序进行运行);
      • 常见的线程同步技术是:加锁;
        • OSSpinLock:自旋锁:等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源;
          • 目前已经不再安全,可能会出现优先级反转的问题;如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
        • os_unfair_lock:从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。
          • 用于取代不安全的OSSpinLock,从iOS10开始才支持:
        • pthread_mutex:互斥锁:等待锁的线程会处于休眠状态;
        • NSLock:是对pthread_mutex普通锁的封装;
        • NSRecursiveLock:是对pthread_mutex递归锁的封装;
        • NSCondition:是对pthread_mutex和cond(唤醒信号条件)的封装;
        • NSConditionLock:是对NSCondition的进一步封装,可以设置具体的唤醒条件值。
        • dispatch_queue:直接使用GCD的串行队列,也是可以实现线程同步的;
        • dispatch_semaphore:信号量:信号量的初始值,可以用来控制线程并发访问的最大数量;
        • @synchronized:是对pthread_mutex递归锁的封装;
        • 递归锁:允许同一个线程对一把锁进行重复加锁;
    44. iOS线程同步方案性能比较:性能从高到底排序:

      • os_unfair_lock
      • OSSpinLock
      • dispatch_semaphore;
      • pthread_mutex;
      • dispatch_queue(DISPATCH_QUEUE_SERIAL);
      • NSLock;
      • NSCondition;
      • pthread_mutex(recursive);
      • NSRecursiveLock;
      • NSConditionLock;
      • @synchronized;
    45. 自旋锁、互斥锁比较:

      • 什么情况使用自旋锁比较划算?
        • 预计线程等待锁的时间很短;
        • 加锁的代码(临界区)经常被调用,但竞争情况很少发生;
        • CPU资源不紧张;
        • 多核处理;
      • 什么情况使用互斥锁比较划算?
        • 预计线程等待锁的时间较长;
        • 单核处理器;
        • 临界区有IO操作;
        • 临界区代码复杂或循环量大;
        • 临界区竞争非常激烈;
    46. atomic:

      • 用于保证属性setter、getter的原子性操作,相当于在setter和getter内部加了线程同步的锁;
      • 它并不能保证使用属性的过程是线程安全的;
    47. iOS中的读写安全方案:

      • 思考如何实现以下场景:
        • 同一时间,只能有1个线程进行写的操作:
        • 同一时间,允许有多个线程进行读的操作:
        • 同一时间,不允许既有写的操作,又有读的操作;
      • 上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:
        • pthread_rwlock:读写锁;
        • dispatch_barrier_async:异步栅栏调用:
          • 这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的;
          • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果;
    48. CADisplayLink、NSTimer使用注意:

      • CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。
      • CADisplayLink:保证调用频率和屏幕的刷帧频率一致,60FPS(每秒60次)。但是会受主线程的影响,所以并不能保证每秒执行60次;
      • NSTimer:
    49. GCD定时器:

      • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时;
      • 而GCD的定时器会更加准时:跟内核挂钩,并且不依赖于RunLoop;
    50. iOS程序的内存布局:

      • 保留区;
      • 代码段;编译之后的代码;
      • 数据段;
        • 字符串常量:比如NSString *str = @"123";
        • 已初始化数据:已初始化的全局变量、静态变量等;
        • 未初始化数据:未初始化的全局变量、静态变量等;
      • 堆;通过alloc、malloc、calloc等动态分配的空间;(分配地址:由低到高)
      • 栈;函数调用开销:比如函数里面的局部变量;(分配地址:由高到低)
      • 内核区;
    51. Tagged Pointer:

      • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储;
      • 在没有使用Tagged Pointer之前,NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;
      • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中;
      • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据;
      • objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销;
      • 如何判断一个指针是否为Tagged Pointer?
        • mac平台:指针的最低有效位是1;
        • iOS平台:指针的最高有效位是1;(第64bit)
    52. OC对象的内存管理:

      • 在iOS中,使用引用计数来管理OC对象的内存;
      • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间;
      • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1;
      • 内存管理的经验总结:
        • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或autorelease来释放它;
        • 想拥有某个对象,就让它的引用计数+1;
        • 不想拥有某个对象,就让它的引用计数-1;
    53. 拷贝的目的:产生一个副本对象,跟源对象互不影响;修改了源对象,不会影响副本对象。修改了副本对象,不会影响源对象。

    54. 深拷贝、浅拷贝:

      • 深拷贝:
        • 内容拷贝,有产生新对象;
      • 浅拷贝:
        • 指针拷贝,没有产生新对象;
    55. copy和mutableCopy:NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary:

      • 不可变对象:copy,还是不可变对象,跟原来指向同一个内存地址,是浅拷贝;mutable,是可变对象,是深拷贝;
      • 可变对象:copy,是不可变,是深拷贝;mutable,是可变对象,是深拷贝;
    56. 引用计数的存储:

      • 在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在sideTable类中;
      • sideTable是一个存放着对象引用计数的散列表;
    57. weak指针的实现原理:

      • 将弱引用存到哈希表里面,当对象要销毁时,就去除该对象对应的弱引用表,把弱引用表里面存储的弱引用都清除掉;
    58. autorelease对象在什么时机会被调用release?

      • iOS在主线程的RunLoop中注册了2个Observer;
        • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
        • 第2个Observer:
          • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
          • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop();
    59. 方法里有局部对象,出了方法后会立即释放吗?

      • 如果局部对象是通过autorelease释放的话,不是立即释放,是在对象所处的RunLoop休眠前释放;
      • 如果ARC是生成release代码的话,是立即释放;
    60. 卡顿产生的原因:CPU、GPU执行了比较耗时的操作:

      • CPU(Central Processing Unit,中央处理器):
        • 对象的创建和销毁;
        • 对象属性的调整;
        • 布局计算;
        • 文本的计算和排版、图片的格式转换和解码;
        • 图像的绘制(Core Graphics);
      • GPU(Graphics Processing Unit,图形处理器):
        • 纹理的渲染;
    61. 在iOS中是双缓冲机制:有前帧缓存、后帧缓存;

    62. 卡顿解决的主要思路:

      • 尽可能减少CPU、GPU资源消耗;
    63. 按照60FPS的刷帧率,每隔16ms就会有一次VSync(垂直同步)信号;

    64. 卡顿优化:

      • CPU:
        • 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView;
        • 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改;
        • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性;
        • Autolayout会比直接设置frame消耗更多的CPU资源;
        • 图片的size最好刚好跟UIImageView的size保持一致;
        • 控制一下线程的最大并发数量;
        • 尽量把耗时的操作放到子线程;
          • 文本处理(尺寸计数、绘制);
          • 图片处理(解码、绘制)
      • GPU:
        • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示;
        • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸;
        • 尽量减少视图数量和层次;
        • 减少透明的视图(alpha < 1),不透明的就设置opaque为YES;
        • 尽量避免出现离屏渲染;
    65. 离屏渲染:

      • 在OpenGL中,GPU有2种渲染方式:
        • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
        • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作;
      • 离屏渲染消耗性能的原因:
        • 需要创建新的缓冲区;
        • 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕;
      • 哪些操作会触发离屏渲染?
        • 光栅化:layer.shouldRasterize = YES;
        • 遮罩:layer.mask;
        • 圆角:同时设置layer.masksToBounds = YES、layer.cornerRadius > 0
          • 优化:可以考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片;
        • 阴影:layer.shadowXXX;
          • 但是如果阴影设置了路线:layer.shadowPath就不会产生离屏渲染;
    66. 卡顿检测:

      • 平时所说的卡顿,主要是因为在主线程执行了比较耗时的操作;
      • 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的;
      • 有封装好的可以参考:LXDAppFluecyMonitor

    知识点

    1. 花指令:破解、反汇编;

    相关文章

      网友评论

          本文标题:面试题

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