Objective-C
OC与C/C++的区别?
- OC和C++都是C的面向对象的扩展,都可以兼容C语法进行编译,但是他们在此之外还有独有的特性。
- C++和OC的应用领域不同,C++专注于高性能开发,而OC则专注于Apple应用的开发。
- OC的头文件为
.h
,而源文件为.m
,m代表message,表示了OC中的一个重要机制——消息机制。
什么是#import指令?
#import
是 #inlcude
指令的增强版,同1个头文件无论被 #import
多少次,都只会被包含1次。
说说OC中新增的基本数据类型?
- BOOL:布尔类型,可以存储YES或者NO中的任意1个数据,其本质是一个宏,YES为1,NO为0。在做条件判断时候要额外注意,最好显式地去判断BOOL值是否为YES或者NO,而不是去将其等同于C++中的bool类型,否则可能导致错误,如下:
BOOL flag = 2; // 非零值,逻辑上为真 if (flag) { ... } // 条件成立(正确) if (flag == YES) { ... } // 可能不成立(因为 YES 是 1,而 flag 是 2)
- nil:空指针,与NULL差不多。但在oc中使用的时候,最好使用nil而不是NULL。
有哪些常用的OC底层框架?
- Cocoa 是 Apple 为 macOS 开发构建的一套原生面向对象的应用程序框架(API)的统称。它是开发 macOS 应用的基础和基石。
- Cocoa Touch是与Cocoa平行的框架体系,专门针对移动端,即iOS。
- Foundation 是 iOS/macOS 开发的底层基础,在Cocoa和Cocoa Touch中都存在,它提供字符串、集合、文件、网络等核心功能,里面的类基本都以NS为开头。
- AppKit:基于 Foundation,提供macOS上的UI 组件。
- UIKit:基于 Foundation,提供iOS上的UI 组件。
如何定义类?
- OC中定义类的关键字为
@interface
(声明)和@implementation
(实现) - 在OC中创建类,需要继承自
NSObject
(或其子类),否则无法利用OC中的大部分特性。
如何定义方法,以及方法有什么类型?
- 类方法:属于类本身,而不是类的某个实例,使用
+号
定义。通过类名直接调用,不能访问实例变量(因为类方法没有self
实例)。 - 实例方法:属于类的实例,必须先创建对象才能调用,使用
-号
定义。通过实例调用,可以访问实例变量(self
指向当前对象)。 - 如果方法只有声明没有实现,在编译时会产生一个警告,但不会报错。如果在运行时调用这个方法,则会报出一个运行时错误。
如何创建实例对象?
- 在OC中创建实例对象,涉及到以下方法:
alloc
是NSObject
的类方法,用于分配内存,创建一个未初始化的对象实例并返回。init
是NSObject
的实例方法,用于初始化对象,可以重写init
方法来自定义初始化逻辑。如果不重写,则默认初始化为零值。new
是NSObject
的类方法,等价于alloc + init
。
- 在OC中创建实例对象,对象中包含的属性值都会跟着对象存储到堆中,栈中只保留了指向对象的指针。而其中的方法则会存储到代码区,不占用对象空间。
方法和函数的不同?
- 函数的声明和实现与C语言相同,但方法的声明与实现和函数完全不同
- 函数可以和方法一样写到类里面,但是这个函数不属于类,通过对象也无法调用这个函数。
OC的对象和C++对象的不同?
- OC中的对象实例全都存储在堆中,调用者只能通过指针操作对象,而不能像C++一样在栈中创建对象实例。
- 在OC中,对象的属性不允许在声明时给属性赋值,只能通过初始化函数赋值。
MRC和ARC的区别?
- OC采用引用计数来管理对象内存,也就是支持多个指针指向一个对象,只有当计数为0时,对象才会被释放。
- MRC:手动引用计数。开发者手动管理内存,必须显式调用
retain
、release
、autorelease
来管理对象生命周期。 - ARC:自动引用计数。编译器自动插入
retain
/release
,开发者只需关注对象引用关系,无需手动管理。- 强引用(
strong
):默认修饰符,对象被强引用时不会被释放。 - 弱引用(
weak
):不增加引用计数,对象释放后自动置nil
(避免野指针)。
- 强引用(
什么是#pragma mark?
#pragma mark
是一种分组导航标记,只是为了在IDE中能够快速定位,方便程序员找到类的声明和实现,它并不影响代码的编译和运行。#pragma mark 分组名
:在导航条对应的位置显示1个标题#pragma mark -
:在导航条对应的位置显示1条水平分隔线#pragma mark - 分组名
:在导航条对应的位置先产生1条水平分割线.再显示标题
说说一些常用的Foundation相关类和方法?
- NSString:存储OC字符串的地址,在使用时,必须要在字符串符号前,加上1个前缀@符号,如
@"hello, world"
。 - NSLog:printf函数的增强版,向控制台输出信息。可以自动换行,输出OC特有的变量类型。如果要使用NSLog函数输出OC字符串的值,那么使用占位符
%@
Static关键字在OC中的作用?
- static在OC中既不能修饰属性,也不能修饰方法
- static可以修饰方法中的局部变量,使其生命周期跟随程序。
如果实例方法和类方法重名了怎么办?
通过类名或类对象来调用的就是类方法,通过对象名来调用的就是实例方法,两者不会因为重名而导致混乱。
什么是访问修饰符?
- 访问修饰符用来修饰属性,可以限定对象的属性在哪一段范围之中访问
- OC中的属性修饰符分为private、public和protected,基本与C++保持一致。
- 子类可以继承父类的所有属性,但不能访问父类中的private属性,除非父类预定义了私有属性的set/get方法。
- 访问修饰符只可以修饰属性,不可以修饰方法。
怎么定义私有方法?
- 方法不在interface中声明,只在implementation中定义,即为私有方法
- 在implementation中也可以定义属性,并且默认就是私有属性,各种访问修饰符都是无效的,外部无法直接访问其中的属性,编译器会直接报错。
说说OC的点语法?
- OC中如果要访问实例对象的属性,需要额外使用set/get方法,无法直接访问。
- get方法是一个实例方法,函数名即为变量名(如果变量名前面有下划线,需要把下划线去掉),无输入,返回变量值。
- set方法是一个实例方法,函数名即为“set+变量名”(如果变量名前面有下划线,需要把下划线去掉,然后方法中对应变量名的首字母需要大写),输入变量值,无返回值。
- OC中提供了特殊的语法糖:点语法。可以通过对象名+"."的形式访问属性,看起来很像其他面向对象语言的访问,但实际上它是调用了其set/get方法。如果不实现set和get方法,也无法使用点语法来访问属性。
说说@property关键字?
- 在xcode4.4之前,@property可以用于生成属性的set/get方法的声明,不需要开发者额外进行声明。因为只是声明,所以需要写在@interface中。
- @synthesize是@property的配合语法,可以自动生成属性的set/get方法的实现。因为是实现,所以需要写在@implementation中。
- @synthesize会自动生成一个在@implementation中的私有属性和对应的set/get方法实现,如果希望@synthesize不自动生成私有属性,而是只生成已有属性的set/get方法,则可以使用
@synthesize @property名称 = 已有属性名
,如@synthesize name=_name;
- @synthesize生成的set/get方法没有做任何的逻辑验证,如果要添加额外的逻辑,则重写该方法即可。
- 在xcode4.4后,@property进行了增强。只需要在@interface中写了@property,就会自动声明属性、声明和实现属性的set/get方法,其真正的属性名会在@property声明的属性名前面加下划线_。
- 通过@property定义的属性默认为私有的,但可以通过set/get方法去访问。
@property的属性有哪些?
- readwrite 是可读可写特性;需要生成 getter 方法和 setter 方法
- readonly 是只读特性,只会生成 getter 方法 不会生成 setter 方法,不希望属性在类外改变时使用
- assign 是赋值特性,setter 方法将传入参数赋值给实例变量;
- retain 表示持有特性,setter 方法将传入参数先保留,再赋值,传入参数的 retain count 会+1;
- copy 表示拷贝特性,setter 方法将传入对象复制一份;需要完全一份新的变量时。
- nonatomic 和 atomic ,决定编译器生成的 setter getter是否是原子操作。 atomic 表示使用原子操作,可以在一定程度上保证线程安全。一般推荐使用 nonatomic ,因为 nonatomic 编译出的代码更快
- 默认的
@property
是 readwrite,assign,atomic
@property什么时候不会自动生成set/get方法
- 有时候,仅声明
@property
是不会自动生成set/get方法的,比如- 只读(readonly)属性只实现了get方法。
- protocol中的属性只是声明了set/get方法,而不会生成实现。
- 当重载父类中的属性时,也必须手动实现set/get方法
- 使用
@dynamic
,它是一个编译器指令,用于告诉编译器:某个属性的 set 和 get 方法不会在编译时自动生成,而是在运行时动态提供
说说类对象(Class类型)?
- 在 Objective-C 中,
Class
类型是一个特殊的类型,用于表示类对象(class object)。它本质上是一个指向 Objective-C 类结构体的指针,存储了类的元数据(metadata),包括方法列表、属性列表、协议列表、父类信息等。 - Class类型有很多用途,包括:
- 动态获取类信息:
Class myClass = [NSArray class]; NSLog(@"Class name: %s", class_getName(myClass)); // 输出 "NSArray"
- 检查对象类型:
if ([someObject isKindOfClass:[NSString class]]) { NSLog(@"This is a NSString"); }
- 动态获取类信息:
- 获取Class类型的方法有:
- 通过类名调用class类方法:
[NSString class]
- 通过实例对象获取class实例属性:
@"hello, world".class
- 通过类名调用class类方法:
- 类对象在程序加载时就会自动创建,并存储在代码段中,其生命周期跟随程序。当我们调用类方法时,本质上就是使用类对象去调用类方法。
self在实例方法和类方法中有什么不同?
- self在实例方法中,指向调用该方法的实例对象(Instance)。可以通过self访问实例属性和调用实例方法,也可以通过self.class来获取其类对象,然后来调用类方法。
- self在类方法中,指向调用该方法的类对象(Class)。只可以通过self来调用类方法,不能访问实例对象的实例属性和调用实例方法。
OC是如何实现面向对象的?说说运行时对象模型?
-
在 Objective-C 中,实例对象(Instance)、Class 类型(类对象)、MetaClass(元类)和 isa 指针构成了运行时对象模型的核心,如下图所示:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Instance │ ──isa─→│ Class │ ──isa─→│ MetaClass │ │ (实例对象) │ │ (类对象) │ │ (元类) │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ superclass │ superclass ↓ ↓ ┌─────────────┐ ┌─────────────┐ │ SuperClass │ ──isa─→│SuperMetaClass│ │ (父类对象) │ │(父类的元类) │ └─────────────┘ └─────────────┘
-
实例对象(Instance):
- 通过
alloc
+init
创建的具体对象 - isa 指向:它的类对象(
Class
) - 存储内容:实例变量(每个对象自身管理的属性值)和isa
- 通过
-
类对象(Class):
- 类的运行时表示
- isa 指向:它的元类(MetaClass)
- 存储内容:实例方法列表(
-
方法)、属性声明(@property
)、协议声明(Protocol
)、superclass
指针(指向父类)
-
元类(Meta Class):
- 类对象的类,存储类方法(
+
方法) - isa 指向:根元类(Root MetaClass,通常是
NSObject
的元类)。而NSObject的元类,其isa一般都是指向自己,形成闭环。 - 存储内容:类方法列表(
+
方法)、superclass
指针(指向父类的元类)
- 类对象的类,存储类方法(
-
isa指针:建立对象与类、类与元类之间的关联,其指向了代码段的某一个内存区域。
什么是SEL?
- SEL全称叫做selector选择器,本质是一个数据类型,专门用于存储方法,并作为运行时识别某个方法名的唯一标识符。
- SEL中会包括方法名、参数名和参数数量等信息。在创建类对象时,会为每个方法创建一个对应的SEL,并且与对应的方法实现的入口地址形成键值对关系。
- SEL与类名、返回值和参数类型无关,只与方法名、参数名和参数数量相关,具有全局唯一性。
- 可以通过
@selector(方法名)
来获取某个方法对应的SEL,需要注意,方法的参数数量和名称也必须按照SEL的命名规则传入进去,如下所示:- (void)doSomething
:doSomething
- (void)doSomething:(id)arg1
:doSomething:
- (void)doSomething:(id)arg1 withOption:(NSInteger)opt
:doSomething:withOption:
- (void)setupWidth:(CGFloat)w height:(CGFloat)h
:setupWidth:height:
什么是RunTime?
Runtime 是 Objective-C 区别于 C 语言这样的静态语言的一个非常重要的特性。对于 C 语言,函数的调用会在编译期就已经决定好,在编译完成后直接顺序执行。但是 OC 是一门动态语言,函数调用变成了消息发送,在编译期不能知道要调用哪个函数。所以 Runtime 无非就是去解决如何在运行时期找到调用方法这样的问题。
说说OC的消息机制?
- 在oc中,方法的调用在底层是通过消息实现的,以
[p1 sayHi]
为例,下面是具体的步骤:- 获取sayHi方法的对应SEL对象
- 将这个SEL消息发送给p1对象
- p1对象获取到SEL消息,通过isa指针获取到类对象的方法列表,检索对应SEL的方法入口地址,即IMP。
- 如果在子类中有匹配,则直接执行,没有则查询父类,直到查询到NSObject。
- 因此,我们也可以手动给对象发送消息来达到调用方法的目的,如performSelector方法,就可以手动发送一个SEL消息。
消息机制是否会导致性能较差?
- 每一次调用方法的时候实际上是调用了一些 runtime 的消息处理函数,导致OC的方法调用相对于C来说会相对较慢,但 OC 也通过引入 cache 机制来很大程度上的克服了这个缺点。
- 每一个类都维护了一个cache,runtime在接收者所属的类的cache中查找与SEL所对应的IMP,如果没有命中就寻找当前类的方法。
什么是objc_msgSend?
- 在 Objective-C 中,
objc_msgSend()
是 Objective-C 运行时系统的核心函数,用于实现消息传递机制。它是 Objective-C 动态特性的基础。 objc_msgSend()
的函数原型为id objc_msgSend(id self, SEL op, ...);
- 其中self为消息的接收者,op为具体的消息,也可以认为是方法名,后面的参数为可变参数,主要用于传递参数。
- 当调用objc_mesSend时,Runtime会执行以下操作:
- 检查接收者是否为
nil
(如果是则直接返回) - 在接收者的类对象中查找方法实现,如果没有找到则沿着继承链向上查找
- 找到方法实现后跳转到该实现执行
- 如果都没有找到则进入消息转发流程。
- 检查接收者是否为
什么是消息转发流程?
- 消息转发流程是当Runtime没有找到对应方法的实现时,而自动进入的一个流程,主要分为动态解析、快转发、慢转发。这3个阶段任何一个阶段可以执行方法调用,则自动返回,不会进入下一阶段。如果3个阶段均失败,则会抛出异常。
- 动态解析:开发者可以重写类方法resolveInstanceMethod,在其中通过class_addMethod将没有实现的调用转为类中已有的其他方法的调用。比如以下示例,就是给eat方法的调用转为addEatMethod的调用。如果返回NO,则表示当前无法处理,进入快转发。
+ (BOOL)resolveInstanceMethod:(SEL)sel { NSString *methodName = NSStringFromSelector(sel); if ([methodName isEqualToString:@"eat"]) { SEL selName = NSSelectorFromString(@"addEatMethod"); Method method = class_getInstanceMethod(self, selName); return class_addMethod(self, sel, method_getImplementation(method), "v@:"); } return [super resolveInstanceMethod:sel]; }
- 快转发:开发者可以重写实例方法forwardingTargetForSelector,将消息转发到另一个对象中,由其他对象去负责响应消息。比如以下示例,就是将eat方法转发给OtherPerson类的对象。如果返回nil,则表示当前无法处理,进入慢转发。
- (id)forwardingTargetForSelector:(SEL)aSelector { NSString *methodName = NSStringFromSelector(aSelector); if ([methodName isEqualToString:@"eat"]){ return [[OtherPerson alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }
- 慢转发:开发者可以重写实例方法methodSignatureForSelector,生成方法签名,目的是验证SEL的合法性。如果返回nil则表示当前无法处理,直接抛出异常。如果可以处理,则进入重写的实例方法forwardInvocation来指定其他的接收者来响应消息。如以下示例。如果依然不能正确响应消息,则抛出异常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSString *methodName = NSStringFromSelector(aSelector); if ([methodName isEqualToString:@"eat"]) { //手动生成方法签名 //NSMethodSignature *signature = [NSMehtodSignature methodSignatureForSelector:aSelector]; NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"]; return signature; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { //获取签名信息 SEL selector = anInvocation.selector; //新建需要转发消息的对象 OtherPerson *person = [OtherPerson new]; //判断是否响应 if ([person respondsToSelector:selector]) { //若可以响应,则将消息转发给该对象处理 [anInvocation invokeWithTarget:person]; //也可以修改实现方法,修改响应对象 //selector = @selector(addEatMethod); //[anInvocation setSelector:selector]; //[anInvocation invokeWithTarget:self]; }else { [super forwardInvocation:anInvocation]; } }
- 即使3个阶段失败抛出异常时,我们也可以通过重写实例方法doesNotRecognizeSelector来处理方法调用时没有找到实现而抛出的异常,如下所示,但不建议这么去做。
-(void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"----Person没有实现eat方法----- "); }
类方法的消息转发流程有什么不同?
- 类方法在底层同样是用objc_msgSend来处理方法调用的,只不过接收者对象从实例对象变成了类对象,方法列表从类中查找变成了从元类中查找。
- 动态解析:开发者重写类方法resolveClassMethod,而不是resolveInstanceMethod。
- 快转发:开发者重写实例方法forwardingTargetForSelector的类方法版本。
- 慢转发:开发者重写实例方法methodSignatureForSelector和forwardInvocation的类方法版本。
什么是实例变量?
- 实例变量直接定义在 {} 中,默认权限为保护,不支持点语法,不支持额外的修饰符进行指定。
- 在方法中访问需要直接使用变量名或者是使用self -> 变量名的语法来访问。它的本质是通过对self指针的偏移来访问,类似于C语言中的结构体。
向nil发送消息会导致异常出现吗?
- 向
nil
发送任何消息是安全的,运行时会自动忽略。不会触发任何实际的方法调用,也不会引发内存访问错误。 - 如果这个消息有返回值的话,则会返回空值。比如nil(对象),或者是0,NO等。
通过实例变量调用方法可能会有什么问题?
- 直接通过实例变量调用方法时,如果
self
为nil
(比如在类中保存的一个block中),访问实例变量会变成nil + 内存偏移
,属于未定义行为(可能崩溃或读取垃圾值)。 - 属性本质是调用 getter 方法,而 Objective-C 对
[nil method]
是安全的,会自动忽略并返回nil
/0
,不会触发内存访问错误。
说说静态类型和动态类型?
- 静态类型就是实例对象定义时的类型,编译器会检查静态类型和相应的方法调用是否合法。
- 动态类型就是实例对象在运行时实际指向的类型,编译器不会去检查动态类型的方法调用是否合法,但运行时可能会报错。比如子类对象指向父类,并且调用了子类自身独有的方法,这在编译时是没问题的,但运行会报错。
说说NSObject指针和id?
- NSObject是所有类的父类,所以NSObject指针能够指向任何对象,但是在调用子类特有方法时就需要做类型转换,否则编译器会编译不通过。
- id是一个万能指针,可以指向任意的OC对象。它弥补NSObject指针的不足,不需要类型转换就可以调用子类特有方法(即能通过编译,不做编译检查)。但是注意id指针只能调用对象方法,但不能使用点语法。
说说instance类型?
- instance是一个有类型的,代表当前类或者其子类的对象的指针,只能作为方法的返回值,而不能在别的地方使用。因此主要用于初始化方法(
init...
)和 工厂方法(+ (instancetype)method
)。 - 有时候,由于设计架构上的需要,我们会写一个类方法来当作工厂方法,以此创建实例对象,如下所示:
@implementation Person + (Person *)person { return [Person new]; } @end
- 但是在使用继承时,考虑以下代码
Man *m = [Man person];
,会遇到几个问题:- 如果方法返回值为当前类的指针,那么子类继承了该类后,外部调用该工厂方法时,返回的对象指针就只能是父类对象了,而不是子类对象。
- 我们需要一个工厂方法,可以在继承时也能返回子类对象,那么在实现中就不能写死类型名,而是使用self,如
[[self alloc] init];
。这样在子类使用该工厂方法时,self也会自动指向子类的Class对象,从而创建出子类对象。 - 但问题还是存在,因为工厂方法的返回值为父类指针,那么即使创建了子类对象,也无法正常返回子类指针。
- 如果方法返回值为id,那么返回的对象指针可以任意赋值,虽然不会产生此问题,但也会导致在类型不匹配时不会自动报错,比如
Dog *d = [Person person];
- 如果方法返回值为instance,则不会有这个问题,因为它会自动匹配当前类的实例对象指针,在遇到类型不匹配的赋值时,会自动报错。
说说OC的动态类型检查?
- OC的动态类型检查的应用场景一般是用在一个遵循某个协议的对象指针上,因为在调用方法之前,我们就需要确定其是否实现了协议中的方法。
- 在运行时,可以使用实例方法respondsToSelectorr:(SEL)aSelector来判断实例对象是否可以执行某个实例方法
- 在运行时,可以使用类方法instancesRespondToSelector:(SEL)aSelector来判断类对象是否可以执行某个类方法
- 在运行时,可以使用实例方法isKindOfClasss:(Class)aClass来判断实例对象的类是否是某个类或者是其子类
- 在运行时,可以使用实例方法isMemberOfClas:(Class)aClass来判断实例对象的类是否是某个类,不包括子类
- 在运行时,可以使用类方法isSubclassOfClass:(Class)aClass来判断类对象是否是某个类的子类
说说super关键字?
super
是一个编译器指令,并非真正的对象指针。它的作用是:跳过当前类,直接从父类开始查找方法或属性,保持方法调用的继承链逻辑。- super关键字可以调用父类的实例方法
- super关键字可以调用父类的类方法
- super关键字可以访问父类实例对象的属性(这只是一种语法糖,本质上是通过set/get方法调用,实际上它无法直接访问父类的属性)。
如何重写构造函数?
- 必须要先调用父类的init方法,确保父类属性被初始化,然后将方法的返回值赋值给self
- 用init方法初始化对象有可能会失败,如果初始化失败,返回的就是ni
- 判断self的值(调用父类init方法后)是否为nil,如果不为nil,说明初始化成功
- 如果初始化成功,就初始化当前对象的属性,并返回self。
- 也可以自定义其他的构造函数,以initWith开头,携带一些特定参数。
什么是分类(Category)?
分类是 Objective-C 中一种强大的机制,以下是它的主要用途
- 扩展已有类:这是最常见的用途。比如,你想为
NSString
添加一个方法来判断字符串是否是有效的邮箱地址,你不需要子类化NSString
,只需要创建一个分类即可。 - 分解大型类:将一个庞大的类按功能划分成多个分类,分别在不同的
.m
文件中实现,使代码结构更清晰。 - 向系统的框架类添加方法:这是系统框架本身也大量使用的技术,比如
NSArray
、UIView
等都有很多系统自己用分类实现的方法。
分类存在什么问题?
- 如果有多个命名 Category 均实现了同一个方法(即出现了命名冲突),那么这些方法在运行时只有一个会被调用,具体哪个会被调用是不确定的。
- 即使只包含其中一个分类的头文件,也有可能会调用其他同名方法,这就是分类的覆盖现象。
- 分类无法添加新的属性
扩展(Extension)是什么?
Extension 可以认为是一种匿名的 Category, Extension 与 Category 有如下几点显著的区别:
- 使用 Extension 必须有原有类的源码
- Extension 声明的方法必须在类的主 @implementation 区间内实现
- Extension 可以在类中添加新的属性,Category 不可以
扩展(Extension)的用途有哪些?
- 声明私有方法:在
.m
文件中创建一个匿名分类(Class Extension,又叫扩展),可以声明一些只在类内部使用的“私有”方法和属性。 - 重定义头文件中的类属性,用于在类内部使用。例如在 interface 中定义为 readonly 类型的属性,在实现中添加 extension,将其重新定义为 readwrite,这样我们在类的内部就可以直接修改它的值,然而外部依然不能调用 setter 方法来修改。
什么是Associated Objects(关联对象)?
- 扩展可以添加属性,但分类却不可以。然而,OC在后续提供了一种新的特性:Associated Objects,专门用于动态的给对象添加属性。
- Associated Objects不依赖继承或子类化,适用于任何 NSObject的子类,基于 Objective-C Runtime,通过 objc_setAssociatedObject和 objc_getAssociatedObject实现。
- objc_setAssociatedObject和objc_getAssociatedObject的主要参数有:object(关联的对象)、key(关联的键)、value(关联的值)和policy(内存管理策略),其用法如下所示
#import <objc/runtime.h> @interface UIButton (UserInfo) @property (nonatomic, strong) NSDictionary *userInfo; @end @implementation UIButton (UserInfo) // 定义关联的 key(通常用 static const void * 确保唯一性) static const void *UserInfoKey = &UserInfoKey; - (void)setUserInfo:(NSDictionary *)userInfo { // 关联对象(策略:强引用+非原子) objc_setAssociatedObject(self, UserInfoKey, userInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDictionary *)userInfo { // 获取关联对象 return objc_getAssociatedObject(self, UserInfoKey); } @end
说说类的初始化?
在 OC 中绝大部分类都继承自 NSObject
,它有两个非常特殊的类方法 load
和 initilize
,用于类的初始化
- load类方法:
- 当类对象被加载到代码区时调用,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。
- 子类的load方法会在它的所有父类的load方法之后执行
- 分类的load方法会在它的主类的load方法之后执行。
- 不同的分类之间的load方法的调用顺序是不确定的。
- load 方法不会被类自动继承,也不需要再load方法中调用父类的load方法。
- initialize类方法
- 在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的initialize方法是永远不会被调用的。
- initialize方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 initialize方法,那么继承自父类的实现会被调用;如果一个类的分类实现了initialize方法,那么就会对这个类中的实现造成覆盖。
类属性是什么?
- 在xcode后续的版本中,OC开始支持类属性,也就是属于类而不是属于某个实例对象的属性。
- 类属性的声明方式与实例属性类似,但需要使用
class
关键字,如@property (class, nonatomic, strong) NSString *classProperty;
- 类属性也需要在@implementation中提供set/get方法,而且必须是类方法。
- 类属性本质上是语法糖,编译器会将其转换为
- 一个 static存储变量(隐藏在 .m文件中)
- 两个对应的set/get类方法
- 类属性默认是 strong,需自行管理生命周期。同时,类属性不支持
@synthesize
,必须手动实现存取方法。
什么是Block?
- Block是OC中的匿名函数,可以作为OC对象进行传递。
- Block可以捕获来自外部作用域的变量,然而,Block 中捕获的到变量是不能修改的。如果想修改,需要在Block创建前,使用__block来声明一个变量。
- 对于类中为block类型的属性应该使用
copy
,因为block在创建时是在栈上的,需要使用copy来拷贝到堆上,确保生命周期。 - block 在捕获外部变量的时候,会保持一个强引用,当在 block 中捕获
self
时,由于对象会对 block 进行copy
,于是便形成了强引用循环。 - 为了避免强引用循环,最好捕获一个
self
的弱引用,这样就避免了循环引用的问题。但是在Block的执行过程中,self可能会被清除,导致弱引用置空,使得后续的代码执行不了。 - 为了避免这个问题,需要在Block中再次强引用self,让 self 在 block 执行期间不会变为 nil。
什么是RunLoop?
- Runloop本质是一种高级循环。一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。
- Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
- 主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。
说说RunLoop的基本结构?
- RunLoop的事件源有Input Source(系统和用户创建的事件) 和 Timer Source(定时器事件)两种。
- Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。比如RunLoop 何时开始处理事件、何时进入休眠等,并插入自定义逻辑进行处理和性能分析。
- RunLoop Mode是 RunLoop 在不同场景下的事件处理策略,它决定了 RunLoop 在当前状态下会处理哪些事件。RunLoop 同一时间只能运行在一种 Mode 下,并通过切换 Mode 来适应不同的任务需求。最常用的是NSDefaultRunLoopMode(默认模式,处理大部分事件)和UITrackingRunLoopMode(界面追踪模式,滑动 UIScrollView及其子类时自动切换)
为什么在界面滑动时,定时器会失效?
- 一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 NSDefaultRunLoopMode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode。而这个时候, Timer 就不会运行
- 一个解决方法是,设置 RunLoop Mode,例如 NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个 Mode 的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
- 另一种解决 Timer 的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新 UI。
能详细说说MRC吗?
- 生成并持有对象:alloc/new/copy/mutableCopy,可以使引用计数+1
- 持有对象:retain,可以使引用计数+1
- 释放对象:release,可以使引用计数-1
- 当引用计数为0时,会自动调用对象的dealloc方法
- 在MRC下,需要注意这几个法则:
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不在需要自己持有对象的时候,释放
- 非自己持有的对象无需释放
什么是autorelease和 autoreleasepool?
- autorelease:将对象标记为“自动释放”,延迟其释放时机。对象会被添加到当前的自动释放池(
autoreleasepool
)中。 - autoreleasepool:管理通过 autorelease标记的对象。当调用drain方法,会向中的所有对象发送release消息(引用计数减 1)。
- 在MRC环境下,autorelease用于显式管理自动释放对象,并手动调用drain方法。
- 在ARC环境下,虽不能使用autorelease,但仍然可以正确管理好内存。因为在RunLoop的每一次循环中系统都自动加入了 Autorelease Pool 的创建和释放。并且,也不需要手动调用drain方法了,编译器会在autoreleasepool块结束时,自动插入drain方法。
- 我们在ARC环境下也可以使用autoreleasepool,主要有两个用途:
- 优化内存峰值,比如在循环中创建大量临时对象,可以显式提前释放这些对象,而不用等RunLoop循环结束再释放。
- 在子线程中未启用 RunLoop 时,用于管理内存。
使用autoreleasepool一定会释放对象吗?
不一定,可能会遇到下面几种情况
- 当autoreleasepool包围着的block 以异常(exception)结束时,pool 不会调用drain。
- Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。
在以模板创建项目时,main.m文件中的外层Autorelease Pool可以删除吗?
- 在 iOS 程序的 main.m 文件中有类似这样的语句
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
- UIApplicationMain 函数是整个 app 的入口,用来创建 application 对象(单例)和 application delegate。
- UIApplicationMain永远不会返回,只有在系统 kill 掉整个 app 时,系统才会把应用占用的内存全部释放出来,因此这里的 autorelease pool 也就永远不会进入到释放那个阶段。
- 而内部的UIApplication 自己会创建 main run loop,因此这里的Auto release pool可以删除,没有影响。
详细说说ARC?
- ARC 是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码。与ARC相关的关键字有:
__strong、__weak、__unsafe_unretained、__autoreleasing
__strong
是默认使用的标识符。只有还有一个强指针指向某个对象,这个对象就会一直存活__weak
声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,弱引用会被置为 nil__unsafe_unretained
声明这个引用不会保持被引用对象的存活,如果对象没有强引用了,它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针- __autoreleasing 用于标示使用引用传值的参数(id *),在函数返回时会被自动释放掉
什么是retained return value和unretained return value?
- 如果一个函数的返回值是指向一个对象的指针,那么这个对象肯定不能在函数返回之前进行 release,这样调用者在调用这个函数时得到的就是野指针了。
- 在函数返回之后也不能立刻就 release,因为我们不知道调用者是不是 retain 了这个对象,如果我们直接 release 了,可能导致后面在使用这个对象时它已经成为 nil 了。
- 为了解决这个纠结的问题, Objective-C 中对对象指针的返回值进行了区分,一种叫做 retained return value,另一种叫做 unretained return value。前者表示调用者拥有这个返回值,后者表示调用者不拥有这个返回值,按照“谁拥有谁释放”的原则,对于前者调用者是要负责释放的,对于后者就不需要了。
- 一般来说,以
alloc
,copy
,init
,mutableCopy
和new
这些方法打头的方法,返回的都是 retained return value。而其他的则是 unretained return value。
在MRC下,如何正确处理函数返回值?
对于 retained return value,外部调用者需要负责释放,此时直接返回对象指针即可。如果是unretained return value,则必须要在函数内部做预处理,比如使用autoreleasepool,保证对象在外部的run loop结束一次循环时,可以被释放。如下所示。比如说,假设我们有一个 property 定义如下
// 注意函数名的区别
+ (MyCustomClass *) myCustomClass
{
return [[[MyCustomClass alloc] init] autorelease]; // 需要 autorelease
}
- (MyCustomClass *) initWithName:(NSString *) name
{
return [[MyCustomClass alloc] init]; // 不需要 autorelease
}
在ARC下,如何正确处理函数返回值?
- 在 ARC 中我们完全不需要考虑这两种返回值类型的区别,直接返回对象指针即可。
- 对于 retained return value,ARC会自动在外部调用者持有返回值对象后,进行release操作,保证引用计数正确。
- 对于unretained return value,ARC 会把对象的生命周期延长,确保调用者能拿到并且使用这个返回值,但是并不一定会使用 autorelease。如果我们能够知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接在适当的地方使用 release 就可以,可以提高性能,减少release pool的负担。比如说当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接返回对象指针即可,然后在外部适当的地方进行release。
在ARC中,将对象置为 nil
有什么用?
- 立即释放强引用,触发可能的对象销毁
- 避免野指针,提升代码安全性
- 显式管理资源,尤其在性能敏感的场景中,比如循环中的临时对象,可以提前进行释放。
说说可变对象和不可变对象?
- Objective-C 的设计遵循一个原则:默认情况下对象应该是不可变的(Immutable),只有在确实需要修改时,才使用可变版本。
- 这种设计有以下优势:不可变对象天然是线程安全的,无需加锁。传递不可变对象时,调用方无需担心数据被意外修改。
- 以NSString和 NSMutableString为例,NSString的所有修改方法(如
appendString:
)返回新对象,而NSMutableString会直接原地修改。
可变对象和不可变对象之间的赋值关系?
- 不可变对象可以直接赋值给不可变对象,此时会进行浅拷贝
- 可变对象可以直接赋值给不可变对象,此时会进行浅拷贝,但如果后续可变对象发生修改时,不可变对象也会发生更改。
- 不可变对象无法直接赋值给可变对象,语义会发生冲突。
说说Copy和MutableCopy?
在 Objective-C 中,copy
和 mutableCopy
是定义在 NSObject
协议中的方法,用于创建对象的不可变副本或可变副本。它们两者默认是浅拷贝,也就是说对于容器类,它们两者都只复制容器本身,容器内的元素仍是原始对象的引用(元素不会被复制)。
- 不可变对象调用 copy:直接返回原对象。
- 不可变对象调用 mutableCopy:创建新的可变对象,内容与原对象相同。
- 可变对象调用 copy:创建新的不可变对象,内容与原对象相同。
- 可变对象调用 mutableCopy:创建新的可变对象,内容与原对象相同。
NSString类型的属性,可以使用strong作为修饰符吗?
通常更推荐使用 copy
,因为如果外部传入的是 NSMutableString
,后续修改会导致属性值意外变化,破坏数据不可变性。
CoCoa Touch
什么是MVC、MVP和MVVM?
-
MVC模式的意思是,软件可以分成三个部分,各部分的通信如下
- 用户在View上进行操作
- View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈
- Controller 非常薄,只起到路由的作用,而 View 非常厚,业务逻辑都部署在 View。
-
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
- 各部分之间的通信,都是双向的
- View 与 Model 不发生联系,都通过 Presenter 传递
- View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
-
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。
说说常用的几个UI相关的类?
- NSObject:OC中的基类,必须继承它才可以使用OC特性。
- UIApplication:每个 iOS/macOS 应用有且只有一个
UIApplication
实例(或其子类实例),它是整个应用的中央控制枢纽,负责管理应用级事件和状态。 - UIWindow:作为应用的顶级容器视图,负责管理视图层次结构(View Hierarchy)和协调屏幕显示。每个应用至少有一个
UIWindow
(主窗口),但可以创建多个(如外接屏幕、画中画)。 - UIEvent:封装一次完整的用户交互事件(如触摸、摇动、远程控制),包含事件类型和所有关联的触摸对象。
- UIResponder:所有可以接收和处理事件的对象的基类。
- UIView:构建用户界面的基础单元,所有屏幕上的可见元素(按钮、标签、图片等)本质上都是 UIView或其子类。它是UIResponder的子类,具备响应事件能力,主要负责内容绘制、子视图布局和用户交互。
- UIViewController:
UIViewController
(视图控制器)是 MVC 架构中的 "Controller",负责管理视图层级、处理用户交互并协调数据流动。它是连接数据模型(Model)和用户界面(View)的核心枢纽。
事件是什么,有多少种?
用户在使用手机的的过程中会产生很多“事件”,例如触摸手机屏幕、摇晃手机、利用耳机上的按键控制手机等。这些事件大体上可以分为三类
- Touch Events (触摸事件):用户的手指或触控笔在屏幕上的操作
- Motion Events(运动事件):这类事件源于设备内置的运动传感器,用于感知设备在物理世界中的移动、旋转和方位。
- Remote Events (远程控制事件):这类事件允许应用程序响应来自外部配件的物理控制命令,比如音量键的控制。
什么是响应者链?
- 当发生事件响应时,必须知道由谁来响应事件。在 iOS 中,由响应者链来对事件进行响应
- 所有事件响应的类都是 UIResponder 的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。响应者链中只要有对象处理事件,事件就停止传递。
- 当发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,也就是用户触摸屏幕的地方。
- 如果它不处理,事件就会被传递给它的视图控制器对象 ViewController(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推,直到顶层视图。
- 接下来会沿着顶层视图(top view)到窗口(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。
什么是事件分发?
事件分发是指找到某个事件的第一响应者,表示该对象正在与用户交互,它是响应者链的开端。以触摸事件为例,下面是其事件分发的步骤:
- 当用户在屏幕上点击时,会产生UITouch(可能有多个,每个代表对应的触摸点),然后系统将其打包到一个UIEvent对象中,并放入当前活动Application 的事件队列。
- UIApplication取出UIEvent,并传递给UIWindow来处理。UIWindow对象首先会使用
hitTest:withEvent:
方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图。 hitTest:withEvent:
的处理步骤很简单:首先是判断触摸点是否在当前视图内,如果是则依次遍历其中所有子视图(从最顶层到最底层,即倒序遍历subviews数组),如果某个子视图认为该触摸事件应该由自身响应,则返回自身。如果所有子视图都不响应,则返回当前视图本身。- 如果最终 hit-test 没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯。
- hit-test会忽略隐藏 (hidden=YES) 的视图,禁止用户操作 (
userInteractionEnabled=NO
) 的视图。如果一个子视图的区域超过父视图的 bound 区域,那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别, 因为父视图会先直接返回 NO。
说说UIApplication?
- UIApplication 的核心作用是提供了 iOS 程序运行期间的控制和协作工作,每一个程序在运行期必须有且仅有一个 UIApplication(或则其子类)的一个实例。
- UIApplication 的一个主要工作是处理用户事件,它维护一个事件队列,并逐个发送事件到对应的目标控件。
- 此外,UIApplication 实例还维护一个在本应用中打开的 window 列表(UIWindow 实例),这样它就 可以接触应用中的任何一个 UIView 对象。
- UIApplication 实例会被赋予一个代理对象,以处理应用程序的生命周期事件(比如程序启动和关闭)、系统事件(比如来电、记事项警告)等等。
说说UIApplication的生命周期?
- 一个 UIApplication 可以有如下几种状态
Not running(未运行)
:程序没启动Inactive(未激活)
:程序在前台运行,不过没有接收到事件,比如来电话了。Active(激活)
:程序在前台运行而且接收到了事件。这也是前台的一个正常的模式Background(后台)
:程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会。时间到之后会进入挂起状态 (Suspended)。有的程序经过特殊的请求后可以长期处于 Background 状态Suspended(挂起)
:程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。
说说UIView?
- UIView 表示屏幕上的一块矩形区域,负责渲染区域的内容,并且响应该区域内发生的触摸事件。它在 iOS App 中占有绝对重要的地位,因为 iOS 中几乎所有可视化控件都是 UIView 的子类。
- UIView 可以负责以下几种任务:绘制和动画、布局和子视图管理、事件处理。
UIView是如何进行绘制的?
- UIView 是按需绘制的,当整个视图或者视图的一部分由于布局变化,变成可见的,系统会要求视图进行绘制。
- 当视图内容发生变化时,需要调用
setNeedsDisplay
或者setNeedsDisplayInRect:
方法,告诉系统该重新绘制这个视图了。 - 视图有 frame,center,bounds 等几个基本几何属性,其中:
- frame 使用的最多,其坐标位置都是相对于父视图的,可以用于确定本视图在父视图中的位置和其自身的大小
- center 的坐标位置也是相对于父视图的,通常用于移动,旋转等动画操作
- bounds是View内部的坐标系,而非其在父视图中的位置,通常情况下就是(0,0,width,height)。
- ContentMode:视图在初次绘制完成后,系统会对绘制结果进行快照,之后尽可能地使用快照,避免重新绘制。如果视图的几何属性发生改变,系统会根据视图的 contentMode 来决定如何改变显示效果。默认的 contentMode 是
UIViewContentModeScaleToFill
,系统会拉伸当前的快照,使其符合新的 frame 尺寸。如果需要重新绘制,可以把 contentMode 设置为UIViewContentModeRedraw
,强制视图在改变大小之类的操作时调用drawRect:
重绘。
说说UIView的布局和子视图管理?
- 除了提供视图本身的内容之外,一个视图也可以表现得像一个容器。当一个视图包含其他视图时,两个视图之间就创建了一个父子关系。在这个关系中子视图被称为 subView ,父视图被称为 superView 。一个视图可以包含多个子视图,它们被存放在这个视图的
subviews
数组里。 - 当一个视图的大小改变时,它的子视图的位置和大小也需要相应地改变。UIView 支持自动布局,也可以手动对子视图进行布局。
- 视图的
autoresizesSubviews
属性决定了在视图大小发生变化时,如何自动调节子视图,它包含了多个掩码位,可以通过位运算符自由组合。 Constraint
是另一种用于自动布局的方法。本质上,Constraint 就是对 UIView 之间两个属性的一个约束,比如说当前View的高度要取决于另一个View的高度。- UIView 当中提供了一个
layoutSubviews
函数,UIView 的子类可以重载这个函数,以实现更加复杂和精细的子 View 布局。但苹果文档专门强调,应该只有在autoresizesSubviews
和Constraint
不能实现所需要的效果时,才使用layoutSubviews
。而且,layoutSubviews 方法只能被系统触发调用,程序员不能手动直接调用该方法。 layoutSubviews
的触发时机包括:- 滚动一个 UIScollView,导致这个 scrollView 以及它的父 View 调用 layoutSubviews
- 旋转设备,导致当前所响应的 ViewController 的主 View 调用 layoutSubviews
说说一些其他常见的UIView?
UIScrollView
是一个 用于显示超出屏幕范围内容的滚动容器,支持垂直/水平滚动(甚至任意方向),可以通过捏合手势缩放内容(需实现代理方法),并实现类似相册翻页效果。UIButton
继承自UIControl
(而UIControl
继承自UIView
),添加了交互事件(如点击、高亮)和状态管理(正常/禁用/选中)UILabel
直接继承自UIView
,专门用于文本显示,支持字体、颜色、对齐等样式配置。
说说UIViewController?
- UIViewController(视图控制器,VC),顾名思义,是 MVC 设计模式中的控制器部分。UIViewController 在 UIKit 中主要功能是用于控制画面的切换,其中的
view
属性(UIView 类型)管理整个画面的外观。 - VC在初始化后,其生命周期如下:
- loadView:控制器需要创建其根视图时触发(仅当未通过 Storyboard/XIB 加载时调用),用于手动创建控制器的根视图(
self.view
)。 - viewDidLoad:视图被加载到内存后(仅调用一次),用于初始化数据、设置静态UI、添加子视图。
- viewWillAppear:视图即将显示在屏幕上(每次进入界面都会调用),用于刷新数据、启动动画、处理隐藏/显示逻辑。
- viewDidAppear:视图已完全显示在屏幕上(动画完成后),用于启动一些耗时的后台操作。
- viewWillLayoutSubviews:视图即将布局其子视图(可能多次调用),用于在自动布局(Auto Layout)计算前调整视图frame。
- viewDidLayoutSubviews:视图已完成子视图布局(可能多次调用),用于获取准确的视图尺寸、执行布局后的操作。
- viewWillDisappear:视图即将从屏幕消失(跳转/返回时触发),用于保存数据、暂停任务、清理临时状态。
- viewDidDisappear:视图已完全从屏幕移除(动画完成后),用于释放资源、取消请求、移除监听。
- loadView:控制器需要创建其根视图时触发(仅当未通过 Storyboard/XIB 加载时调用),用于手动创建控制器的根视图(
说说UINavigationController?
UINavigationController
(导航控制器)是 iOS 开发中用于管理 层级式页面导航 的核心组件,它继承自UIViewController
,但拥有更强大的功能。它通过 栈(Stack) 结构管理多个视图控制器(UIViewController
),提供 前进(Push) 和 后退(Pop) 的导航功能。- 它支持顶部显示标题、返回按钮和自定义控件(如搜索框、按钮),并支持边缘右滑返回。
说说IOS中的多线程?
- Cocoa 中封装了 NSThread,它是一个控制线程执行的对象,通过它我们可以方便的得到一个线程并控制它。
- NSThread 的线程之间的并发控制,是需要我们自己来控制的,可以通过 NSCondition 实现。它的缺点是需要自己维护线程的生命周期和线程的同步和互斥等,优点是轻量,灵活。
- 在现代 Objective-C 中,苹果已经不推荐使用 NSThread 来进行并发编程,而是推荐使用 GCD 和 NSOperation
什么是GCD?
- Grand Central Dispatch(GCD) 是苹果在 Mac OS X 10.6 以及 iOS 4.0 开始引入的一个高性能并发编程机制
- GCD底层实现仍然依赖于线程,但是使用 GCD 时完全不需要考虑下层线程的有关细节,使开发者更专注于任务管理。GCD 会自动对任务进行调度,以尽可能地利用处理器资源。
- GCD中有几个重要概念:
- Dispatch Queue:一个用于维护任务的队列,它可以接受任务,然后在适当的时候执行队列中的任务
- Dispatch Sources:可以将任务注册到系统事件上,例如 socket 和文件描述符,类似于 Linux 中 epoll 的作用,当事件发生时,会自动调用任务
- Dispatch Groups:可以把一系列任务加到一个组里,组中的每一个任务都要等待整个组的所有任务都结束之后才结束.
- Dispatch Semaphores:信号量,可以实现更加复杂的并发控制,防止资源竞争
Dispatch Queue有多少种?
- Serial (串行队列):串行队列中任务会按照添加到 queue 中的顺序一个一个执行。可以创建多个串行队列,这些队列中的任务是串行执行的,但是这些队列本身可以并发执行。以下是创建串行队列的示例。
dispatch_queue_t queue; queue = dispatch_queue_create("com.example.MyQueue", NULL); // OS X 10.7 和 iOS 4.3 之前 queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_SERIAL); // 之后
- Concurrent(并行队列): 可以并发地执行多个任务,但是任务开始的顺序仍然是按照被添加到队列中的顺序。具体任务执行的线程和任务执行的并发数,都是由 GCD 进行管理的。以下是创建并行队列的示例。
dispatch queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);
系统提供的Dispatch Queue有哪些?
- 系统默认提供了四个全局可用的并行队列,其优先级不同,分别为
DISPATCH_QUEUE_PRIORITY_HIGH
,DISPATCH_QUEUE_PRIORITY_DEFAULT
,DISPATCH_QUEUE_PRIORITY_LOW
,DISPATCH_QUEUE_PRIORITY_BACKGROUND
,优先级依次降低。优先级越高的队列中的任务会更早执行dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- Main Dispatch Queue(主队列): 一个全局可见的串行队列,其中的任务会在主线程中执行。主队列通过与应用程序的 runloop 交互,把任务安插到 runloop 当中执行。因为主队列比较特殊,其中的任务确定会在主线程中执行,通常主队列会被用作同步的作用,比如更新UI。
dispatch_queue_t aQueue = dispatch_get_main_queue()
自己创建的队列与系统队列有什么不同?
- 事实上,我们自己创建的队列,最终会把任务分配到系统提供的主队列和四个全局的并行队列上,这种操作叫做 Target queues。
- 具体来说,我们创建的串行队列的 target queue 就是系统的主队列,我们创建的并行队列的 target queue 默认是系统 default 优先级的全局并行队列。所有放在我们创建的队列中的任务,最终都会到 target queue 中完成真正的执行。
- 那岂不是自己创建队列就没有什么意义了?其实不是的。通过我们自己创建的队列,可以实现比较复杂的任务之间的同步。
Dispatch Queue需要自己管理生命周期吗?
- 在 iOS6 之前,使用
dispatch_queue_create
创建的 queue 需要使用dispatch_retain
和dispatch_release
进行管理,在 iOS 6 系统把 dispatch queue 也纳入了 ARC 管理的范围,就不需要我们进行手动管理了。使用这两个函数会导致报错。 - 在iOS6之前,需要使用 assign 来修饰 queue 对象。而iOS6之后,就需要使用 strong 或者 weak 来修饰,不然会报错。
Dispatch Queue执行任务的方式有多少种?
- 给 queue 添加任务有两种方式,同步和异步。同步方式会阻塞当前线程的执行,等待添加的任务执行完毕之后,才继续向下执行。异步方式不会阻塞当前线程的执行。
- 同步和异步添加,与队列是串行队列和并行队列没有关系。可以同步地给并行队列添加任务,也可以异步地给串行队列添加任务。同步和异步添加只影响是不是阻塞当前线程,和任务的串行或并行执行没有关系。
- 如果在任务 block 中创建了大量对象,可以考虑在 block 中添加 autorelease pool。尽管每个 queue 自身都会有 autorelease pool 来管理内存,但是 pool 进行 释放 的具体时间是没办法确定的。如果应用对于内存占用比较敏感,可以自己创建 autorelease pool 来进行内存管理。
Dispatch Queue的线程安全性?
Dispatch Queue 本身是线程安全的,换句话说,你可以从系统的任何一个线程给 queue 添加任务,不需要考虑加锁和同步问题
GCD有什么缺陷?
- GCD无法直接设置任务之间的依赖关系
- 一旦将任务提交到GCD队列,很难取消正在执行或未执行的任务
- GCD不提供任务执行状态(是否正在执行、是否完成等)的查询机制
- GCD无法直接限制一个队列中同时执行的任务数量
什么是NSOperation和NSOperationQueue?
- NSOperation 是一个抽象类,我们需要继承它并且实现我们的子类。然后实现几个必要方法,比如start,cancel,main等。单独使用NSOperation,在同步模式下跟调用方法没区别,在异步模式下,跟创建一个单独的线程也没啥区别,这时候就要用到NSOperationQueue了。
- NSOperationQueue 是一个专门用于执行 NSOperation 的队列,它总是会把 operation 放到后台线程中执行。不管operation是不是异步的,queue的执行都是不会造成主线程的阻塞的。
- NSOperation和NSOperationQueue是为了解决GCD缺陷而引入的更高层封装,支持设置任务之间的依赖关系、在外部取消正在执行的任务,可以查询任务状态,并限制一个队列中同时执行的任务数量。
什么是KVO?
- KVO(键值观察)是Objective-C中的一种观察者模式实现,它允许对象监听另一个对象特定属性的变化。
- KVO包含以下几个核心概念:
- 观察者:想要监听变化的对象
- 被观察者:属性可能变化的对象
- 键路径(Key Path):被观察属性的路径
- 被观察者需要使用addObserver添加观察者和键路径,观察者需要实现observeValueForKeyPath来接收变更通知。
- 在观察者被释放前,被观察者必须使用removeObserver来移除观察者,否则会导致崩溃。
什么是NSNotification?
NSNotification
是 iOS/macOS 开发中用于实现 观察者模式(Observer Pattern) 的核心类,它允许对象之间通过广播消息的方式进行松耦合通信。NSNotification
由几个部分组成:- NSNotification:封装消息的对象,包含名称(name)、发送者(object)和附加信息(userInfo)
- NSNotificationCenter:单例中心(default),负责管理通知的注册和发送
- 观察者(Observer):监听特定通知的对象
NSNotification
的特点:- 一对多通信:一个通知可被多个观察者接收。
- 松耦合:发送者和接收者无需直接引用彼此。
- 通知的发送和接收会在同一线程,若在后台线程发送通知,而接收者需要去更新UI,则必须手动在接收者方法中切换到主线程更新UI,否则会导致 崩溃 或 UI 更新延迟。
UIView可以展示动画吗?
- UIView提供了一些方法来展示简单动画,比如animateWithDuration可以设置 UIView 动画结束后最终的效果和动画持续时间,iOS 就会自动补充中间帧,形成动画。或者是利用animateKeyframesWithDuration,设置动画中间的几个关键帧状态,实现关键帧动画。
- UIView的动画实际上是对底层CALayer的一种封装,可以使用CALayer实现更复杂的动画效果。比如基本动画、关键帧动画、组动画(多个动画一起运行)、切换动画(用于转场)
- CADisplayLink 是一个计时器对象,可以周期性的调用某个 selecor 方法。比如可以让我们以和屏幕刷新率同步的频率(每秒60次)来调用绘制函数,实现界面连续的不停重绘,从而实现动画效果。
- UIDynamicAnimator:它是 iOS 7 引入的一个新类,可以创建出具有物理仿真效果的动画,比如重力、碰撞等。
说说iOS的网络编程?
- CoreFoundation中提供了一个类:NSURLConnection 。专门用于处理用户的网络请求,NSURLConnection 基本可以满足我们大多数的网络请求操作。它可以通过发送网络请求(同步或异步),然后再通过回调获取相应的结果。
- NSURLConnection 的异步方法实际上还是有可能会阻塞主线程,因为其网络请求的回调方法还是在主线程中执行的,如果其中有一些耗时的操作,就会产生阻塞。此外,NSURLConnection 默认会跑在当前的 runloop 中,并且跑在 Default Mode,当用户执行滚动的 UI 操作时会发生 runloop mode 的切换,也就导致了 NSURLConnection 不能及时执行和完成回调。
- 为了解决阻塞问题,可以让NSURLConnection跑在独立的线程中,并执行RunLoop保活,以此保证后续回调方法的执行。在回调成功后,再结束RunLoop。这种方法目前的最佳实践就是创建一个NSOperation,在start中开启RunLoop,在cancel中关闭RunLoop,并且交由NSOperationQueue来进行管理。
说说@synchronized和dispatch_once?
@synchronized
是 Objective-C 中用于实现 线程同步 的语法糖,它提供了一种简单的方式来创建 互斥锁(Mutex Lock),确保同一时间只有一个线程能执行被保护的代码块。它需要传入一个对象来作为锁的标识(通常使用self
或一个共享对象)。dispatch_once
是 Grand Central Dispatch (GCD) 提供的一个函数,用于确保某个代码块在应用程序的整个生命周期内仅执行一次。它的底层使用原子操作和内存屏障优化,比手动加锁(如@synchronized
)更高效。
说说iOS中常用的设计模式?
- 单例模式:比如
NSNotificationCenter
就使用了单例模式,可以使用@synchronized和dispatch_once来确保线程安全。对于现在的iOS开发而言,更推荐dispatch_once,因为性能更高。 - 工厂模式:将对象的创建逻辑封装起来,而不是在代码中直接使用
new
或alloc/init
,比如创建遵循同一个协议的不同对象。 - 委托模式:一个对象(委托方)将某些职责交给另一个对象(委托对象)来完成,从而实现对象间的通信和代码解耦,委托模式通常使用协议(protocol)来实现。
- 观察者模式:它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,并在主题对象更新时自动调用自定义逻辑。iOS中常用的观察者模式有KVO和NSNotificatio。
一张图片是如何显示到屏幕上?
- 存储:图片以压缩格式(如 JPG、PNG)存储在磁盘或应用包里。
- 加载:APP将压缩的图片数据读取到内存中。
- 解码:CPU执行复杂的数学运算,将压缩数据“解压”成 GPU 能直接看懂的原始像素矩阵(位图 Bitmap)。这个矩阵包含了每个像素的 RGBA(红、绿、蓝、透明度)信息。
- 纹理: GPU 将 CPU 解码好的位图数据,从内存搬运到自己的专用高速内存——显存(VRAM) 中。此时,这份数据被称为纹理(Texture)。
- 渲染:GPU 开始进行一系列高速并行计算,得出图片层和所有其他图层(文字、背景、阴影等)叠加后的最终颜色,然后将数据存储到帧缓冲区(Frame Buffer) 中,然后在合适的时机就可以显示到屏幕上。
什么是离屏渲染?
- 在某些情况下,GPU无法直接将图层绘制到当前的帧缓冲区。它需要先开辟一个新的临时内存区域(离屏缓冲区,Off-Screen Buffer),在这个临时区域中完成渲染,再将结果复制或合成到帧缓冲区。
- 离屏渲染是为了解决一些无法在单一通道中完成的绘制任务。触发离屏渲染的常见场景都是因为系统需要获取图层的中间渲染结果,以便进行后续处理。比如圆角、裁剪、阴影和高斯模糊等。
- 离屏渲染的代价是昂贵的,主要体现在额外的内存开销、多次上下文切换(帧缓冲区和离屏缓冲区之间切换绘制上下文)、多次合成和缓冲区复制的耗时。
如何优化离屏渲染?
- 一个常见的性能优化的例子就是如何给 UIView/UIImageView 加圆角,如下是三种加圆角的方式:
- 设置 cornerRadius
- UIBezierPath
- Core Graphics(为 UIView 加圆角)与直接截取图片(为 UIImageView 加圆角)
- cornerRadius:会产生离屏渲染,如果在滚动页面中这么做的话就会遇到性能问题。可以通过
shouldRasterize = YES
会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存。 - UIBezierPath:同样会导致离屏渲染。
- Core Graphics:可以在图片本身解码后的位图上截取图片,实现圆角,这样就不会导致离屏渲染问题。
什么是CPU渲染?
- 像使用Core Graphics来进行图片截取的操作,可以将其理解为是一种CPU的预渲染,不需要使用GPU来进行渲染。
- 由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。
- 总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。
评论