面试题

面试题

  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. 花指令:破解、反汇编;

你可能感兴趣的:(面试题)