面试题
面试题
-
一个NSObject对象占用多少内存?
- 实际上分配了16个字节的存储空间给NSObject对象
- 真正有使用的空间是:一个指针变量所占用的大小(64位:8个字节,32位:4个字节)
- 结构体:继承遵循内存对齐原则:结构体的最终大小必须是最大成员大小的倍数。如果父类内存对齐后有多余的字节,子类继承后声明的变量可以放到父类多余的字节当中。并且OC底层定义小于16个字节的,都给分配16个字节;InstanceSize:最小对齐单位为isa指针的大小8。malloc_size:最小对齐单位为OC定义的最小字节大小16。
-
对象的isa指针指向哪里?
- instance的isa指向class;
- 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。
- class的isa指向meta-class;
- 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。
- meta-class的isa指向基类的meta-class;
- 基类的meta-class的isa指向自己;
- instance的isa指向class;
-
对象的superclass指针指向哪里?
- class的superclass指向父类的class;
- 如果没有父类,superclass指针为nil。
- meta-class的superclass指向父类的meta-class;
- 基类的meta-class的superclass指向基类的class;
- class的superclass指向父类的class;
-
OC的类信息存放在哪里?
- ;
-
Objective-C中的对象,简称OC对象,主要可以分为3种:
- instance对象(实例对象):通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。
- class对象(类对象):
- objectClass1 ~ objectClass5 都是NSObject的class对象(类对象)。
- 它们是同一个对象,每个类在内存中有且只有一个class对象。
- meta-class对象(元类对象):
- 每个类在内存中有且只有一个meta-class对象。
- meta-class对象和class对象的内存结构是一样的,但用途不一样。
-
instance对象在内存中存储的信息包括:
- isa指针;
- 其他成员变量;
-
class对象在内存中存储的信息包括:
- isa指针;
- superclass指针;
- 类的属性信息(@property)
- 类的对象方法信息(instance method)
- 类的协议信息(protocol)
- 类的成员变量信息(ivar)
- ......
-
meta-class对象在内存中存储的信息包括:
- isa指针
- superclas指针
- 类的类方法信息(class method)
- ......(其他类似class的信息,是空的)
-
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用Runtime的API动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数:
- willChangeValueForKey
- 父类原来的setter实现
- didChangeValueForKey,这个方法内部又会调用监听器(observer)的监听方法
- 内部又会调用监听器(observer)的监听方法:observeValueForKeyPath:ofObject:change:context:
-
_NSSetXXXValueAndNotify的内部实现:
- 调用willChangeValueForKey
- 调用原来的setter实现
- 调用didChangeValueForKey
- didChangeValueForKey内部会调用observer的observeValueForKeyPath:ofObject:change:context方法
-
如何手动触发KVO?
- 手动调用willChangeValueForKey和didChangeValueForKey;
-
通过KVC修改属性会触发KVO吗?
- 会触发KVO(相当于setValue:forKey:内部手动调用了KVO的_NSSetXXXValueAndNotify方法)
-
KVC:setValue:forkey:的原理:
- 按照setKey、_setKey顺序查找方法:
- 如果找到了传递参数,调用方法。
- 如果找不到,查看accessInstanceVariableDirectory方法的返回值:
- 返回NO:调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException
- 返回YES:按照_key、_isKey、key、isKey顺序查找成员变量,查找到直接赋值,查找不到抛出同上NO的异常。
- 按照setKey、_setKey顺序查找方法:
-
KVC:valueForKey:的原理:
- 按照getKey、key、isKey、_key顺序查找方法:
- 如果找到了,调用方法。
- 如果找不到,查看accessInstanceVariableDirectory方法的返回值:
- 返回NO:调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
- 返回YES:按照_key、_isKey、key、isKey顺序查找成员变量,查找到直接取值,查找不到抛出同上NO的异常。
- 按照getKey、key、isKey、_key顺序查找方法:
-
KVC的赋值和取值过程是怎样的?原理是什么?
- 赋值过程即上面的setValue:forKey:的原理;
- 取值过程即上面的valueForKey:的原理;
-
什么是Runloop?
- 运行循环
- 在程序运行过程中循环做一些事情
-
runloop的基本作用:
- 保持程序的持续运行
- 处理APP中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
-
runloop内部实现逻辑?
- sources0:
- 触摸事件处理;
- performSelector:onThread:
- sources1:
- 基于port的线程间通信;
- 系统事件捕捉(比如点击事件是sources1捕捉,然后分发给sources0去处理);
- timers:
- NSTimer;
- performSelector:withObject:afterDelay;
- observers:
- 用于监听RunLoop的状态;
- UI刷新(BeforeWaiting);
- Autorelease pool;
- sources0:
-
runloop和线程的关系?
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为Value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
-
RunLoop休眠的实现原理:
- 休眠:从用户态切换到内核态:
- 内核态:等待消息;
- 没有消息就让线程休眠;
- 有消息就唤醒线程;
- 唤醒:从内核态切换到用户态,来处理消息;
-
RunLoop的几种状态?
- ;
-
Timer与RunLoop的关系?
- Timer是运行在RunLoop里面的;
-
RunLoop是怎么响应用户操作的,具体流程是什么样的?
- sources1捕捉事件;
- 交给sources0去处理;
-
Core Foundation中关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
-
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会立马退出。
-
CFRunLoopModeRef:目前一直的Mode有5种:
- kCFRunLoopDefaultMode:APP的默认Mode,通常主线程是在这个Mode下运行。
- UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
- kCFRunLoopCommonModes:这是一个占位用的Mode,不是一个真正的Mode。
- UIInitializationRunLoopMode:在刚启动APP时进入的第一个Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。
-
Category的实现原理是什么?
- Category编译之后的底层结构是struct category_t:里面存储着分类的对象方法、类方法、属性、协议信息。
- 在程序运行的时候,Runtime会将Category的数据,喝杯冰岛类信息中(类对象、元类对象中)
-
Category和Class Extension的区别是什么?
- Class Extension在编译的时候,它的数据就已经包含在类信息中。
- Category是在运行时,才将数据合并到类信息中。
-
Category中有load方法吗?load方法是什么时候调用的?load方法能继承吗?
- 有load方法;
- 在Runtime加载类、分类的时候调用;
- +load方法可以继承,但是一般不会主动去调用load方法,都是让系统自动调用。
-
分类的对象方法、类方法也是分别存放在类对象、元类对象的方法列表。类里面的方法是在编译时就放进去,分类是通过runtime动态将分类的方法合并到类对象、元类对象中。
-
分类里面添加属性:
- 只会生成set、get方法的声明;
- 不会生成set、get方法的具体实现;
- 不会生成属性的成员变量;
-
Category的加载处理过程:
- 通过Runtime加载某个类的所有Category数据。
- 把所有Category的方法、属性、协议数据,合并到一个大数组中。
- 后面参与编译的Category数据,会被放在数组的前面。
- 将合并后的数据(方法、属性、协议),插入到类原来数据的前面。
-
+load方法:
- +load方法会在Runtime加载类、分类时调用。(是通过指针直接找到方法调用的,不是通过消息机制调用)
- 每个类、分类的+load,在程序运行过程中只调用一次。
- 调用顺序:
- 先调用类的+load;
- 按照编译先后顺序调用(先编译,先调用)
- 调用子类的+load之前会先调用父类的+load;
- 再调用分类的+load;
- 按照编译先后顺序调用(先编译,先调用)(不会先调用父类的分类)。
- 先调用类的+load;
-
load、initialize方法的区别是什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
- 调用方式:
- +load是直接找到对应的方法地址直接调用;
- +initialize是通过objc_msgSend调用的;
- 调用时刻:
- +load是Runtime加载类、分类的时候调用(只会调用一次)
- +initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
- 调用方式:
-
+initialize方法:
- +initialize方法会在类第一次接收消息时调用;(如果从来没接收过消息,就不会调用)
- 调用顺序:
- 先初始化父类的+initialize,
- 再初始化子类的+initialize;(可能最终调用的是父类的initialize方法,但不代表又初始化了父类,只是调用了父类的方法,初始化的是子类,因为每个类只会被初始化一次)
- 只会初始化一次;(如果子类没有实现+initialize,会调用父类的+initialize;(所以父类的+initialize可能会被调用多次))
- +initialize是通过objc_msgSend进行调用的;所以具备以下特点:
- 如果分类实现了+initialize,就会覆盖类本身的+initialize调用。
- 如果子类没有实现+initialize,会调用父类的+initialize;(所以父类的+initialize可能会被调用多次)
-
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不可以直接给Category添加成员变量;
- 但是可以通过添加关联对象,间接实现Category有成员变量的效果;
-
如何实现给分类添加关联对象?
- ;
-
block的原理是怎样的?本质是什么?
- block本质上也是一个OC对象,它内部也有个isa指针;
- block是封装了函数调用以及函数调用环境的OC对象;
- block的底层结构如图: 截屏2021-07-25 上午11.59.07.png
-
block的变量捕获(capture)
- 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制:
- 局部变量:auto:能够捕获到block内部。访问方式:值传递。
- 局部变量:static:能够捕获到block内部。访问方式:指针传递。
- 全局变量:不能捕获到block内部。访问方式:直接访问;
- 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制:
-
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。
-
Block的Copy:在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时;
- 将block赋值给__strong指针时;
- block作为Cocoa API中方法名含有usingBlock的方法参数时;
- block作为GCD的方法参数时;
-
当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;
-
被__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);
-
__block的作用是什么?有什么使用注意点?__block修饰符:
- __block可以用于解决block内部无法修改auto变量值的问题;
- __block不能修饰全局变量、静态变量(static);
- 编译器会将__block变量包装成一个对象;
-
__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);
-
解决循环引用问题:
- ARC环境下:
- __weak解决:不会产生强引用,指向的对象销毁时,会自动让指针置为nil;
- __unsafe_unretained解决:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变;
- __block解决:必须要调用block,并且在block内部必须要把引用的对象置为nil;
- MRC环境下:不支持__weak;
- __unsafe_unretained解决;
- __block解决:不用调用block方法,因为MRC下,__block变量不会对对象进行retain操作;
- ARC环境下:
-
block的属性修饰词为什么是copy?使用block有哪些使用注意?
- block一旦没有进行Copy操作,就不会在堆上;
- 使用注意:循环引用问题;
-
block在修饰NSMutableArray,需不需要添加__block?
- 不需要;([array addObject:xxx],这个方法是对array里面的内容进行修改,不是修改array本身,所以不需要。如果是修改array本身,则是需要的,比如在block里面执行:array = nil、array = [NSMutableArray alloc]);
-
isa详解:
- 位域:
-
OC中的方法调用,其实都是转换为objc_msgSend函数的调用。objc_msgSend执行流程:
- 消息发送;
- 动态方法解析;
- 消息转发;
-
[super message]的底层实现:
- 只是从父类开始查找方法的实现;
- 消息接收者仍然是子类对象;
-
什么是Runtime?
- OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行;
- OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数;
- 平时编写的OC代码,底层都是转换成了RuntimeAPI进行调用;
-
Runtime平时项目中有用过么?
- 利用关联对象(AssociatedObject)给分类添加属性;
- 遍历类的所有成员变量(修改TextField的占位文字颜色、字典转模型、自动归档解档);
- 交换方法实现(交换系统的方法);
- 利用消息转发机制解决方法找不到的异常;
- weak的底层实现也是依赖于Runtime;
- ...
-
类簇:NSString、NSArray、NSDictionary,真是类型是其他类型;
-
RunLoop的应用范畴?
- 定时器(Timer)、PerformSelector;
- GCD Async Main Queue;
- 事件响应、手势识别、界面刷新;
- 网络请求;
- AutoreleasePool;
-
RunLoop在实际开发中的应用:
- 控制线程生命周期(线程保活,比如AFNetworking);
- 解决NSTimer在滑动时停止工作的问题;
- 监控应用卡顿;
- 性能优化;
-
iOS中的常见多线程方案:
- pthread:
- C语言;
- 线程生命周期:程序员管理;
- 简介:
- 一套通用的多线程API;
- 适用于Unix、Linux、Windows等系统;
- 跨平台、可移植;
- 使用难度大;
- 使用频率:几乎不用;
- NSThread;
- OC语言;
- 线程生命周期:程序员管理;
- 简介:
- 使用更加面向对象;
- 简单易用,可直接操作线程对象;
- 其实底层是pthread;
- 使用频率:几乎不用;
- GCD:
- C语言;
- 线程生命周期:自动管理;
- 简介:
- 旨在替代NSThread等线程技术;
- 充分利用设备的多核;
- 其实底层是pthread;
- 使用频率:经常使用;
- NSOperation:
- OC语言;
- 线程生命周期:自动管理;
- 简介:
- 基于GCD(底层是GCD);
- 比GCD多了一些更简单实用的功能;
- 使用更加面向对象;
- 其实底层是pthread;
- 使用频率:经常使用;
- pthread:
-
GCD的常用函数的执行方式:
- 同步:dispatch_sync(dispatch_queue_t queue, dispatch_block_t block):
- queue:队列;
- block:任务;
- 异步:dispatch_async(dispatch_queue_t queue, dispatch_block_t block):
- 同步:dispatch_sync(dispatch_queue_t queue, dispatch_block_t block):
-
GCD的队列可以分为2大类型:
- 并发队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行(自动开启多线程同时执行任务);
- 并发功能只有在异步(dispatch_async)函数下才有效;
- 串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务);
- 主队列:也是一种串行队列;
- 并发队列(Concurrent Dispatch Queue):
-
容易混淆的术语:
-
同步和异步主要影响:能不能开启新的线程;
- 同步:在当前线程中执行任务,不具备开启新线程的能力;
- 异步:在新的线程中执行任务,具备开启新线程的能力;
-
并发和串行主要影响:任务的执行方式;
- 并发:多个任务并发(同时)执行;
- 串行:一个任务执行完毕后,再执行下一个任务;
-
-
各种队列的执行效果:
- 同步(sync):
- 并发队列:
- 没有开启新线程;
- 串行执行任务;
- 串行队列:
- 没有开启新线程;
- 串行执行任务;
- 主队列:
- 没有开启新线程;
- 串行执行任务;
- 并发队列:
- 异步(async):
- 并发队列:
- 会开启新线程;
- 并发执行任务;
- 手动创建的串行队列:
- 会开启新线程;
- 串行执行任务;
- 主队列:
- 没有开启新线程;
- 串行执行任务;
- 并发队列:
- 同步(sync):
-
使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁);
-
多线程的安全隐患:
- 资源共享:
- 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源;
- 比如多个线程访问同一个对象、同一个变量、同一个文件;
- 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题;
- 资源共享:
-
多线程安全隐患的解决方案:
- 使用线程同步技术(同步,就是协同步调,按预定的先后次序进行运行);
- 常见的线程同步技术是:加锁;
- 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递归锁的封装;
- 递归锁:允许同一个线程对一把锁进行重复加锁;
- OSSpinLock:自旋锁:等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源;
-
iOS线程同步方案性能比较:性能从高到底排序:
-
os_unfair_lock
; - OSSpinLock
- dispatch_semaphore;
- pthread_mutex;
- dispatch_queue(DISPATCH_QUEUE_SERIAL);
- NSLock;
- NSCondition;
- pthread_mutex(recursive);
- NSRecursiveLock;
- NSConditionLock;
- @synchronized;
-
-
自旋锁、互斥锁比较:
- 什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短;
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生;
- CPU资源不紧张;
- 多核处理;
- 什么情况使用互斥锁比较划算?
- 预计线程等待锁的时间较长;
- 单核处理器;
- 临界区有IO操作;
- 临界区代码复杂或循环量大;
- 临界区竞争非常激烈;
- 什么情况使用自旋锁比较划算?
-
atomic:
- 用于保证属性setter、getter的原子性操作,相当于在setter和getter内部加了线程同步的锁;
- 它并不能保证使用属性的过程是线程安全的;
-
iOS中的读写安全方案:
- 思考如何实现以下场景:
- 同一时间,只能有1个线程进行写的操作:
- 同一时间,允许有多个线程进行读的操作:
- 同一时间,不允许既有写的操作,又有读的操作;
- 上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有:
- pthread_rwlock:读写锁;
- dispatch_barrier_async:异步栅栏调用:
- 这个函数传入的并发队列必须是自己通过dispatch_queue_create创建的;
- 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果;
- 思考如何实现以下场景:
-
CADisplayLink、NSTimer使用注意:
- CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。
- CADisplayLink:保证调用频率和屏幕的刷帧频率一致,60FPS(每秒60次)。但是会受主线程的影响,所以并不能保证每秒执行60次;
- NSTimer:
-
GCD定时器:
- NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时;
- 而GCD的定时器会更加准时:跟内核挂钩,并且不依赖于RunLoop;
-
iOS程序的内存布局:
- 保留区;
- 代码段;编译之后的代码;
- 数据段;
- 字符串常量:比如
NSString *str = @"123";
- 已初始化数据:已初始化的全局变量、静态变量等;
- 未初始化数据:未初始化的全局变量、静态变量等;
- 字符串常量:比如
- 堆;通过alloc、malloc、calloc等动态分配的空间;(分配地址:由低到高)
- 栈;函数调用开销:比如函数里面的局部变量;(分配地址:由高到低)
- 内核区;
-
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)
-
OC对象的内存管理:
- 在iOS中,使用引用计数来管理OC对象的内存;
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间;
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1;
- 内存管理的经验总结:
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或autorelease来释放它;
- 想拥有某个对象,就让它的引用计数+1;
- 不想拥有某个对象,就让它的引用计数-1;
-
拷贝的目的:产生一个副本对象,跟源对象互不影响;修改了源对象,不会影响副本对象。修改了副本对象,不会影响源对象。
-
深拷贝、浅拷贝:
- 深拷贝:
- 内容拷贝,有产生新对象;
- 浅拷贝:
- 指针拷贝,没有产生新对象;
- 深拷贝:
-
copy和mutableCopy:NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary:
- 不可变对象:copy,还是不可变对象,跟原来指向同一个内存地址,是浅拷贝;mutable,是可变对象,是深拷贝;
- 可变对象:copy,是不可变,是深拷贝;mutable,是可变对象,是深拷贝;
-
引用计数的存储:
- 在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在sideTable类中;
- sideTable是一个存放着对象引用计数的散列表;
-
weak指针的实现原理:
- 将弱引用存到哈希表里面,当对象要销毁时,就去除该对象对应的弱引用表,把弱引用表里面存储的弱引用都清除掉;
-
autorelease对象在什么时机会被调用release?
- iOS在主线程的RunLoop中注册了2个Observer;
- 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
- 第2个Observer:
- 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
- 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop();
- iOS在主线程的RunLoop中注册了2个Observer;
-
方法里有局部对象,出了方法后会立即释放吗?
- 如果局部对象是通过autorelease释放的话,不是立即释放,是在对象所处的RunLoop休眠前释放;
- 如果ARC是生成release代码的话,是立即释放;
-
卡顿产生的原因:CPU、GPU执行了比较耗时的操作:
- CPU(Central Processing Unit,中央处理器):
- 对象的创建和销毁;
- 对象属性的调整;
- 布局计算;
- 文本的计算和排版、图片的格式转换和解码;
- 图像的绘制(Core Graphics);
- GPU(Graphics Processing Unit,图形处理器):
- 纹理的渲染;
- CPU(Central Processing Unit,中央处理器):
-
在iOS中是双缓冲机制:有前帧缓存、后帧缓存;
-
卡顿解决的主要思路:
- 尽可能减少CPU、GPU资源消耗;
-
按照60FPS的刷帧率,每隔16ms就会有一次VSync(垂直同步)信号;
-
卡顿优化:
- CPU:
- 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView;
- 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改;
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性;
- Autolayout会比直接设置frame消耗更多的CPU资源;
- 图片的size最好刚好跟UIImageView的size保持一致;
- 控制一下线程的最大并发数量;
- 尽量把耗时的操作放到子线程;
- 文本处理(尺寸计数、绘制);
- 图片处理(解码、绘制)
- GPU:
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示;
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸;
- 尽量减少视图数量和层次;
- 减少透明的视图(alpha < 1),不透明的就设置opaque为YES;
- 尽量避免出现离屏渲染;
- CPU:
-
离屏渲染:
- 在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就不会产生离屏渲染;
- 在OpenGL中,GPU有2种渲染方式:
-
卡顿检测:
- 平时所说的卡顿,主要是因为在主线程执行了比较耗时的操作;
- 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的;
- 有封装好的可以参考:
LXDAppFluecyMonitor
;
知识点
- 花指令:破解、反汇编;