iOS 基础知识总结之 OC 篇

104 阅读22分钟

1.  weak 和 assign的区别

weak主要用可能出现循环引用的地方,比如在delegate,weak就是弱引用,保证在对象被销毁的时候,不会强持有此对象,导致对象无法释放。assign用在对基础数据类型的修饰上面。weak在对象被销毁和释放时,会将该属性赋为nil。

2.  copy关键词

一般有可变类型的数据类型,比如NSString,NSArrary,NSDictionary,还有block。

copy就是深拷贝,拷贝的是值。如果用strong修饰这些数据类型,如果传递过来的是可变类型比如NSMutableString,那么它的值可能会被外部修改。

对atomic和nonatomic修饰符的区别,atomic就是保证在访问此属性时,在同一线程中可以保证其不会被同时访问,但是因为无法保证线程安全,所以一般不用atomic,atomic为了保证其原子性,iOS对其做了额外处理,相较于nonatomic会增加不必要的开销。

为保证此属性被访问时的安全,一般采用NSLock,加锁机制。

3.  @property

property本质上是一个结构体,由name和value组成,记录着实例变量、属性名、原子性、类型。完成property定义后,编译器会自动生成setter和getter方法。

属性怎么实现的?

1.  属性距离这个类的起始地址的偏移量

2.  会在这个类的方法列表增加setter和getter的方法描述,增加这个属性的setter和getter方法,在这个类的成员变量列表中增加此属性的成员变量,这个类的属性列表中增加这个属性。

4.  @protocol 和 category 中如何使用 @property

它只会实现getter和setter方法,并不会自动生成实例变量,需要借助runtime,objcsetassoiated 动态实现。

5.  被weak修饰的对象释放原理

id obja = weak obj0

id objc = weak obj0

obja / objc 存储的是obj0的地址作为key,会讲这个key放入到一个weak的hash表中,当obj0被释放是,会反查key所对应的所有对象,并且赋值为nil。

runtime中用两个函数来表示以上一段原理的描述,objc_initWeak(key,value),key 代表被原对象的地址,value代表被weak修饰对象的地址。

objc_destroryKey();当key为nil时,value自动赋值为nil。

6.  property中nonenullble,class等

这些修饰符基本为了适配swift的optional类型,和类方法。swift中class方法和static方法的区别。两者都属于静态函数,class方法,可以被继承的子类重写,而static方法无法被重写。

7.  @synthesize和@dynamic分别有什么作用?

声明为synthesize后,编译器会自动生成getter和setter方法,声明dynamic则需要自己手动实现setter和getter方法。

8.  什么都不写,@property默认是哪几个修饰符

基础类型:atomic, readwrite, assign

对象:atomic, readwrite, strong

9.  copy 和mutable copy 的区别

1.  对于非集合类对象,比如说NSString类型,除了不可变对象的copy为浅拷贝,其余三种情况都为深拷贝。为了不影响原可变对象,无论是copy还是mutable copy,都是深拷贝,会创建一个新的对象并赋值。从不可变对象mutable copy 变成可变对象,也是深拷贝。不可变对象,copy,还是原来的值,系统认为是没有必要的,所有就变为浅拷贝。

2.  对于集合类对象,比如Array,Dictionary,对于集合对象本身来讲,不可变集合的copy是指针拷贝,也就是浅拷贝,mutable copy是深拷贝,会创建一个新的可变集合。而可变集合无论是copy 还是mutable copy,都会创建一个新的可变集合。但是无论是否为可变对象,是否为copy 或者 mutable copy,其集合内的元素都不会进行值拷贝,集合内的元素都是浅拷贝,集合内存储的是元素的地址。

10.  什么情况下不会autosynthesis(自动合成)

1.  被dynamic定义的属性

2.  protocol、category 不会自动合成

3.  实现getter和setter方法的

4.  override 属性 也不会自动合成

11.  objc的类中有哪些元素组成? 给一个nil对象发送消息,会出现什么情况?

1.  指向其元类的指针

2.  方法列表

3.  属性列表

4.  成员变量列表

5.  协议列表

6.  父类

给一个nil对象发送消息,程序并不会出现奔溃,因为OC中发送消息首先会去寻找该类的方法列表中是否能找到,再去父类的方法列表中寻找,如果找不到,那么不会调用任何的消息发送。如果调用函数返回的是一个对象,那么在会默认返回nil,如果是基础类型,则会返回默认的基础类型。因为nil它不是一个类,它是表示控对象的指针,它的isa指针返回的地址为0,你也找不到他对应的方法列表。

12.  OC中的nil、null、NSNull分别代表什么?

nil是对象类型的空指针,null是非对象类型的空指针,NSNull表示的是空对象。nil和null都是表示一个特殊的空值。

13. objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

通过clang查看c++源码,其实obj调用foo方法,本质上是调用了消息转发机制,objc_msgSend(reciever, method) ==> objc_msgSend(obj,@selector(foo));

14.  什么时候会报unrecognized selector的异常? 并且说明一下消息转发机制的过程和原理。

当一个对象通过其isa指针,在其类和父类、根类的函数列表中并未找到此方法名,并且此类并未做消息转发机制的处理,系统就会抛出此异常,并且奔溃。

在正式抛出此异常前,系统会进行消息转发机制处理:

1.  发现此对象的方法列表中未找到方法名后,系统会调用一个类方法叫+(id)resolveinstanceMethod();在此方法中,如果你在此方法中动态添加此方法名,那么系统会重新进行一次消息发送过程。

2.  如果在resolveInstanceMethod方法中未实现此方法名,就会进入fast 消息转发机制,此时系统会调用forwardtargetSelector方法,在此方法中,你可以把消息转发的对象传递给其他对象,让其他对象去完成消息转发机制的过程。

3.  如果在forwardTargertSelector方法中,并未将消息转发对象进行传递,那么就会进入nornal forward 转发机制,首先会调用signatureForMethod,传递函数的参数、返回值,如果此时返回nil,那么程序就会奔溃,如果返回一个函数签名(函数的参数、返回值),那么会进入forwardInvocation方法,在此方法中可以将Invocation(包含方法名、参数、返回值),可以通过【Invocation setTargert】将,消息转发机制传递给其他对象(或者什么都不做也没问题)。

resoleveInstanceMethod是runtime为你提供的一个可以动态替换/动态增加方法的一个方法。

15.  一个objc对象如何进行内存布局?(考虑有父类的情况)

一个对象其内存布局有,isa指针指向这个类对象,类对象中有包含,父类,元类,方法列表(包含其父类的方法列表),属性列表、实例变量(成员变量)列表(包含父类的成员变量),协议列表等。

类、元类、根元类,类中的isa指针指向其元类,根元类的isa指针指向其自己。根元类的父类指向NSObject。

16.  self 和 super的区别

比如下面的init方法 :

  • (instanceType) init {

self = [super init];

if(self) {

[super init] 和 [self init] 返回的对象都是

}

}

self 和 super两个关键词其实都是指向了当前这个调用方法的实例,只是super的意思是,调用父类的方法。

17.  类方法和实例方法区别

1.  类方法属于类对象,实例方法属于实例对象。

2.  类方法可以直接调用,实例方法必须创建实例后才能调用

3.  类对象无法调用实例方法,实例方法可以直接调用类方法

18.  _objc_msgForward函数是做什么的,直接调用它将会发生什么?

消息转发机制函数指针,当objc找不到对象的方法名时,调用此函数会直接进入消息转发,一般用在消息传递至forwardInvocation方法的时候,作为函数调用,此函数包含几个参数,调用者、方法名、参数。

19.  能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

不能向编译后的类中增加实例变量,因为一旦编译完成,该类的内存布局已经确定,此类的实例变量地址,以及实例变量大小都已确定,如果增加实例变量,就会改变实例变量的在内存中的地址和实例变量的大小,可能会导致实例变量内存访问的问题,也有可能会出现内存重叠的问题。

可以在运行时创建的类中添加实例变量,因为在运行时创建的类,没有固定的实例变量内存布局,可以动态灵活的创建实例变量。

20.  runloop和线程的关系

blog.ibireme.com/2015/05/18/…

runloop就是运行循环,是线程的基础架构,没有线程,那么也就没有存在runloop的必要。主线程的runloop是默认开启的,在你启动App时,进入启动函数,就开启了主线程。

比如打印一个“hello world”,打印完后,这个线程就结束了,直线线程不需要循环。runloop默认是不创建的,当你去取当前线程的runloop时才会创建runloop,runloop的销毁原理类似于weak对象的销毁原理,也是将线程作为Key,runloop作为value,当线程销毁时,顺便销毁对应的runloop。

21.  runloop的组成,及概念

runloop 有可以有多个Mode组成,每个Mode内有Source、Observer、Timer组成,Mode有多种类型。

Mode分为两种一种时Defualt Mode, 一种时UITrackingMode,一种时Common Mode 则是包含两种Mode,举例就是Timter时间计时器,在Defualt Mode下,如果滑动列表时,会暂停计时器。想要在滑动列表时,同时Timer正常运行,只需要将Timer所在的runloop mode设置为common Mode就可以了。

Source 就是runloop的事件源,Source 有两种类型,一种是Source0,是一个函数指针,并不会主动触发runloop的运行,runloop接受到Source0后需要,手动触发runloop。另一种时Source1,是由mach_port 和函数指针组成,可以直接触发runloop,其作用主要用在线程间的通信。

Observer是runloop中的观察者,用于监听runloop的各个状态,比如有source事件、runloop进入休眠事件等等。

Timter是runloop中的定时机制,可以允许开发者定制,事件的循环时间精度和触发次数等。

runloop内部运行逻辑:

1.  observer 通知runloop 即将进入loop。

2.  处理Timer (循环开始处)

3.  处理Source0

4.  如果有Source1事件来源,直接处理唤醒事件,处理完成后,开始下一次的循环。

5.  处理Source0后,进入休眠状态,等待下一次的Source唤醒

22.  iOS 系统架构

有4层,分别是:

1.  cocoa touch 层,包含UIKit,常见开发者的用户交互、界面UI等

2.  媒体层,比如 core graphics,open GL,av foundation 等等

3.  核心服务层,比如Foundation框架,定义的一些基础数据类型,也包括了文件的访问、地理位置等等

4.  核心操作系统层,主要是与内核和操作系统通信,比如文件系统、硬件的通信,比如runloop中的mach port,icloud,蓝牙、wifi等。

23.  AutoreleasePool 自动释放池,一般在runloop创建时会创建一个自动释放池,在runloop进入休眠状态时,会将autorelease pool释放后,重新创建一个新的自动释放池,当runloop退出时,会释放自动释放池。

24.  事件响应

25.  手势识别

一般在touchbegin 函数中可以判断,是什么手势。

boundInsetBy(x: y:),可以使控件的点击区域扩大或者缩小

26.  界面更新

27.  定时器

28.  objc使用什么机制管理对象内存?并说明release对象的各种情况。

objc使用引用计数管理对象的内存,当release时,如果引用计数为0,则释放对象。

1.  此对象被销毁,调用dealloc函数

2.  临时对象在作用域结束的时候

3.  runloop中的autoreleasepool对象在runloop进入休眠状态以及runloop退出时,都会release对象

29.  ARC通过什么方式帮助开发者管理内存?

系统会在编译和运行时,自动管理内存。

30.  BAD_ACCESS在什么情况下出现?

访问已经被释放的对象

31.  苹果是如何实现autoreleasepool的?

主要是通过三个方法,autorelease_push, autorelease_pop, autorelease,控制整个autoreleasepool的过程。

举例说明,比如在一个runloop中,创建一个autorelease pool,在此时创建的对象,会被加入到此autorelease pool中,也就是调用 autorelease push,当此runloop进入休眠状态时,会调用autorelease pop, 并且调用autorelease释放池中的对象。

32. 使用block时什么情况会发生引用循环,如何解决?

一个对象强引用了此block,并且在block内强引用了此对象,就会产生引用循环。

在对象强引用此block前,在self前加__weak 关键词,使对象不强引用此block,打破引用循环,当对象释放时,此block也会设置为nil。

还有一种方法是unsafe_unretained,但是被此关键词修饰的,在对象被释放后,block不会自动设置为nil,还会指向原来的内存地址。

32.  在block内如何修改block外部变量?

block分为3中,一种是global block,一种是stack block, 一种时malloc block,它们分别存储于不同的内存区域,global block 存储于全局数据段,也就是与静态变量同一数据段,stack block 顾名思义,位于栈区域,malloc block 位于堆区域。

当同时copy这三种block时,表现也不同,global不会产生变化,stack 的会从栈copy到堆区域,malloc 只是引用计数加1。stack的copy行为,也是iOS系统在编译时会自动为局部block这些代码块copy到堆区。

在 block 内为什么不能修改 block 外部变量?

在block内部访问外部的变量,这种行为被称为捕获,系统对于block捕获这种行为,是复制外部变量的值。至于为什么不能直接改变外部变量,是考虑到线程安全和数据的一致性,如果在多个block同时访问外部变量,可能会引起此类问题。

解决方案是在外部变量前 加__block修饰符,告诉编译器,需要改变外部变量,此时编译器就会将捕获的此外部变量,变成地址的拷贝,也就是引用的方式被捕获。

当然还可以把此变量设置为static 静态变量,进入到block内部时,也会以引用的方式被block捕获。

被捕获的外部变量,会与block一样,自动拷贝至堆区(除了静态变量、全局变量以外)。

为什么要把block从栈区拷贝至堆区,因为栈区的内存由系统管理,出了作用域就会被释放,堆区的内存由开发者自己管理。

以上所说的不允许修改block外部变量的前提是,此变量是位于栈区的变量。

以下代码编译可以通过,并且在 block 中成功将 a 的从 Tom 修改为 Jerry。

 NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
 void (^foo)(void) = ^{        
 a.string = @"Jerry";
 //a = [NSMutableString stringWithString:@"William"]; //编译报错
 };
 foo();

因为它在使用变量而非修改变量,所以不会编译报错 (捕获的外部变量默认为 const 类型,意味着你不能改变变量 a 的指针(即不能让 a 指向另一个对象),但可以修改它指向的对象的内容,因为你本质上持有该对象的引用。)。

33.  栈和堆的概念,并且栈和堆中主要存放哪些数据?

栈是一个先进后出的数据结构,主要存放函数的调用信息:包括函数的地址,函数返回值地址,函数调用顺序,一些函数内的静态类型数据,比如int、float等等。

堆则是一个动态分配内存的区域,主要存alloc,malloc等创建的对象。内存管理和释放有开发者可以控制。

栈的存取效率要高于堆,因为它只涉及到指针的移动,生命周期,栈上的周期只在其代码块内或者函数内,代码块结束后,则释放。堆上的生命周期则相对灵活,可以夸函数和代码块存在,直到被释放。

34.  系统API中【UIView animation】,【NotificatoinCeter default】,GCD中的block,是否会有循环引用的产生

UIView中的animation中的block并不会产生循环引用,即使block中会强持有self,但是UIView的animaion并不是被self强持有。

而NotificaitonCenter 单例中的block持有self,self持有单例,单利持有block,其实就形成了循环引用。

GCD中的block并不会产生循环引用,GCD在内部会在短暂持有block,任务结束后,会自动释放对block的持有。

我们可以通过Xcode 的 instrument leak 查看对象之间的持有情况,或者通过facebook的FBRetainCycle 三方库

35.  GCD的队列(dispatch_queue_t)分哪两种类型?如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

GCD对象分为两种,一种时Serail串行队列,一种是Concurrent 并行队列。可以创建一个dispatch_queue_group(), 队列组,将这些异步队列加入组中,然后再group_notify监听所有队列完成事件。

36.  dispatch_barrier_async的作用是什么?

dispatch_barrier_async的作用是当进行多个异步任务时,有时某一个任务的优先级较高,需要完成这个最高优先级的异步任务后,才可以进行其他的异步队列。比如对一个数组数据,当对数据进行写入(增加数组)时,需要保证数据的安全性,此时可以对数组数据的增删动作放在barreir_async中进行,而对数组的读取任务则可以放在普通的异步队列中。barrier搭配gloabel_queue,会使barrier异步队列与普通异步队列效果一样,这是因为全局队列,不允许用户对其行为进行控制。

37. 以下代码运行结果如何?并说明原因

  - (void)viewDidLoad {      
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2");       
     });
     NSLog(@"3");   
  }

会出现线程死锁的现象,main_queue是主线程队列,他会在当前UI完成后(也就是当ViewDidload函数完成后),才会执行队列中的任务,而dispatch_sync是同步队列,它会在完成队列内任务后才会执行下面的操作,也就是它在等待block内的任务完成。所以最后只会打印“1”,然后页面被卡死。

37.  kVO原理

首先系统会对被观察的对象的类,通过runtime动态创建一个他的子类,并且重写此子类的setter方法,然后将原类的isa指针指向这个新的子类,当属性发生变化时,会调用这个字类的setter方法,在setter方法中会调用willchangevalueforkey , 赋值,didchangevalueforkey,通知观察者修改了属性值(也就是我们的回调函数)。当取消观察者时,又会讲子类的isa指针指回到原来的类中。 willchangevalueforkey 和 didchangevalueforkey是为了确保kvo监听系统的完整性和统一性。

38.  KVC 和 KVO的区别

KVC时键值编码,KVO时键值观察。KVC可以通过Keypath的方式获取对象的属性,也可以通过setvalue forKeypath来设置属性的值。而KVO可以通过监听keypath的方式官擦对象属性的变化。KVC是双向机制,可取属性,也可改变属性,而KVO属性单向机制,只能监听属性的变化。

39.  关闭系统自带的KVO监听

automaticllyNotificationObserversForKey: 函数,返回值返回NO就可以了。

40.  点击屏幕某个控件,事件的传递链是怎样传递的下去,最后响应的。

1.  点击事件的传递 UIApplication --> UIWindow --> 根控制器 --> UIViewController --> UIView --> .... --> 第一响应View ,一般是通过 HitTest函数传递

2.  点击事件的响应,正好相反,是从最近的View 向父视图寻找响应的视图

41.  为什么runtime的交换方法都放在一个类的load方法中进行

在启动app时,系统会加载所有类的load方法,它发生在 main 函数执行之前,可以保证在交换方法在被实际执行之前,一定完成交换。并且load方法在启动app后,只加载一次,不会出现多次交换的,避免重复交换。

42.  #import 和 #include 两个方法的区别

#import 属于OC特有的导入方式,可以防止文件被重复导入,#include是C和C++特有的导入方式,将.h的内容导入当前文件

编译器如何检测重复导入

#import

编译器通过内部维护一个已包含的文件列表,#import 指令会检查这个列表。如果文件已经被包含过,它会跳过该文件。

#include

编译器不自动防止重复包含,所以需要手动添加头文件保护。每次编译器遇到 #include 指令时,它会根据头文件保护的定义来决定是否包含文件内容。

三方库导入

在导入第三方库时,可以使用 "" 和 <> 两种方式,分别有不同的用途:

使用 ""

使用 "" 包围的文件路径是相对路径,编译器首先在当前文件所在目录查找头文件。

如果在当前目录找不到,会继续在标准系统目录中查找。

这种方式通常用于导入项目中的头文件或本地第三方库。

#include "MyLocalLibrary.h"

使用 <>

使用 <> 包围的文件路径是绝对路径,编译器只在标准系统目录中查找头文件,不会在当前文件所在目录查找。

这种方式通常用于导入系统库或全局路径中的第三方库。

#include <UIKit/UIKit.h>

43.  PerformSelector

1.  NSObject 下的 PerformSelector

NSObject下的PerformSelctor,就是一个简单的消息发送。将消息发送到指定的函数指针,也就是SEL。从runtime运行后,可以观察到都是执行了简单的消息发送。

方法底层实现
performSelector:((id(*)(id, SEL))objc_msgSend)(self, sel)
performSelector:withObject:((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj)
performSelector:withObject:withObject:((id(*)(id, SEL, id, id))objc_msgSend)(self, sel, obj1, obj2)
2.  Runloop相关的 PerformSelector

performSelector:WithObject:afterDelay:

底层代码实现:

- (void) performSelector: (SEL)aSelector
	      withObject: (id)argument
	      afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop		*loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer	*item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
					     target: self
					   argument: argument
					      delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}


/*
 * The GSTimedPerformer class is used to hold information about
 * messages which are due to be sent to objects at a particular time.
 */
@interface GSTimedPerformer: NSObject
{
@public
  SEL		selector;
  id		target;
  id		argument;
  NSTimer	*timer;
}

- (void) fire;
- (id) initWithSelector: (SEL)aSelector
		 target: (id)target
	       argument: (id)argument
		  delay: (NSTimeInterval)delay;
- (void) invalidate;
@end

从底层代码,做个简单的总结就是,首先会获取当前线程的runloop,然后包装一个TimerPerformer对象,此对象中包含函数指针,NSTimer,delay的时间等,将包装的timerperformer对象加入到当前的runloop中,并且在当前runloop的中添加timerperformer的timer以及指定Mode的类型。

3.  NSRunLoop 的分类 NSOrderedPerform
- (void)jh_performSelectorTargetArgumentOrderModes
{
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop performSelector:@selector(runloopTask5) target:self argument:nil order:5 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask1) target:self argument:nil order:1 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask3) target:self argument:nil order:3 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask2) target:self argument:nil order:2 modes:@[NSRunLoopCommonModes]];
    [runloop performSelector:@selector(runloopTask4) target:self argument:nil order:4 modes:@[NSRunLoopCommonModes]];
}

- (void)runloopTask1
{
    NSLog(@"runloop 任务1");
}

- (void)runloopTask2
{
    NSLog(@"runloop 任务2");
}

- (void)runloopTask3
{
    NSLog(@"runloop 任务3");
}

- (void)runloopTask4
{
    NSLog(@"runloop 任务4");
}

- (void)runloopTask5
{
    NSLog(@"runloop 任务5");
}

// 输出
2020-03-12 14:23:27.088636+0800 PerformSelectorIndepth[62976:972980] runloop 任务1
2020-03-12 14:23:27.088760+0800 PerformSelectorIndepth[62976:972980] runloop 任务2
2020-03-12 14:23:27.088868+0800 PerformSelectorIndepth[62976:972980] runloop 任务3
2020-03-12 14:23:27.088964+0800 PerformSelectorIndepth[62976:972980] runloop 任务4
2020-03-12 14:23:27.089048+0800 PerformSelectorIndepth[62976:972980] runloop 任务5

原理和Runloop的一样,只是多了一个步骤,把多个timerperformer对象按照order的顺序放入数组中去了。然后再从数组中取出来放到runloop中去执行。

  for (i = 0; i < end; i++)
            {
                GSRunLoopPerformer	*p;
                
                p = GSIArrayItemAtIndex(performers, i).obj;
                if (p->order > order)
                {
                    GSIArrayInsertItem(performers, (GSIArrayItem)((id)item), i);
                    break;
                }
            }
            if (i == end)
            {
                GSIArrayInsertItem(performers, (GSIArrayItem)((id)item), i);
            }
4.  Thread 中的PerformSelector

主要是三种方法,

// 1. 传入的线程要保证线程的runloop是否已经开启
performSelector:onThread:withObject:waitUntilDone:  performSelector:onThread:withObject:waitUntilDone:modes:

// 2. 祝线程上执行,无需开启线程
performSelectorOnMainThread:withObject:waitUntilDone:  performSelectorOnMainThread:withObject:waitUntilDone:modes:

// 3. 开启一个子线程
performSelectorInBackground:withObject: