1.SEL/IMP/Method的关系
SEL:是选择器,也就是方法的名字 IMP:指向方法实现的首地址的指针 Method: 是一个结构体,包含一个SEL方法名,一个IMP指向首地址,一个char*(表示函数的类型,包括了返回值,参数类型)
可以这么理解:一个类class持有一系列的方法Method,在load类的时候,runtime会将所有方法的选择器SEL ha sh后映射到一个集合(NSSet中的元素不能重复)中,当需要发信息时,会根据选择器SEL去查找方法,找到之后,用Method结构体里的函数指针(IMP)去调用,这样在运行时查找selector的速度就会非常快。
2._objc_msgForward函数是做什么的,直接调用他会发生什么
_objc_msgForward是IMP类型,用于消息转发:当向一个对象发送一条消息,但它并没有实现时,_objc_msgForward会尝试做消息转发 创建一个_objc_msgForward IMP imp = _objc_msgForward;
在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP
_objc_msgForward消息转发做的几件事:
- 调用
resolveInstanceMethod:方法 (或resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。 - 调用
forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。(讨论见: 《forwardingTargetForSelector返回self不会死循环吧。 #64》 - 调用
methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSInvocation 并传给forwardInvocation:。 - 调用
forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。(讨论见: 《_objc_msgForward问题 #106》 - 调用
doesNotRecognizeSelector:,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
一点调用_objc_msgForward,将跳过查找IMP过程,直接出发“消息转发”(即时实现了这个方法)
什么是NSProxy?
NSProxy是一个进行消息重定向封装的抽象类,类似一个中间代理;通过继承该基类,实现两个消息转发方法,来进行消息转发给真实对象;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
使用:
1》.实现多继承 2》.解决了NSTimer&CADisplayLink创建时对self强引用问题
3.runtime如何实现weak变量的自动置nil?
runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil
伪代码 id objc1; objc_storeWeak(&objc1, objc); objc_destoryWeak(&objc1)
4.能否为编译后得到的类中增加实例变量?能否向运行时创建的类中增加实例变量?为什么?
不能向编译后得到的类中添加实例变量。:因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime会调用class_setIvarLayout或class_setWeakIvarLayout来处理stong weak引用,所以不能向存在的类中添加实例变量;
能向运行时创建的类中添加实例变量:调用class_addIvar函数,但是得在objc_allocateClassPair(分配空间)之后,objc_registerClassPair(注册这个类,使类可以使用)之前。
5.runloop的mode的作用是什么?
runloop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动中DefaultMode切换到TrackingMode来保障ScrollView的流畅。
6.isKindOfClass/isMemberOfClass
+isKindOfClass: 从左边类对象的元类开始,按照父继承链,能否找到右边参数类 -isKindOfClass: 从左边对象开始,按照父继承链,能否找到右边参数类 +isMemberOfClass:左边类对象的元类是否等于右边类 -isMemberOfclass: 左边对象所属类是否等于右边类??
所有类的元类最终都是NSObject的元类。NSObject的元类的父类是NSObject, NSObject的父类是nil
7.Tagged Pointer是什么?
为了节省内存和提交执行效率,提出了tagged pointer技术,用于优化NSNumber/NSData/NSString等小对象的存储;
引入之前:为了使用NSNumber,就需要8个字节的指针内存(栈),32个字节的对象内存(堆)。因为继承于NSObject,所以它有isa指针。 需要在堆上为其分配内存,还要维护它的引用计数,管理它的生命周期,影响效率。
引用之后,NSNumber对象的值直接存储在指针上。 指针中存储的数据变成了 Tag+Data形式。这样当使用一个NSNumber对象只需要8个字节指针内存,当指针的8个字节不够用时,才会将对象存储在堆上。
objc_msgsend能识别出tagged pointer,比如NSNumber的intValue方法,直接从指针提取数据,不会进行objc_msgsend的三大流程。
NSString为NSTaggedPointerString类型,在objc_release函数中会判断是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免过度释放对象导致crash,因为根本就没执行release操作。
- 定时器类型
1> CADisplayLink: 它可以让你在每秒钟屏幕更新时执行一段代码,精度非常高,因为它是根屏幕刷新频率同步,所以可以确保动画的执行丝滑。其调用方法是通过RunLoop执行的,所以是线城安全的。(屏幕的刷新频率是固定的,CADisplayLink正常情况下,在每一次刷新结束都会被调用)
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
注意循环引用(weakProxy解决)
2> NSTimer: 它可以让你在一段时间后执行一段代码,NSTimer定时器的精度相对较低,因为它不是和屏幕刷新频率同步的,所以在一些对精度要求比较高的场景下可能不适用。另外NSTimer定时器的调用方法是通过RunLoop进行的,所以它也是线程安全的。(- NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围)
NSTimer在使用时也会存在循环引用问题,同CADisplayLink
总结:CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
3> ### GCD定时器
相比于传统的NSTimer和CADisplayLink,GCD定时器具有更高的精度和更好的性能,尤其是在多线程场景下表现更为优秀。
GCD定时器的实现原理是使用GCD的dispatch_source_t来创建一个定时器源,然后将该定时器源与需要执行的任务关联起来。通过GCD的API可以设置定时器的触发时间、重复次数等参数,并且可以很方便地在多线程环境下使用。
需要注意的是,在使用GCD定时器时,我们需要确保在合适的时间停止定时器,并释放相关资源。
DispatchSourceTimer精度很高,因为是系统级别,且是不受RunLoop影响。GCD定时器相比其他两个定时器是最准时的,因为和系统内核直接挂钩
总结:CADisplayLink主要用于渲染动画,NSTimer用于周期性执行任务,而GCD定时器则更加灵活,可以在不同线程中执行任务。在使用CADisplayLink时,要注意循环引用的问题;在使用NSTimer时,要注意循环引用和线程阻塞的问题;在使用GCD定时器时,要注意定时器的生命周期和线程安全的问题。
9.子线程如何做内存管理:
子线程默认有autoreleasepool,不需要手动创建,子线程默认不开启runloop, 所以跟主线程的销毁方式不同。
监听变量的赋值过程查看堆栈, 可以看到[NSThread exit] -> [pthread exit] -> [_pthread_tsd_cleanup] -> [tls dealloc] -> objc_autoreleasePoolpop -> 对象的dealloc
子线程中pool是在线程销毁时从TLS中销毁的, TLS是Thread Local Storage
如果线程需要保活, 就可能导致无法释放, 后果可想而知...
小建议: 子线程中默认有pool, 但是不管是主线程或子线程, 有大量临时变量(可能延迟释放)的情况下最好是手写个pool, 这样在pool的作用域结束时就会pop, 避免内存暴涨或内存泄漏
不要在viewDidLoad中创建过多的延迟释放对象, 你会发现viewDidAppear会延迟调用