03 知识点梳理:iOS底层
1 KVC原理
KVC
可以通过Key
直接访问对象的属性,可以方便的在运行时对对象的属性进行访问和修改。
需要注意的是: 有时我们通过
KVC
去修改系统控件未暴露出的属性的时候,会出现部分内部API进行了调整导致崩溃。
1.1 API
forKeyPath
用于访问对象内对象属性的属性,如:user.name
1.2 写过程
查看
setKey:
方法是否存在, 如果存在直接调用, 如果不存在进入下一步查看
_setKey:
方法是否存在, 如果存在直接调用, 如果不存在进入下一步查看
+ (BOOL)accessInstanceVariablesDirectly
方法的返回值, 默认返回YES
YES
: 可以访问成员变量, 进入下一步NO
: 不可以访问成员变量, 同时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法, 如果方法不存在会抛出异常
调用成员变量:
_key
,_isKey
,key
,isKey
调用顺序, 从左到右, 只有发现存在成员变量, 就不会在调用后续变量
如果没有成员变量, 会调用
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法, 如果方法不存在会抛出异常
1.3 读过程
判断是否有这几个方法:
getKey
,key
,isKey
,_key
从左到右, 如果有方法, 直接调用, 取值结束
如果没有进入下一步
调用+ (BOOL)accessInstanceVariablesDirectly查看是否可以访问成员变量. 默认
YES
YES
: 可以访问成员变量, 进入下一步NO
: 不可以访问成员变量, 判断是否实现- (id)valueForUndefinedKey:(NSString *)key
方法, 实现时调用, 未实现报错
判断是否有这几个成员变量:
_key
,_isKey
,key
,isKey
从左到右, 如果有成员变量, 直接访问, 取值结束
如果没有这几个成员变量, 直接进入下一步
判断是否实现
- (id)valueForUndefinedKey:(NSString *)key
方法, 实现时调用, 未实现报错
1.4 如何禁用KVC
如果重写了类方法 + (BOOL)accessInstanceVariablesDirectly
返回 NO
的话,那么会直接调用 valueForUndefinedKey:
方法,默认是抛出异常
2 讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
A: atomic的实现机制
atomic
是property
的修饰词之一,表示是原子性的,使用方式为@property(atomic)int age
;此时编译器会自动生成getter/setter
方法,最终会调用objc_getProperty
和objc_setProperty
方法来进行存取属性。若此时属性用
atomic
修饰的话,在这两个方法内部使用os_unfair_lock
来进行加锁,来保证读写的原子性。锁都在PropertyLocks
中保存着(在iOS平台会初始化8个,mac平台64个),在用之前,会把锁都初始化好,在需要用到时,用对象的地址加上成员变量的偏移量为key
,去PropertyLocks
中去取。因此存取时用的是同一个锁,所以atomic能保证属性的存取时是线程安全的。注:由于锁是有限的,不用对象,不同属性的读取用的也可能是同一个锁
B: atomic为什么不能保证绝对的线程安全?
atomic
在getter/setter
方法中加锁,仅保证了存取时的线程安全,假设我们的属性是@property(atomic)NSMutableArray *array
;可变的容器时,无法保证对容器的修改是线程安全的.在编译器自动生产的
getter/setter
方法,最终会调用objc_getProperty
和objc_setProperty
方法存取属性,在此方法内部保证了读写时的线程安全的,当我们重写getter/setter
方法时,就只能依靠自己在getter/setter
中保证线程安全
3 Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
Autoreleasepool
是由多个AutoreleasePoolPage
以双向链表的形式连接起来的.Autoreleasepool
的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage
中设置一个标记位,在此期间,当有对象调用autorelsease
时,会把对象添加AutoreleasePoolPage
中若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为
hotPage
,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release
方法,直到遇到标志位。
AutoreleasePoolPage
结构如下
4 iOS中内省的几个方法?class方法和objc_getClass方法有什么区别?
4.1 什么是内省?
在计算机科学中,内省是指计算机程序在运行时(Run time)检查对象(Object)类型的一种能力,通常也可以称作运行时类型检查。 不应该将内省和反射混淆。相对于内省,反射更进一步,是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。
4.2 iOS中内省的几个方法?
isMemberOfClass
//对象是否是某个类型的对象isKindOfClass
//对象是否是某个类型或某个类型子类的对象isSubclassOfClass
//某个类对象是否是另一个类型的子类isAncestorOfObject
//某个类对象是否是另一个类型的父类respondsToSelector
//是否能响应某个方法conformsToProtocol
//是否遵循某个协议*
4.3 class
方法和object_getClass
方法有什么区别?
实例
class
方法就直接返回object_getClass(self)
类
class
方法直接返回self
,而object_getClass
(类对象),则返回的是元类
5 分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
分类
主要用来为某个类添加方法,属性,协议(我一般用来为系统的类扩展方法或者把某个复杂的类的按照功能拆到不同的文件里)
扩展
主要用来为某个类原来没有的成员变量、属性、方法。注:方法只是声明(我一般用扩展来声明私有属性,或者把.h的只读属性重写成可读写的)
分类和扩展的区别:
分类是在运行时把分类信息合并到类信息中,而扩展是在编译时,就把信息合并到类中的
分类声明的属性,只会生成
getter/setter
方法的声明,不会自动生成成员变量和getter/setter
方法的实现,而扩展会分类不可用为类添加实例变量,而扩展可以
分类可以为类添加方法的实现,而扩展只能声明方法,而不能实现
分类的局限性:
无法为类添加实例变量,但可通过关联对象进行实现,注:关联对象中内存管理没有weak,用时需要注意野指针的问题,可通过其他办法来实现,具体可参考iOS weak 关键字漫谈
分类的方法若和类中原本的实现重名,会覆盖原本方法的实现,注:并不是真正的覆盖
多个分类的方法重名,会调用最后编译的那个分类的实现
分类的结构体里有哪些成员
6 能不能简述一下 Dealloc 的实现机制
Dealloc 的实现机制是内容管理部分的重点,把这个知识点弄明白,对于全方位的理解内存管理的只是很有 必要。
1.Dealloc 调用流程
1.首先调用
_objc_rootDealloc()
2.接下来调用
rootDealloc()
3.这时候会判断是否可以被释放,判断的依据主要有 5 个,判断是否有以上五种情况
NONPointer_ISA
weakly_reference
has_assoc
has_cxx_dtor
has_sidetable_rc
4-1.如果有以上五中任意一种,将会调用
object_dispose()
方法,做下一步的处理。4-2.如果没有之前五种情况的任意一种,则可以执行释放操作,
C 函数的 free()
。5.执行完毕。
2.object_dispose() 调用流程。
1.直接调用
objc_destructInstance()
。2.之后调用 C 函数的 free()。
3.objc_destructInstance()
调用流程
1.先判断
hasCxxDtor
,如果有 C++ 的相关内容,要调用object_cxxDestruct()
,销毁 C++ 相关的内容。2.再判断
hasAssocitatedObjects
,如果有的话,要调用object_remove_associations()
, 销毁关联对象的一系列操作。3.然后调用
clearDeallocating()
。4.执行完毕。
4.clearDeallocating()
调用流程。
1.先执行
sideTable_clearDellocating()
。2.再执行
weak_clear_no_lock
,在这一步骤中,会将指向该对象的弱引用指针置为nil
。3.接下来执行
table.refcnts.eraser()
,从引用计数表中擦除该对象的引用计数。4.至此为止,
Dealloc
的执行流程结束。
7 静态链接了解么?静态库和动态库的区别?
静态链接是指将多个目标文件合并为一个可执行文件,直观感觉就是将所有目标文件的段合并。需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否“可执行”。
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
8 App网络层有哪些优化策略?
优化DNS解析和缓存
对传输的数据进行压缩,减少传输的数据
使用缓存手段减少请求的发起次数
使用策略来减少请求的发起次数,比如在上一个请求未着地之前,不进行新的请求
避免网络抖动,提供重发机制
9 [self class] 与 [super class]
self和super的区别:
self
是类的一个隐藏参数,每个方法的实现的第一个参数即为self
。super并不是隐藏参数,它实际上只是一个**”编译器标示符”**,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
在调用[super class]
的时候,runtime
会去调用objc_msgSendSuper
方法,而不是objc_msgSend
在objc_msgSendSuper
方法中,第一个参数是一个objc_super
的结构体,这个结构体里面有两个变量,一个是接收消息的receiver
,一个是当前类的父类super_class
。
入院考试第一题错误的原因就在这里,误认为[super class]
是调用的[super_class class]
。
objc_msgSendSuper
的工作原理应该是这样的:
从
objc_super
结构体指向的superClass
父类的方法列表开始查找selector,找到后以
objc->receiver
去调用父类的这个selector
。注意,最后的调用者是objc->receiver
,而不是super_class
!
那么objc_msgSendSuper
最后就转变成
10 isKindOfClass 与 isMemberOfClass
下面代码输出什么?
先来分析一下源码这两个函数的对象实现
首先题目中NSObject 和 Sark分别调用了class方法。
+ (BOOL)isKindOfClass:(Class)cls
方法内部,会先去获得object_getClass
的类,而object_getClass
的源码实现是去调用当前类的obj->getIsa()
,最后在ISA()
方法中获得meta class
的指针。接着在
isKindOfClass
中有一个循环,先判断class
是否等于meta class
,不等就继续循环判断是否等于super class
,不等再继续取super class
,如此循环下去。
[NSObject class]
执行完之后调用isKindOfClass
,第一次判断先判断NSObject
和NSObject
的meta class
是否相等,之前讲到meta class
的时候放了一张很详细的图,从图上我们也可以看出,NSObject
的meta class
与本身不等。接着第二次循环判断
NSObject
与meta class
的superclass
是否相等。还是从那张图上面我们可以看到:Root class(meta)
的superclass
就是Root class(class)
,也就是NSObject
本身。所以第二次循环相等,于是第一行res1
输出应该为YES
。同理,
[Sark class]
执行完之后调用isKindOfClass
,第一次for
循环,Sark
的Meta Class
与[Sark class]
不等,第二次for循环
,Sark
Meta Class的super class
指向的是NSObject Meta Class
, 和Sark Class
不相等。第三次for循环,
NSObject Meta Class
的super class
指向的是NSObject Class
,和Sark Class
不相等。第四次循环,NSObject Class
的super class
指向nil
, 和Sark Class
不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO
。如果把这里的Sark改成它的实例对象,
[sark isKindOfClass:[Sark class]
,那么此时就应该输出YES
了。因为在isKindOfClass
函数中,判断sark的meta class
是自己的元类Sark
,第一次for循环就能输出YES
了。isMemberOfClass
的源码实现是拿到自己的isa指针
和自己比较,是否相等。第二行
isa
指向NSObject
的Meta Class
,所以和NSObject Class
不相等。第四行,isa
指向Sark
的Meta Class
,和Sark Class
也不等,所以第二行res2
和第四行res4
都输出NO。
11 Class与内存地址
下面的代码会?Compile Error / Runtime Crash / NSLog…?
这道题有两个难点。
难点一:
obj
调用speak
方法,到底会不会崩溃。难点二:如果
speak
方法不崩溃,应该输出什么?
首先需要谈谈隐藏参数self和_cmd的问题。 当[receiver message]
调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self
和_cmd
,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self
在已经明白了,接下来就来说说_cmd
。_cmd
表示当前调用方法,其实它就是一个方法选择器SEL
。
难点一,能不能调用
speak
方法?
答案是可以的。obj
被转换成了一个指向Sark Class
的指针,然后使用id
转换成了objc_object
类型。obj
现在已经是一个Sark
类型的实例对象了。当然接下来可以调用speak的方法。
难点二,如果能调用
speak
,会输出什么呢?
很多人可能会认为会输出sark相关的信息。这样答案就错误了。
正确的答案会输出
my name is <ViewController: 0x7ff6d9f31c50>
内存地址每次运行都不同,但是前面一定是
ViewController。why?
我们把代码改变一下,打印更多的信息出来。
我们把对象的指针地址都打印出来。输出结果:
objc_msgSendSuper2 解读
objc_msgSendSuper2方法入参是一个objc_super *super。
所以按viewDidLoad执行时各个变量入栈顺序从高到底为self
, _cmd
, self.class
, self
, obj
。
第一个
self
和第二个_cmd
是隐藏参数。第三个
self.class
和第四个self
是[super viewDidLoad]
方法执行时候的参数。在调用
self.name
的时候,本质上是self
指针在内存向高位地址偏移一个指针。在32位下面,一个指针是4字节=4*8bit=32bit
。(64位不一样但是思路是一样的)从打印结果我们可以看到,
obj
就是cls
的地址。在obj
向上偏移32bit
就到了0x7fff543f5aa8
,这正好是ViewController
的地址。
所以输出为my name is <ViewController: 0x7fb570e2ad00>
。
至此,Objc
中的对象到底是什么呢?
实质:
Objc
中的对象是一个指向ClassObject
地址的变量,即id obj = &ClassObject
, 而对象的实例变量void *ivar = &obj + offset(N)
加深一下对上面这句话的理解,下面这段代码会输出什么?
由于加了一个字符串,结果输出就完全变了,[(__bridge id)obj speak]
;这句话会输出“my name is halfrost”
原因还是和上面的类似。按
viewDidLoad
执行时各个变量入栈顺序从高到底为self
,_cmd
,self.class
,self
,myName
,obj
。obj
往上偏移32位,就是myName
字符串,所以输出变成了输出myName
了。
12 iOS开发中的加密方式
iOS加密相关算法框架:CommonCrypto
。
1:对称加密: DES、3DES、AES
加密和解密使用同一个密钥。
加密解密过程:
明文->密钥加密->密文
,密文->密钥解密->明文
。
优点:算法公开、计算量少、加密速度快、加密效率高、适合大批量数据加密;
缺点:双方使用相同的密钥,密钥传输的过程不安全,易被破解,因此为了保密其密钥需要经常更换。
AES:AES又称高级加密标准,是下一代的加密算法标准,支持128、192、256位密钥的加密,加密和解密的密钥都是同一个。iOS一般使用ECB模式,16字节128位密钥。
AES算法主要包括三个方面:轮变化、圈数和密钥扩展。
优点:高性能、高效率、灵活易用、安全级别高。
缺点:加密与解密的密钥相同,所以前后端利用AES进行加密的话,如何安全保存密钥就成了一个问题。
DES:数据加密标准,DES算法的入口参数有三个:
Key
、Data
、Mode
。
其中Key为7个字节共56位,是DES算法的工作密钥;Data为8个字节64位,是要被加密或被解密的数据;Mode为DES的工作方式,有两种:加密、解密。
缺点:与AES相比,安全性较低。
3DES:3DES是DES加密算法的一种模式,它使用3条64位的密钥对数据进行三次加密。是DES向AES过渡的加密算法,是DES的一个更安全的变形。它以DES为基本模块,通过组合分组方法设计出分组加密算法。
2.非对称加密:RSA加密
非对称加密算法需要成对出现的两个密钥,公开密钥(
publickey
) 和私有密钥(privatekey
) 。**加密解密过程:**对于一个私钥,有且只有一个与之对应的公钥。生成者负责生成私钥和公钥,并保存私钥,公开公钥。
公钥加密,私钥解密;或者私钥数字签名,公钥验证。公钥和私钥是成对的,它们互相解密。
特点:
1). **对信息保密,防止中间人攻击:**将明文通过接收人的公钥加密,传输给接收人,因为只有接收人拥有对应的私钥,别人不可能拥有或者不可能通过公钥推算出私钥,所以传输过程中无法被中间人截获。只有拥有私钥的接收人才能阅读。此方法通常用于交换对称密钥。
2). **身份验证和防止篡改:**权限狗用自己的私钥加密一段授权明文,并将授权明文和加密后的密文,以及公钥一并发送出来,接收方只需要通过公钥将密文解密后与授权明文对比是否一致,就可以判断明文在中途是否被篡改过。此方法用于数字签名。
**优点:**加密强度小,加密时间长,常用于数字签名和加密密钥、安全性非常高、解决了对称加密保存密钥的安全问题。
**缺点:**加密解密速度远慢于对称加密,不适合大批量数据加密。
3. 哈希算法加密:MD5加密
、.SHA加密
、HMAC加密
哈希算法加密是通过哈希算法对数据加密,加密后的结果不可逆,即加密后不能再解密。
**特点:**不可逆、算法公开、相同数据加密结果一致。
**作用:**信息摘要,信息“指纹”,用来做数据识别的。如:用户密码加密、文件校验、数字签名、鉴权协议。
MD5加密:**对不同的数据加密的结果都是定长的32位字符。
**.SHA加密:**安全哈希算法,主要适用于数字签名标准(
DSS
)里面定义的数字签名算法(DSA)。对于长度小于2^64
位的消息,SHA1
会产生一个160位的消息摘要。当接收到消息的时候,这个消息摘要可以用来验证数据的完整性。在传输的过程中,数据很可能会发生变化,那么这时候就会产生不同的消息摘要。当然除了SHA1
还有SHA256
以及SHA512
等。
**HMAC加密:**给定一个密钥,对明文加密,做两次“散列”,得到的结果还是32位字符串。
4. Base64加密
一种编码方式,严格意义上来说不算加密算法。其作用就是将二进制数据编码成文本,方便网络传输。
用
base64
编码之后,数据长度会变大,增加了大约 1/3,但是好处是编码后的数据可以直接在邮件和网页中显示;虽然
base64
可以作为加密,但是base64
能够逆运算,非常不安全!base64 编码有个非常显著的特点,末尾有个 ‘=’ 号。
原理:
1). 将所有字符转化为ASCII码;
2). 将ASCII码转化为8位二进制;
3). 将二进制三位一组不足补0,共24位,再拆分成6位一组共四组;
4). 统一在6位二进制前补两个0到八位;
5). 将补0后的二进制转为十进制;
6). 最后从Base64编码表获取十进制对应的Base64编码。
13 App安全,数字签名,App签名,重签名
因为应用实际上是一个加壳的ipa文件,但是有可能被砸壳甚至越狱手机下载的ipa包直接就是脱壳的,可以直接反编译,所以不要在plist文件
、项目中的静态文件
中存储关键的信息。所以敏感信息对称加密存储或者就存储到keychain
里。而且加密密钥也要定期更换。
数字签名是通过HASH算法
和RSA加密
来实现的。 我们将明文数据加上**通过RSA加密
的数据HASH值
**一起传输给对方,对方可以解密拿出HASH值
来进行验证。这个通过RSA加密HASH值
数据,我们称之为数字签名。
App签名
1.在Mac开发机器上生成一对公钥和私钥,这里称为公钥L,私钥L(L:Local)。
2.苹果自己有固定的一对公钥和私钥,私钥在苹果后台,公钥在每个iOS设备上。这里称为公钥A,私钥A(A:Apple)。
3.把开发机器上的公钥L传到苹果后台,用苹果后台的私钥A去签名公钥L。得到一个包含公钥L以及其签名数据证书。
4.在苹果后台申请AppID,配置好设备ID列表和APP可使用的权限,再加上第③步的证书,组成的数据用私钥A签名,把数据和签名一起组成一个
Provisioning Profile
描述文件,下载到本地Mac开发机器。5.在开发时,编译完一个APP后,用本地的私钥L对这个APP进行签名,同时把第④步得到的
Provisioning Profile
描述文件打包进APP里,文件名为embedded.mobileprovision
,把 APP安装到手机上。6.在安装时,iOS系统取得证书,通过系统内置的公钥A,去验证
embedded.mobileprovision
的数字签名是否正确,里面的证书签名也会再验一遍。7.确保了
embedded.mobileprovision
里的数据都是苹果授权的以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 APP 里的 Entitlements 对应等。
14 OC数据类型
① 基本数据类型
C语言基本数据类型(如
short、int、float
等)在OC中都不是对象,只是一定字节的内存空间用于存储数值,他们都不具备对象的特性,没有属性方法可以被调用。OC中的基本数据类型:
NSInteger
(相当于long型整数)、NSUInteger
(相当于unsigned long型整数)、CGFloat
(在64位系统相当于double,32位系统相当于float)等。他们并不是类,只是用
typedef
对基本数据类型进行了重定义,他们依然只是基本数据类型
。**枚举类型:**其本质是无符号整数。
**BOOL类型:**是宏定义,OC底层是使用signed char来代表BOOL。
② 指针数据类型
指针数据类型包括: 类class
、id
。
类class:
NSString
、NSSet
、NSArray
、NSMutableArray
、NSDictionary
、NSMutableDictionary
、NSValue
、NSNumber(继承NSValue)
等,都是class,创建后便是对象,继承NSObject。
OC中提供了NSValue
、NSNumber
来封装C语言的基本类型,这样我们就可以让他们具有面向对象的特征了。
id:
id
是指向Objective-C对象的指针,等价于C语言中的void*
,可以映射任何对象指针指向他,或者映射它指向其他的对象。常见的id类型就是类的delegate属性。
集合NSSet和数组NSArray区别:
都是存储不同的对象的地址;
但是NSArray是有序的集合,NSSet是无序的集合,它们俩可以互相转换。
NSSet会自动删除重复元素。
集合是一种哈希表,运用散列算法,查找集合中的元素比数组速度更快,但是它没有顺序。
③ 构造类型
**构造类型包括:**结构体、联合体
结构体:
struct
,将多个基本数据类型的变量组合成一个整体。结构体中访问内部成员用点运算符访问。联合体(共用体):
union
,有些类似结构体struct
的一种数据结构,联合体(union
)和结构体(struct
)同样可以包含很多种数据类型和变量。
结构体和联合体的区别:
结构体(
struct
)中所有变量是“共存”的,同一时刻每个成员都有值,其sizeof
为所以成员的和。**优点:**是“有容乃大”,全面;
**缺点:**是struct内存空间的分配是粗放的,不管用不用,全分配,会造成内存浪费。
联合体(
union
)中各变量是“互斥”的,同一时刻只有一个成员有值,其sizeof
为最长成员的sizeof
。**优点:**是内存使用更为精细灵活,也节省了内存空间。
**缺点:**就是不够“包容”,修改其中一个成员时会覆盖原来的成员值;
15 property和属性修饰符
@property的本质是 ivar(实例变量)
+ setter
+ getter
.
我们每次增加一个属性时内部都做了什么:
1.系统都会在
ivar_list
中添加一个成员变量的描述;2.在
method_list
中增加setter
与getter
方法的描述;3.在属性列表中增加一个属性的描述;
4.然后计算该属性在对象中的偏移量;
5.给出
setter
与getter
方法对应的实现,在setter
方法中从偏移量的位置开始赋值,在getter
方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转。
修饰符:
MRC下:
assign、retain、copy、readwrite、readonly、nonatomic、atomic
等。ARC下:
assign、strong、weak、copy、readwrite、readonly、nonatomic、atomic、nonnull、nullable、null_resettable、_Null_unspecified
等。
下面分别解释
assign
:用于基本数据类型,不更改引用计数。如果修饰对象(对象在堆需手动释放内存,基本数据类型在栈系统自动释放内存),会导致对象释放后指针不置为nil 出现野指针。retain
:和strong一样,释放旧对象,传入的新对象引用计数+1;在MRC中和release成对出现。strong
:在ARC中使用,告诉系统把这个对象保留在堆上,直到没有指针指向,并且ARC下不需要担心引用计数问题,系统会自动释放。weak
:在被强引用之前,尽可能的保留,不改变引用计数;weak引用是弱引用,你并没有持有它;它本质上是分配一个不被持有的属性,当引用者被销毁(dealloc)时,weak引用的指针会自动被置为nil。可以避免循环引用。copy
:一般用来修饰不可变类型属性字段,如:NSString
、NSArray
、NSDictionary
等。用copy修饰可以防止本对象属性受外界影响,在NSMutableString
赋值给NSString
时,修改前者 会导致 后者的值跟着变化。还有block
也经常使用 copy 修饰符,但是其实在ARC中编译器会自动对block进行copy操作,和strong的效果是一样的。但是在MRC中方法内部的block是在栈区,使用copy可以把它放到堆区。readwrite
:可以读、写;编译器会自动生成setter/getter方法。readonly
:只读;会告诉编译器不用自动生成setter方法。属性不能被赋值。nonatomic
:非原子性访问。用nonatomic意味着可以多线程访问变量,会导致读写线程不安全。但是会提高执行性能。atomic
:原子性访问。编译器会自动生成互斥锁,对 setter 和 getter 方法进行加锁来保证属性的 赋值和取值 原子性操作是线程安全的,但不包括可变属性的操作和访问。比如我们对数组进行操作,给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。原子性访问的缺点是会消耗性能导致执行效率慢。nonnull
:设置属性或方法参数不能为空,专门用来修饰指针的,不能用于基本数据类型。nullable
:设置属性或方法参数可以为空。null_resettable
:设置属性,get方法不能返回为空,set方法可以赋值为空。_Null_unspecified
:设置属性或方法参数不确定是否为空。
后四个属性应该主要就是为了提高开发规范,提示使用的人应该传什么样的值,如果违反了对规范值的要求,就会有警告。
weak修饰的对象释放则自动被置为nil的实现原理:
Runtime
维护了一个weak表
,存储指向某个对象的所有weak指针
。weak表
其实是一个hash(哈希)表
,Key
是所指对象的地址,Value
是weak
指针的地址数组(这个地址的值是所指对象的地址)。
weak 的实现原理可以概括一下三步:
1、初始化时:
runtime
会调用objc_initWeak
函数,初始化一个新的weak指针指向对象的地址。2、添加引用时:
objc_initWeak
函数会调用objc_storeWeak()
函数,objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。3、释放时,调用
clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有weak
指针地址的数组,然后遍历这个数组把其中的数据设为nil
,最后把这个entry从weak表中删除,最后清理对象的记录。
16 成员变量ivar
和属性property
的区别,以及不同关键字的作用
ivar
和属性property
的区别,以及不同关键字的作用**成员变量:**成员变量的默认修饰符是@protected
、不会自动生成set和get方法,需要手动实现、不能使用点语法调用,因为没有set和get方法,只能使用->
。
**属性:**属性会默认生成带下划线的成员变量和setter/getter
方法、可以用点语法调用,实际调用的是set和get方法。
注意:分类中添加的属性是不会自动生成 setter/getter
方法的,必须要手动添加。
**实例变量:**class类进行实例化出来的对象为实例对象
关键字作用:
访问范围关键字
@public
:声明公共实例变量,在任何地方都能直接访问对象的成员变量。@private
:声明私有实例变量,只能在当前类的对象方法中直接访问,子类要访问需要调用父类的get/set方法。@protected
:可以在当前类及其子类对象方法中直接访问(系统默认)。@package
:在同一个包下就可以直接访问,比如说在同一个框架。关键字
@property
:声明属性,自动生成一个以下划线开头的成员变量_propertyName(默认用@private修饰)、属性setter、getter方法的声明、属性setter、getter方法的实现。**注意:**在协议@protocol
中只会生成getter和setter方法的声明,所以不仅需要手动实现getter和setter方法还需要手动定义变量。@sythesize
:修改@property自动生成的_propertyName成员变量名,@synthesize propertyName = newName;
。@dynamic
:告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。**谨慎使用:**如果对属性赋值取值可以编译成功,但运行会造成程序崩溃,这就是常说的动态绑定。@interface
:声明类@implementation
:类的实现@selecter
:创建一个SEL,类成员指针@protocol
:声明协议@autoreleasepool
:ARC中的自动释放池@end
:类结束
17 类簇
类簇是Foundation框架中广泛使用的设计模式。类簇在公共抽象超类下对多个私有的具体子类进行分组。以这种方式对类进行分组简化了面向对象框架的公共可见体系结构,而不会降低其功能丰富度。类簇是基于抽象工厂设计模式的。
常见的类簇有 NSString
、NSArray
、NSDictionary
等。**以数组为例:**不管创建的是可变还是不可变的数组,在alloc
之后得到的类都是 __NSPlaceholderArray
。而当我们 init
一个不可变的空数组之后,得到的是 __NSArray0
;如果有且只有一个元素,那就是 __NSSingleObjectArrayI
;有多个元素的,叫做 __NSArrayI
;init
出来一个可变数组的话,都是 __NSArrayM
。
优点:
可以将抽象基类背后的复杂细节隐藏起来。
程序员不会需要记住各种创建对象的具体类实现,简化了开发成本,提高了开发效率。
便于进行封装和组件化。
减少了 if-else 这样缺乏扩展性的代码。
增加新功能支持不影响其他代码。
缺点:
已有的类簇非常不好扩展。
我们运用类簇的场景:
出现 bug 时,可以通过崩溃报告中的类簇关键字,快速定位 bug 位置。
在实现一些固定且并不需要经常修改的事物时,可以高效的选择类簇去实现。例:
针对不同版本,不同机型往往需要不同的设置,这时可以选择使用类簇。
app 的设置页面这种并不需要经常修改的页面,可以使用类簇去创建大量重复的布局代码。
18 设计模式
创建型模式:
单例模式:
在整个应用程序中,共享一份资源。保证在程序运行过程中,一个类只有一个实例,而且该实例只提供一个全局访问点供外界访问,从而方便控制实例个数,节约系统资源。
优点是:提供了对唯一实例的受控访问、可扩展、避免频繁创建销毁对象影响性能。
缺点是:延长了声明周期,一直存在占用内存。如果两个单例循环依赖会造成死锁,所以尽量不去产生单例间的依赖关系。
工厂方法模式:
通过类继承创建抽象产品,创建一种产品,子类化创建者并重载工厂方法以创建新产品。
抽象工厂模式:
通过对象组合创建抽象产品,可以创建多系列产品,必须修改父类的接口才能支持新的产品。
结构型模式:
代理模式
代理用来处理事件的监听和参数传递。
@required
修饰必须实现这个协议方法方法,@optional
修饰是可选实现。使用方法时最好先判断方法是否实现respondsToSelector:
,避免找不到方法而崩溃。
delegate和block、Notification对比优缺点:delegate和block是一对一通信、block比delegate更加简洁清晰,但是如果通信事件较多时delegate运行成本较低且不易造成循环引用;通知适合一对多通信,代码清晰简单,但问题查找溯源会比较困难,并且注册通知要注意在合适的时间移除,避免对野指针发送消息引起崩溃(**注意:**iOS9之后已经做了弱引用处理不需要移除了,之前版本使用不安全引用__unsafe_unretained是为了兼容旧版本)。
类簇:
见上边 5. 类簇
装饰模式
在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。如:分类。
享元模式:
使用共享物件,减少同一类对象的大量创建。如:
UITableviewCell
复用。
行为型模式:
观察者模式:
其本质上是一种
发布-订阅
模型,用来消除具有不同行为的对象之间的耦合,通过这一模式,不同对象可以协同工作。如:KVO。
命令模式:
是一种将方法调用封装为对象的设计模式,在iOS中具体实现为NSInvocation。下边为
NSInvocation
的实现代码。
MVC和MVVM算是架构。
19 架构设计
MVC:
M 是数据模型
Model
,负责处理数据,以及数据改变时发出通知(Notification、KVO
),Model
和View
不能直接进行通信,这样会违背MVC设计模式;V 是视图
View
,用来展示界面,和用户进行交互,为了解耦合一般不会直接持有 或者 操作数据层中的数据模型(可以通过action-target
、delegate
、block
等方式解耦);C 是控制器
Controller
用来调节Model
和View
之间的交互,可以直接与Model还有View进行通信,操作Model进行数据更新,刷新View。优点:
View
、Model
低耦合、高复用、容易维护。缺点:
Controller
的代码过于臃肿,如果View
与Model
直接交互会导致View
和Model
之间的耦合性比较大、网络逻辑会加重Controller
的臃肿。
MVVM:Model - View - ViewModel
MVVM衍生于MVC,是MVC的一种演进,促进了UI代码和业务逻辑的分离,抽取
Controller
中的展示逻辑放到ViewModel
里边。M: 数据模型
Model
。V: 就是
View
和Controller
联系到一起,视为是一个组件View
。View和Controller都不能直接引用模型Model,可以引用视图模型ViewModel。ViewController 尽量不涉及业务逻辑,让 ViewModel 去做这些事情。ViewController 只是一个中间人,负责接收 View 的事件、调用 ViewModel 的方法、响应 ViewModel 的变化。VM:
ViewModel
负责封装业务逻辑、网络处理和数据缓存。使用ViewModel会轻微的增加代码量,但是总体上减少了代码的复杂性。ViewModel
之间可以有依赖。
注意事项:
View
引用ViewModel
,但反过来不行,因为如果VM跟V产生了耦合,不方便复用。即不要在viewModel
中引入#import UIKit.h
,任何视图本身的引用都不应该放在viewModel
中 (注意:基本要求,必须满足)。ViewModel
可以引用Model
,但反过来不行。
优点:
低耦合、可复用、数据流向清晰、而且兼容MVC,便于代码的移植、并且ViewModel可以拆出来独立开发、方便测试。
缺点: 类会增多、ViewModel
会越来越庞大、调用复杂度增加、双向绑定数据会导致问题调试变得困难。
总结:
MVVM其实是MVC的变种。
MVVM
只是帮MVC
中的Controller
瘦身,把一些逻辑代码和网络请求分离出去。不让Controller处理更多的东西,不会变得臃肿,MVVM
和MVC
可以根据实际需求进行灵活选择。MVVM 在使用当中,通常还会利用双向绑定技术,使得
Model
变化时,ViewModel
会自动更新,而ViewModel
变化时,View
也会自动变化。OC中可以用**RAC(ReactiveCocoa)**函数响应式框架来实现响应式编程。
20 ReactiveCocoa的使用及优缺点
ReactiveCocoa简称RAC,是函数响应式编程框架,因为它具有函数式编程和响应式编程的特性。
由于该框架的编程思想,使得它具有相当魅惑人心的功能,它能实现传统设计模式和事件监听所能实现的功能,比如KVO、通知、block回调、action、协议等等,它的全面性并不是它最为优越的特色,RAC最值得炫耀的是它提供了统一的消息传递机制,这种机制使得它的代码更加的简洁,同一功能代码块更少,这正是符合了我们编程的思想:高聚合、低耦合,它非常适合MVVM设计模式的开发。
不过它也并不能完全取代传统的编码方式,在多人开发和代码维护方面,RAC还是有着一些让人头痛的问题。
优点:
使用灵活方便、代码简洁、逻辑清晰
缺点:
维护成本较高、问题溯源困难
使用:
RAC的统一消息传递机制,其所以动作都离不开信号(sigal)
。
1). 信号的创建、发送、接收
2). RAC的ControlEvents 这个方法可以简单的实现监听操作,并且逻辑在其后的
block
中处理,而且也能添加手势并进行监听。
3). RAC的KVO
4). RAC的通知
5). RAC的协议
6). RAC遍历数组和字典
相当于枚举遍历,但是效率相比更高
7). RAC信号处理(map、filter、combine)
① 对信号不做处理
② 对信号进行过滤(filter) 可以对信号进行条件判断是否处理。
③ 对信号进行映射(map) 映射也可以理解为转换,第一个block返回的是id类型,如果返回"map now"
,就相当于信号转换,第二个block打印的值就是你return的值"map now"
。
④ 信号的联合(combine)
⑤ 信号关联
21 类的继承,类能否多继承,协议能不能做继承
OC的类不支持多继承只支持单继承。
协议可以实现多继承,遵循多个协议即可。
消息的转发也可以实现多继承,但并不建议,维护成本高。
继承和类别在实现中有何区别?
category
可以在不获悉、不改变原来代码的情况下往里面添加并且只能添加方法不能删除修改。 如果类别和原来类中的方法产生名称冲突,则类别将覆盖原来的方法,因为类别具有更高的优先级。类别不会影响到其他类与原有类的关系。类别主要有3个作用:
(1)将类的实现分散到多个不同文件或多个不同框架中。
(2)创建对私有方法的前向引用。
(3)向对象添加非正式协议。
继承可以增加,修改或者删除方法,并且可以增加属性。
22 分类(category)和类扩展(extension)的区别
1). 分类实现原理
Category
编译之后的底层结构是struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息。在程序运行的时候,runtime
会将Category
的数据,合并到类信息中(类对象、元类对象中)。
2). Category和Extension的区别是什么?
类扩展可以为类添加私有变量和私有方法,在类的源文件中书写,不能被子类继承,类扩展在编译的时候,它的数据就已经包含在类信息中。
分类可以为类添加方法并且可以被子类继承,因为分类是运行时才会将数据合并到类信息中。但是分类不能直接添加属性,需要借助运行时关联对象。
3). 分类为啥不能添加成员变量?
从结构体可以知道,有
属性列表
,所以分类可以声明属性,但是分类只会生成该属性对应的get和set的声明
,没有去实现该方法。结构体没有成员变量列表,所以不能声明成员变量。
23 如何实现week
weak
该属性定义了一种非拥有关系。为属性设置新值时,设置方法既不持有新值,也不释放旧值。
weak实现原理:
当一个对象被weak指针指向时,这个weak指针会以对象为key,存储到
sideTable类
的weak_table
散列表上对应的一个weak指针数组里面。当一个对象的
dealloc
方法被调用时,Runtime
会以obj
为key
,从sideTable
的weak_table
散列表中,找出对应的weak指针列表,然后将里面的weak指针逐个置为nil
。
key
是weak指向的对象内存地址,value
是所有指向该对象的weak指针表。
24 字典注意事项:setvalue和setobject的区别
setObject:ForKey
:是NSMutableDictionary
特有的。setValue:ForKey
:是KVC
的主要方法。setobject
中的key
和value
可以为nil以外的任何对象。setValue
中的key只能为字符串,value可以为nil
也可以为空对象[NSNull null]
以及全部对象
25 事件传递和响应机制
在iOS中只有继承了UIResponder
的对象才能接受并处理事件。
事件传递:事件的传递是从上到下(父控件到子控件)
产生触摸事件A后,触摸事件会被添加到由
UIApplication
管理的事件队列中(首先接收到事件的是UIApplication)。UIApplication
会从事件队列中取出最前面的事件(此处假设为触摸事件A),将事件对象由上往下传递(UIApplication
-keyWindow
-父控件-子控件),查找最合适的控件处理事件只要事件传递给控件,就会调用自身的
hitTest:withEvent:
方法,寻找能够响应事件最合适的view(其内部会调用pointInside:withEvent:
判断触摸点是否在自己身上)。
响应机制:从下到上(顺着响应者链条向上传递:子控件到父控件) 当事件传递到某个控件,但是最终hitTest:withEvent:
没有找到第一响应者,那么该事件会沿着响应者链向上回溯,如果UIWindow实例
和UIApplication实例
都不能处理该事件,则该事件会被丢弃。
**响应者链条:**在iOS程序中视图的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫响应者链。也可以说,响应者链是由多个响应者对象连接起来的链条。
如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
UIView有三种情况不能接收事件:
不接收用户交互:
userInteractionEnabled = NO
隐藏:
hidden = YES
透明:
alpha<0.01
子视图在父视图之外区域点击是否有效?
无效**(父视图的clipsToBounds=NO,这样超过父视图bound区域的子视图内容也会显示),因为父视图的
pointInside:withEvent:
方法会返回NO,就不会向下遍历子视图了。但是可以通过重写
pointInside:withEvent:
来处理。
26 runloop
runloop
:通过系统内部维护的循环进行事件/消息管理的一个对象。runloop实际上就是一个do...while
循环,有任务时开始,无任务时休眠。
其本质是通过mach_msg()
函数接收、发送消息。
RunLoop 与线程的关系:
RunLoop
的作用就是来管理线程的,当线程的RunLoop
开启后,线程就会在执行完任务后,处于休眠状态,随时等待接受新的任务,不会退出。只有主线程的
RunLoop
是默认开启的,其他线程的RunLoop
需要手动开启。所以当程序开启后,主线程会一直运行,不会退出。
runloop 事件循环机制内部流程
RunLoop主要涉及五个类:
CFRunLoop
:RunLoop对象、
CFRunLoopMode
:五种RunLoop运行模式、
CFRunLoopSource
:输入源/事件源,包括Source0
和 Source1
CFRunLoopTimer
:定时源,就是NSTimer、
CFRunLoopObserver
:观察者,用来监听RunLoop。
CFRunLoop
:RunLoop对象CFRunLoopMode
:RunLoop运行模式,有五种:kCFRunLoopDefaultMode
:默认的运行模式,通常主线程是在这个 Mode 下运行的。UITrackingRunLoopMode
:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。UIInitializationRunLoopMode
:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。GSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到。kCFRunLoopCommonModes
:是一个伪模式,可以在标记为CommonModes的模式下运行,RunLoop会自动将_commonModeItems
里的Source
、Observer
、Timer
同步到具有此标记的Mode里。
CFRunLoopSource
:输入源/事件源,包括Source0 和 Source1两种:Source1
:基于mach_Port,处理来自系统内核或其它进程的事件,比如点击手机屏幕。Source0
:非基于Port的处理事件,也就是应用层事件,需要手动标记为待处理和手动唤醒RunLoop。简单举例:一个APP在前台静止,用户点击APP界面,屏幕表面的事件会先包装成
Event
告诉source1(mach_port)
,source1
唤醒RunLoop
将事件Event
分发给source0
,由source0
来处理。
CFRunLoopTimer
:定时源,就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer 是不准确的。 因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。CFRunLoopObserver
:观察者,用来监听以下时间点:CFRunLoopActivitykCFRunLoopEntry
:RunLoop准备启动kCFRunLoopBeforeTimers
:RunLoop将要处理一些Timer相关事件kCFRunLoopBeforeSources
:RunLoop将要处理一些Source事件kCFRunLoopBeforeWaiting
:RunLoop将要进行休眠状态,即将由用户态切换到内核态kCFRunLoopAfterWaiting
:RunLoop被唤醒,即从内核态切换到用户态后kCFRunLoopExit
:RunLoop退出kCFRunLoopAllActivities
:监听所有状态
各数据结构之间的联系:
1:
Runloop
和线程
是一对一的关系2:
Runloop
和RunloopMode
是一对多的关系3:
RunloopMode
和RunloopSource
是一对多的关系4:
RunloopMode
和RunloopTimer
是一对多的关系5:
RunloopMode
和RunloopObserver
是一对多的关系
为什么 main 函数能够保持一直存在且不退出?
在 main 函数内部会调用
UIApplicationMain
这样一个函数,而在UIApplicationMain
内部会启动主线程的runloop
,可以做到有消息处理时,能够迅速从内核态到用户态的切换,立刻唤醒处理,而没有消息处理时通过用户态到内核态的切换进入等待状态,避免资源占用。因此 main 函数能够一直存在且不退出。
27 runtime
什么是runtime?
runtime 一套c、c++、汇编编写的API,为OC提供运行时功能。能够将数据类型的确定由编译期推迟到运行时。
问1:方法的本质,问2:runtime的消息机制
方法的本质其实就是发送消息。
发送消息主要流程:
快速查找:
objc_msgSend
查找cache_t
缓存消息慢速查找:递归自己和父类查找方法
lookUpImpOrForward
查找不到消息,进行动态方法解析:
resolveInstanceMethod
消息快速转发:
forwardingTargetForSelector
消息慢速转发:消息签名
methodSignatureForSelector
和分发forwardInvocation
最终仍未找到消息:程序crash,报经典错误信息
unrecognized selector sent to instance xxx
SEL是什么?IMP是什么?两者有什么联系?
SEL
是方法编号,即方法名称,在dyld加载镜像时,通过read_image方法加载到内存的表中了。IMP
是函数实现指针,找IMP就是找函数的过程两者的关系:
sel
相当于书本的目录标题,imp
就是书本的页码。查找具体的函数就是想看这本书里面具体篇章的内容:1). 我们首先知道想看什么,也就是title -sel
2). 然后根据目录对应的页码 -imp
3). 打开具体的内容 -方法的具体实现
runtime应用:
1.方法的交换:具体应用拦截系统自带的方法调用(Method Swizzling黑魔法)
2.实现给分类增加属性
3.实现字典的模型和自动转换
4.JSPatch替换已有的OC方法实行等
5.aspect 切面编程
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
1.不能向编译后得到的类中增加实例变量。
2.可以向运行时创建的类中添加实例变量。
3.因为编译后的类已经注册在 runtime 中,类结构体中的
objc_ivar_list
实例变量的链表和instance_size
实例变量的内存大小已经确定,同时runtime
会调用class_setIvarLayout
或class_setWeakIvarLayout
来处理strong
、weak
引用,所以不能向存在的类中添加实例变量。
运行时创建的类是可以添加实例变量,调用class_addIvar
函数。但是得在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同上。
Category中添加属性和成员变量的区别
Category
它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。分类的结构体指针中,没有属性列表,只有方法列表。原则上它只能添加方法,不能添加属性(成员变量),但是可以借助运行时关联对象
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
、objc_getAssociatedObject(self,@selector(name));
。分类中的可以写
@property
,但不会生成setter/getter方法声明和实现,也不会生成私有的成员变量,会编译通过,但是引用变量会报错。如果分类中有和原有类同名的方法,会优先调用分类中的方法,就是说会忽略原有类的方法,同名方法调用的优先级为 分类 本类 父类,因为方法是放在方法栈中,遵循先进后出原则;
28 isa指针
isa
是一个Class类型
的指针,其源码结构为isa_t
联合体,在类中以Class对象存在,指向类的地址,大小为8字节(64位)。每个实例对象都有
isa
的指针指向对象的类。Class
里也有个isa
的指针指向meteClass
(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。元类(meteClass)也是类,它也是对象,也有isa指针。
isa的指向:对象的isa
指向类,类的isa
指向元类(meta class),元类isa
指向根元类,根元类的isa
指向本身,形成了一个封闭的内循环。isa可以帮助一个对象找到它的方法。
isa指向图中类的继承关系:Teacher -> Person -> NSObject -> nil
。这里需要注意的是根元类的父类是 NSObject
,NSObject
的父类是nil
。
29 block
什么是Block Block是将函数及其执行上下文封装起来的对象。
什么是Block调用 Block调用即是函数的调用。
Block的几种形式(类型)
Block有三种形式,包括:
全局Block(
_NSConcreteGlobalBlock
):当我们声明一个block时,如果这个block没有捕获外部的变量,那么这个block就位于全局区(已初始化数据(.data)区)。栈Block(
_NSConcreteStackBlock
):1). ARC环境下,当我们声明并且定义了一个block,系统默认使用
__strong修饰符
,如果该Block
捕获了外部变量,实质上是从__NSStackBlock__
转变到__NSMallocBlock__
的过程,只不过是系统帮我们完成了copy操作,将栈区的block迁移到堆区,延长了Block的生命周期。对于栈block
而言,变量作用域结束,空间被回收。2). ARC的环境下,如果我们在声明一个block的时候,使用了
__weak
或者__unsafe__unretained
的修饰符,那么系统就不会做copy的操作,也就不会将其迁移到堆区。
堆Block(
_NSConcreteMallocBlock
):1). 在MRC环境下,我们需要手动调用copy方法才可以将block迁移到堆区
2). 而在ARC环境下,
__strong
修饰的(默认)block捕获了外部变量就会位于堆区,NSMallocBlock
支持retain
、release
,会对其引用计数+1或 -1。只有局部变量 - 和定义的属性 才会拷贝到堆区
1). 存储在程序的数据区域,在 block 内部没有引用任何外部变量。
2). 使用外部变量并且未进行copy操作的block是栈block。
3). 对栈block
进行copy操作,就是堆block
。对堆Block
进行copy,将会增加引用计数
。对全局block
进行copy,仍是全局block
。
在什么场景下使用__block修饰符呢?
1). 对截获变量进行赋值操作需要添加
__block修饰符
(赋值 != 使用)。2). 对局部变量(基本数据类型和对象类型)进行赋值需要
__block修饰符
。 其内部其实是对该__block对象
进行拷贝,所以通过__block
可以修改被截获变量的值且不会和外部变量互相影响。3). 对静态局部变量、全局变量、静态全局变量不需要
__block修饰符
。