OC语法篇
面向对象
1. 一个NSObject对象占用多少内存?
系统分配了16个字节给NSobject对象(通过malloc_size函数获得),但NSobject对象内部只是用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
2. 对象的isa指针指向哪里?
instance对象的isa指针指向class对象;class对象的isa指针指向meta-class对象;meta-class对象的isa指针指向基类的meta-class对象
3. OC的类信息存放在哪里?
对象方法、属性、成员变量、协议信息,存放在class对象中;类方法存放在meta-class对象中;成员变量的具体值,存放在instance对象中。
OC对象分为三种对象
instance对象:实例对象(包含isa指针、成员变量)
class对象:类对象(包含isa指针、superclass、属性、对象方法、协议、成员变量···)
meta-class对象:元类对象(包含isa指针、superclass、类方法)
4. isa、superclass总结
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基类的meta-class
class的superclass指向父类的class
如果没有父类,superclass指针为nil
meta-class的superclass指向父类的meta-class
基类的meta-class的superclass指向基类的class
instance调用对象方法的轨迹
isa找到class,方法不存在,就通过superclass找父类
class调用类方法的轨迹
isa找meta-class,方法不存在,就通过superclass找父类
KVO and KVC
1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
2. 如何手动触发KVO?
手动调用willChangeValueForKey:和didChangeValueForKey:
3. 通过KVC修改属性会触发KVO么?
会触发KVO
4. KVC的赋值和取值过程是怎样的?原理是什么?
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
常见的API有
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
赋值过程:
按照setKey:、_setKey顺序查找方法,找到了方法传递参数,调用方法;若没找到,查看accessInstanceVariablesDirectly方法的返回值,若为NO则调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException,若返回YES则按照_key、_isKey、key、isKey顺序查找成员变量,找到直接赋值,找不到则调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException
取值过程
按照getKey:、key、_isKey、key顺序查找方法,找到了方法,调用方法;若没找到,查看accessInstanceVariablesDirectly方法的返回值,若为NO则调用valueForUndefinedKey:并抛出异常NSUnknownKeyException,若返回YES则按照_key、_isKey、key、isKey顺序查找成员变量,找到直接取值,找不到则调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
Category
1. Category的使用场合是什么?
2. Category的实现原理
Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
3. Category和Class Extension的区别是什么?
Class Extension在编译的时候,它的数据就已经包含在类信息中
Category是在运行时,才会将数据合并到类信息中
4. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
有load方法
load方法在runtime加载类、分类的时候调用
load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
5. load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?
+load方法会在runtime加载类、分类时调用
每个类、分类的+load,在程序运行过程中只调用一次
调用顺序:先调用类的+load,按照编译先后顺序调用(先编译,先调用),调用子类的+load之前会先调用父类的+load;再调用分类的+load,按照编译先后顺序调用(先编译,先调用)
+initialize方法会在类第一次接收到消息时调用,调用顺序:先调用父类的+initialize,再调用子类的+initialize,(先初始化父类,再初始化子类,每个类只会初始化1次)
+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次);如果分类实现了+initialize,就覆盖类本身的+initialize调用
6. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
添加关联对象:void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
获得关联对象:id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象:void objc_removeAssociatedObjects(id object)
block
1. block的原理是怎样的?本质是什么?
block本质上也是一个OC对象,它内部也有个isa指针,封装了函数调用以及调用环境的OC对象
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
__NSGlobalBlock__(_NSConcreteGlobalBlock):存储在数据区,没有访问auto变量
__NSStackBlock__(_NSConcreteStackBlock):存储在栈区,访问auto变量
__NSMallocBlock__(_NSConcreteMallocBlock):存储在堆区,__NSStackBlock__调用了copy
2. __block的作用是什么?有什么使用注意点?
__block可以用于解决block内部无法修改auto变量值的问题
__block不能修饰全局变量、静态变量(static)
编译器会将__block变量包装成一个对象
3. block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上
使用注意:循环引用问题
__weak typeof(self)weakSelf = self;
__unsafe _unretained id weakSelf = self;
__block id weakSelf = self;
Runtime
1. 讲一下 OC 的消息机制
OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
objc_msgSend底层有3大阶段
消息发送(当前类、父类中查找)、动态方法解析、消息转发
2. 什么是Runtime?平时项目中有用过么?
OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
平时编写的OC代码,底层都是转换成了Runtime API进行调用
具体应用
利用关联对象(AssociatedObject)给分类添加属性;遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档); 交换方法实现(交换系统的方法);利用消息转发机制解决方法找不到的异常问题
RunLoop
1. 讲讲 RunLoop,项目中有用到吗?
运行循环,在程序运行过程中循环做一些事情
应用范畴:定时器(Timer)、PerformSelector;GCD Async Main Queue;事件响应、手势识别、界面刷新;网络请求;AutoreleasePool
RunLoop的基本作用:保持程序的持续运行;处理App中的各种事件(比如触摸事件、定时器事件等);节省CPU资源,提高程序性能:该做事时做事,该休息时休息
2. runloop内部实现逻辑?
3. runloop和线程的关系?
每条线程都有唯一的一个与之对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
RunLoop会在线程结束时销毁
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
4. timer 与 runloop 的关系?
5. 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
6. runloop 是怎么响应用户操作的, 具体流程是什么样的?
7. 说说runLoop的几种状态
eg:添加Observer监听RunLoop的所有状态
CFRunLoopObserverRef observe = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, yearMask, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");//即将进入Loop
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");//即将处理Timers
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");//即将处理Sources
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");//即将进入休眠
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");//刚从休眠中唤醒
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");//即将退出Loop
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observe, kCFRunLoopCommonModes);
CFRelease(observe);
8. runloop的mode作用是什么?
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会立马退出
常见的2种Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
多线程
1. iOS中的线程同步方案
性能从高到低排序
OSSpinLock
OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
需要导入头文件#import <libkern/OSAtomic.h>
//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
bool result = OSSpinLockTry(&lock);
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnLock(&lock);
os_unfair_lock
os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
需要导入头文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
pthread_mutex
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
需要导入头文件#import <pthread.h>
dispatch_semaphore:
semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
//信号量的初始值
int value = 1;
//初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
//如果信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
//如果信号量的值>0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//让信号量的值加1
dispatch_semaphore_signal(semaphore);
dispatch_queue(DISPATCH_QUEUE_SERIAL)
直接使用GCD的串行队列,也是可以实现线程同步的
dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//任务
});
NSLock:对mutex普通锁的封装
NSRecursiveLock
NSCondition:对mutex和cond的封装
NSConditionLock:对NSCondition的进一步封装,可以设置具体的条件值
@synchronized:是对mutex递归锁的封装
什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少发生
CPU资源不紧张
多核处理器
什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长
单核处理器
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈
内存管理
1. 使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
解决方案
使用block 弱引用weakSelf
使用代理对象(NSProxy)
NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时,而GCD的定时器会更加准时
//创建一个定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
//设置时间(start是几秒后开始执行,interval是时间间隔)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (int64_t)(start * NSEC_PER_SEC)), (uint64_t)(interval ) * NSEC_PER_SEC);
//设置回调
dispatch_source_set_event_handler(timer, ^{
});
//启动定时器
dispatch_resume(timer);
2. 介绍下内存的几大区域
代码段:编译之后的代码
数据段
字符串常量:比如NSString *str = @"123"
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等
栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
3. 讲一下你对 iOS 内存管理的理解
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
性能优化
1. 列表卡顿的原因可能有哪些?你平时是怎么优化的?
CPU
- 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
- 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
- Autolayout会比直接设置frame消耗更多的CPU资源
- 图片的size最好刚好跟UIImageView的size保持一致
- 控制一下线程的最大并发数量
- 尽量把耗时的操作放到子线程: 文本处理(尺寸计算、绘制);图片处理(解码、绘制)
GPU
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
- 尽量减少视图数量和层次
- 减少透明的视图(alpha<1),不透明的就设置opaque为YES
- 尽量避免出现离屏渲染
2. 什么是离屏渲染?
在OpenGL中,GPU有2种渲染方式
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
3. 离屏渲染消耗性能的原因
需要创建新的缓冲区
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
4. 哪些操作会触发离屏渲染?
- 光栅化,layer.shouldRasterize = YES
- 遮罩,layer.mask
- 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0(考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片)
- 阴影,layer.shadowXXX(如果设置了layer.shadowPath就不会产生离屏渲染)
5. 怎么检测卡顿?
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
暂时先整理这些,如果有错误,感谢各位大佬指正。