这不是面试题,这是oc的全部。。。
1.为什么说OC是一门动态语言?
oc的动态主要体现在三方面
动态类型
对象的类型在运行时确定。比如id。
动态绑定
基于动态类型,即常说的发消息,只有类型确定了,才会确定给谁发消息
资源动态加载
Assets中@2x,@3x只会加载一套,因设备而定
2.讲一下MVC、MVVM、MVP?
MVC(iOS)
M应该做的事:
给ViewController提供数据
给ViewController存储数据提供接口
C应该做的事:
管理View的生命周期
监听来自View与业务有关的事件,协调View与Model的合作,来完成对应事件的业务。
V应该做的事:
响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
界面元素表达
MVVM
在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM,另外M应该是瘦模型。
VIPER
View,Interactor,Presenter,Entity,Routing
就是把MVC拆个四分五裂
交互器-- 包括关于数据和网络请求的业务逻辑,例如创建一个实体(数据),或者从服务器中获取一些数据。为了实现这些功能,需要使用服务、管理器,但是他们并不被认为是VIPER架构内的模块,而是外部依赖。
展示器-- 包含UI层面的业务逻辑以及在交互器层面的方法调用。
实体-- 普通的数据对象,不属于数据访问层次,因为数据访问属于交互器的职责。
路由器-- 用来连接VIPER的各个模块。
MVP
3. 为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?
代理使用weak用来避免循环引用。
delegate指操作,dataSource指数据
block即代码块,可以直接访问上下文,会使得代码显得更加连贯,但是也会显得有些臃肿
delegate代理,因为方法的声明和实现分离开来,代码的连贯性不如block,但是不会产生臃肿
所以使用block还是delegate,理论上哪个都可以。个人习惯在回调函数较多时,采用delegate,其他情况多用block。
4. 属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?
@property = ivar + getter + setter
对象: readwrite,strong,atomic
基本类型:atomic,readwrite,assign
@synthesize 自动生成getter和setter
@dynamic getter和setter自己实现,不自动生成
5. NSString的关键字用copy和strong的区别?
首先要说明,不是非得用copy,事情况而定。
其实不光是NSString,所有子类有可变类型的都是这样的,比如NSArray。
copy的意思是不论赋值一个可变对象还是不可变对象,得到的都是一个不可变对象,这样即使原对象是可变类型,发生改变也不会影响被赋值对象。
strong则没有上面的特点,原对象的改变也会导致被赋值对象的改变。
至于需不需要跟着变,要看需求,所以不是必须用copy。
另外对于可变类型的属性,不能使用copy修饰,原因就是上面说的使用copy会得到一个不可变对象。
还有,如果重写了setter方法,记得和属性声明保持一致,即如果是copy,重写的setter方法中也要进行copy操作。
6. 如何令自己所写的对象具有拷贝功能?
遵守NSCopying/NSMutableCopying协议,并实现对应方法
7. 可变集合类和不可变集合类的copy和mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么?
非集合类对象的 copy 与 mutableCopy
[不可变对象 copy] // 浅复制
[不可变对象 mutableCopy] //深复制
[可变对象 copy] //深复制
[可变对象 mutableCopy] //深复制
类对象的 copy 与 mutableCopy
[不可变对象 copy] // 浅复制
[不可变对象 mutableCopy] //单层深复制
[可变对象 copy] //单层深复制
[可变对象 mutableCopy] //单层深复
集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。
8. 为什么IBOutlet修饰的UIView也适用weak关键字?
因为IBOutlet连接的视图是已经添加到其他view上的,那个view的subviews已经对它做了强引用。
9. nonatomic和atomic的区别?atomic是绝对线程安全么?为什么?如果不是,应该怎么实现?
对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。
不过atomic可并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。
举个形象点的例子。
假如有多个线程在修改属性a的值,而你在另一个线程中做了如下操作:
if (self.a > 1) { //这行代码在判断的时候self.a可能确实是大于1
self.a ..... //但是在这行self.a的值可能已经发生了改变,因为atomic在第一个getter之后就释放了,其他setter可能在执行这行代码之前先执行了
}
所以,想要实现绝对的线程安全,不能只是在setter和getter方法里加锁,像上面的应该在if判断的外面加锁,当然这个锁应该和修改值(不是setter)时用同一把。
iOS中常用的锁:
@synchronized:@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。会造成额外开销,所以性能上略微差一点。
dispatch_semaphore:dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
NSLock:没啥好说的
NSRecursiveLock:递归锁,lock和unlock要成对。
NSConditionLock:设置条件
NSCondition:主动控制wait、signal
pthread_mutex:pthread_mutex_init,pthread_mutex_lock,pthread_mutex_tylock,pthread_mutex_unlock,pthread_mutex_destroy
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
升级为递归锁。
OSSpinLock:自旋锁,OS_SPINLOCK_INIT,OSSpinLockLock,OSSpinLockUnlock,大神说它不安全了,还是不要用了https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/
剩下的效率最高的是dispatch_semaphore,最次的是@synchronized和NSConditionLock
10. 用StoryBoard开发界面有什么弊端?如何避免?
多人开发同时修改同一个storyboard容易产生文件冲突,利用storyboard会使得控制器加载很卡很慢,不过真机还好,另外就是复用性没有代码好,操作不规范很容易产生很多问题,例如IBOutlet的关联等。
可以根据功能划分,建立多个storyboard,尽量避免多人同时修改同一个storyboard
但是用storyboard开发确实会快一些,形象直观,不用老是运行起来看效果,而且拖控件那必须要比写代码快。
11 .进程和线程的区别?同步异步的区别?并行和并发的区别?
进程和线程的区别:
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4)线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
同步异步的区别:
同步就是顺序执行,一个接一个,需要等待上一个完成才会执行下一个。异步就是不需要等待,各干各的。
并行与并发的区别:
并行就是肩并肩一起走,并发就是一起走,并行只存在于多核处理器,并发不一定是并行。
12. 线程间通信?
performSelector系列
GCD
13. GCD的一些常用的函数?(group、barrier、信号量、线程同步)
group:
dispatchgroupenter(group)和dispatchgroupleave(group),这两个接口的调用数必须平衡或者都不调用,否则group就无法知道是不是处理完所有的Block了。当所有block都处理完后,可以通过dispatchgroupnotify告诉其他线程,并且不会阻塞线程。
barrier:
在使用dispatchasync异步提交时,是无法保证这些工作的执行顺序的,如果需要某些工作在某个工作完成后再执行,那么可以使用Dispatch Barrier接口来实现,barrier也有同步提交dispatchbarrierasync(queue, block)和异步提交dispatchbarrier_sync(queue, block)两种方式。例如:
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_barrier_async(queue, block3);
dispatch_async(queue, block4);
dispatch_async(queue, block5);
dispatchbarrierasync是异步的,调用后立刻返回,即使block3到了队列首部,也不会立刻执行,而是等到block1和block2的并行执行完成后才会执行block3,完成后再会并行运行block4和block5。注意这里的queue应该是一个并行队列,而且必须是dispatchqueuecreate(label, attr)创建的自定义并行队列,否则dispatchbarrierasync操作就失去了意义。
semaphore:
当信号量为0时,dispatch_semaphore_wait会阻塞线程,可以理解为signal会使信号量+1,wait会使信号量-1,当信号量>=0时,线程才不会被阻塞,相当于如果返回值小于0,会按照先后顺序等待其他信号量的通知
以上三种都可以实现线程同步,workItem也可以
14. 如何使用队列来避免资源抢夺?
在一个串行队列中访问资源。
15. 数据持久化的几个方案?
plist、preference、keychains、NSKeyedArchiver、SQLite(FMDB)、CoreData、Realm
16. 说一下AppDelegate的几个方法,第一次启动调用了哪些方法?从后台到前台调用了哪些方法?从前台到后台调用了哪些方法?
第一次启动(冷启动):willFinishLaunching、didFinishLaunching、didBecomeActive
从后台到前台(热启动):willEnterForeground、didBecomeActive
从前台到后台:willResignActive、didEnterBackground、(willTerminate)
17. NSCache优于NSDictionary的几点?
当系统资源将要耗尽时,NSCache可以自动删减缓存。
NSCache不会“拷贝”键,而是会“保留”它。
NSCache是线程安全的。
18. 使用Designated Initializer的注意点?
1. 子类如果有指定初始化函数(无论是重写还是添加),那么指定初始化函数实现时必须调用它的直接父类的指定初始化函数。
2. 如果子类有指定初始化函数,那么便利初始化函数必须调用自己的其它初始化函数(包括指定初始化函数以及其他的便利初始化函数),不能调用super的初始化函数。
19. 实现description方法能取到什么效果?
description主要用于debug模式下打印OC对象, 默认实现返回的格式是 <类名: 对象的内存地址>
所以在必要情况下,我们需要重写description方法以达到改变输出结果目的,覆盖description方法的默认实现。
不要在description方法中[NSString stringWithFormat:@"%@", self],会造成程序死循环,因为这里的self就是要调用description方法。
20. oc的对象内存管理机制?
引用计数器
21. block的实质是什么?一共有几种block?都是什么情况下生成的?
block对象就是一个结构体,里面包含定义时执行代码所处的函数指针,描述信息,捕获到的变量等
根据isa指针,block一共有3种类型的block
_NSConcreteGlobalBlock 全局静态
_NSConcreteStackBlock 保存在栈中
_NSConcreteMallocBlock 保存在堆中
当block在函数内部,且定义的时候就使用了函数内部的变量,那么这个 block是存储在栈上的。
当block定义在函数体外面,或者定义在函数体内部且当时函数执行的时候,block体中并没有需要使用函数内部的局部变量时,也就是block在函数执行的时候只是静静地待在一边定义了一下而不使用函数体的内容,那么block将会被编译器存储为全局block。
全局block存储在堆中,对全局block使用copy操作会返回原函数指针;而对栈中的block使用copy操作,会产生两个不同的block地址,也就是两个匿名函数的入口地址。
ARC机制优化会将stack的block,转为heap的block进行调用。
22. 为什么默认情况下无法修改被block捕获的变量?__block做了什么?
ARC会将stack转为heap,当一个Block被复制到堆上时,与之相关的__block变量也会被复制到堆上。Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。栈区是红灯区,堆区才是绿灯区。
23. 模拟一下循环引用的一个情况?block实现界面反向传值如何实现?
self.block = ^{self.....};
cell.clickOn = ^int(int a){return a;};
Runtime:http://www.cocoachina.com/ios/20141111/10186.html,这是个系列文章1~6,看完runtime应该就能理解了
24. oc在向一个对象发送消息时,发生了什么?
((void()(id,SEL))(void)objc_msgSend)((id)obj,sel_registerName("foo"));
首先,编译器将代码[obj run];转化为objc_msgSend(obj, @selector (run));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method,若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。如果还是没有找到,则进行消息转发机制,首先是通过resolveInstanceMethod:检查是否动态添加方法,如果没有,检查是否实现了快速转发forwardingTargetForSelector:,如果返回其他对象,则会发消息到那个对象,否则,进入标准消息转发,首先通过methodSignatureForSelector:获取方法签名,获取成功会调用forwardInvocation:转发消息,否则调用doesNotRecognizeSelector导致程序崩溃。
25. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?方法呢?为什么?
不能向编译后得到的类增加实例变量
可以向运行时创建的类中添加实例变量
编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,runtime会调用class_setvarlayout或class_setWeaklvarLayout来处理strong weak引用.并且确定内存分布,访问实例变量时时通过内存偏移量来进行访问的,所以不能向存在的类中添加实例变量。
运行时创建的类是可以添加实例变量,调用class_addIvar函数.但是的在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
动态添加方法是可以的,因为方法的查找是通过selector找imp,跟内存分布没有关系。
同理,分类也是在运行时加载然后合并到类中,所以只能添加方法,不能添加实例变量(分类的结构体对象中也没有实例变量列表),另外分类中的方法在合并到类中的时候会插到方法列表的前面,从而达到分类中的方法优先调用。多个分类的话前后顺序根据load的顺序决定,load的顺序在Compile Sources中可以改变。
26. runtime如何实现weak变量的自动置nil?
OC的类结构中一个静态的键值对表变量,它保存着对象的弱引用属性,其中的键为指向弱引用的内存地址,值为弱引用,当对象销毁时通过键查表,然后将对应的弱引用从表中移除。
(weak的实现http://blog.csdn.net/iJason92/article/details/72808387)
27. 给类添加一个属性后,在类结构体里哪些元素会发生变化?
我没有测,猜的
objc_ivar_list(成员变量列表,因为自动生成实例变量)、instance_size(实例变量大小,因为自动生成实例变量)、objc_method_list(方法定义链表,因为自动生成getter 和setter方法)
Runloop:http://www.cocoachina.com/ios/20150601/11970.html把这文章好好看个四五遍,在用代码跑跑,验证一下,runloop应该就理解了。
28. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启runloop了么?子线程呢?
runloop就是一个死循环。因为是死循环才得以保证main函数一直没有结束,从而保持程序的持续运行,在运行期间处理各种事件(触摸、定时器、selector等),节省CPU资源、提高性能,因为它没事会睡觉,有事就醒了。
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。主线程默认开启了一个runloop。
29. runloop的mode是用来做什么的?有几种model?
每个runloop可以包含若干个mode,每个mode可以包含若干个Source/Timer/Observer,但是runloop每次启动的时候只能指定一个mode,如果要切换mode,需要退出runloop,重新指定mode进入,这样是为了每组Source/Timer/Observer隔离开。
系统默认注册了5个mode:
kCFRunLoopDefaultMode:默认mode
UITrackingRunLoopMode:界面跟踪mode
UIInitializationRunLoopMode:刚启动app时的第一个mode,用完就不用了
GSEventReceiveRunLoopMode:接收系统时间的内部mode
kCFRunLoopCommonModes:占位mode,一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
能用到的基本也就是前两个,最后一个算不上是真正的mode。
30. 为什么把NSTimer对象以NSDefaultRunLoopMode添加到主运行循环以后,滑动scrollView的时候timer停了?
滑动的时候切换mode,不是defaultMode了,所以就停了,想不停就添加kCFRunLoopCommonModes,主线程已经给预设的kCFRunLoopDefaultMode和UITrackingRunLoopMode标记为commons,切换的时候会自动同步加到这个mode下的源。
31. 苹果是如何实现Autorelease Pool的?
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
32. isa指针?(对象的isa,类对象的isa,元类的isa)
33. 类方法和实例方法有什么区别?
类方法供类对象调用,实例方法供实例对象调用。类方法列表存储在元类,实例方法存储在类。
34. 运行时能增加成员变量么?能增加属性么?如果能,怎么加?如果不能,为什么?
不能增加成员变量,因为objc_ivar_list和instance_size已经确定,不能修改。但是可以通过添加关联对象的方式实现相同效果。
可以增加属性,属性
35. objc中向一个nil对象发送消息将会放生什么?(返回值是对象,是标量,结构体)
在 Objective-C 中向 nil 发送消息是完全有效的——只是在运行时不会有任何作用:
1.如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。
2. 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。
3. 如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。
4. 如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。
36. UITableView的优化方法(缓存高度、异步绘制、减少层级、hide、避免离屏渲染)
针对我的项目里的做法,在这一部分,采用YYText来做缓存高度和异步绘制,减少层级就是不要乱加没用的视图,简单的只负责显示用的话可以用layer代替,减少透明,避免像素混合,网络下载的图片因为scale和size的原因可能造成像素不对齐,所以重新绘制图片到显示的size及scale,解决离屏渲染主要是对图片的剪切,不要直接使用mask,而是重新绘制来解决,并且将处理后的图片进行缓存(直接利用SDWebImage的机制)。
37. 有没有用过运行时,用它都能做什么?(交换方法、创建类、给新创建的类增加方法、改变isa指针)
对,就是这么用
38. 看过哪些第三方框架的源码?都是如何实现的?
太多了,不记得了
39. 多图下载设计?(SDWebImage)
1. 首先到图片缓存池中取(定义一个存放图片的的字典属性),如果有直接设置;
2. 如果图片缓存池没有,再到沙盒缓存cache中查看是否存在,如果有直接设置,并把沙盒中的图片写入到图片缓存池中;
3. 如果沙盒cache中也没有,就需要开线程下载;
4. 首先判断当前图片是否有任务在下载(定义一个存放任务的字典属性);
5. 如何没有当前没有任务在下载,就开启一个子线程进行数据下载;
6. 下载得到一个图片
7. 下载的图片写入到图片缓存池中
8. 下载的图片同时写入到沙盒中
9. 在主线程中刷新数据
10. 将操作添加到任务缓存池中
11. 队列中添加操作
40. SDWebImage的缓存策略?
三级缓存:内存、硬盘、网络
41. AFN为什么添加一条常驻线程?
创建了一条常驻线程专门处理所有请求的回调事件,这个模型跟 nodejs 有点类似。网络请求回调处理完,组装好数据后再给上层调用者回调,这时候回调是抛回主线程的,因为主线程是最安全的,使用者可能会在回调中更新UI,在子线程更新UI会导致各种问题,一般使用者也可以不需要关心线程问题。
新版AFN,没有看到有这个常驻线程。。。
42. KVO的使用?实现原理?(为什么要创建子类来实现)
http://www.jianshu.com/p/e59bb8f59302
43. KVC的使用?实现原理?KVC拿到key以后是如何赋值的?知不知道集合操作符,能不能访问私有属性,能不能直接访问实例变量?
http://www.jianshu.com/p/45cbd324ea65
补充:
1.如果服务器返回的数据是以init开头的,怎么声明属性?
两种方式
一种是另外声明一个其他名字的属性来接收服务器返回的这个值(比如MJExtension中有对应的协议方法)。
另一种方式是这样声明这个属性@property (nonatomic, copy, getter=p_initName, setter=setP_initName:) NSString *initName;这样就可以避免编译报错。
2. 下面的代码输出什么?(self、super)
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:
都输出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。
我们都知道:self 是类的隐藏参数,指向当前调用方法的这个类的实例。那 super 呢?
很多人会想当然的认为“ super 和 self 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。
上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。
这也就是为什么说“不推荐在 init 方法中使用点语法”,如果想访问实例变量 iVar 应该使用下划线( _iVar ),而非点语法( self.iVar )。
点语法( self.iVar )的坏处就是子类有可能覆写 setter 。假设 Person 有一个子类叫 ChenPerson,这个子类专门表示那些姓“陈”的人。该子类可能会覆写 lastName 属性所对应的设置方法:
#import "ChenPerson.h"
@implementation ChenPerson
@synthesize lastName = _lastName;
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class]));
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
//设置方法一:如果setter采用是这种方式,就可能引起崩溃
// if (![lastName isEqualToString:@"陈"])
// {
// [NSException raise:NSInvalidArgumentException format:@"姓不是陈"];
// }
// _lastName = lastName;
//设置方法二:如果setter采用是这种方式,就可能引起崩溃
_lastName = @"陈";
NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"会调用这个方法,想一下为什么?");
}
@end
在基类 Person 的默认初始化方法中,可能会将姓氏设为空字符串。此时若使用点语法( self.lastName )也即 setter 设置方法,那么调用将会是子类的设置方法,如果在刚刚的 setter 代码中采用设置方法一,那么就会抛出异常,
为了方便采用打印的方式展示,究竟发生了什么,我们使用设置方法二。
如果基类的代码是这样的:
#import "Person.h"
@implementation Person
- (instancetype)init
{
self = [super init];
if (self) {
self.lastName = @"";
//NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
//NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName);
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
NSLog(@"类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不会调用这个方法");
_lastName = @"炎黄";
}
@end
那么打印结果将会是这样的:
类名与方法名:-[ChenPerson setLastName:](在第36行),描述:会调用这个方法,想一下为什么?
类名与方法名:-[ChenPerson init](在第19行),描述:ChenPerson
类名与方法名:-[ChenPerson init](在第20行),描述:ChenPerson
我在仓库里也给出了一个相应的 Demo(名字叫:Demo_21题_下面的代码输出什么)。有兴趣可以跑起来看一下,主要看下他是怎么打印的,思考下为什么这么打印。
接下来让我们利用 runtime 的相关知识来验证一下 super 关键字的本质,使用clang重写命令:
$ clang -rewrite-objc test.m
将这道题目中给出的代码被转化为:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));
从上面的代码中,我们可以发现在调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:
id objc_msgSend(id self, SEL op, ...)
我们把 self 做为第一个参数传递进去。
而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下:
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 。第二个成员是记录当前类的父类是什么。
所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
objc Runtime开源代码对- (Class)class方法的实现:
- (Class)class {
return object_getClass(self);
}
而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。
第二步是去 Father这个类里去找 - (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,
此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。
3. 对象的内存销毁时间表(关联对象无论在arc还是mrc,无论内存策略如何,都不需要手动释放)
1. 调用 -release :引用计数变为零
* 对象正在被销毁,生命周期即将结束.
* 不能再有新的 __weak 弱引用, 否则将指向 nil.
* 调用 [self dealloc]
2. 子类 调用 -dealloc
* 继承关系中最底层的子类 在调用 -dealloc
* 如果是 MRC 代码 则会手动释放实例变量们(iVars)
* 继承关系中每一层的父类 都在调用 -dealloc
3. NSObject 调 -dealloc
* 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
4. 调用 object_dispose()
* 为 C++ 的实例变量们(iVars)调用 destructors
* 为 ARC 状态下的 实例变量们(iVars) 调用 -release
* 解除所有使用 runtime Associate方法关联的对象
* 解除所有 __weak 引用
* 调用 free()
4. BAD_ACCESS在什么情况下出现?
访问了悬垂指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环。
5. 导致程序崩溃的原因有哪些?
watchdog超时机制:
application:didFinishLaunchingWithOptions:
applicationWillResignActive:
applicationDidEnterBackground:
applicationWillEnterForeground:
applicationDidBecomeActive:
applicationWillTerminate:
上面所有这些方法,应用只有有限的时间去完成处理。如果花费时间太长,操作系统将终止应用。
低内存终止:
当内存使用达到一定程度时,操作系统将发出一个 UIApplicationDidReceiveMemoryWarningNotification 通知。同时,调用 didReceiveMemoryWarning 方法。另外,值得一提的是在极短时间内分配一大块内存将给系统内存带来巨大负担。这样,也会产生内存警告的通知。处理不及时或处理不当会导致程序崩溃。
Exception Type异常类型
Signal信号和EXC_BAD_ACCESS。
Exception Codes:异常编码
0x8badf00d: 读做 “ate bad food”! (把数字换成字母,是不是很像 :p)该编码表示应用是因为发生watchdog超时而被iOS终止的。 通常是应用花费太多时间而无法启动、终止或响应用系统事件。
0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。
0xdead10cc: 读做 “dead lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止 。
0xdeadfa11: 读做 “dead fall”! 该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 然后长按 Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 因为大多数是强制退出是因为应用阻塞了界面。
Signal信号的类型:
SIGABRT–程序中止命令中止信号(SIGABRT 异常是由于某个对象接收到未实现的消息引起的。 或者,用简单的话说,在某个对象上调用了不存在的方法。)
SIGALRM–程序超时信号
SIGFPE–程序浮点异常信号
SIGILL–程序非法指令信号
SIGHUP–程序终端中止信号
SIGINT–程序键盘中断信号
SIGKILL–程序结束接收中止信号
SIGTERM–程序kill中止信号
SIGSTOP–程序键盘中止信号
SIGSEGV–程序无效内存中止信号(一般是表示内存不合法)
SIGBUS–程序内存字节未对齐中止信号(SIGBUS程序内存字节未对齐中止信号)
SIGPIPE–程序Socket发送失败中止信号
EXC_BAD_ACCESS是一个比较难处理的crash了,当一个app进入一种毁坏的状态,通常是由于内存管理问题而引起的时,就会出现出现这样的crash。通常Signal信号错误都会提醒EXC_BAD_ACCESS。
6. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法
7. load、initialize的区别,以及它们在category重写的时候的调用的次序。
区别在于调用方式和调用时刻
调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用
调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)
调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。
8. main之前的加载过程
1)dyld 开始将程序二进制文件初始化
2)交由ImageLoader 读取 image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP)
3)由于runtime 向dyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理
4)runtime 接手后调用map_images做解析和处理
5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class的+load和其他Category的+load方法
6)至此 所有的信息都被加载到内存中
7)最后dyld调用真正的main函数
注意:dyld会缓存上一次把信息加载内存的缓存,所以第二次比第一次启动快一点
网友评论