YTKNetwork 源码解读 (四) 之 YTKBatchRequest 和 YTKChainRequest

这篇是 YTKNetwork 源码阅读的最后一篇,主要讲解的是 YTKBaseRequest(批量请求),YTKChainRequest(链式请求) 两个部分。

YTKBatchRequest

YTKBatchRequest 用于完成批量请求的任务。当初始化时,需要指定 YTKBatchRequest 的数目,后续无法再添加新的请求。随后按添加顺序依次发起请求,当其中的一个请求失败时判定为 YTKBatchRequest 请求失败,同时 cancel 其它正在执行的请求

协议,用于 YTKBatchRequest 请求失败或者成功的回调

这些是 YTKBatchRequest 的属性,大部分看注释就能理解了。

需要注意的是:

  1. 代理的执行顺序在 block 之前
  2. tag 需要自己手动赋值来区分 YTKBatchRequest,默认值都是 0

初始化方法,在这里你需要指定 YTKRequest 数组。
个人觉得最好将其它默认的初始化方法给禁用掉

设置 YTKBatchRequest 请求成功和失败的回调,当请求结束后,会调用 -clearCompletionBlock 将 block 置为 nil

YTKRequestAccessory 协议用于执行请求不同阶段的回调

请求的开启和取消

请求的数据是否来自于缓存。所有请求中,如果有一个请求的数据来自于缓存,那么这个方法就会返回 YES

初始化方法,将参数 requestArray 赋值给属性 _requestArray,并且会判断里面的元素是否是 YTKRequest 实例,如果不是的话会返回。
初始化内部属性 finishedCount,该属性表示已完成请求的数目

方法开头会判断属性 finishedCount 的值是否大于 0,如果 0 的话则返回。这说明当 YTKBatchRequest 实例创建只能调用 start 方法一次,再次调用的话可能会出现重复开启任务的问题,这里个人还是觉得要进行下判断比较好。
将 YTKRequest 的 delegate 设置为自己,同时清空 YTKRequest 的 block 回调,以确保只有 YTKBatchRequest 处理请求完成后的回调,最后开启请求

stop

触发即将停止回调 toggleAccessoriesWillStopCallBack,并调用 clearRequest 方法循环将 YTKRequest 进行停止操作,随后将自己从 YTKBatchRequestAgent 中移除。

clearRequest

循环调用 YTKRequest 的 stop 方法

设置 YTKBatchRequest 的完成回调,当所有请求都成功调用或者其中一个失败时使用

isDataFromCache

可以看到,只要一个请求的数据来自于缓存,那么方法的返回结果就是 NO

当请求成功后,YTKBatchRequest 在这个方法里面处理数据。

  1. _finishedCount 的值 +1
  2. 如果 _finishedCount 的值等于请求的个数,说明所有的请求都已请求成功,这种情况表示 YTKBatchRequest 请求成功,执行相应的回调

YTKBatchRequest 在这个方法里面处理请求失败的情况。
一个请求失败意味着 YTKBatchRequest 的失败,所以也不用接着处理那些完成的请求。在这里会循环取消 YTKRequest 请求
接着执行失败回调,并将自己从 YTKBatchRequestAgent 移除。

YTKBatchRequestAgent 是一个单例,作用是强引用 YTKBatchRequest,避免被销毁。当任务执行完毕后,将 YTKBatchRequest 移除

小总结

YTKBatchRequest 是一个批量执行请求的类,能够批量执行 YTKRequest 类型的请求。
请求的开启顺序按照任务的添加顺序执行,单个 YTKRequest 完成后,不会执行 YTKRequest 自己的回调 block,代理等回调方法。
当所有请求成功后,执行 YTKBatchRequest 的代理回调,回调 block; 否则执行 YTKBatchRequest 的失败回调

YTKChainRequest

YTKChainRequest 也是批量处理请求的类,只不过是当前面的请求完成后再执行下一个请求。

YTKChainRequest 完成的回调。可以看到在失败回调中多了一个参数 failedBaseRequest

比较简单,看注释就好了。
id<YTKRequestAccessory> 是实现了协议 YTKRequestAccessory 的实例,这个协议里面有请求各种状态的方法,例如将要发起请求,将要结束请求

通过该方法向 YTKChainRequest 里面添加请求,同时设定该请求的完成回调。
我们知道 YTKBaseRequest 可以通过设定属性来设置完成回调,但这里为什么还需要这个方法来设定呢?通过属性设定的回调是否会触发呢?这些在后面会讲到的

一些内部用到的属性

  1. requestArray:请求数组
  2. requestCallbackArray:请求完成回调数组,有的请求没有设定回调,那么会往数组里面添加一个什么都不做的空回调
  3. nextRequestIndex:下一个执行的请求的序号
  4. emptyCallback:空回调

初始化实例,可以看到 _emptyCallback 其实是一个不执行任何操作的 block

开启任务后,通过 startNextRequest 方法开启第一个请求,并将 YTKChainRequest 添加到单例 YTKChainRequestAgent 中,防止被销毁

startNextRequest

该方法用来开启下一个未执行的请求。
_nextRequestIndex 表示该请求在数组中的位置,如果超出了数组范围则返回。
通过 _nextRequestIndex 得到该请求,随后将 delegate 设置为自己,并清除其完成回调,随后开启请求,返回 YES; 否则返回 NO

stop

取消 YTKChainRequest,并将 YTKChainRequest 从单例 YTKChainRequestAgent 中移除。
通过调用 clearRequest 方法取消当前的请求

clearRequest

获取到当前的 YTKBaseRequest,由于 _nextRequestIndex 表示下一个未执行的请求位置,所以这里要 -1。
清空 _requestArray 和 _requestCallbackArray 这两个数组

添加请求及其完成回调。
在 startNextRequest 方法中,我们知道,开启单个任务 YTKBaseRequest 之前,会将 YTKBaseRequest 的 delegate 设置为自己,并清除其回调 block。
所以,这里我们需要通过 _requestCallbackArray 这种方式来额外添加回调
需要注意的是该回调仅在请求成功时被调用

requestFinished

这里是 YTKBaseRequest 的代理方法,当请求成功后调用。
从 _requestCallbackArray 数组中获取该请求的完成回调。当所有的请求都获取成功后,执行 YTKChainRequestAgent 的完成回调

requestFailed

这里是 YTKBaseRequest 的代理方法,当请求失败后调用。需要注意的是,当单个 YTKBaseRequest 请求失败,判断 YTKChainRequestAgent 请求失败,执行其完成回调
如果你想执行 YTKBaseRequest 的失败回调,你可以在代理方法 chainRequestFailed:failedBaseRequest: 中,通过 YTKBaseRequest 得到响应数据,随后执行后续操作

小总结

YTKChainRequest 是一个批量同步执行请求的类,能够批量执行 YTKBaseRequest 类型的请求。
你可以自定义 YTKChainRequest 的实例方法添加 YTKBaseRequest 和 成功回调。

不同于 YTKBatchRequest,YTKChainRequest 中可添加的 request 类型是 YTKBaseRequest 而不是 YTKRequest

YTKNetwork 源码解读 (三) 之 YTKNetworkAgent

这一篇主要用来介绍 YTKNetworkAgent 这个类。当我们生成一个 YTKBaseRequest 实例,使用 -start 方法来发起请求,底层其实是使用 YTKNetworkAgent 来帮助序列化请求 request,以及请求结果 reponse 的序列化和回调。

YTKNetworkConfig

定义了一个叫做 AFURLSessionTaskDidFinishCollectingMetricsBlock 的 block 类型,看过 AF 源码的同学应该知道在 AFURLSessionManager.m 文件中同样定义了这个名字的 block,在同一个文件中 typedef 重复定义貌似不会报错。

NSURLSessionTaskMetrics 是 session 任务指标的封装,每个实例均包含下面几个属性:

  • taskInterval:表示任务从创建到完成花费的总时间,任务的创建时间是任务被实例化时的时间;任务完成时间是任务的内部状态将要变为完成的时间
  • redirectCount:表示被重定向的次数
  • transactionMetrics:包含了任务执行过程中每个请求/响应事务中收集的指标,指标是 NSURLSessionTaskTransactionMetrics 类型

这个 block 用于 NSURLSessionTaskDelegate 协议的回调方法 URLSession:task:didFinishCollectingMetrics:,简单的,你可以在这个方法中统计网络流量

YTKUrlFilterProtocol

YTKUrlFilterProtocol 协议里面只有一个方法 - (NSString *)filterUrl:(NSString *)originUrl withRequest:(YTKBaseRequest *)request,该方法在方法 -buildRequestUrl 中被调用,用来加工 requestUrl 返回的字符串 Url。
YTKNetworkConfig 可以添加多个实现 YTKUrlFilterProtocol 协议的实例

YTKCacheDirPathFilterProtocol

YTKCacheDirPathFilterProtocol 协议里面只有一个方法 - (NSString *)filterCacheDirPath:(NSString *)originPath withRequest:(YTKBaseRequest *)request,用来加工缓存的默认目录 /Library/LazyRequestCache,YTKNetworkConfig 可以添加多个实现了 YTKCacheDirPathFilterProtocol 的实例

NS_UNAVAILABLE 这个宏的作用是你无法使用这个宏修饰的方法,也就是你只能只用 +sharedConfig 方法来生成实例,也就是单例

  • baseUrl: 域名,类似 http://www.yuantiku.com,默认值为 nil
  • cdnUrl:cdn url,默认值是 nil
  • urlFilters:只读,你可以使用实例方法 -addUrlFilter: 来添加实现了 YTKUrlFilterProtocol 协议的实例,该协议在上面已经提到过了
  • cacheDirPathFilters:只读,你可以使用实例方法 -addCacheDirPathFilter: 来添加实现了 YTKCacheDirPathFilterProtocol 协议的实例,该协议在上面已经提到过了
  • securityPolicy:安全策略,与证书相关
  • debugLogEnabled:是否开启 debug 模式,开启后可打印 log
  • sessionConfiguration:session config
  • collectingMetricsBlock:收集指标后的回调

这些就不讲了

对应于 .h 文件中的 urlFilters, cacheDirPathFilters 属性,只是一个为只读,而上面的是成员变量可以直接使用

单例的写法,并在 init 进行初始化。
需要注意的是安全策略 securityPolicy 为默认策略,也就是无条件信任服务器的证书

比较简单就不说了


YTKNetworkConfig 是一个单例,你可以用它来:

  1. 设定域名 baseUrl
  2. 设定 cdn Url
  3. 设定安全策略
  4. 开启 log
  5. 对接口 url 进行修改
  6. 对缓存路径进行修改

YTKNetworkAgent

跟上面一样

  • addRequest:生成 NSURLSessionDataTask,并开启任务; 通过此方法会添加到一个字典集合中
  • cancelRequest:取消任务,并从集合中移除
  • cancelAllRequests:取消所有的任务,并从集合中移除

buildRequestUrl

构建请求的 url 字符串

互斥锁,用来保持线程安全。这里使用宏来简化锁的使用
使用方式为:

  1. 初始化锁:pthread_mutex_init(&_lock, NULL)
  2. 上锁:pthread_mutex_lock(&_lock)
  3. 解锁:pthread_mutex_unlock(&_lock)
  4. 销毁锁,虽然在文件中并没有这步操作:pthread_mutex_destroy(&_lock)

这里是一些 YTKNetworkAgent 的私有成员变量

  1. _manager:序列化参数,上传的附件,响应结果并发起请求
  2. _config:网络配置,上一节已经讲过了
  3. _jsonResponseSerializer:将响应结果序列化成 json
  4. _xmlParserResponseSerialzier:将响应结果序列化成 xml
  5. _requestsRecord:task 集合,强引用 task,使其在请求过程中不会被释放
  6. _processingQueue:并行队列,用来分配请求成功时的回调
  7. _lock:互斥锁
  8. _allStatusCodes:状态码集合,用来验证响应结果

生成 YTKNetworkAgent 实例并初始化,值得注意的是这里的状态码集合范围是 (100, 500),不同于 AFN 的 (200, 300)

懒加载 _jsonResponseSerializer、_xmlParserResponseSerialzier 这两个序列化器,它们只有当 responseSerializerType 为 YTKResponseSerializerTypeJSON、YTKResponseSerializerTypeXMLParser 才会被使用

buildRequestUrl

这个方法的作用是构建 request 的 url

先让我们了解一下 URL 的组成,这里举一个例子:http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument

其中:

  • scheme:因特网服务类型,这里是 http
  • domain:因特网域名,在这里是 www.example.com
  • port:主机上的端口号,默认端口号是 80
  • path:服务器上的资源路径,在这里是 /path/to/myfile.html
  • parameter:提供给服务器的额外参数,在这里是 ?key1=value1&key2=value2

在这个方法中,如果一个 url 存在 host 跟 scheme,那么就会把它当做一个完整的 url,不需要拼接直接返回

这个方法的作用是创建一个请求序列化器

① 前面部分是根据 requestSerializerType 类型创建不同的 requestSerializer
红色框里面的内容是服务器如果需要 账号/密码 验证时,使用 setAuthorizationHeaderFieldWithUsername:password:将这些信息添加到请求头中
① 后面部分的内容是设置你自定义的请求头信息

这个方法的作用是创建 NSURLSessionTask task

  1. 根据参数 YTKBaseRequest 获取到请求 method,url,参数,要上传的文件信息,进度 block以及请求序列化器
  2. 获取到上面的信息后,根据 method 选择不同的构建方法创建 NSURLSessionTask

当 method 是 GET 时,有两种情况,一种是下载任务,一种是数据任务

构建下载任务

这个方法用来构建下载任务。
下载任务有断点下载的作用,所以在这里我们需要判断之前是否下载了部分数据,如果是的话则继续下载,否则的话则重头开始下载。

  1. 首先我们需要构建 request,这部分由 AFN 的请求序列器 requestSerializer 完成,简单点讲就是设置请求头,设置请求体(将参数编码),创建 NSMutableURLRequest
  2. 代码块 1 的作用是确保下载路径是一个文件路径,而不是文件夹路径。下载任务完成后,AFN 使用方法 moveItemAtURL:toURL:error: 将文件移动到指定路径,如果此时该路径下已经有一个文件存在,就会失败,所以 YTK 这里在下载前就先判断是否文件,如果存在的话则移除这个文件
  3. 代码块 2 的作用是恢复断点下载
    • 首先获取到之前下载好的数据,这些数据会被保存在 /tmp/Incomplete 文件夹下面
    • 获取这些数据,并进行验证。断点数据被保存时会被当做一个 plist 文件,包含了许多的 key 来记录下载信息,所以 YTK 使用 validateResumeData 方法来对这些数据进行验证
    • 当断点数据存在并且有效时,使用 AFHTTPSessionManager 的实例方法 downloadTaskWithResumeData:progress:destination:completionHandler 方法来创建一个下载任务
    • 到这里如果没有断点下载,则使用 AFHTTPSessionManager 的实例方法 downloadTaskWithRequest:progress:destination:completionHandler 方法重新创建一个下载任务

这个方法的作用是创建数据任务,流程大概就是:

  1. requestSerializer 生成 NSMutableURLRequest,需要注意的是如果有文件上传的话需要用另一个方法来初始化 request
  2. 使用 AFHTTPSessionManager 生成 NSURLSessionDataTask,具体实现还是自己翻 AFN 的代码吧

这个方法的作用是生成任务 task,添加到字典集合中强引用避免被销毁,开启任务

  1. 代码块 1 的作用是初始化 NSURLSessionTask 实例
    • 如果我们使用了 -buildCustomUrlRequest 自定义了 NSURLRequest,那么将使用 AFHTTPSessionManager 的方法 dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler 来创建 NSURLSessionTask,你对 YTKBaseRequest 一些属性的设置都将失效
    • 如果没有自定义 NSURLRequest,那么将使用 sessionTaskForRequest:error: 来创建 NSURLSessionTask,这个方法我们在上面已经讲过了
  2. 代码块 2 的作用是在创建 NSURLSessionTask 的过程中失败了,则直接执行失败回调
  3. 代码块 3 的作用是为 NSURLSessionTask 设置优先级,因为这个属性是 iOS8 之后才有的,所以先用 respondsToSelector: 方法判断了下
  4. 当 NSURLSessionTask 创建成功后,使用 -addRequestToRecord: 添加到字典集合 _requestsRecord, key/value 对应于 task.taskIdentifier/request
  5. 由于刚创建的 NSURLSessionTask 处于 suspend 暂停状态,所以需要使用 resume 方法来开启

cancelRequest

该方法用来取消 request。

当 task 是下载任务时,且 YTKBaseRequest 的 resumableDownloadPath 属性不为 nil,取消任务会将已经下载好的数据保存在本地文件中,以便于下次下载时不重复下载。需要注意时,这些数据不只是你要下载的文件数据,而是一个 plist 文件,里面有许多键值对,例如 NSURLSessionDownloadURL,NSURLSessionResumeBytesReceived,NSURLSessionResumeCurrentRequest
数据保存的文件路径是 /tmp/Incomplete/resumableDownloadPath,其中 resumableDownloadPath 部分是你自己设定的值

取消任务 task 之后,会将 task 从集合 _requestsRecord 中移除

cancelAllRequests

该方法用来取消所有添加到集合中的任务,值得注意的是锁的使用,这里将锁作用在数据的添加和读取上

该方法用来验证 request 的响应结果,此时请求已经完成,且响应数据已经被序列化赋值给了 YTKBaseRequest 实例。

  1. 首先验证状态码,有效的状态码范围是 (200,299),在这个范围内说明请求被服务器接收并处理。如果无效则返回
  2. 如果 YTKBaseRequest 的 responseJSONObject 属性和方法 -jsonValidator 返回值不为 nil,则需要验证 JSON,如果 JSON 中相应字段的值不为 nil,则验证成功; 如果失败则返回

代码块 1:
当我们取消 task 后,底层的 AFN 会立刻调用失败回调,进而调用这个方法。为了防止这种情况,我们在这里做了一个判断,如果 request 为 nil 的时候立即返回

代码块 2:
序列化响应结果,根据 responseSerializerType 属性的不同,将数据序列化成不同类型,比如 JSON Object,NSXMLParser。具体实现过程由 AFN 的响应序列化器实现

  1. 到这一步如果都没有出现 error,则调用方法 -validateResult:error: 验证响应结果,这个方法我们在上面已经讲过
  2. 如果验证成功,则调用 -requestDidSucceedWithRequest: 方法处理; 否则,调用 -requestDidFailWithRequest:error: 处理,这两个方法我们随后会讲到
  3. 无论验证成功还是失败,最后都要将 request 从集合 _requestsRecord 中移除,并清空自己的 successCompletionBlock、failureCompletionBlock、uploadProgressBlock

请求成功后的处理方法,会依次调用下面几个方法

  1. requestCompletePreprocessor:如果使用 cache 的话是在主线程,否则的话在其它线程执行。下面的方法均在主线程执行
  2. requestWillStop
  3. requestCompleteFilter
  4. requestFinished
  5. successCompletionBlock
  6. requestDidStop

请求失败的处理方法

代码块 1:
如果此时有断点数据且 resumableDownloadPath 属性不为 nil,则将其保存在本地,用于下载任务

代码块 2:
对于断点下载的任务,后面恢复下载的任务开启时,responseObject 的值会被赋予断点数据的文件路径。
如果下载任务请求失败了,那么将把断点数据读取出来,然后把文件删除,不是很明白为什么把数据读取出来

代码块 3:
执行一些失败的回调,执行顺序如下:

  1. requestFailedPreprocessor:如果使用 cache 的话是在主线程,否则的话在其它线程执行。下面的方法均在主线程执行
  2. requestWillStop
  3. requestFailedFilter
  4. requestFailed
  5. failureCompletionBlock
  6. requestDidStop

总结

YTKNetworkAgent 是 YTK 实现请求的类,它序列化了请求以及响应数据,并且实现了断点下载的功能。

YTKNetwork 源码解读 (二) 之 YTKRequest

YTKCacheMetadata

YTKCacheMetadata 表示缓存元数据,保存了跟缓存相关的一些信息,例如创建时间,创建时的版本号,app 版本号等。
通过比较 YTKCacheMetadata,来判断本地缓存是否有效。如果无效的话则重新发起请求,有效的话则从本地缓存加载 reponse 数据

下面几种比较 YTKCacheMetadata 的情况会被认为本地无效:

  1. version 不匹配
  2. sensitiveDataString 不匹配。不要被它的名字给误导了,其实它就相当于一个额外的一个标识符,自己可以设定,如果 sensitiveDataString 不相等说明不匹配
  3. creationDate 取创建日期到现在的时间戳,如果超过了设定的缓存有效时间说明缓存无效
  4. appVersionString 不匹配,app 版本号不匹配

还有一个属性 NSStringEncoding stringEncoding 用于将本地数据转换成字符串时使用。

另外 YTKCacheMetadata 还支持 NSSecureCoding 协议,协议里的方法就不讲了。

定义了一些跟缓存相关的一些状态码

YTKRequest

YTKRequest 是 YTKBaseRequest 的子类,为 YTKBaseRequest 增加了缓存功能

response 数据是否保存到本地需要满足两个条件:

  1. 当前 response 不是来自于缓存,即 response 需要来自于 request
  2. 缓存有效时间 cacheTimeInSeconds 需要大于 0

-cacheVersion-cacheSensitiveData 这两个标识符都是自己设定的。
保存缓存时,也会将缓存元数据保存在本地,当需要加载缓存时,会先把缓存元数据取出来,比较 cacheVersion,cacheSensitiveData 这两个标识符跟当前的标识符是否相等,如果有不相等说明缓存无效

接下来是 YTKRequest.m 部分

两个宏 NSFoundationVersionNumber_iOS_8_0NSFoundationVersionNumber_With_QoS_Available 用来处理系统在 iOS8 之前和之后两种情况,不过在 iOS11 之后,我们可以使用 if (@available(iOS 10, macOS 10.12, watchOS 3, tvOS 10, *)) 这个关键字来处理系统版本不同的问题

这里创建了一个串行队列,对于 iOS8 之后的系统,还指定了它的队列优先级为 QOS_CLASS_BACKGROUND。需要注意的是 QOS_CLASS_BACKGROUND 的优先级别是最低的,可能是因为不想占用太多系统资源吧。
这个队列用来将 response 保存到本地文件中

上面几种不同类型的缓存对应 YTKResponseSerializerType 的类型

  1. cacheData 和 cacheString 对应于 YTKResponseSerializerTypeHTTP
  2. cacheData 和 cacheJSON 对应于 YTKResponseSerializerTypeJSON
  3. cacheData 和 cacheXML 对应于 YTKResponseSerializerTypeXMLParser

因为 YTKRequest 是 YTKBaseRequest 的子类,在这里覆写了它的 start 方法。

有三种情况会导致不使用缓存数据:

  1. 设置 YTKRequest 的属性 ignoreCache 为 YES,这样会忽略缓存直接进行请求
  2. 如果当前任务是一个下载任务,此时不会对 response 结果进行缓存,自然也就不会有读取缓存这个过程
  3. 加载缓存时出现了问题。例如缓存元数据验证时出现问题,或者是读取缓存文件时出现的问题

如果加载缓存成功,会在主队列分发一个异步任务,用来执行请求完成后的一些方法,这些我们在讲 YTKBaseRequest 的时候提到过了
需要注意的是下面这行代码

1
YTKRequest *strongSelf = self;

这是为了防止执行异步任务的时候 self 突然被释放了。不过我的理解是,既然 GCD 已经强引用了 self,那么在 block 执行完毕之前,self 都是不会被释放掉的,所以这句代码貌似有点多余了

使用 -loadCacheWithError 方法来加载本地缓存,分这么几步:

  1. 判断是否设置了缓存有效时间,如果没有的话说明不使用缓存了,直接返回
  2. 加载缓存元数据,如果元数据不存在,说明缓存也不存在
  3. 验证缓存元数据,判断缓存是否有效
  4. 加载本地缓存

在这里,我们先讲一下第三步验证缓存元数据的方法,加载缓存元数据和加载缓存后面再讲

validateCacheWithError

  1. 首先比较缓存的有效时间,因为缓存元数据中保存了缓存的创建日期,所以这里比较简单
  2. 比较缓存版本号,因为是 long long 类型,所以直接比较就可以
  3. 比较 sensitiveDataString 标识符,因为是字符串类型,所以需要使用 isEqualToString 方法比较。值得注意的是,对于符号 ||,当前面那个条件满足之后就不会再去比较后面那个条件了,所以我们写代码的时候可以把比较简单的判断条件放在前面,同理还有 &&
  4. 比较 app 版本号。这里我觉得只需要使用 dispatch_once 进行一次比较就可以了。如果变 app 版本号变更了,则清除缓存,后面就不再需要比较; 如果版本号没有变更,后面也不需要比较了
1
2
3
4
5
6
// 忽略缓存,开启请求
- (void)startWithoutCache {
// 清空跟缓存相关的属性
[self clearCacheVariables];
[super start];
}

如果不使用缓存,则调用这个方法来开启任务。

清空跟缓存相关属性的值,需要注意的是,并不会清除本地的缓存文件(如果有)

requestCompletePreprocessor 方法在得到请求结果后调用。由于 YTKRequest 覆写了父类 YTKBaseRequest 的 requestCompletePreprocessor 方法,所以首先需要调用父类的 requestCompletePreprocessor 方法。
如果 YTKRequest 的 writeCacheAsynchronously 属性值为 YES,则表示在执行请求回调的线程(一般为主线程),将请求结果保存在本地文件中; 如果值是 NO,则在私有的串行队列 ytkrequest_cache_writing_queue 添加一个异步任务来处理

saveResponseDataToCacheFile

saveResponseDataToCacheFile 方法的作用是将数据保存到本地。

  1. 检查数据是否是来自缓存,如果是的话则返回; 否则执行下一步
  2. 将数据保存到本地,文件名字根据接口名字、参数、请求方式来生成
  3. 生成一个 YTKCacheMetadata 实例,保存缓存元数据,然后使用归档的方式将其保存到本地中,其文件名字为 “缓存名字.metadata”

覆写这些方法,用来控制请求缓存的使用

  • cacheTimeInSeconds:缓存使用时间
  • cacheVersion:缓存版本,充当一个标记
  • cacheSensitiveData:也是一个标记
  • writeCacheAsynchronously:是否异步保存缓存

属性 _dataFromCache 用来标记此时的请求结果是否来自于缓存
根据请求格式 responseSerializerType 的不同,缓存将被转换成的对象也不相同,所以这里有很多成员变量 _cacheData/_cacheXML/_cacheJSON/_cacheString

loadCacheMetadata

加载缓存元数据,使用归档加载,如果加载成功,则该方法返回 YES。
这里需要注意的是 YTKLog 这个宏

YTKLog

展开来其实是一个 C 函数,参数是 format 和可变参数。
方法的实现被包含在了一个 #ifdef/#endif 里面,这样做的好处是在正式版本环境 RELEASE 时,不会再打印 log,减少系统开销

loadCacheData

这个方法的作用是加载本地缓存

首先判断缓存文件是否存在,存在的话则读取数据 data,随后根据属性 responseSerializerType 的值将 data 转换成不同格式的对象

clearCacheVariables

清除跟缓存相关的几个属性,需要注意的是并没有主动的清除本地缓存

createBaseDirectoryAtPath

该方法用来创建文件夹,需要注意的是 createDirectoryAtPath:withIntermediateDirectories:attributes:error:,该方法可以用来创建中间目录。
例如我们创建一个名字为 app/doc/user/info 的文件夹,而当前只存在 app 这个文件夹的话,使用该方法会帮我们创建好 doc,user,info 这几个文件夹

根据 path 来查找是否存在该文件夹,如果不存在的话则使用 createBaseDirectoryAtPath 来创建文件夹; 如果存在的话则判断是否是文件,如果是文件的话则移除该文件,然后使用 createBaseDirectoryAtPath 来创建文件夹

可以看到 createBaseDirectoryAtPath,createBaseDirectoryAtPath 两个方法的使用是为了确保生成指定名字的文件夹

该方法用来创建缓存存放的文件目录

默认的文件夹位置是 /Library/LazyRequestCache,但是我们可以使用 YTKNetworkConfig 的实例方法 addCacheDirPathFilter 来修改文件夹的名字
在确定好文件夹名字之后,使用 createDirectoryIfNeeded 创建该文件夹

该方法用来确定缓存文件的名字

得到请求的 url, basicUrl,参数,然后拼接成一个字符串 requestInfo,随后使用 YTKNetworkUtils 的方法 md5StringFromString,从 requestInfo 中提取 md5,将其作为缓存文件的名字

得到了文件夹名字和文件名字,我们就可以将其拼接成缓存文件的路径啦

总结

YTKRequest 作为 YTKBaseRequest 的子类,为其添加了缓存这个功能。在我们的使用中自定义的 request 子类也是需要直接继承与 YTKRequest 的

YTKNetwork 源码解读 (一) 之 YTKBaseRequest

因为 YTKBaseRequest .h/.m 里代码数目不多,所以这里将从头开始逐行介绍,有一些直接就写在注释里了,有一些需要注意的地方会特别摘出来讲解的。

不知道你有没有注意到 NS_ENUM 后面没有跟着指定一个名字,是的,如果你不需要指定一个类型名字的话,可以直接这样子写。
YTKRequestValidationErrorInvalidStatusCode 值的是 reponse.statusCode 不在 200~299 这个区间内,跟 AFHTTPResponseSerializer 的 acceptableStatusCodes 的范围一致

常见的请求方法

请求的序列化样式,相对于 AFNetworking 少了 AFPropertyListRequestSerializer,可能是因为这种编码方式比较少见吧。

相应的序列化样式,相对于 AFNetworking 少了 AFXMLDocumentResponseSerializer,AFPropertyListResponseSerializer,AFImageResponseSerializer,AFCompoundResponseSerializer 几个类型

对应于 NSURLSessionTask 的 priority 属性,需要在 iOS8 以后的系统中使用,不过一般也不需要再兼容之前的系统了吧。。。

AFMultipartFormData 在 AFNetworking 用于 Content-Type application/form-data 的请求,将数据添加到请求体中。一般用于 upload task 中。
在 AFConstructingBlock 类型的 block 中,你可以将数据添加数据添加到请求体中
在 AFURLSessionTaskProgressBlock 的 block 你可以追踪上传的进度

你可以实现这两个协议,以便在 request 的不同阶段进行相应的处理
需要注意的是这些方法的执行顺序不要搞错了

接下来介绍 YTKBaseRequest 这个类,它是一个抽象类,提供了构建 requeset 时的许多选项

这里大部分的属性是映射 NSHTTPURLResponseNSURLRequestNSURLSessionTask 这几个类的属性的。

需要注意的是 responseObject,如果 resumableDownloadPath 不为空并且 requestTask 是 DownloadTask 类型的,那么这个属性的值就一个文件路径(NSURL),用来保存下载数据的

tag 可以用来标记 YTKBaseRequest,默认值是 0
userInfo 可以用来添加额外信息

requestAccessories 是一个数组,可以用来保存多个实现了 YTKRequestAccessory 协议的对象
constructingBodyBlock 用来将数据添加到请求体中
resumableDownloadPath 是保存下载数据文件的路径,当下载请求失败时,部分的下载数据会自动保存到这个文件中,否则数据会保存到 responseData/responseString 中
resumableDownloadProgressBlock 可以用来追踪下载进度
uploadProgressBlock 可以用来追踪上传进度

-setCompletionBlockWithSuccess:failure: 用来添加请求 成功/失败 的回调
-clearCompletionBlock: 将请求 成功/失败 的回调 block 置为 nil,避免循环引用
-addAccessory: 用来添加实现 YTKRequestAccessory 的对象

  1. -start:开启任务。需要注意是为了让 task 在完成之前不被释放掉,会在这个 task 添加到单例 YTKNetworkAgent 的成员变量 _requestsRecord
  2. -stop:取消任务。我感觉这个方法叫做 cancle 比较合适,我一开始看到 stop 还以为是 suspend 的意思
  3. -startWithCompletionBlockWithSuccess:failure:,设置请求 失败/成功 的回调,并开启任务

这部分是你实现 YTKBaseRequest 子类时可以覆写的方法

-requestCompletePreprocessor,-requestCompleteFilter,-requestFailedPreprocessor,-requestFailedFilter:这几个方法都是对请求 成功/失败 结果的处理,结合之前讲过 YTKRequestDelegateYTKRequestAccessory 两个协议里面的方法,下面给出这些方法的执行顺序.

请求成功后回调的执行顺序:

  1. requestCompletePreprocessor:如果使用 cache 的话是在主线程,否则的话在其它线程执行。下面的方法均在主线程执行
  2. requestWillStop
  3. requestCompleteFilter
  4. requestFinished
  5. successCompletionBlock
  6. requestDidStop

请求失败后回调的执行顺序

  1. requestFailedPreprocessor:如果使用 cache 的话是在主线程,否则的话在其它线程执行。下面的方法均在主线程执行
  2. requestWillStop
  3. requestFailedFilter
  4. requestFailed
  5. failureCompletionBlock
  6. requestDidStop

方法 -cacheFileNameFilterForRequestArgument 用来对请求参数 argument 进行过滤后返回一个新的参数,用在获取缓存文件名字上。

方法 -requestAuthorizationHeaderFieldArray,用于身份验证,在这个方法中你需要返回一个容量为 2 的数组,第一个元素表示账号,第二个元素表示密码。
该认证方式使用用户的 账号/密码 作为凭证信息,进行 base64 编码添加到请求头 Authorization 中传输到服务器中

方法 buildCustomUrlRequest,在这个方法里面你可以放回一个自定义的 request,而不是使用 AFNetworking 的 AFHTTPRequestSerializer 生成。
如果你返回了一个不为 nil 的对象,那么将忽略 requestUrl, requestTimeoutInterval, requestArgument, allowsCellularAccess, requestMethod and requestSerializerType

方法 jsonValidator,在这个方法里面,你可以对 reponse 序列化后的 JSON 对象进行验证。
举个例子,我们要向网址 http://www.yuantiku.com/iphone/users 发送一个 GET 请求,请求参数是 userId 。我们想获得某一个用户的信息,包括他的昵称和等级,我们需要服务器必须返回昵称(字符串类型)和等级信息(数值类型),则可以覆盖 jsonValidator 方法,实现简单的验证。

1
2
3
4
5
6
- (id)jsonValidator {
return @{
@"nick": [NSString class],
@"level": [NSNumber class]
};
}

方法 statusCodeValidator,我觉得如果是验证状态码的话好得加个状态码的参数啊,不过无所谓啦,反正可以自己获取状态码然后再进行判断。该方法返回一个布尔值,如果返回的是 NO 的话将会报错。

接下来讲 .m 文件

在 .h 文件中这些属性都是只读的,在 .m 文件中改成可读写,防止外部修改这些属性的值。

NSURLSessionTaskNSURLResponse 的一些属性映射成自己的属性,便于使用

设置请求 成功/失败 的回调
需要注意的是可以添加多个实现了 YTKRequestAccessory 协议的对象

开启/关闭 请求的方法。
在 start 方法中:

1
2
3
4
5
6
7
8
// 触发 YTKRequestAccessory 代理
- (void)toggleAccessoriesWillStartCallBack {
for (id<YTKRequestAccessory> accessory in self.requestAccessories) {
if ([accessory respondsToSelector:@selector(requestWillStart:)]) {
[accessory requestWillStart:self];
}
}
}

addRequest方法实现

在成功创建 task 后,task 将会被添加到 _requestsRecord 属性中避免被释放。随后调用 resume 开启任务
如果我们没有覆写方法 buildCustomUrlRequest 返回自定义的 request,系统会根据 YTKBaseRequest 创建 request。这部分在 sessionTaskForRequest:error: 中实现

获取 YTKBaseRequest 实例上各种属性的值,例如方法类型,参数,然后在根据 method 的不同,使用不同的方法创建 task。
因为这部分内容都在 YTKNetworkAgent,这里就简单提下。在后面讲解 YTKNetworkAgent 部分的时候再仔细说

stop 方法:

1
2
3
4
5
6
7
8
// 执行代理
[self toggleAccessoriesWillStopCallBack];
// 将 delegate 置为 nil
self.delegate = nil;
// task cancle
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
// 执行代理
[self toggleAccessoriesDidStopCallBack];

如果你的请求是下载任务,并且你指定了一个缓存文件名,那么下载好的部分数据将会写入这个临时文件中,在下次恢复下载时使用。
当然如果要使用断点下载,还需要满足下面的几个条件:

  1. 这个资源自你第一次请求后没有改变
  2. 这个任务是一个 HTTP 或者 HTTP GET 请求
  3. 服务器在 reponse header 提供了 ETag 或者 Last-Modified 字段
  4. 服务器支持字节范围请求
  5. 本地临时文件没有被删除

由于 YTKBaseRequest 是一个基类,所以在这些需要子类覆写的方法里面内容不多

覆写了 -description 方法,方便打印信息

从源码读懂 Runloop

runloop 是和线程密切相关的一个组件,它帮助线程管理需要被处理的事件和消息,例如网络连接,异步回调,定时器…
当有事件或消息要处理时唤醒线程处理,否则休眠等待接收 mach 消息。

阅读更多

探究 block 数据结构及内存管理

本文首先将介绍一些 block 的基础, 随后着重介绍下面的内容

  • block 的数据结构
  • block 的内存管理(retain,release)

会用到下面这个
可编译的源码

基础

语法

Block 的语法比较难记, 以至于出现了 fuckingblocksyntax 这样的网站专门用于记录 block 的语法, 摘录如下:

作为变量

1
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

作为属性

1
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

作为函数声明参数

1
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;

作为函数调用中的参数

1
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

作为 typedef

1
2
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

捕获外部变量

block 可以捕获来自外部作用域的变量(id 类型, C++类型, 基础数据类型, block), 这是 block 一个很强大的特性

1
2
3
4
5
6
7
- (void)foo {
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
testBlock();
}

正常情况下, 捕获的外部变量在 block 里做的修改,在外部是不起作用的。如果想要在外部起作用,需要使用 __block 来声明变量:

1
int __block anInteger = 42

所以,根据变量是否被 __block 修饰,可以将变量分为两类:

  • by ref: 引用类型。该类的变量被 __block 修饰,在 block 内部对其修改,外部也生效
  • by copy:拷贝类型。该类的变量不被 __block 修饰,在 block 内部对其修改,外部不生效(全局/静态 变量除外)

至于原因进阶部分会进行详细的探究

进阶

数据结构

运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef void(^BLK)(void);

- (void)foo
{
static int staticInt = 5;
int ret = 5;

BLK globalBlock = ^{
int a = staticInt;
};
BLK mallocBlock = ^{
int a = ret;
};

NSLog(@"global = %@", globalBlock);
NSLog(@"malloc = %@", mallocBlock);
NSLog(@"stack = %@", ^{int a = ret;});
}

打印结果为

1
2
3
global = <__NSGlobalBlock__: 0x104bb22b0>
malloc = <__NSMallocBlock__: 0x281a21bf0>
stack = <__NSStackBlock__: 0x16b8266d8>

在 iOS 平台中, 一共有三种类型的 block:

  • _NSConcreteGlobalBlock: 在 .data 区域, block 内部没有访问任何的外部非(静态变量 && 全局变量)的变量(^{;}同样是该类型)
  • _NSConcreteMallocBlock: 在堆中创建内存, 使用__strong修饰的 block
  • _NSConcreteStackBlock: 在栈中创建内存, 使用__weak修饰的 block 或者是匿名 block

优先级为 _NSConcreteGlobalBlock > _NSConcreteMallocBlock == _NSConcreteStackBlock,即满足 _NSConcreteGlobalBlock 条件的 block 就是 _NSConcreteGlobalBlock 类型的

_NSConcreteGlobalBlock 类型的 block 我不知道初始化的时候是否直接在 .data 区域创建。
_NSConcreteMallocBlock 和 _NSConcreteStackBlock类型的 block 在初始化时在栈中创建,随后如果有 __storng 强指针引用的话,
则进行 retain 操作,将其内存拷贝到堆中,后续的 retain 操作则只是增加 block 的引用计数


使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 YOUR_FILE_NAME将下面的代码转换成 C++ 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)foo
{
static int staticInt = 5;
NSObject *commonObject = [[NSObject alloc] init];
__weak NSObject *weakObject = commonObject;
__block NSObject *byrefObject = [[NSObject alloc] init];
__block __weak NSObject *byrefWeakObject = commonObject;
BLK blockObject = ^{
NSObject *val = commonObject;
};

BLK malocBlock = ^{
staticInt++;
NSLog(@"%@", commonObject);
NSLog(@"%@", byrefWeakObject);
NSLog(@"%@", weakObject);
NSLog(@"%@", byrefObject);
NSLog(@"%@", byrefWeakObject);
NSLog(@"%@", blockObject);
};
NSLog(@"malloc = %@", malocBlock);
}

转换得到的 C++ 实现, 只截取部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

struct __Block_byref_byrefObject_0 {
void *__isa;
__Block_byref_byrefObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong byrefObject;
};
struct __Block_byref_byrefWeakObject_1 {
void *__isa;
__Block_byref_byrefWeakObject_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__weak byrefWeakObject;
};

struct __TestObject__foo_block_impl_0 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_0* Desc;
NSObject *__strong commonObject;
__TestObject__foo_block_impl_0(void *fp, struct __TestObject__foo_block_desc_0 *desc, NSObject *__strong _commonObject, int flags=0) : commonObject(_commonObject) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__foo_block_func_0(struct __TestObject__foo_block_impl_0 *__cself) {
NSObject *__strong commonObject = __cself->commonObject; // bound by copy

NSObject *val = commonObject;
}
static void __TestObject__foo_block_copy_0(struct __TestObject__foo_block_impl_0*dst, struct __TestObject__foo_block_impl_0*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __TestObject__foo_block_dispose_0(struct __TestObject__foo_block_impl_0*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __TestObject__foo_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_0*, struct __TestObject__foo_block_impl_0*);
void (*dispose)(struct __TestObject__foo_block_impl_0*);
} __TestObject__foo_block_desc_0_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_0), __TestObject__foo_block_copy_0, __TestObject__foo_block_dispose_0};

struct __TestObject__foo_block_impl_1 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_1* Desc;
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref
__TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestObject__foo_block_func_1(struct __TestObject__foo_block_impl_1 *__cself) {
__Block_byref_byrefWeakObject_1 *byrefWeakObject = __cself->byrefWeakObject; // bound by ref
__Block_byref_byrefObject_0 *byrefObject = __cself->byrefObject; // bound by ref
int *staticInt = __cself->staticInt; // bound by copy
NSObject *__strong commonObject = __cself->commonObject; // bound by copy
NSObject *__weak weakObject = __cself->weakObject; // bound by copy
__strong BLK blockObject = __cself->blockObject; // bound by copy

(*staticInt)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_0, commonObject);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_1, (byrefWeakObject->__forwarding->byrefWeakObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_2, weakObject);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_3, (byrefObject->__forwarding->byrefObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_4, (byrefWeakObject->__forwarding->byrefWeakObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_5, blockObject);
}
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static struct __TestObject__foo_block_desc_1 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_1*, struct __TestObject__foo_block_impl_1*);
void (*dispose)(struct __TestObject__foo_block_impl_1*);
} __TestObject__foo_block_desc_1_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_1), __TestObject__foo_block_copy_1, __TestObject__foo_block_dispose_1};

static void _I_TestObject_foo(TestObject * self, SEL _cmd) {
static int staticInt = 5;
NSObject *commonObject = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
__attribute__((objc_ownership(weak))) NSObject *weakObject = commonObject;
__attribute__((__blocks__(byref))) __Block_byref_byrefObject_0 byrefObject = {(void*)0,(__Block_byref_byrefObject_0 *)&byrefObject, 33554432, sizeof(__Block_byref_byrefObject_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
__attribute__((__blocks__(byref))) __attribute__((objc_ownership(weak))) __Block_byref_byrefWeakObject_1 byrefWeakObject = {(void*)0,(__Block_byref_byrefWeakObject_1 *)&byrefWeakObject, 33554432, sizeof(__Block_byref_byrefWeakObject_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, commonObject};
BLK blockObject = ((void (*)())&__TestObject__foo_block_impl_0((void *)__TestObject__foo_block_func_0, &__TestObject__foo_block_desc_0_DATA, commonObject, 570425344));

BLK malocBlock = ((void (*)())&__TestObject__foo_block_impl_1((void *)__TestObject__foo_block_func_1, &__TestObject__foo_block_desc_1_DATA, &staticInt, commonObject, weakObject, blockObject, (__Block_byref_byrefWeakObject_1 *)&byrefWeakObject, (__Block_byref_byrefObject_0 *)&byrefObject, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_h0_ybj03b8d0mj52n8dx82v2br40000gn_T_TestObject_1a9b07_mi_6, malocBlock);
}

需要注意的是,上面生成的 C++ 代码中 block 的结构体,并不符合最新版本 objc4-779.1 源码里 block 的结构定义

在 objc4-779.1 里 block 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};

不是有三种类型的 BLOCK_DESCRIPTOR 结构体,只是根据功能将其分为三部分,其实是一个整体
注意下面两个结构体在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct SampleA {
int a;
int b;
int c;
};

struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};

Block_layout 成员变量介绍:

  • void *isa: isa 指针
  • int flags: 使用位域来保存信息, 例如引用计数, 是否正在被销毁等信息
  • int reserved: 保留变量
  • void (*invoke)(void *, …): 函数指针, 指向 block 实现函数的调用地址
  • struct Block_descriptor *descriptor: block 的附加描述信息,一般来说都包含 Block_descriptor_1,但是是否包含 Block_descriptor_2 和 Block_descriptor_3 需要根据捕获外部变量的类型来判断
  • 还有一些捕获的外部变量
位域名 位置 含义
BLOCK_DEALLOCATING 0x0001 1 表示正在被销毁
BLOCK_REFCOUNT_MASK 0xfffe block 是引用计数
BLOCK_NEEDS_FREE 1 << 24 1 表示 block 已拷贝到堆中
BLOCK_HAS_COPY_DISPOSE 1 << 25 block 是否有 copy/dispos 函数,即 descriptor 是否包含 Block_descriptor_2
BLOCK_HAS_CTOR 1 << 26 copy/dispose helper 函数里面有 C++代码
BLOCK_IS_GC 1 << 27 1 表示使用 GC 管理内存,iOS 平台中不使用 GC
BLOCK_IS_GLOBAL 1 << 28 1 表示是个全局 block
BLOCK_USE_STRET 1 << 29 arm64 架构下没用,不知道干嘛的
BLOCK_HAS_SIGNATURE 1 << 30 是否有函数类型编码
BLOCK_HAS_EXTENDED_LAYOUT 1 << 31 GC 下使用

Block_descriptor 成员变量介绍:

  • unsigned long int reserved: 预留变量
  • unsigned long int size: block 结构体的 size 大小
  • void (*copy)(void *dst, void *src): copy 函数,将 block 成员变量 从栈拷贝到堆中。后面会再介绍
  • void (*dispose)(void *): dispose 函数, 对 block 成员变量内存回收
  • const char *signature:函数的类型编码
  • const char *layout: GC 下使用,不知道具体作用

让我们对照着 C++ 实现捋一遍,因为实现里的 block 结构体是老版本所以跟上面讲的可能会有出入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __TestObject__foo_block_impl_1 {
struct __block_impl impl;
struct __TestObject__foo_block_desc_1* Desc;
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref
__TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

在结构体 __TestObject__foo_block_impl_1 有很多捕获的外部变量充当的成员变量,如下所示

1
2
3
4
5
6
int *staticInt;
NSObject *__strong commonObject;
NSObject *__weak weakObject;
__strong BLK blockObject;
__Block_byref_byrefWeakObject_1 *byrefWeakObject; // by ref
__Block_byref_byrefObject_0 *byrefObject; // by ref

为了尽可能的谈论多的情况,在示例代码中我在 block 加了许多不同类型的外部变量
可以看到,全局/静态 变量和 __block 变量,都是将变量的地址保存在成员变量中,这样做的目的是为了在内部修改该变量在外部也会生效。
而其它非 __block 变量则仅仅拷贝了值,类似于浅拷贝

__Block_byref_byrefWeakObject_1 是 Block_byref 类型的结构体。__block 变量在编译时变成对应的 Block_byref 实例,且实例持有该变量

Block_byref 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
};

struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
};

struct Block_byref_3 {
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
};

类似 Block_descriptor,根据功能将其分为三部分
成员变量介绍:

  • void *isa:一般指向 0x0,如果该变量还被 __weak 修饰,则指向 _NSConcreteWeakBlockVariable
  • struct Block_byref *forwarding:指向该结构体。在拷贝到堆的过程中,在堆中新建一个结构体实例,此时栈中的实例并没有被销毁,将栈中实例 forwarding 指向堆中的实例
  • int flags:引用计数使用的 bit 数目和位置与 block 相同,其它不再介绍
  • int size:Block_byref 结构体的字节长度
  • void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src):Block_byref 的 copy 函数,帮助将实例持有的变量拷贝到堆中
  • void (*byref_destroy)(struct Block_byref *):Block_byref 的 dispose 函数,帮助将持有的变量销毁
  • const char *layout:Block_byref 持有的变量

为了方便,后面将 Block_layout 的 copy/dispose 函数简称为 Block copy/dispose 函数; Block_byref 的 byref_keep/byref_destroy 函数简称为 __block copy/dispose 函数

foo() 函数中我们在 block 捕获了两个 __block 修饰的变量,下面是其中一个的 Block_byref 结构体

1
2
3
4
5
6
7
8
9
struct __Block_byref_byrefObject_0 {
void *__isa;
__Block_byref_byrefObject_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *__strong byrefObject;
};

编译时,编译器会将 __block 修饰的变量 byrefObject,转换成对应的 Block_byref 结构体 __Block_byref_byrefObject_0 实例,然后让实例持有该变量NSObject *__strong byrefObject;


block 结构体 __TestObject__foo_block_impl_1 里面还定义了一个初始化方法:

1
2
3
4
5
6
_TestObject__foo_block_impl_1(void *fp, struct __TestObject__foo_block_desc_1 *desc, int *_staticInt, NSObject *__strong _commonObject, NSObject *__weak _weakObject, __strong BLK _blockObject, __Block_byref_byrefWeakObject_1 *_byrefWeakObject, __Block_byref_byrefObject_0 *_byrefObject, int flags=0) : staticInt(_staticInt), commonObject(_commonObject), weakObject(_weakObject), blockObject(_blockObject), byrefWeakObject(_byrefWeakObject->__forwarding), byrefObject(_byrefObject->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
  • 其中类似 : commonObject(_commonObject) 的写法是将形参NSObject *__strong _commonObject赋值给成员变量 _commonObject
  • 参数 flags 有一个默认值 0, 在本例中传入的值为 570425344, 用二进制表示为 0b00100010000000000000000000000000,即第 30 位(BLOCK_USE_STRET), 第 26 位(BLOCK_HAS_COPY_DISPOSE) bit 的值 1
  • 参数 fp 是函数指针, 在本例中它的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
static void __TestObject__foo_block_func_1(struct __TestObject__foo_block_impl_1 *__cself) {
__Block_byref_byrefWeakObject_1 *byrefWeakObject = __cself->byrefWeakObject; // bound by ref
__Block_byref_byrefObject_0 *byrefObject = __cself->byrefObject; // bound by ref
int *staticInt = __cself->staticInt; // bound by copy
NSObject *__strong commonObject = __cself->commonObject; // bound by copy
NSObject *__weak weakObject = __cself->weakObject; // bound by copy
__strong BLK blockObject = __cself->blockObject; // bound by copy

(*staticInt)++;

// 省略。。。
}

也就是 block 里面的代码。
可以看到非 __block 变量后面都写了注释 bound by copy,__block 变量后面的注释是 bound by ref,__block 变量都会转换成 Block_byref 实例保存在 block 中
为了方便,后面将 __block 修饰的变量称为引用变量,否则称为拷贝变量

  • block 初始化参数 desc 的类型是__TestObject__foo_block_desc_1 *

结构体的定义如下:

1
2
3
4
5
6
static struct __TestObject__foo_block_desc_1 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __TestObject__foo_block_impl_1*, struct __TestObject__foo_block_impl_1*);
void (*dispose)(struct __TestObject__foo_block_impl_1*);
} __TestObject__foo_block_desc_1_DATA = { 0, sizeof(struct __TestObject__foo_block_impl_1), __TestObject__foo_block_copy_1, __TestObject__foo_block_dispose_1};

并且初始化了一个结构体实例 __TestObject__foo_block_desc_1_DATA,其中 reserved 的值为 0, Block_size 的值为结构体 __TestObject__foo_block_impl_1 的字节长度
copy 和 dispose 两个函数指针分别指向函数 __TestObject__foo_block_copy_1 和 __TestObject__foo_block_dispose_1
实现如下:

1
2
3
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) {_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) {_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);}

这两个函数很容易看懂,有多少捕获的外部变量,就调用多少次 _Block_object_assign()_Block_object_dispose() 函数。
需要注意的是 _Block_object_assign() 的第三个参数,根据变量的类型不同传入不同的标记,后面会详细讲

与其类似的是 Block_byref 的 copy/dispose 函数
在本例中它们的实现如下:

1
2
3
4
5
6
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

可以看到,Block_byref 的 copy/dispose 函数最终也是调用 _Block_object_assign/_Block_object_dispose 函数

至于参数为什么要强转成 char*, 我的理解是这样的:

举个例子,定义一个 int 类型的数组, int a[10]。我们可以使用指针来代替数组的下标, 例如用(int *)a + 1来表示数组的第二个元素, 其距离第一个元素偏移了 4 个字节长度,所以强制转换成 char* 类型是为了每次偏移 1 个字节。上面的代码表示偏移了 40 个字节

而老版本 Block_byref 的定义(因为 C++ 实现符合源码)

1
2
3
4
5
6
7
8
9
struct Block_byref {
void *isa;
struct Block_byref *forwarding;
int flags; /* refcount; */
int size;
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
/* long shared[0]; */
};

Block_byref 偏移 40 个字节后的位置刚好是持有变量的首地址,所以在这里传入的参数即是引用变量(被 __block 修饰的变量)
至于后面的数字 131 后面再讲

好了,数据结构就分析到这啦。
目前我们可以知道的是,block 以及 __block 变量在编译时会生成对应的 Block_layout,Block_byref 结构体,它们都有各自的 copy/dispose 函数

验证 block 数据结构

这一节主要使用 lldb 用来验证 block 的数据结构

打个断点,使用命令 x/8xg ptr

block 内部分布

根据上一节 block 结构体内容,我们可以知道各成员变量的值

block 成员变量
void *isa 0x00000001ca53c0f0
volatile int32_t flags 0xc3000002
int32_t reserved 0x0
void (*invoke)(void *, …) 0x0000000104bcce80
struct Block_descriptor_1 *descriptor 0x000000010519e290
捕获变量 _commonObject 0x0000000281eaac10
Block_byref * _byrefObject 0x0000000281225170

验证一下:

isa 验证

isa 指针指向 __NSMallocBlock__,没问题

flags 用二进制表示为 0b11000011000000000000000000000010,即位域 BLOCK_HAS_SIGNATURE,BLOCK_HAS_EXTENDED_LAYOUT,BLOCK_HAS_COPY_DISPOSE,BLOCK_NEEDS_FREE 为 1 引用计数 BLOCK_REFCOUNT_MASK 为 1

invoke 验证
可以通过上面的方法打印出函数指针的内容

成员变量 _commonObject 的值于 commonObject 相同,均为 0x0000000281eaac10,说明是浅拷贝

下面来验证 descriptor
0x000000010519e290 为 Block_descriptor_1 结构体的首地址

descriptor 内存分布

Block_descriptor 成员变量 解释
uintptr_t reserved 0x0 预留字段
uintptr_t size 0x0000000000000030 十进制为 48,即 block 结构体的字节长度
void (*copy)(void *dst, const void *src) 0x0000000104bccee8 copy 函数指针
void (*dispose)(const void *) 0x0000000104bccf54 dispose 函数指针
const char *signature 0x000000010511847f 函数类型编码
const char *layout 0x0000000000000110 不知道干嘛的

打印下 block 函数的类型编码

函数类型编码

成员变量 const char *layout 应该是在 GC 下使用的,具体作用不明白


接下来验证 Block_byref 的结构

Block_byref 内存分布

Block_byref 成员变量 解释
void *isa 0x0 isa
struct Block_byref *forwarding 0x0000000281225170 十进制为 48,即 block 结构体的字节长度
volatile int32_t flags 0x33000004 标记,二级制表示为 0b00110011000000000000000000000100
uint32_t size 0x00000030 Block_byref 结构体长度
byref_keep 0x000000010511847f __block copy 函数指针
byref_destroy 0x0000000000000110 __block dispose 函数指针
const char *layout 0x0000000281eaac50 持有的变量 byrefObject

flags 表示引用计数为 2,因为初始化有一个,然后 block 有一个。位域 BLOCK_BYREF_LAYOUT_STRONG 为 1,表示该变量是 __strong 类型
const char *layout 表示其持有的变量

如何将 block 从栈拷贝到堆中

现在我们来探究一下 block 是如何从栈中拷贝到堆中的吧。
除 global block 类型的 block 均在栈中创建,当被强引用,即 retain block 的话,block 就会从栈拷贝到堆中,如果已经在堆中,则增加其引用计数

断点调试

step into

_Block_copy 的函数实现在 clang-800 源码 中可以看到

1
2
3
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}

_Block_copy_internal 实现

我们主要看红色框框里面的代码:

  • 首先通过 malloc() 在堆中新建一个 block 结构体实例,接着使用memmove()将旧实例的数据拷贝过去
  • 重置新实例成员变量 flags 的 BLOCK_REFCOUNT_MASK(引用计数)部分
  • 将新实例成员变量 flags 的位域 BLOCK_NEEDS_FREE 设置为 1, 表示该 block 在堆中
  • 将新实例的 isa 指向 _NSConcreteMallocBlock
  • 如果存在 Block copy 函数,则调用

上一节中已经提到过,这里再贴一下它的实现:

1
2
3
4
5
6
7
8
static void __TestObject__foo_block_copy_1(struct __TestObject__foo_block_impl_1*dst, struct __TestObject__foo_block_impl_1*src) 
{
_Block_object_assign((void*)&dst->commonObject, (void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->byrefWeakObject, (void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->weakObject, (void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->byrefObject, (void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->blockObject, (void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

需要注意的是,如果外部变量是 C++类型,则不会调用 _Block_object_assign()函数,而是其对应的 const 拷贝构造方法。注释如下:
In these cases helper functions are synthesized by the compiler for use in Block_copy and Block_release, called the copy and dispose helpers. The copy helper emits a call to the C++ const copy constructor for C++ stack based objects and for the rest calls into the runtime support function _Block_object_assign. The dispose helper has a call to the C++ destructor for case 1 and a call into _Block_object_dispose for the rest.

_Block_object_assign函数的实现如下:

_Block_object_assign 函数实现

参数 flags 有以下几种情况:

由 Block copy 函数调用:

  • id                   3       
    
    • (^Block)             7
      
  • __block 8
    • __weak __block       8+16
      

flags 有 4 种可能: 3, 7, 8, (8+16)

由 __block copy 函数调用:

BLOCK_BYREF_CALLER (128):表示由 __block copy 调用
此时, 传入的 flags 有 4 种可能:

  • __block id                   128+3       
    
    • __block (^Block)             128+7
      
  • __weak __block id 128+3+16
    • __weak __block (^Block)      128+7+16
      

总共有以上 8 种情况

需要注意的是,Block copy 调用该函数,第一个参数是指针的地址(void **),第二个参数传入的是指针的值(void *)
而 __block copy 调用该函数,传入的前两个参数均为指针的值(void *)

下面根据 case 条件分几步来讲解这个函数:

  • 代码块 1:

Block copy 函数调用,且该变量是 id 类型的

代码_Block_retain_object()最终会调用下面这个函数

1
2
3
static void _Block_retain_object_default(const void *ptr) {
(void)ptr;
}

代码_Block_assign()最终会调用下面这个函数

1
2
3
static void _Block_assign_default(void *value, void **destptr) {
*destptr = value;
}

不知道调用 _Block_retain_object 函数的目的是什么
这一分支仅做了浅拷贝,拷贝指针内容

  • 代码块 2:

Block copy 函数调用,且该变量也是一个 block

这里首先调用了 _Block_copy_internal() 函数, 先将 block 类型的变量拷贝到堆中
然后调用 _Block_assign 将其堆中的地址赋值给 Block 对应的成员变量

  • 代码块 3:

Block copy 函数调用,且变量被 __block 修饰

_Block_byref_assign_copy 实现

这里传入的第一个参数 void *dest 表示的是指针的地址

_Block_byref_assign_copy 实现中代码块 1 的逻辑如下

  • 声明一个布尔值 isWeak, 用来表示该变量是否还被 __weak 修饰
  • _Block_allocator 函数最终调用 malloc() 函数, 在堆上拷贝一份同样内存大小的 Block_byrefs 实例
  • 将旧实例的成员变量 flags 拷贝到新实例中, 并将新实例的引用计数设置为 2。 1 份是因为栈上有一个实例, 1 份是因为堆上也有一个, 1+1 就等于 2 了
  • 将旧实例和新实例的成员变量 forwarding 均赋值为新实例
  • 如果 isWeak 为 true, 则将 Block_byrefs 实例的 isa 指针指向 _NSConcreteWeakBlockVariable
  • 如果实例有 __block copy/dispose helpers(还是调用 _Block_object_assign 函数), 则调用它对实例持有的变量进行拷贝到堆操作; 如果没有的话则将旧实例中 size 后面的成员变量拷贝到新实例的 bit 中

函数里面有一行看起来比较让人困扰的代码

1
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);

在前面的声明中,src 被声明成了 Block_byref * 类型,所以 src + 1 的意思是从 src 的首地址偏移 sizeof(Block_byref) 个字节,即 Block_byref_2 的首地址
结构体拷贝完成后,随后将 Block_byref 持有的变量通过函数 *src2->byref_keep 也拷贝到堆中
_Block_byref_assign_copy 实现中代码块 2 的作用是如果 Block_byref 持有的变量已经拷贝到堆中了, 则增加其引用计数

  • 代码块 4:

__block copy 函数调用,且持有的变量不被 __weak 修饰

最终会调用下面那个函数

1
2
3
static void _Block_assign_default(void *value, void **destptr) {
*destptr = value;
}

浅拷贝

  • 代码块 5:

__block copy 函数调用,且持有的变量被 __weak 修饰

最终会调用下面那个函数:

1
2
3
static void _Block_assign_weak_default(const void *ptr, void *dest) {
*(void **)dest = (void *)ptr;
}

这里讲一下我的理解:
void * 是一个指针,里面保存了一个地址,但是我们不能 *(void *) 这样使用它,因为我们不知道它指向的结构是什么类型的。如果要使用的话就需要将其转换成其它类型,例如 int *,所以在这里可以仅仅把它看成是变量,保存了一个地址。
而 void **,其表示指向指针的指针,不同于 void *,我们可以 *(void **) 这样使用它,因为我们知道 void ** 指向的内容是一个指针。

在 _Block_assign_weak_default 函数中,我们先将 dest 强转成 void ** 类型,然后就可以对其进行赋值操作啦


至此,整个拷贝流程已经讲的差不多了,这里总结一下:

  • 一个 block 可能捕获多个外部变量
  • block 在栈中生成,retain 后,将栈中的内容拷贝到堆中
  • block 会调用 Block copy 函数,对其捕获的变量也进行拷贝操作
    • 如果是 C++ 类型,则调用其 const 拷贝构造函数到堆中
    • 如果是 block 类型,则将其拷贝到堆中
    • 如果是 id 类型,因为已经在堆中了,所以进行浅拷贝,仅复制指针值
    • 如果是 __block 修饰的变量,则将其对应的 Block_byref 结构体拷贝到堆中,随后调用 __block copy 函数将其持有的变量也拷贝到堆中

如何销毁 block

这一节我们将探究如何销毁 block

创建一个 malloc block 类型的 block,foo() 结束,block 会被回收。
在函数尾巴那里打个断点

step into

将断点停留在objc_release()函数
runtime 通过该函数对 block 进行 release 操作,如果其引用计数变成 0,则销毁

继续 step into

block 是特殊的类,自己重写了release()函数,所以代码 ISA()->hasCustomRR() 返回的结果是 true,将执行自己重写的 release() 函数
继续 step into

因为没有 block 的 release()源码,通过调用栈发现随后调用了 _Block_release() 函数
可编译的源码中可以找到该函数的实现(直接在文件夹中搜索)

_Block_release 实现

根据 if 的判断将函数分为三个部分

  • 判断条件 1:如果是全局 block 或者其引用计数已经为 0(表示已经在销毁了), 则返回
  • 判断条件 2:如果使用 GC 管理内存,则执行什么什么操作。因为 iOS 平台不使用 GC,所以略过
  • 判断条件 3:如果 block 已经在堆上了,则将其引用计数减 1,如果减为 0,则调用下面三个函数
1
2
3
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
_Block_deallocator(aBlock);
  • 第一个函数 _Block_call_dispose_helper

在同个文件中搜索该函数,其实现如下

1
2
3
4
5
6
7
static void _Block_call_dispose_helper(struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;

(*desc->dispose)(aBlock);
}

如果存在 Block dispose 函数,则调用

1
2
3
4
5
6
7
8
9
static void __TestObject__foo_block_dispose_1(struct __TestObject__foo_block_impl_1*src) 
{
_Block_object_dispose((void*)src->byrefInt, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->commonObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->byrefWeakObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->weakObject, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->byrefObject, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->blockObject, 7/*BLOCK_FIELD_IS_BLOCK*/);
}

_Block_object_dispose 实现

通过判断条件将 _Block_object_dispose 分为几部分

  • 判断条件 1:变量由 __block 修饰

_Block_byref_release 实现

代码byref = byref->forwarding;,因为可能会有 byref 在栈中,而 forwarding 此时却在堆中的情况。随后判断 byref 是否在栈中,如果是的话则立即返回。
对 Block_byref 的引用计数减 1 随后判断是否为 0,如果是的话则调用其 byref_destroy 函数(也就是 _Block_object_dispose),销毁其持有的变量
最后将 Block_byref 结构体的内存释放掉

  • 判断条件 2:变量是 Block 类型

_Block_destroy 实现

调用 _Block_release 来销毁该 block

  • 判断条件 3:变量是 id 类型

_Block_release_object 最终调用的函数如下

1
2
3
static void _Block_release_object_default(const void *ptr) {
(void)ptr;
}

等于什么都没做,这是因为 id 类型的对象由 ARC 管理其内存。即不再被强指针引用时引用计数减 1

  • 判断条件 4

什么都没做,如果走到走一步说明可能系统有异常


接着讲 block 销毁时调用的第二个函数 _Block_destructInstance
调试时,调用栈里也有这个函数

接着 step into

使用该函数,主要是为了将在弱引用表中注册的使用 __weak 引用 block 的变量置为 nil,因为 block 已经要被销毁了
这里不仔细讲了


最后调用 _Block_deallocator() 函数,将 block 结构体的内存销毁


这里总结一下 block 的内存销毁流程:

  • 先将 block 捕获的外部变量进行销毁
  • 将弱引用 block 的指针置为 nil
  • 将 block 结构体的内存销毁

探究runtime碰到的类和结构体

在研究 runtime 时能遇到许多的类和结构体, 因为不可能每篇文章碰到就介绍一遍, 所以在这里统一把这些碰到的类和结构体介绍一下

SideTable

这个类的作用是存放引用计数表和弱引用表, 并使用自旋锁来防止操作表结构时可能的竞态条件
定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}

~SideTable() {
_objc_fatal("Do not delete SideTable.");
}

void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }

// Address-ordered lock discipline for a pair of side tables.

template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

有下面几个成员变量

  • spinlock_t slock: 自旋锁

  • RefcountMap refcnts: 额外引用计数存储表

  • weak_table_t weak_table: 弱引用表, 用来存储弱引用者(weak 修饰的变量)

    其中RefcountMap是类型objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable>的别名

DisguisedPtr

定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template <typename T>
class DisguisedPtr {
uintptr_t value;

static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}

static T* undisguise(uintptr_t val) {
return (T*)-val;
}

public:
DisguisedPtr() { }
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }

DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}

operator T* () const {
return undisguise(value);
}
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}

// pointer arithmetic operators omitted
// because we don't currently use them anywhere
};

这个类的作用是伪装指针, 避免引用计数表和弱引用表里面保存的指针被内存泄漏工具leaks当做是内存泄漏.

  • 它的成员变量value存储的是经过函数disguise()处理后的指针.
  • 你可以像操作指针一样操作DisguisedPtr实例. 为了实现这个功能, DisguisedPtr重载了许多的操作符, 例如->, *, =, ==, []. 不了解的同学可以自己查资料, 关键字 运算符重载operator

DisguisedPtr一般用来充当 Key, 保存在结构体中

RefcountMapValuePurgeable

定义如下:

1
2
3
4
5
struct RefcountMapValuePurgeable {
static inline bool isPurgeable(size_t x) {
return x == 0;
}
};

只有一个内联函数RefcountMapValuePurgeable, 用来判断这个值需要是否析构释放内存

DenseMapInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-2);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};

它有两个内联函数:

  • getEmptyKey(): 生成 empty 的标记
  • getTombstoneKey(): 生成墓碑标记(即代表之前被人使用过但现在已经没人用了)

有两个静态函数:

  • getHashValue(): 根据指针返回一个哈希值
  • isEqual(): 判断两个参数是否相等

DenseMapInfo常常跟DisguisedPtr一起使用. 它的两个内联函数用来标记存储数据的DenseMapPair实例 bucket 的状态是否是 empty 或者 tombstoneKey. 它的getHashValue函数用来生成一个哈希值来确定 bucket 的序号

detail::DenseMapPair

部分定义如下:

1
2
3
4
5
6
7
8
9
10
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {

// FIXME: Switch to inheriting constructors when we drop support for older
// clang versions.
// NOTE: This default constructor is declared with '{}' rather than
// '= default' to work around a separate bug in clang-3.8. This can
// also go when we switch to inheriting constructors.
DenseMapPair() {}
}

它的父类std::pair是一个结构体模板, 它将两个数据组合成一个数据, 类似于我们经常用的字典.
DenseMapPair被用来在引用计数表中保存引用计数. 其中 key 的类型为DisguisedPtr

DenseMap

部分定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo<ValueT>,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

// Lift some types from the dependent base class into this class for
// simplicity of referring to them.
using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;

public:
/// Create a DenseMap wth an optional \p InitialReserve that guarantee that
/// this number of elements can be inserted in the map without grow()
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
}

这个类就是之前提到的引用计数表, 它的成员里面有一个存储数组, 用来保存引用计数. 数组的元素类型为 detail::DenseMapPair
如果一个对象的引用计数曾经溢出保存到表中, 当对象被销毁时, 会将表中对象使用过的存储器 bucket 标记为墓碑状态

1
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

用于声明一个友元类, 这样DenseMapBase就能访问DenseMap里面的私有属性和私有方法了

1
using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>添加一个类型别名

1
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }

上面那行代码的作用是显示的声明一个构造方式, 这样这类就不能隐式转换了

成员变量如下:

  • Buckets: 一个 bucket 数组, 用于保存数据. 可扩容
  • NumEntries: Buckets 数组中已经被使用的数目
  • NumTombstones: Buckets 数组中 tombstone 的数目
  • NumBuckets: Buckets 数组的数目

weak_table_t

1
2
3
4
5
6
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

这个结构体用来存储弱引用条目, 弱引用条目里面保存着对象以及它的弱引用者们.
当数组数量超过 1024 且被使用的数量占比小于 1/16 时, 数组长度会缩小为原来的 1/8

它有 4 个成员变量:

  • weak_entry_t *weak_entries: weak_entry_t 类型的数组. 弱引用条目, 用来保存弱引用者(被 weak 修饰的指针)
  • size_t num_entries: 已经被使用的条目数量
  • uintptr_t mask: 条目数组的数量
  • uintptr_t max_hash_displacement: 用来记录数组中被使用条目的 index 的最大值

weak_entry_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

这个结构体用来保存引用对象以及它的弱引用者, 属于一对多的关系
当弱引用比较少的时候会将弱引用者保存在结构体里面, 当弱引用者数量超过 4 时会保存到外部的数组中

它有两个成员变量, 一个是DisguisedPtr<objc_object> referent;, 另一个是联合体 union.
referent 表示引用对象, 当一个弱引用者引用了一个新的对象, 那么我们需要从弱引用条目中(weak_entry_t)移除该弱引用者

weak_referrer_tDisguisedPtr<objc_object *> 类型的别名

第二个成员变量 union 里面有两个结构体, 我这里称呼它们为 s1, s2.
因为是 union, 所以 s1 里面的 out_of_line_ness 跟 s2 里面的 inline_referrers 的第二个元素的低 2 个 bit 是重合的. 数组 inline_referrers 的元素是 weak_referrer_t 类型. 在 arm64 架构下, 指针 8 字节对齐, 意味着指针低 3 位肯定都是 0, 经过DisguisedPtr的伪装后, 它的低 2 位都是 1, 也就是 s1 的 out_of_line_ness 的值为 0b11.
这个特性用来标记是否使用内部数组来保存弱引用者, 当使用外部数组时, 内部数组被清空, 将 out_of_line_ness 赋值为 0b10 来表示使用外部数组

当使用内部数组时

  • s1 的成员变量均没有意义
  • 使用 s2 的 inline_referrers 数组来保存弱引用者.

当使用外部数组时

  • s1 的成员变量referrers是指向外部数组的指针; out_of_line_ness为常量 2, 表示使用了外部数组; mask表示外部数组的长度 - 1; num_refs表示外部数组中被使用的数量; max_hash_displacement表示哈希最大偏移量
  • 此时 s2 的成员变量 inline_referrers 被清空

iOS weak弱引用底层实现

源码版本:objc4-779.1
当然还是推荐使用这个来学习:可编译的源码

weak是一个所有权修饰符, 它提供弱引用的功能, 即弱引用者(weak 修饰的变量, 后统称为弱引用者)不能持有引用对象, 当引用对象被释放时, 此弱引用者被置为 nil.
此文将探究弱引用在底层是如何实现

弱引用者如何注册到弱引用表

首先我们研究弱引用者是如何注册到弱引用表中的
调试代码如下(代码需要在开头提到的可编译版本里面才能运行, 后面不再解释), 注意里面打了个断点, 运行程序

点击 step into, 会跳转到函数objc_initWeak()

这个函数的作用是初始化弱引用者.
需要注意的是storeWeak模板里面的两个参数 DontHaveOld, DoHaveNew. 它们分别是HaveOldHaveNew枚举的变量. 枚举里面变量的含义如下

  • DontHaveOld: 表示弱引用者之前没有引用对象, 例如在用__weak id weakPtr = [[NSObject alloc] init];初始化弱引用者

  • DoHaveOld: 表示弱引用者之前有引用对象. 此时需要将弱引用者从弱引用表中注销

  • DontHaveNew: 表示弱引用者没有引用一个新的对象

  • DoHaveNew: 表示弱引用者引用了一个新的对象. 此时需要将弱引用者注册到弱引用表中

继续 step into, 跳转到storeWeak()函数里面

函数里面代码比较多, 为了方便, 我把它分成了几部分来说明

SideTable跟弱引用的实现息息相关, 在这里可以看到对这个类的详细说明.
这里我就简单的介绍一下, 它是存放引用计数表和弱引用表的结构. 它有三个成员变量:引用计数表, 弱引用表, 自旋锁. 在我们操作引用计数表或者弱引用表的时候自旋锁会加锁防止竞态条件的出现.

代码块 1 的作用:

  • 加锁, 防止在修改哈希表时可能出现的竞态条件. 按锁地址(从低到高)顺序加锁, 防止可能出现的锁的排序问题.
  • 如果弱引用者的引用对象改变了(这是因为如果并发修改弱引用者的引用对象, 在加锁之前, 可能原先的引用对象会改变), 则重复执行retry部分的代码

代码块 2 的作:

  • 保证引用对象的 isa 指针已经初始化过. 如果未初始化, 则对该对象的非元类 Class 进行初始化.

代码块 3 的作:

  • 在弱引用表上注销弱引用者, 后面会再来讲这个部分

代码块 4 的作用:

  • 在弱引用表上注册弱引用者

代码块 5 的作用:

  • 对象被弱引用者引用了, 则修改对象 isa 指针上位域的信息, 将weakly_referenced置为 YES, 表示该对象被一个弱引用者引用.

需要注意的是, 如果这个对象后续不再被弱引用者引用了, isa 指针上的weakly_referenced值仍旧是 YES

接着, 我们把断点移动到weak_register_no_lock()函数里面, 同样, 我把它分成几部分来讲解.

referent 表示引用对象, referrer 表示弱引用者

代码块 1 的作用是:

  • 保证引用对象是可用的. 如果引用对象正处于被销毁的状态, 那么程序会崩溃

代码块 2 的作用是:

  • 根据引用对象找到对应的 weak_entry_t 实例(后面统称为为弱条目), 并将该弱引用者保存到弱条目中

下面是append_referrer()函数的实现, 写了点注释就不展开讲了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// 使用内部数组来保存弱引用者. 遍历内部数组, 如果有空位则用该空位保存弱引用者并返回
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}

// 如果执行到这里, 说明内部数组都被使用了. 所以需要扩容, 即创建一个外部数组来保存弱引用者
// 先初始化一个跟内部数组长度一样的数组, 并将内部数组的数据转移过去
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}

ASSERT(entry->out_of_line());
// 如果外部数组中有 3/4 的元素都被使用了, 则进行扩容
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
// 扩容的长度为原来的 2 倍, 最小值为 8.
return grow_refs_and_insert(entry, new_referrer);
}
// 根据指针的哈希值来计算位置 index
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
// 发生了哈希碰撞, 则增加下标, 查看下一个位置是否为空
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
// 重新计算 entry 的最大哈希偏移量
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
// 将新的弱引用者保存到弱引用条目中
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
// num_refs + 1
entry->num_refs++;
}

代码块 3 的作用:

生成一个新的弱引用条目, 将弱引用者保存到弱条目中. 随后将弱条目插入到弱引用表中

  • 第一行代码的作用是初始化一个weak_entry_t的实例 new_entry
  • 第二行代码里面函数的作用是, 如果弱引用表的成员变量weak_entries数组里面有 3/4 的数量的弱条目被使用了, 则进行扩容. 长度为原来的 2 倍, 最小为 64
  • 第三行代码的作用是将新生成的弱条目插入到弱引用表中

至此, 我们已经大致了解了弱引用者是如何注册到弱引用条目的, 这里小总结一下:

  • 首先判断引用对象是否处于销毁状态, 否则是的话程序会崩溃
  • 根据引用对象获取到对应的SideTable, 它里面有成员变量弱引用表weak_table_t
  • 在弱引用表中根据引用对象来查找对应的弱引用条目, 如果存在则将该弱引用者保存在该条目中. 如果不存在则新生成一个条目, 将弱引用者保存到该条目, 随后将条目插入到弱引用表中
  • 在弱引用表中插入弱条目根据引用对象的哈希值来计算 index, 在弱条目中插入弱引用者根据弱引用者的哈希值来计算 index
  • 在此过程中, 不对任何的弱引用者进行赋值, 即不对 *referrer 进行赋值操作

弱引用者如何从弱引用表中注销

这部分用来探究弱引用者如何从弱引用表中注销
实验代码如下

点击step into跳转到objc_storeWeak()函数里面

首先弱引用者 w1 引用了对象 o1, w1 会被注册到 o1 对应的弱引用条目中. 随后我们给它设置了一个新的引用对象 o2, 那么, w1 会先从 o1 的条目中注销, 然后注册到 o2 的条目中.

继续调试, 将断点移动到storeWeak()函数里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);

Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;

// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}

// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);

// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;

goto retry;
}
}

// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected

// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

return (id)newObj;
}

函数里面大部分内容已经在上一节中介绍过, 这里就不重复了
这里我们要讲解的是weak_unregister_no_lock()函数. 这个函数的作用是将销弱引用者从引用对象对应的弱引用条目中注销. 函数实现如下:

referent表示原先的引用对象, referrer表示弱引用者

代码块 1 的作用是根据引用对象查找弱引用条目, 然后存在则将该弱引用者从条目中注销

函数remove_referrer()的实现如下, 写了点注释就不展开讲了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
if (! entry->out_of_line()) {
//使用内部数组保存弱引用者, 此时遍历该数组, 找到对应的弱引用者, 如果找到的话则将其从数组中移除
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == old_referrer) {
entry->inline_referrers[i] = nil;
return;
}
}
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
// 此时使用外部数组来保存弱引用者
// 根据弱引用者的哈希值来得到对应元素的 index
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer) {
// 如果发生了哈希碰撞, 则偏移+1 往下继续查找该弱引用者.当偏移量超过最大偏移量时程序崩溃
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
// 找到对应弱引用则将其从外部数组中移除
entry->referrers[index] = nil;
entry->num_refs--;
}

代码块 2 的作用是判断弱引用条目中注册的弱引用者的数量是否为 0, 如果为 0 则将该弱引用条目从弱引用表中清除.

weak_entry_remove()函数的实现如下, 写了点注释就不展开讲了

1
2
3
4
5
6
7
8
9
10
11
12
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
// 如果使用的外部数组, 则将外部数组销毁. 如果使用内部数组, 则会在销毁条目时将内部数组也销毁
if (entry->out_of_line()) free(entry->referrers);
// 销毁弱引用条目
bzero(entry, sizeof(*entry));

weak_table->num_entries--;
// 当条目数量超过 1024, 并且使用了不到 1/16, 则将条目数量缩小为原来的 1/8
weak_compact_maybe(weak_table);
}

至此, 我们已经大致了解了弱引用者如何从弱引用表中注销的, 我小总结一下:

  • 根据旧引用对象获取SideTable
  • 根据就引用对象在SideTable的弱引用表中获取对应的弱引用条目
  • 在弱引用条目中查找是否注册了该弱引用者, 如果存在的话则移除
  • 若弱引用者移除后弱引用表中弱引用者的数量为 0, 则将该条目从弱引用表中移除
  • 如条目移除后, 条目的数量超过 1024, 且使用的数量少于 1/16, 则将条目的容量缩小为原先的 1/8

引用对象销毁后弱引用者如何置为 nil

我们都知道当引用对象被销毁后, 弱引用者会被置为 nil.
这部分用来粗略的讲解其实现

添加一个symbolic breakpoint, 在symbol里面输入[NSObject dealloc]

点击 continue program execution, 跳转到dealloc()函数中

对 o1 的引用计数 -1 , o1 的引用计数就变成了 0, 需要销毁. 即调用 NSObject 的 dealloc 方法

随后一直跳转到objc_object::clearDeallocating_slow()函数里面.
这个函数的作用是清除该对象在SideTable中存在于引用计数表或者弱引用表中的数据

因为研究的是对弱引用的操作, 所以这边我们只需要关注weak_clear_no_lock()函数.
它的实现如下, 写了注释就不展开讲了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
// referent 表示引用对象, 即这里要被销毁的对象
objc_object *referent = (objc_object *)referent_id;
// 根据引用对象找到对应的弱引用条目
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}

// zero out references
// 数组指针
weak_referrer_t *referrers;
// 数组长度
size_t count;

if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}

for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
// 将弱引用者置为 nil
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 将所有弱引用者置为 nil 后, 将该条目从弱引用表中删除
weak_entry_remove(weak_table, entry);
}

至此, 我们已经大致了解了引用对象销毁后弱引用者如何置为 nil 的, 这里小总结一下:

当对象的引用计数为 0 时, 会调用 dealloc 方法用来销毁对象. 如果有弱引用者引用者该对象, 那么会从弱引用表中找到对象的弱引用条目, 将条目中所有注册的弱引用者置为 nil, 随后将该条目从弱引用表中删除.

iOS如何配置Bugly符号表(2020年最新)

本文将粗略的讲解 Bugly 的集成过程, 其中配置符号表部分会说的仔细一点

符号表就是内存地址与函数名, 文件名, 行号的映射表. 通过上传符号表, 我们能将 App 发生 Crash 的程序堆栈进行解析还原.

Bugly 是我在用的一个崩溃日志收集分析工具. 集成使用简单并且免费.
之前一直使用Xcode + sh方式自动配置符号表, 直到有一天看到 crash 堆栈没有被解析, 才知道这个方法已无法上传符号表. 上网找过一些解决方法, 大致就是找到符号表后延迟以后再上传, 但也没什么效果. 随后再上官网查看文档, 使用官方手动配置的方法, 成功的上传了符号表和解析堆栈.

创建应用

官网 创建产品.
创建成功后获取应用的 App ID

集成

使用Cocoapods集成
pod 'Bugly'

初始化 SDK

在工程AppDelegate.mapplication:didFinishLaunchingWithOptions:方法中初始化:

1
2
3
4
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[Bugly startWithAppId:@"此处替换为你的AppId"];
return YES;
}

手动配置符号表

下载 最新版 请点击我这个连接去下载, 这个是 3.0.0 版本的.

包里面有几个文件:

  • buglySymboliOS.jar: 符号表工具JAR包
  • buglySymboliOS.bat: Windows的脚本
  • buglySymboliOS.sh: Shell脚本
  • settings.txt: 默认符号表配置文件
  • 符号表工具iOS版-使用指南

运行需要Java 运行环境 (JRE􏲀JDK􏰅􏱺􏱿􏰎>=1.6)
使用脚本时, 请保证脚本和 jar 包在同个目录下!

获取 dSYM 文件

iOS 平台中, 􏲜􏲝􏲐􏱓dSYM 文件是指具有调试信息的目标文件, 文件名通常为: dSYM􏰔􏰕􏰫􏰈􏰄􏰳􏲁􏲂􏲃􏲄􏰞􏲅􏲆􏰔􏰕􏱓􏰔􏰕􏲡􏰻􏳋􏱈􏱣xxx.app.dSYM
我使用下面的方式来找到它:




将最后的 dSYM 文件拷贝出来一份集中备份

dSYM 的符号表生成和上传

  • 生成

我使用下面的方式来生成和上传 dSYM 文件

1
2
cd /Users/dqh/Desktop/buglySymboliOS3.0.0
java -jar buglySymboliOS.jar -i /Users/AAAA_1.0.15.app.dSYM -u -id 123456 -key 123456-2608-4a57-8af8-ff9c4d481f3e -package com.kikido.youappname -version 1.3.5

你需要更改以下选项信息:

  • i /Users/AAAA_1.0.15.app.dSYM: 更改为 [i 自己 dSYM 文件存放的地址]
  • id 123456: 更改为 [id 你自己在 Bugly 申请到的 apple id]
  • key 123456-2608-4a57-8af8-ff9c4d481f3e: 更改为 [key 你自己在 Bugly 申请到的 apple key]
  • package com.kikido.youappname: 更改为 [package 你自己 app 的 bundle id]
  • version 1.3.5: 更改为 [version 你自己 app 的 version]

工具选项说明如下

选项 说明
-i 指定文件路径, 可指定目录(必选)􏰈􏰭􏰔􏰕􏲯􏲰􏱓􏲇􏰈􏰭􏲅􏲒􏱪􏲟􏰡􏱫􏰈􏰭􏰔􏰕􏲯􏲰􏱓􏲇􏰈􏰭􏲅􏲒
-o 􏲱􏲲􏰞􏰀􏰁􏰂输出的符号表 zip 文件的路径, 必须是 zip 文件
-d 调试模式开关(默认关闭)
-s 指定配置文件(默认读取 JAR 目录下的 settings.text 文件)
-u 上传开关
-id app id
-key app key
-package app 包名
-version app 版本
-symbol 生成 symbol 文件

更多信息请查看符号表工具iOS版-使用指南.pdf

使用上面的命令之后就会在本地生成符号表文件, 并上传到符号表.

上传成功

下面是 crash 的堆栈解析, 完美

深入了解 iOS 引用计数

iOS 通过引用计数来管理内存,简单的说就是当一个 id 类型的对象引用计数变成 0 的时候就会被销毁,回收内存。
本文将通过断点调试来探究引用计数的存储及读取在底层的实现。全程无聊,请配合源码阅读

源码版本:objc4-779.1
当然还是推荐使用这个来学习:可编译的源码

引用计数存储在哪

引用计数只适用于 id 类型的对象

有一种特殊的 id 类型对象,Tagged Pointer,如果不知道的话可以看我另一篇博客 深入了解Tagged Pointer了解一下

因为 Tagged Pointer 并不是真正的对象,所以它并不通过引用计数来管理其内存。
用下面的代码求 n1 的引用计数,输出结果为 9223372036854775807

1
2
3
// 创建一个 Tagged Pointer 对象
NSNumber *n1 = [NSNumber numberWithInt:1];
NSLog(@"n1 rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(n1)));

我们知道每个 id 类型对象都有一个 isa 指针,其中只有 33 位用来保存其父类或者元类信息,剩余的31位也不能浪费啊,这其中就会一些 bit 用来保存引用计数。
下面是 arm64 架构下的 isa 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
#endif
};

可以发现,isa_t 是一个联合体(union),几个不同类型的变量 clas 和 bits 及 struct 共用同一段内存。当 isa 指针的第一位nonpointer为 1 时表示它是优化的 isa 指针,即将多余的 bit 用来存储其它信息,从它的名字nonpointer也可以看出它不再是一个纯粹的指针了。当为 1 时,第一个成员变量cls是没有用的,因为 isa 不再使用它来指向父类 cls。下面是 isa 指针各位域的含义

变量名 含义
nonpointer 表示是否对 isa 指针开启优化 0:纯isa指针,1:存储了额外信息
has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
shiftcls 类的指针
magic 固定值 0x1a, 用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

在 arm64 下, isa 使用 19 个 bit 用来存储引用计数. 当引用计数超过这个数时,将会把 RC_HALF 数量的引用计数存储在一个全局哈希表中,此时 has_sidetable_rc 变为 1

增加引用计数

下面让我们通过调试来探究具体的存储过程
因为是在模拟器中运行的,isa 最多只能使用 8 个 bit 来存储引用计数, 即在 isa 中只能存储不超过 256 的数目。所以我们只要将对象的引用计数增加到 256 以上,那么系统就会将多余的引用计数存储到哈希表。

1
2
3
4
5
6
void foo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 500; i++) {
_objc_rootRetain(o1);
}
}

_objc_rootRetain()方法最终会调用objc_object::rootRetain(bool tryRetain, bool handleOverflow), 这个方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;

isa_t oldisa;
isa_t newisa;

do {
// 是否将引用计数转移到索引表中
transcribeToSideTable = false;
// LoadExclusive 的作用是让读取操作原子化
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// if (slowpath) else (),表示大概率执行 else 后面的语句,用来提高效率。
// fastpath 则表示大概率执行 if 后面的语句
if (slowpath(!newisa.nonpointer)) {
// arm64 架构下,一般的对象 nonpointer 为YES,但也有特例,例如 block 的 nonpointer 为 False,即不使用 isa 保存额外信息
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
// 用来表示 isa 指针的值是否溢出。因为引用计数存储在指针的高位,当引用计数增加到一定程度,会超过最高位的数字,此时 carry 的值不等于 0,表示溢出
uintptr_t carry;
// addc() 是一个内置函数,作用是将 isa 里面的引用计数 + 1 后保存
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
// 移除的话则需要对多余的引用计数保存到索引表中。
// 刚开始进入的时候 handleOverflow == false
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// 加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// 此时 isa 指针存储的引用计数应该是满的, extra_rc 的 19 位都是 1,将 extra_rc 赋值为原来的一半,也就是将 isa 的最高位赋值为 0,这样做的目的是下次引用计数增加的时候可以直接存储在 isa 中,而不需要调用索引表来存储。将 has_sidetable_rc 赋值为 1,标记有额外的引用计数存储在索引表中
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
// StoreExclusive(), 如果&isa.bits和oldisa.bits相等,那么就把newisa.bits的值赋给&isa.bits,并且返回true。
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
// 如果溢出的话,则将 isa 存储的引用计数赋值为最大值的一半,即 RC_HALF,那么减少的一半则转移到索引表中
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

写了点注释帮助你们理解. 现在我们在sidetable_addExtraRC_nolock(RC_HALF);那一行打个断点,启动程序

step into进入这个方法

因为用的模拟器,所以 RC_HALF == delta_rc == 128,符合预期。

代码SideTable& table = SideTables()[this],通过计算指针值得到哈希值获取到对应的 sidetable
让我们点开方法SideTables()

1
2
3
4
5
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}

class ExplicitInit 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Type>
class ExplicitInit {
alignas(Type) uint8_t _storage[sizeof(Type)];

public:
template <typename... Ts>
void init(Ts &&... Args) {
new (_storage) Type(std::forward<Ts>(Args)...);
}

Type &get() {
return *reinterpret_cast<Type *>(_storage);
}
};

ExplicitInit的作用是生成一个模板类型 Type 的实例。alignas(Type) uint8_t _storage[sizeof(Type)];成员变量_storage是一个 sizeof(Type) 长度的 uint8_t 数组,而 uint8_t 占用一个字节,所以实际上_storage的长度跟一个 Type 实例所占用的内存是一样多的。成员函数 Type &get() 将 _storage 数组指针用reinterpret_cast<Type *>强转成了 Type * 类型指针,前面再加一个 *,说明返回的实际上是 Type 实例内存的首地址。而另一个成员函数init用来初始化生成的 Type 实例。

所以static StripedMap<SideTable>& SideTables()函数实际上返回了一个全局静态StripedMap<SideTable>的实例。
下面是class StripedMap的部分定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif

struct PaddedT {
T value alignas(CacheLineSize);
};

PaddedT array[StripeCount];

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...省略
}
  • 局部结构体PaddedT的作用是用来内存对齐,常量CacheLineSize是固定值 64。在 arm64 架构下,成员变量array是一个长度为 8,元素类型为 T,每个元素占 64 字节的数组,而在模拟器下是一个长度为 64,每个元素占 64 字节的数组。
  • 成员函数indexForPointer用来根据指针来计算哈希值,确定对应于array里面第几个元素。
  • T& operator[] (const void *p)使用 operator 关键字重载了[]符号。举个例子,StripedMap 有一个实例 sm,有一个 int 类型的变量 a,当执行sm[&a]的代码时,就会跳转到T& operator[] (const void *p)这个方法里面。而在上面的重载[]函数中,传入的参数为指针类型,然后计算出其数组 index,返回对应的元素 T,也就是 SideTable

继续调试,断点此时在SideTable& table = SideTables()[this];这一行。点击 step into

_storage的数组长度为 4096,也就是 64 * 64,符合预期。继续step into,最终会跳转到下面这里

跳转到了重载的[]方法里面了。

目前已知道的情报就是,有一个全局的StripedMap条目索引,里面包含了若干个SideTable索引表。在使用时,根据指针得到对应的 sidetable。SideTable 的部分定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// ...省略
};

SideTable 的成员对象里面有一个自旋锁,引用映射表RefcountMap refcnts,弱引用表weak_table_t weak_table。在上面我们提到了每个 sidetable 占用 64 个字节, 那是因为 spinlock_t 占用 4, RefcountMap 占用 24, weak_table_t 占用 32, 加起来就是 64.
在这里我们只了解下 RefcountMap, 因为它跟我们引用计数相关.
RefcountMap是一个类型别名,它的定义如下:

1
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;

先别急着查看DenseMap的定义,我们先看看DisguisedPtr是个什么类。它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template <typename T>
class DisguisedPtr {
uintptr_t value;

static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}

static T* undisguise(uintptr_t val) {
return (T*)-val;
}

public:
DisguisedPtr() { }
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }

DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}

operator T* () const {
return undisguise(value);
}
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}
};
  • 简单的说, 你可以像指针Type *一样的使用DisguisedPtr,但是其成员变量value保存的是伪装后的指针值。伪装函数是disguise(),0 和 0x800…00 经过伪装之后还是本身,但指针值也不可能是这两个值,所以无所谓的。
  • 为了能像指针Type *一样的使用DisguisedPtr,在DisguisedPtr内部,重载了许多符号,例如 [], =, *, ()。具体就不分析了。
  • 该类的作用是为了避免直接引用指针, 防止被 leaks 内存检测工具检测出内存泄漏

class RefcountMapValuePurgeable的定义如下:

1
2
3
4
5
struct RefcountMapValuePurgeable {
static inline bool isPurgeable(size_t x) {
return x == 0;
}
};

它只有一个内联函数,根据 x 是否为 0 返回一个 bool 值。它的作用就是根据参数判断是否需要析构这个实例
好了,了解完了DisguisedPtrRefcountMapValuePurgeable,接下来我们来看DenseMap,它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo<ValueT>,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;

public:
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
}
  • 代码friend class DenseMapBase的作用是声明一个友元类DenseMapBase,这样DenseMapBase在内存可以直接访问DenseMap private 的成员函数。
  • 代码using BaseT = DenseMapBase的作用是为DenseMapBase指定一个别名BaseT
  • 函数explicit DenseMap(), explicit的作用是用来声明类构造函数是显示调用的,而非隐式调用。

DenseMap里面有 4 个成员变量,Buckets 和 NumEntries,NumTombstones, NumBuckets。

  • Buckets, 存储引用计数的数组, 元素类似于字典, key 为指针, value 为引用计数. 数组可扩容
  • NumEntries 表示数组中用来存储引用计数的元素有几个
  • NumTombstones 数组中元素被用来存储一个对象的引用计数, 当这个对象被销毁后, 该元素被标记为墓碑. 该变量用来表示有多少个这种元素
  • NumBuckets 数组 Buckets 的长度

再来看一下模板里的几个类型 DenseMapInfo<KeyT>, detail::DenseMapPair<KeyT, ValueT>

DenseMapInfo 有很多的模板定义,可以适用于各种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-2);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};

内联函数getEmptyKeygetTombstoneKey均返回一个特征值,用来标记 bucket 的状态 Empty 和 Tombstone。
静态函数getHashValue根据指针计算出哈希值。
ptr_hash()的实现如下:

1
2
3
4
5
6
7
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}

看不懂,知道是计算哈希值的就好了。

最后一个静态函数isEqual用来比较两个参数是否相等。

接着看detail::DenseMapPair<KeyT, ValueT> 它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {
DenseMapPair() {}

DenseMapPair(const KeyT &Key, const ValueT &Value)
: std::pair<KeyT, ValueT>(Key, Value) {}

DenseMapPair(KeyT &&Key, ValueT &&Value)
: std::pair<KeyT, ValueT>(std::move(Key), std::move(Value)) {}

template <typename AltKeyT, typename AltValueT>
DenseMapPair(AltKeyT &&AltKey, AltValueT &&AltValue,
typename std::enable_if<
std::is_convertible<AltKeyT, KeyT>::value &&
std::is_convertible<AltValueT, ValueT>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltKeyT>(AltKey),
std::forward<AltValueT>(AltValue)) {}

template <typename AltPairT>
DenseMapPair(AltPairT &&AltPair,
typename std::enable_if<std::is_convertible<
AltPairT, std::pair<KeyT, ValueT>>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltPairT>(AltPair)) {}

KeyT &getFirst() { return std::pair<KeyT, ValueT>::first; }
const KeyT &getFirst() const { return std::pair<KeyT, ValueT>::first; }
ValueT &getSecond() { return std::pair<KeyT, ValueT>::second; }
const ValueT &getSecond() const { return std::pair<KeyT, ValueT>::second; }
};

看起来很复杂的样子,但其实只要知道它的父类std::pair是干嘛的就好了。
std::pair的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。它是一个模板结构体,主要的两个成员变量是first和second,这两个变量可以直接使用。
再根据注释,可以推测出DenseMapPair作用是将 keyT 和 ValueT 两个类型的值保存在一起

继续之前的调试,在代码size_t& refcntStorage = table.refcnts[this];打个断点,然后点击 continue program execution 跳转过去。

注意这里的size_t&符号不是取内存地址意思,& 在这里表示引用的意思。举个例子

1
2
3
int a = 1;
int& b = a;
b = 10;

对 b 赋值 10 后,a 的值也变成了 10。好了,继续

继续 step into

首先会跳转到类DisguisedPtr的一个初始化方法里面,这里对指针进行伪装并赋值给了成员对象 value。
继续 step into

跳转到了这里。可以看出来DenseMap的父类DenseMapBase也重载了[]方法。
参数类型为KeyT &&,也就是DisguisedPtr<objc_object> &&类型。这里的&&符号以及下面的std::move(Key)函数都是为了确定参数是右值引用,实现移动语义,减少不必要的参数拷贝。

继续 step into,跳转到了下面这里

之前我们讲DenseMapPair<KeyT, ValueT>的时候说到过,这个类的作用是将两个类型的值的组合成一个整体,类似于我们平常使用的字典。key 对应 first, value 对应 second。在FindAndConstruct方法中,首先会通过LookupBucketFor()查找是否已经有存在的 bucket,如果没有则通过InsertIntoBucket函数生成并返回对应的 bucket。

下面是LookupBucketFor()函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
// 返回成员变量 Buckets
const BucketT *BucketsPtr = getBuckets();
// 返回成员变量 NumBuckets
const unsigned NumBuckets = getNumBuckets();

if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}

// 保存在查找过程中碰到的墓碑
const BucketT *FoundTombstone = nullptr;
// DenseMapInfo 的内联函数 getEmptyKey
const KeyT EmptyKey = getEmptyKey();
// DenseMapInfo 的内联函数 getTombstoneKey
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");

//DenseMapInfo 的静态函数 getHashValue 计算哈希值,并与 NumBuckets-1 进行与运算,得到对应 BucketT 实例的 index
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
// 查找次数
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
// 找到了对应的 BucketT 实例
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}

// If we found an empty bucket, the key doesn't exist in the set.
// Insert it and return the default value.
// 如果对应的 bucket 是空的。如果是第一次查找的话则在这个 bucket 里面插入 key 和 value(在InsertIntoBucket函数中执行)。如果不是第一次并且之前找到了墓碑,则使用墓碑bucket插入key和value
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
// If we've already seen a tombstone while probing, fill it in instead
// of the empty bucket we eventually probed to.
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}

// If this is a tombstone, remember it. If Val ends up not in the map, we
// prefer to return it than something that would require more probing.
// Ditto for zero values.
// 如果找到一个墓碑,则使用 FoundTombstone 将这个墓碑记录下来
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
!FoundTombstone)
FoundTombstone = ThisBucket; // Remember the first tombstone found.
// 如果 FoundTombstone 为空并且 对应的 bucket 的 second 是 0,则用FoundTombstone保存下这个bucket
if (ValueInfoT::isPurgeable(ThisBucket->getSecond()) && !FoundTombstone)
FoundTombstone = ThisBucket;

// Otherwise, it's a hash collision or a tombstone, continue quadratic
// probing.
// 如果查找次数多余 NumBuckets ,则报错
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
// 重新计算 BucketNo,重新查找
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}

在上面的代码里写了点注释帮助理解。
bucket 有两种状态,一种是 Empty,这个表示当前这个 bucket 还没有被使用的标记;另一个状态是 TombstoneKey,当一个 bucket 被使用了,当对象释放后该 bucket 被定义为这种状态

通过FindAndConstruct函数的分析我们知道,如果已经存在 bucket,则返回 YES,并将指针 TheBucket 指向这个 bucket。如果存在一个墓碑或者空的 bucket,则返回 false,并将指针 TheBucket 指向这个 bucket。

接着分析InsertIntoBucket(),下面是它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);

// 保存 key 到 bucket 的成员变量 first 中
TheBucket->getFirst() = std::forward<KeyArg>(Key);
// 初始化 ValueT 的实例并保存到 bucket 的成员变量 second 中
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}

template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// If the load of the hash table is more than 3/4, or if fewer than 1/8 of
// the buckets are empty (meaning that many are filled with tombstones),
// grow the table.
//
// The later case is tricky. For example, if we had one empty bucket with
// tons of tombstones, failing lookups (e.g. for insertion) would have to
// probe almost the entire table until it found the empty bucket. If the
// table completely filled with tombstones, no lookup would ever succeed,
// causing infinite loops in lookup.
unsigned NewNumEntries = getNumEntries() + 1;
unsigned NumBuckets = getNumBuckets();
// 当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则增加 buckets 数组的长度。
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
// 扩增原来的 NumBuckets,新的 buckets 长度为旧的两倍,长度至少为 4。新的 buckets 生成后将旧数组里面的数据转移过去,并且旧数组删除
this->grow(NumBuckets * 2);
// 因为重新生成了 buckets 数组,所以需要使用LookupBucketFor重新查找对应bucket,并用指针 TheBucket 指向
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);

// Only update the state after we've grown our bucket space appropriately
// so that when growing buckets we have self-consistent entry count.
// If we are writing over a tombstone or zero value, remember this.
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
// 使用空bucket,NumEntries + 1
incrementNumEntries();
} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
// 使用墓碑 bucket,NumEntries + 1, NumTombstones - 1
incrementNumEntries();
decrementNumTombstones();
} else {
// we should be purging a zero. No accounting changes.
// 如果 second 等于 0,则析构 second
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}

return TheBucket;
}

通过InsertIntoBucket函数我们了解到,当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则会生成一个新的 buckets,长度为原来的2倍(最小为4),并将旧数组的数据转移到新数组中,并重新查找 TheBucket 的 index。最后将伪装后的指针保存到bucket的first中,将引用计数保存到bucket的second中。

理论分析到这里。这几个函数的调试我就不贴出来了,比较长,你们可以自己调试下。

接下来在代码size_t oldRefcnt = refcntStorage;中打个断点,点击 continue program execution 跳转过去。

让我们回到objc_object::sidetable_addExtraRC_nolock函数中。因为我们刚刚在 sidetable 索引表里面插入了 bucket,所以这里 oldRefcnt 的值 为 0。
接下来会对 oldRefcnt 进行校验,这说明 oldRefcnt 并没有把所有的 bit 都用来保存引用计数。

  • 低 1 位(从低位往高位数第一位)用来标记是否是弱引用
  • 低 2 位用来标记该变量是否在析构
  • 高 1 位(从高位往低位数第一位)用来标记保存的引用计数是否已经溢出。

下面的代码我们好像看到过了,对,在将引用计数保存在 isa 指针那里

1
2
3
4
5
6
7
8
9
10
11
12
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}

这几句的代码的作用是将 newRefcnt = oldRefcnt + delta_rc,如果值太大了溢出了则将 carry 赋值为1,并将 refcntStorage 的高1位赋值为1,并返回true。如果没有溢出的话则将 newRefcnt 赋值到 refcntStorage,并返回 false。

到这里,我们探究了当引用计数增加时,如何保存多余的引用计数。简单的归纳下:

  • 有一个静态全局变量 SideTablesMap,实际上是 StripedMap 的实例。以便于在程序初始化的时候就能用到
  • 变量 SideTablesMap 里面有一个长度为 8,类型为 SideTable 的数组成员array,每个元素占用的内存为64(spinlock_t内存4, RefcountMap内存24,weak_table_t内存32)。在保存引用计数的时候,根据指针地址来计算出相应 SideTable 的 index。
  • 获取到相应的 SideTable 后,根据指针地址计算出相应的哈希值,然后查找 bucket。如果 bucket 不存在,则使用空 bucket 或者 墓碑 bucket。如果 bucket 的数量不够则扩容。将指针为 key 和 引用计数 为 value 保存到 bucket 当中。

注意:当一个对象创建后它的引用计数为 1,但是 isa 指针里的引用计数还是 0。因为初始化的时候并没有对引用计数进行操作。

减少引用计数

这里我们只讨论有额外引用计数存储在 sidetable,且 isa 指针里面存储的引用计数为 0 时的临界情况。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void aoo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 512; i++) {
_objc_rootRetain(o1);
}
for (int i = 0; i < 129; i++) {
if (i == 128) {
int a = 1;
}
_objc_rootRelease(o1);
}
}

在代码int a = 1;处打个断点,此时 isa 保存的引用计数为 0。一直 step over,直到跳转到objc_object::rootRelease(bool performDealloc, bool handleUnderflow)函数。


一直点击 step over,直到停在newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);那一行

用调试命令打印看一下此时 isa 指针里面的内容

因为使用的是模拟器,所以 isa 里面只有最高的 8 个bit用来保存引用计数,之前我们减少它的引用计数 128。所以此时它 isa 指针里面保存的引用计数为 0。调试输出结果extra_rc也为0,符合预期(0b是二进制符号)

接下来系统要将引用保存在 isa 指针的引用计数 -1,并判断是否溢流。此刻它的引用计数已经为 0 了,那么对它进行 -1 操作会发生什么呢?我们接着调试,点击 step over


可以看到变量carry == 1,表示出现溢流。让我们再看一下 isa 指针里面发生了什么。


我们可以看到保存引用计数的 8 个 bit 都变成了 1,这其实就是 -1 在无符号整数中的表现形式,不懂的同学可以去了解一下补码。

接着一直 step over,我们可以看到因为溢流所以跳转到了underflow部分。一直 step over 直到跳转到如下的位置

rootRelease_underflow最终调用的还是objc_object::rootRelease()函数,只不过参数 handleUnderflow 变成了 YES。
进入该函数后,又会执行之前的那些代码,过程也是一样的,就不重复说了。接下来会来到这个位置

是的,因为没上锁,所以又会回到retry部分的代码。注释里写着是因为

Need to start over to avoid a race against the nonpointer -> raw pointer transition.

不是很明白,但是过了。继续 step over

之前引用计数溢出时,我们将RC_HALF数目的引用计数存储到了sidetable当中,此时因为溢流,所以我们从sidetable中取回同样数目的引用计数。

step into 进入objc_object::sidetable_subExtraRC_nolock()函数

在最开始的代码中,我们增加了变量o1 512 的引用计数, 在 isa 指针中保存了 128, 还剩余 384. 但是因为最低两位没有用来保存数据, 所以在 bucket 中存储的数字应该是 384 << 2, 即 1536, 符合预期

接下来的代码作用就是判断保存在 bucket 中的引用计数是否合理, 如何合理的话则将原有的数据减去RC_HALF并重新保存. 点击 step out 返回之前的函数

  • 代码块1的作用是将从 sidetable 取回了保存着的一部分引用计数, -1 保存到 isa 指针里面
  • 如果代码块1保存失败, 则用代码块2重新保存一次
  • 如果代码块2保存失败了, 则将从 sidetable 取出的引用计数重新放回去. 然后又跳回到 retry 部分
  • 保存成功则解锁, 并返回 false. 返回 true 表示这个对象需要 dealloc 了

另一种情况, 如果一个对象 isa 引用计数为 0, 且没有额外的引用计数. 那么它从 sidetable ‘借来的’ borrowed 为 0, 那么会将 isa 的 deallocating 赋值为 1, 并随后对其发送 delloc 消息进行内存消耗

最后

以上就是此次对引用计数的探究, 欢迎留言告诉我错的或者不明白的地方~