iOS 进阶知识总结(一)
3~5年开发经验 的 iOS工程师 应该知道的知识点~本篇总结了以下内容
- 对象
- 类对象
- 分类
- runtime
- 消息与消息转发
导航
- 对象
- 类对象
- 分类
- runtime
- 消息与消息转发
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
- 视图渲染和离屏渲染
- 事件传递和响应链
- crash处理和性能优化
- 编译流程和启动流程
- 内存管理
- 野指针处理
- autoreleasePool
- weak
- 单例、通知、block、继承和集合
- 网络基础
- AFNetWorking
- SDWebImage
对象
id
和NSObject *
的区别?
id
是struct objc_object
结构体指针,可以指向任何OC对象
,理解为万能指针NSObject *
是指向NSObject
对象的指针
id
类型为什么无法用点语法
id
类型无法确定所指的类型,无法查找setter、getter
。
Null、nil、Nil、NSNull
NULL
,指针是空值,用来判断C指针
nil
是指一个OC对象
,指针为空Nil
是指一个OC类
为空NSNull
则用于填充集合元素;这个类只有一个方法null
==、 isEqualToString、isEqual区别?
- 1、
==
比较的是两个内存地址是否相同。 - 2、
isEqualToString
比较的是两个字符串是否相等。 - 3、
isEqual
判断两个对象在类型和值上是否都一样。
isEqual和hash的关系
isEqual
一般用于比较对象是否相等,可以重写,通过判断地址指针,判断类型,判断属性等方式。hash
,哈希值,hash
可能冲突。可以重写,一般用地址或者唯一标识合成。hash
,在对象添加到NSSet
和NSDictionary
的时候被调用,查找效率高hash
和isEqual
没有必然联系
iOS中内省的几个方法?
isMemberOfClass
:obj
是否某类的对象isKindOfClass
:obj
是否某类及其子类的对象isSubclassOfClass
某类是否是另一个类的子类isAncestorOfObject
某类是否是另一个类的父类respondsToSelector
是否能响应某方法conformsToProtocol
是否遵循某协议
深拷贝和浅拷贝
- 1、浅拷贝是引用的复制
- 2、深拷贝是值的复制,会新建一个对象
- 3、
copy
出来的对象是不可变类型,mutableCopy
出来的对象是可变类型
阻止编译器自动合成,有哪几种方式呢?
@dynamic
readonly
关键字
NSString
类型为什么要用copy
修饰 ?
- 主要防止
NSString
被修改。 - 当
NSString
的赋值来源是NSString
时,strong
和copy
作用相同。 - 当
NSString
的赋值来源是NSMutableString
,copy
会做深拷贝重新生成一个新的对象,修改赋值来源不会影响NSString
的值。
int * const p 、int const *p 、const int *p 、 const int * const p
int const *p 、const int *p
,p 可以改,*p 不可以改int * const p
,p 不可以改,*p 可以改const int * const p
p 和 *p 都不可以改
@public、@protected、@private、@package
声明各有什么含义?
@public
任何地方都能访问;@protected
该类和子类中访问,是默认的;@private
只能在本类中访问;@package
本包内使用,跨包不可以。
@synthesize
和 @dynamic
分别有什么作用
@property
默认使用@syntheszie, var = _var;
@synthesize
:如果没有手动实现setter & getter
,编译器会自动加上这两个方法。@dynamic
:告知编译器,setter & getter
由程序员实现,不需要自动生成。如果没有实现setter & getter
,编译的时候不会报错,但是使用到的时候会因找不到方法实现crash
static
有什么作用?
static
修饰的变量可以被本文件此语句之后的所有函数访问,外部文件无法访问static
的变量能且只能被初始化一次,默认为0static
修饰的函数可以被类方法访问
类
@property
的本质是什么?ivar、getter、setter
是如何生成并添加到这个类中的?
- 声明属性,系统给当前类自动生成一个带下划线的成员变量,
setter、getter
方法。@property = ivar(实例变量) + getter + setter
- 通过自动合成(
autosynthesis
),编译器在编译期自动编写访问这些属性所需的方法。除了生成getter、setter
外,编译器自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
final
关键字有什么用
- 这个关键字可以用来修饰
class、func、var
。 - 被修饰的对象无法被继承,无法被重写。
实例方法和类方法的区别?
- 1、实例方法能访问成员变量。
- 2、类方法中必须创建或者传入对象才能调用对象方法。
- 3、实例方法存储在类对象的方法列表里,类方法存在于元类的方法列表里。
- 4、类方法可以和对象方法重名。
如果在类方法中调用self
会发生什么?
- 访问到的是类对象而不是实例对象。可以通过
self
调用其他的类方法,但是无法调用对象方法。
讲一下对象,类对象,元类结构体的组成以及他们是如何相关联的?
- 1、实例对象的结构体是
objc_object
,主要存储的是isa
以及相关的一些函数,例如getIsa
、initIsa
……还有一些关于对象内存管理的相关方法,isTaggedPointer
、isWeaklyReferenced
…… - 2、类对象和元类对象其实都是
Class
,其结构体都是objc_class
,里面存储了isa
、superClass
、成员变量集合、方法集合、协议集合、cache
(方法缓存)等等…… - 3、对象的
isa
指向其类对象、类对象的isa
指向元类、元类的isa
指向根元类,也就是NSObject
的元类。
为什么对象方法中没有保存在对象结构体里面,而是保存在类对象的结构体里面?
- 每个对象都存储同一份实例方法列表太浪费。调用的时候对象只需要通过
isa
找到类对象,在其方法列表里查找就可以了。
类方法存在哪里? 为什么要有元类的存在?
- 1、类方法存储在元类的方法列表里,元类保存了类对象以及类方法的信息。
- 2、为了调用类方法,这个类对象的
isa
指针必须指向一个包含类方法的objc_class
结构体。
objc_getClass
、object_getClass
、class
方法有什么区别?
objc_getClass
- 传入类名字符串,返回类对象(
Class
)
- 传入类名字符串,返回类对象(
class
方法- 实例对象调用
class
实际上调用object_getClass(self)
,返回的是类对象 - 类对象调用
class
返回的是本身
- 实例对象调用
object_getClass
- 传入实例对象(
instance
),返回类对象(Class
) - 传入类对象(
Class
),返回元类(meta-class
)对象 - 传入元类(
meta-class
),返回基类的元类(NSObject
的meta-class
)
- 传入实例对象(
类与类之间的消息传递,有哪几种方式呢?
- 委托代理
delegate
- 消息通知
Notification
KVO
键值监听block
什么情况使用 weak
关键字,和 assign
有什么不同?
weak
weak
关键字解决了一些循环引用的问题, 比如delegate
,block
,xib
连线出来的控件一般也是weak
(也可以用strong
)weak
表明了一种“非拥有的关系”,不保留新值,也不释放旧值。weak
只能修饰OC对象。
assign
assign
一般用于修饰非OC对象,常用于基本数据类型,MRC时可以修饰OC对象。assign
修饰对象时,当对象释放后(因为不存在强引用,离开作用域对象内存可能被回收),指针的地址还是存在的。指针并没有被置为nil,下次再访问该对象就会造成野指针异常。assign
修饰基本数据类型时,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针。
copy assign retain weak关键字
copy
:建立一个索引计数为1的对象,然后释放旧对象assign
:简单赋值,不更改引用计数retain
:释放旧对象,将新对象赋予成员变量,再提高新对象的引用计数weak
:非持有关系
assign可以用于对象吗
- 这么写大概率出现野指针崩溃。因为没有强引用对象,对象会被释放但是指针还在。
什么时候用copy
关键字?
NSString、NSArray、NSDictionary
等类型有可变的子类型,为了确保其不被更改,常用copy
修饰- copy修饰的本质是为了设置
setter
方法 - 例如
setName:
传进一个nameStr
,使用copy
修饰词传给成员变量_name
的其实是[nameStr copy];
。
- copy修饰的本质是为了设置
block
用copy
修饰,是MRC沿用下来的习惯。在 MRC 中方法内部的block
是在栈区的, 使用copy
可以把它复制到堆区。在 ARC 中可以用strong
或者copy
修饰,因为block
的retain
操作也是靠copy
实现。
不用copy
会有什么问题?
strong
修饰的NSString
类型的name
属性,传一个NSMutableString
,可能有后续问题。- 例如把可变字符串
mutableString
赋值给name
,改变mutableString
的值会导致name
也跟随变化。而copy
修饰下,不会有这种变化。 - 当修饰可变类型的属性时,如
NSMutableArray、NSMutableDictionary、NSMutableString
,用strong
。当修饰不可变类型的属性时,如NSArray、NSDictionary、NSString
,用copy
。
自定义类的对象怎样才能用copy
修饰符?
- 自定义类的对象具有拷贝功能,需要实现
NSCopying
协议。 - 如果自定义的对象分为可变版本与不可变版本,要同时实现
NSCopying
与NSMutableCopying
协议。
MRC如何重写带copy
关键字的setter
?
- (void)setName:(NSString *)name {
[_name release];
_name = [name copy];
}
怎样实现外部只读的属性,让它不被外部篡改?
- 头文件用
readonly
修饰并声明该属性。 - 正常情况下,属性默认是
readwrite
,可读写,如果我们设置了只读属性,就表明不能使用setter
方法。 - 在
.m
文件中不能使用self.ivar = @"aa";
只能使用实例变量_ivar = @"aa";
- 外界想要修改只读属性的值,需要用到kvc赋值
[object setValue:@"mm" forKey:@"ivar"];
。
编程题:实现以下功能
- 1、编写一个自定义类:
Person
,父类为NSObject
。 - 2、该类有两个属性,外部只读的属性
name
,还有一个属性age
。 - 3、为该类编写一个初始化方法
initWithName:(NSString *)nameStr
,并依据该方法参数初始化name
属性。 - 4、如果两个
Person
类的name
相等,则认为两个Person
相等
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithName:(NSString *)nameStr;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)nameStr {
if (self = [super init]) {
_name = nameStr;
}
return self;
}
- (BOOL)isEqual:(id)object {
if ([object isMemberOfClass:[Person class]] == NO) {
return NO;
}
Person *p = (Person *)object;
NSString *name1 = self.name;
NSString *name2 = p.name;
if (!name1 && !name2) {
return YES;
}
return [name1 isEqualToString:name2];
}
@end
- 初始化的方法中为什么将参数赋给
_name
,为什么这样写就能访问到属性声明的示例变量?- 编辑器在编译期会自动补全出
@synthesize name = _name
的代码
- 编辑器在编译期会自动补全出
- 初始化方法中的
_name
是在什么时候生成的?分配内存的时候还是初始化的时候?- 编译的时候自动的为
name
属性生成一个实例变量_name
- 编译的时候自动的为
_name
存放在什么地方- 成员变量存储在堆中(当前对象对应的堆得存储空间中) ,不会被系统自动释放,只能有程序员手动释放。
- 初始化
return
的self
是在上面时候生成的?-
在
alloc
时候分配内存,在init
初始化的。
-
分类
category
的作用
- 从架构上说,可以分门别类存放代码,增加代码的可读性,外部可以按需加载功能
- 为已有的类做扩展,添加属性、方法、协议
- 复写方法
- 公开私有方法
- 模拟多继承
category
的实现原理,如何被加载的
category
编译完成的时候和类是分开的,在程序运行时才通过runtime
合并在一起。_objc_init
是Objcet-C runtime
的入口函数,主要读取Mach-O
文件完成OC
的内存布局,以及初始化runtime
相关数据结构。这个函数里会调用到两外两个函数,map_images
和load_Images
map_images
追溯进去发现其内部调用了_read_images
函数,_read_images
会读取各种类及相关分类的信息。- 读取到相关的信息后通过
addUnattchedCategoryForClass
函数建立类和分类的关联。 - 建立关联后通过
remethodizeClass -> attachCategories
重新规划方法列表、协议列表、属性列表,把分类的内容合并到主类 - 在
map_images
处理完成后,开始load_images
的流程。首先会调用prepare_load_methods
做加载准备,这里面会通过schedule_class_load
递归查找到NSObject
然后从上往下调用类的load
方法。 - 处理完类的
load
方法后取出非懒加载的分类通过add_category_to_loadable_list
添加到一个全局列表里 - 最后调用
call_load_methods
调用分类的load
函数
load
方法加载顺序
- 类的
load
方法在其父类load
方法后执行 - 分类的
load
方法在主类load
方法后执行 - 两个分类的
load
方法执行顺序遵循先编译先执行
load
、initialize
方法的区别什么?在继承关系中他们有什么区别
load
方法在运行时调用,加载类或分类的时候调用一次,继承关系参考load
方法加载顺序。initialize
在第一次自身或子类接受objc_msgSend
消息的时候调用。如果子类没有实现initialize
,会调用父类的。所以父类的initialize
可能被调用多次load
方法是直接通过方法调用地址调用的,initialize
则是通过isa
走位查找调用的load
方法不会被覆盖,initialize
可以覆盖
代码写在load
和initialize
中会影响启动吗
load
方法中添加操作会影响启动速度,因为load
方法在启动时调用initialize
方法在第一次调用方法时触发,对启动影响相对较小
多个category
的都有同名方法,会使用哪一个
- 重新规划方法列表的时候,后处理的分类的方法会被放在方法列表前面。
- 调用的时候只要找到考前的那个方法就会立刻执行,所以就会执行后加载的那个分类的方法。
Build Phases -> Compile Source
靠后的分类。
category
& extension
区别,能给NSObject
添加extension
吗?
extension
扩展是特殊的category
,称为匿名分类或者私有分类,可以为类添加成员变量和方法。extension
在编译期决议,category
则是在运行时加载。extension
一般用来隐藏私有信息,category
可以公开私有信息- 无法给系统类添加
extension
,但是可以给系统类添加category
extension
可以添加成员变量,而category
不可以extension
和category
都可以添加属性,但是category
的属性不能自动生成成员变量、getter、setter
分类中添加实例变量和属性分别会发生什么,还是什么时候会发生问题?为什么
- 添加实例变量编译时报错。
- 添加属性没问题,但是在运行的时候使用这个属性程序crash。原因是没有实例变量也没有
set/get
方法。 - 可以通过关联对象去实现
分类中为什么不能添加成员变量(runtime
除外)?
- 类对象在创建的时候已经定好了成员变量,但是分类是运行时加载的,无法添加。
- 类对象里的
class_ro_t
类型的数据在运行期间不能改变,再添加方法和协议都是修改的class_rw_t
的数据。 - 分类添加方法、协议是把
category
中的方法,协议放在category_t
结构体中,再拷贝到类对象里面。但是category_t
里面没有成员变量列表。 - 虽然
category
可以写上属性,其实是通过关联对象实现的,需要手动添加setter & getter
。
分类可以添加那些内容
- 实例方法
- 类方法
- 协议
- 属性
关联对象的实现和原理
- 关联对象不存储在关联对象本身内存中,而是存储在一个全局容器中;
- 这个容器是由
AssociationsManager
管理并在它维护的一个单例Hash
表AssociationsHashMap
;- 第一层
AssociationsHashMap
:类名object :bucket(map) - 第二层
ObjectAssociationMap
:key(name):ObjcAssociation(value和policy)
- 第一层
AssociationsManager
使用AssociationsManagerLock
自旋锁保证了线程安全。- 通过
objc_setAssociatedObject
给某对象添加关联值 - 通过
objc_getAssociatedObject
获取某对象的关联值 - 通过
objc_removeAssociatedObjects
移除某对象的关联值
使用关联对象,需要在主对象 dealloc 的时候手动释放么?
- 不需要,主对象通过
dealloc -> object_dispose -> object_remove_assocations
进行关联对象的释放。
能否向编译后得到的类中增加实例变量, 能否向运行时创建的类中添加实例变量?
- 不可以,编译完成的类已经生成了不可变的成员变量集合。
- 可以,通过
class_addIvar
函数给运行时创建的类添加实例变量。
主类存在了foo
方法,分类也存在foo
方法,调用时会出现什么情况? 如果想执行主类的foo
方法,如何去做?
-
主类的方法不会被调用,分类的方法会被调用。分类和主类的
foo
方法都存在方法列表里,只是分类方法在前,主类方法在后, 调用的时候会首先找到第一次出现的方法。 -
如果想要要执行主类的方法,需要逆序遍历方法列表,第一次遍历到的foo方法就是主类的方法
- (void)foo{ [类 invokeOriginalMethod:self selector:_cmd]; } + (void)invokeOriginalMethod:(id)target selector:(SEL)selector { uint count; Method *list = class_copyMethodList([target class], &count); for ( int i = count - 1 ; i >= 0; i--) { Method method = list[i]; SEL name = method_getName(method); IMP imp = method_getImplementation(method); if (name == selector) { ((void (*)(id, SEL))imp)(target, name); break; } } free(list); }
runtime
OC
是动态运行时语言是什么意思?
- 动态类型:运行时确定对象的类型,编译时期能通过,但不代表运行过程中没有问题
- 动态绑定:运行时才确定对象调用的方法(消息转发)
- 动态加载:动态库的方法实现不拷贝到程序中,只记录引用,直到使用相关方法的时候才到库里面查找方法实现
runtime
能做什么?
- 获取类的成员变量、方法、协议
- 为类添加成员变量、方法、协议
- 动态改变方法实现
class_copyIvarList
与class_copyPropertyList
的区别?
- 1、
class_copyIvarList
可以获取.h
和.m
中的所有属性以及@interface
大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)。 - 2、
class_copyPropertyList
只能获取由@property
声明的属性(包括.m
),获取的属性名称不带下划线。
class_ro_t
和class_rw_t
的区别?
class_rw_t
提供了运行时对类拓展的能力,class_rw_t
结构体中存储了class_ro_t
。class_ro_t
存储的是类在编译时已经确定的信息,是不可改变的。- 二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。简单的说
class_rw_t
存储列表使用的二维数组,class_ro_t
使用的一维数组。 - 运行时修改类的方法,属性,协议等都存储于
class_rw_t
中
什么是 Method Swizzle(黑魔法),什么情况下会使用?
Method Swizzle
是改变一个已存在的选择器(SEL
)对应的实现(IMP
)的过程。- 类的方法列表存放着
SEL
的名字和IMP
的映射关系。 - 开发者可以利用
method_exchangeImplementations
来交换2个方法中的IMP
- 开发者可以利用
method_setImplementation
来直接设置某个方法的IMP - 这就可以在运行时改变
SEL
和IMP
的映射关系,从而实现方法替换。
Method Swizzle
注意事项
- 为了确保
Swizzle Method
方法替换一定被执行调用,可以在load
中执行 +load
里面使用的时候不要调用[super load]
。如果多次调用了[super load]
,可能会出现“Swizzle无效”的假象- 避免调用
[super load]
导致Swizzling
多次执行,在load
中使用dispatch_once
确保交换只被执行一次。 - 子类替换没有实现的继承方法,会替换掉父类中的实现,影响父类及其他子类
+initialize
里面使用要加dispatch_once
- 进行版本迭代的时候需要进行一些检验,防止系统库的函数发生了变化
如何hook
一个对象的方法,而不影响其它对象
- 方法1:新建一个子类重写方法
- 方法2:让这个对象的类遵循某个协议,
hook
时判断。弊端是其他对象遵循了这个协议会受到影响。 - 方法3:运行时创建一个新的子类,修改对象
isa
指针指向子类,hook
时使用isKindOf
判断类型
消息发送
消息机制
- 1、快速查找,方法缓存
- 2、慢速查找,方法列表
- 3、消息转发
- 3-1、方法的动态解析,
resolveInstanceMethod
- 3-2、快速消息转发,
forwardingTargetForSelector
- 3-3、标准消息转发,
methodSignatureForSelector & forwardInvocation
- 3-1、方法的动态解析,
objc
中向一个nil
对象发送消息将会发生什么?
- 在寻找对象的
isa
指针时,返回地址0x0
,不回做任何操作,也不会有任何错误。
objc
在向一个对象发送消息时,发生了什么?
- 方法调用实际上是发送消息,通过调用
objc_msgSend()
实现的。 - 首先,通过
obj
的isa
指针找到对应的class
。 - 然后,开启快速查找流程。在
class
的缓存方法列表(objc_cache
)里查找方法,如果找到就直接返回对应IMP
。 - 如果在缓存中找不到,开始慢速查找流程。在
class
的Method List
查找对应方法,找到了返回对应IMP
。 - 都找不到就会走消息转发流程
_objc_msgForward
函数是做什么的?
_objc_msgForward
用于消息转发:向一个对象发送一条消息,但它并没有实现的时候,就调用_objc_msgForward
尝试做消息转发。
为什么需要做方法缓存?
- 每次执行这个方法的时候都查一遍
Method List
太消耗性能。 - 使用
objc_cache
把调用过的方法做一个缓存, 把method_name
作为key
,method_IMP
作为value
。 - 下次接收到消息的时候,直接通过
objc_cache
去找到对应的IMP
即可, 避免每一次都去遍历objc_method_list
一直都找不到方法怎么办?
- 会触发消息转发机制,我们一共有三次机会补救以防止
crash
- 方法的动态解析,通过
resolveInstanceMethod
添加一个IMP
使其执行。 - 快速消息转发,在
forwardingTargetForSelector
返回一个可以执行该方法的对象。 - 标准消息转发,
methodSignatureForSelector
创建相同方法类型的方法签名(NSMethodSignature
),然后重写forwardInvocation
并把拥有该签名的方法赋值到anInvocation.selector
。
消息转发机制的优劣
- 优点:消息转发机制提供了找不到方法时的补救机会。
- 缺点:一般情况下会在基类做crash处理,那么有可能把一部分的crash忽略过去导致无法暴露问题。
IMP
、SEL
、Method
的区别和使用场景
SEL
相当于一个代号,方便查找方法的代号,处理通知/定时器等都会用到IMP
是指向方法实现的指针,动态方法解析的时候会用到Method
是一个对象,里面就存有SEL
和IMP
,消息转发流程获取方法签名的时候会用到
转载自:https://juejin.cn/post/7075694342265896990