原子类源码
㈠ Netty 源码解析 ——— ChannelConfig 和 Attribute
嗯,本文与其说是ChannelConfig、Attribute源码解析,不如说是对ChannelConfig以及Attribute结构层次的分析。因为这才是它们在Netty中使用到的重要之处。
在 Netty 源码解析 ——— 服务端启动流程 (下) 中说过,当我们在构建NioServerSocketChannel的时候同时会构建一个NioServerSocketChannelConfig对象赋值给NioServerSocketChannel的成员变量config。
而这一个NioServerSocketChannelConfig是当前NioServerSocketChannel配置属性的集合。NioServerSocketChannelConfig主要用于对NioServerSocketChannel相关配置的设置(如,网络的相关参数配置),比如,配置Channel是否为非阻塞、配置连接超时时间等等。
NioServerSocketChannelConfig其实是一个ChannelConfig实例。ChannelConfig表示为一个Channel相关的配置属性的集合。所以NioServerSocketChannelConfig就是针对于NioServerSocketChannel的配置属性的集合。
ChannelConfig是Channel所需的公共配置属性的集合,如,setAllocator(设置用于channel分配buffer的分配器)。而不同类型的网络传输对应的Channel有它们自己特有的配置,因此可以通过扩展ChannelConfig来补充特有的配置,如,ServerSocketChannelConfig是针对基于TCP连接的服务端ServerSocketChannel相关配置属性的集合,它补充了针对TCP服务端所需的特有配置的设置setBacklog、setReuseAddress、setReceiveBufferSize。
DefaultChannelConfig作为ChannelConfig的默认实现,对ChannelConfig中的配置提供了默认值。
接下来,我们来看一个设置ChannelConfig的流程:
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
我们可以在启动服务端前通过ServerBootstrap来进行相关配置的设置,该选项配置会在Channel初始化时被获取并设置到Channel中,最终会调用底层ServerSocket.setReuseAddress方法来完成配置的设置。
ServerBootstrap的init()方法:
首先对option和value进行校验,其实就是进行非空校验。
然后判断对应的是哪个常量属性,并进行相应属性的设置。如果传进来的ChannelOption不是已经设定好的常量属性,则会打印一条警告级别的日志,告知这是未知的channel option。
Netty提供ChannelOption的一个主要的功能就是让特定的变量的值给类型化。因为从’ChannelOption<T> option’和’T value’可以看出,我们属性的值类型T,是取决于ChannelOption的泛型的,也就属性值类型是由属性来决定的。
这里,我们可以看到有个ChannelOption类,它允许以类型安全的方式去配置一个ChannelConfig。支持哪一种ChannelOption取决于ChannelConfig的实际的实现并且也可能取决于它所属的传输层的本质。
可见ChannelOption是一个Consant扩展类,Consant是Netty提供的一个单例类,它能安全去通过’==’来进行比较操作。通过ConstantPool进行管理和创建。
常量由一个id和name组成。id:表示分配给常量的唯一数字;name:表示常量的名字。
如上所说,Constant是由ConstantPool来进行管理和创建的,那么ConstantPool又是个什么样的类了?
首先从constants中get这个name对应的常量,如果不存在则调用newConstant()来构建这个常量tempConstant,然后在调用constants.putIfAbsent方法来实现“如果该name没有存在对应的常量,则插入,否则返回该name所对应的常量。(这整个的过程都是原子性的)”,因此我们是根据putIfAbsent方法的返回来判断该name对应的常量是否已经存在于constants中的。如果返回为null,则说明当前创建的tempConstant就为name所对应的常量;否则,将putIfAbsent返回的name已经对应的常量值返回。(注意,因为ConcurrentHashMap不会允许value为null的情况,所以我们可以根据putIfAbsent返回为null则代表该name在此之前并未有对应的常量值)
正如我们前面所说的,这个ConstantPool<ChannelOption<Object>> pool(即,ChannelOption常量池)是ChannelOption的一个私有静态成员属性,用于管理和创建ChannelOption。
这些定义好的ChannelOption常量都已经存储数到ChannelOption的常量池(ConstantPool)中了。
注意,ChannelOption本身并不维护选项值的信息,它只是维护选项名字本身。比如,“public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");”👈这只是维护了“SO_RCVBUF”这个选项名字的信息,同时泛型表示选择值类型,即“SO_RCVBUF”选项值为Integer。
好了,到目前为止,我们对Netty的ChannelOption的设置以及底层的实现已经分析完了,简单的来说:Netty在初始化Channel时会构建一个ChannelConfig对象,而ChannelConfig是Channel配置属性的集合。比如,Netty在初始化NioServerSocketChannel的时候同时会构建一个NioServerSocketChannelConfig对象,并将其赋值给NioServerSocketChannel的成员变量config,而这个config(NioServerSocketChannelConfig)维护了NioServerSocketChannel的所有配置属性。比如,NioServerSocketChannelConfig提供了setConnectTimeoutMillis方法来设置NioServerSocketChannel连接超时的时间。
同时,程序可以通过ServerBootstrap或Boostrap的option(ChannelOption<T> option, T value)方法来实现配置的设置。这里,我们通过ChannelOption来实现配置的设置,ChannelOption中已经将常用的配置项预定义为了常量供我们直接使用,同时ChannelOption的一个主要的功能就是让特定的变量的值给类型化。因为从’ChannelOption<T> option’和’T value’可以看出,我们属性的值类型T,是取决于ChannelOption的泛型的,也就属性值类型是由属性来决定的。
一个attribute允许存储一个值的引用。它可以被自动的更新并且是线程安全的。
其实Attribute就是一个属性对象,这个属性的名称为AttributeKey<T> key,而属性的值为T value。
我们可以通过程序ServerBootstrap或Boostrap的attr方法来设置一个Channel的属性,如:
serverBootstrap.attr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString());
当Netty底层初始化Channel的时候,就会将我们设置的attribute给设置到Channel中:
如上面所说,Attribute就是一个属性对象,这个属性的名称为AttributeKey<T> key,而属性的值为T value。
而AttributeKey也是Constant的一个扩展,因此也有一个ConstantPool来管理和创建,这和ChannelOption是类似的。
Channel类本身继承了AttributeMap类,而AttributeMap它持有多个Attribute,这些Attribute可以通过AttributeKey来访问的。所以,才可以通过channel.attr(key).set(value)的方式将属性设置到channel中了(即,这里的attr方法实际上是AttributeMap接口中的方法)。
AttributeKey、Attribute、AttributeMap间的关系:
AttributeMap相对于一个map,AttributeKey相当于map的key,Attribute是一个持有key(AttributeKey)和value的对象。因此在map中我们可以通过AttributeKey key获取Attribute,从而获取Attribute中的value(即,属性值)。
Q:ChannelHandlerContext和Channel都提供了attr方法,那么它们设置的属性作用域有什么不同了?
A:在Netty 4.1版本之前,它们两设置的属性作用域确实存在着不同,但从Netty 4.1版本开始,它们两设置的属性的作用域已经完全相同了。
若文章有任何错误,望大家不吝指教:)
圣思园《精通并发与Netty》
㈡ 新手求教linux下的原子操作该怎么写
linux中关于原子操作
2016年08月02日
- 一.整型原子操作定义于#include<asm/atomic.h>分为 定义,获取,加减,测试,返回。void atomic_set(atomic_t *v,int i); //设置原子变量v的值为iatomic_t v = ATOMIC_INIT(0); //定义原子变量v,并初始化为0;atomic_read(atomic_t* v); //返回原子变量v的值;void atomic_add(int i, atomic_t* v); //原子变量v增加i;void atomic_sub(int i, atomic_t* v); void atomic_inc(atomic_t* v); //原子变量增加1;void atomic_dec(atomic_t* v);int atomic_inc_and_test(atomic_t* v); //先自增1,然后测试其值是否为0,若为0,则返回true,否则返回false;int atomic_dec_and_test(atomic_t* v); int atomic_sub_and_test(int i, atomic_t* v); //先减i,然后测试其值是否为0,若为0,则返回true,否则返回false;注意:只有自加,没有加操作int atomic_add_return(int i, atomic_t* v); //v的值加i后返回新的值;int atomic_sub_return(int i, atomic_t* v); int atomic_inc_return(atomic_t* v); //v的值自增1后返回新的值;int atomic_dec_return(atomic_t* v);二.位原子操作定义于#include<asm/bitops.h>分为 设置,清除,改变,测试void set_bit(int nr, volatile void* addr); //设置地址addr的第nr位,所谓设置位,就是把位写为1;void clear_bit(int nr, volatile void* addr); //清除地址addr的第nr位,所谓清除位,就是把位写为0;void change_bit(int nr, volatile void* addr); //把地址addr的第nr位反转;int test_bit(int nr, volatile void* addr); //返回地址addr的第nr位;int test_and_set_bit(int nr, volatile void* addr);//测试并设置位;若addr的第nr位非0,则返回true; 若addr的第nr位为0,则返回false;int test_and_clear_bit(int nr, volatile void* addr);//测试并清除位;int test_and_change_bit(int nr, volatile void* addr);//测试并反转位;上述操作等同于先执行test_bit(nr,voidaddr)然后在执行xxx_bit(nr,voidaddr)
- 举个简单例子:为了实现设备只能被一个进程打开,从而避免竞态的出现static atomic_t scull_available = ATOMIC_INIT(1);//init atomic在scull_open 函数和scull_close函数中:int scull_open(struct inode *inode, struct file *filp){ struct scull_dev *dev; // device information dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev;// for other methods if(!atomic_dec_and_test(&scull_available)){ atomic_inc(&scull_available); return -EBUSY; } return 0; // success}int scull_release(struct inode *inode, struct file *filp){ atomic_inc(&scull_available); return 0;}
#if__LINUX_ARM_ARCH__>=6
......(通过ldrex/strex指令的汇编实现)
#else/*ARM_ARCH_6*/
#ifdef CONFIG_SMP
#errorSMPnotsupportedonpre-ARMv6 CPUs
#endif
......(通过关闭CPU中断的C语言实现)
#endif/*__LINUX_ARM_ARCH__*/
......
#ifndef CONFIG_GENERIC_ATOMIC64
......(通过ldrexd/strexd指令的汇编实现的64bit原子变量的访问)
#else/*!CONFIG_GENERIC_ATOMIC64*/
#include<asm-generic/atomic64.h>
#endif
#include<asm-generic/atomic-long.h>
/*
*ARMv6 UP 和 SMP 安全原子操作。 我们是用独占载入和
*独占存储来保证这些操作的原子性。我们可能会通过循环
*来保证成功更新变量。
*/
static inline void atomic_add(inti,atomic_t*v)
{
unsigned long tmp;
intresult;
__asm__ __volatile__("@ atomic_add "
"1: ldrex %0, [%3] "
" add %0, %0, %4 "
" strex %1, %0, [%3] "
" teq %1, #0 "
" bne 1b"
:"=&r"(result),"=&r"(tmp),"+Qo"(v->counter)
:"r"(&v->counter),"Ir"(i)
:"cc");
}
A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
E:没有标记为CPU1独占访问,不会进行存储,并返回1(操作失败)。
F:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
原子操作:就是在执行某一操作时不被打断。
linux原子操作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。
对于临界区的操作可以加锁来保证原子性,对于全局变量或静态变量操作则需要依赖于硬件平台的原子变量操作。
因此原子操作有两类:一类是各种临界区的锁,一类是操作原子变量的函数。
对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令。
但是linux种对于c/c++程序(一条c编译成多条汇编),由于上述提到的原因不能保证原子性,因此linux提供了一套函数来操作全局变量或静态变量。
假设原子变量的底层实现是由一个汇编指令实现的,这个原子性必然有保障。但是如果原子变量的实现是由多条指令组合而成的,那么对于SMP和中断的介入会不会有什么影响呢?我在看ARM的原子变量操作实现的时候,发现其是由多条汇编指令(ldrex/strex)实现的。在参考了别的书籍和资料后,发现大部分书中对这两条指令的描诉都是说他们是支持在SMP系统中实现多核共享内存的互斥访问。但在UP系统中使用,如果ldrex/strex和之间发生了中断,并在中断中也用ldrex/strex操作了同一个原子变量会不会有问题呢?就这个问题,我认真看了一下内核的ARM原子变量源码和ARM官方对于ldrex/strex的功能解释,总结如下:
一、ARM构架的原子变量实现结构
对于ARM构架的原子变量实现源码位于:arch/arm/include/asm/atomic.h
其主要的实现代码分为ARMv6以上(含v6)构架的实现和ARMv6版本以下的实现。
该文件的主要结构如下:
这样的安排是依据ARM核心指令集版本的实现来做的:
(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):
1.2.2.Exclusive monitors
4.2.12.LDREX和STREX
(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。
对于(2),非常好理解。
但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考《ARM GCC内嵌汇编手册》
二、内核对于ARM构架的atomic_add源码分析
源码分析:
注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。
(1)ldrex %0, [%3]
意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是将result保存到&v->counter指向的内存中,此时Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。
(4)teq %1, #0
测试strex是否成功(tmp == 0??)
(5)bne 1b
如果发现strex失败,从(1)再次执行。
通过上面的分析,可知关键在于strex的操作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。以下通过可能的情况分析ldrex/strex指令机制。(请阅读时参考4.2.12.LDREX和STREX)
1、UP系统或SMP系统中变量为非CPU间共享访问的情况
此情况下,仅有一个CPU可能访问变量,此时仅有Local monitor需要关注。
假设CPU执行到(2)的时候,来了一个中断,并在中断里使用ldrex/strex操作了同一个原子变量。则情况如下图所示:
虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。
当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子操作是可以信任的。
㈢ “okhttp3 4.9.3 版本简单解析”
关于okhttp3的解析网上已经有非常多优秀的博文了,每每看完都觉得醍醐灌顶,豁然开朗。但等不了几天再回头看,还是跟当初一样陌生,究其根本原因,我们不过是在享受着别人的成果跟着别人的思路云阅读源码了一遍。okhttp从早期的Java版本到Kotlin版本一直不断优化升级,实现细节上也作出了调整。重读源码加上自身的思考能深刻地理解okhttp的实现原理。
从execute()开始,发现其实是一个接口中的方法(Call),这个很好理解根据官方的解释,Call其实是一个待执行的请求,并且这个请求所要的参数已经被准备好;当然既然是请求,那么它是可以被取消的。其次代表单个请求与响应流,因此不能够被再次执行。
Call接口具体的代码实现,重点关注同步执行方法execute()与异步请求enqueue():
Call作为接口,那么具体的实现细节则需要看它的实现类,RealCall作为Call的实现类,先找到对execute()的重写。
代码很少,逐步分析,首先对同步请求进行了检查-判断请求是否已经被执行过了;而这里使用的是并发包下的原子类CAS乐观锁,这里使用CAS比较算法目的也是为提升效率。其次是超时时间的判断,这个比较简单。在看callStart()的具体实现。上代码:
看名称猜测应该是事件监听之类的,可能是包括一些信息的记录与打印。回到RealCall类中,看看这个eventListener的作用到底是什么:
internal val eventListener: EventListener = client.eventListenerFactory.create(this)
继续向下可以知道这个EventListener是一个抽象类,而项目中其唯一实现类为LoggingEventListener,猜测还是有依据的,继续往下看:
实现类LoggingEventListener中对此方法的具体实现:
总结这个callStart()的作用,当同步请求或者异步请求被加到队列时,callStart()会被立即执行(在没有达到线程限制的情况下)记录请求开始的时间与请求的一些信息。如下:
代卖第四段#4 client.dispatcher.executed(this),看样子是在这里开启执行的,可实际真是如此嘛?回到OkHttpClient类中,看看这个分发器dispatcher到底是什么。
具体实现类Dispatcher代码(保留重要代码):
根据注释的信息可以知道,Dispatcher是处理异步请求的执行的策略,当然开发可以实现自己的策略。
知道了Dispatcher的作用,再回到client.dispatcher.executed(this),也即:
结合execute()与Dispatcher分析
到这里请求其实还没有真正的执行,只是在做一些前期的工作,回到Call接口中看看对方法同步请求方法execute()的说明:同步请求可以立即执行,阻塞直到返回正确的结果,或者报错结束。
到#4步执行后,return () //#5这个方法才是请求一步步推进的核心。也是okhttp网络请求责任链的核心模块。
分析()方法之前有必要看看OkHttpClient的构造参数,使用的Builder模式,参数很多,可配置化的东西很多,精简一下主要关注几个参数:
到这里有个疑问,这个添加自定义拦截器与添加自定义网络拦截器有什么区别呢?方法看上去是差不多的,查看官方的说明可以发现一些细节。文档中解释了Application Interceptor与Network Interceptors的细微差别。先回到RealCall中查看()是如何对拦截器结合组装的:
看#1与#2分别对应添加的自定义拦截器与自定义网络拦截器的位置,自定义拦截器是拦截器链的链头,而自定义网络拦截器在ConnectInterceptor拦截器与CallServerInterceptor拦截器之间被添加。总结一下:
Don’t need to worry about intermediate responses like redirects and retries.
Are always invoked once, even if the HTTP response is served from the cache.
Observe the application’s original intent. Unconcerned with OkHttp-injected headers like If-None-Match.
Permitted to short-circuit and not call Chain.proceed().
Permitted to retry and make multiple calls to Chain.proceed().
Can adjust Call timeouts using withConnectTimeout, withReadTimeout, withWriteTimeout.
Able to operate on intermediate responses like redirects and retries.
Not invoked for cached responses that short-circuit the network.
Observe the data just as it will be transmitted over the network.
Access to the Connection that carries the request.
综上可以得出整个链的顺序结构,如果都包含自定义拦截器与自定义网络拦截器,则为自定义拦截器->RetryAndFollowUpInterceptor->BridgeInterceptor->CacheInterceptor->ConnectInterceptor->自定义网络拦截器->CallServerInterceptor;那么链是如何按照顺序依次执行的呢?okhttp在这里设计比较精妙,在构造RealInterceptorChain对象时带入index信息,这个index记录的就是单个拦截器链的位置信息,而RealInterceptorChain.proceed(request: Request)通过index++自增一步步执行责任链一直到链尾。
简单的分析推进过程:
1.RealInterceptorChain的构造参数中携带了index的信息,index++自增通过proceed方法不断执行。
2.拦截器统一实现Interceptor接口,接口中fun proceed(request: Request): Response保证了链式链接。当然拦截器的顺序是按照一定的规则排列的,逐个分析。
1.重试拦截器规定默认的重试次数为20次
2.以response = realChain.proceed(request)为分界点,包括其他的拦截器,在责任链传递之前所做的工作都是前序工作,然后将request下发到下一个拦截器。
3.response = realChain.proceed(request)后的代码逻辑为后续工作,即拿到上个拦截器的response结果,有点递归的意思,按照责任链的执行一直到最后一个拦截器获得的结果依次上抛每个拦截器处理这个response完成一些后序工作。
4.当然并不是每个请求都会走完整个链,如CacheInterceptor当开启了缓存(存在缓存)拿到了缓存的response那么之后的拦截器就不会在继续传递。
1.桥接拦截器主要对请求的Hader的信息的补充,包括内容长度等。
2.传递请求到下一个链,等待返回的response信息。
3.后序的操作包括Cookie、gzip压缩信息,User-Agent等信息的补充。
1.缓存拦截器默认没有被开启,需要在调用时指定缓存的目录,内部基于DiskLruCache实现了磁盘缓存。
2.当缓存开启,且命中缓存,那么链的调用不会再继续向下传递(此时已经拿到了response)直接进行后序的操作。
3.如果未命中,则会继续传递到下一个链也即是ConnectInterceptor。
1.建立与目标的服务器的TCP或者TCP-TLS的链接。
2.与之前的拦截器不同,前面的拦截器的前序操作基于调用方法realChain.proceed()之前,但是ConnectInterceptor 没有后序操作,下发到下一个拦截器 。
1.实质上是请求与I/O操作,将请求的数据写入到Socket中。
2.从Socket读取响应的数据 TCP/TCP-TLS对应的端口 ,对于I/O操作基于的是okio,而okhttp的高效请求同样离不开okio的支持。
3.拿到数据reponse返回到之前包含有后序操作的拦截器,但ConnectInterceptor除外,ConnectInterceptor是没有后续操作的。
整个拦截器流程图如下:
1.排除极端的情况,System.exit()或者其他, finally 块必然执行,不论发生异常与否,也不论在 finally 之前是否有return。
2.不管在 try 块中是否包含 return, finally 块总是在 return 之前执行。
3.如果 finally 块中有 return ,那么 try 块和 catch 块中的 return 就没有执行机会了。
Tip:第二条的结论很重要,回到execute()方法,dispatcher.finished(this)在结果response结果返回之前执行,看finished()具体实现。
1.#1方法一,calls.remove(call)返回为 true ,也即是这个同步请求被从runningSyncCalls中移除释放;所以idleCallback为空。
2.#3很显然asyncCall的结果为空,没有异步请求,在看#4具体实现,runningSyncCalls的size为1。则isRunning的结果为 true 。idleCallback.run()不会被执行,并且idleCallback其实也是为空。
1.从 OkHttpClient().newCall(request).execute() 开启同步请求任务。
2.得到的 RealCall 对象作为 Call 的唯一实现类,其中同步方法 execute() 是阻塞的,调用到会立即执行 阻塞 到有结果返回,或者发生错误 error 被打断阻塞。
3. RealCall 中同步 execute() 请求方法被执行,而此时 OkHttpClient 实例中的异步任务分发器 Dispatcher 会将请求的实例 RealCall 添加到双端队列 runningSyncCalls 中去。
4.通过 RealCall 中的方法 () 开启请求拦截器的责任链,将请求逐一下发,通过持有 index 并自增操作,其次除 ConnectInterceptor 与链尾 CallServerInterceptor 其余默认拦截器均有以 chain.proceed(request) 为分界点的前序与后序操作,拿到 response 后依次处理后序操作。
5.最终返回结果 response 之前,对进行中的同步任务做了移除队列的操作也即 finally 中 client.dispatcher.finished(this) 方法,最终得到的结果 response 返回到客户端,至此整个 同步请求 流程就结束了。
Github
Square
㈣ Apple 源码用到的一些数据结构
本篇英文名叫 CWC:Kitchen Tools That Cook Loves ,翻译过来的意思是 苹果源码中出现的一些数据结构 ,不断积累更新。
CWC : Cooking With Cook ,翻译过来的中文意思就是 作为一个长期热爱苹果的苹果开发者,我们要陪着水果公司一起积累和成长。
目前: entsize_list_tt 、 list_array_tt 、 cache_t's buckets ...
entsize_list_tt 其实就是一个通用的容器,可以获取 内部的迭代器,用于遍历内部存储的元素
出现场景:
三者的声明头如下:
entsize_list_t 定义源码,省略大部分方法:
这个类用来表示一个空、单数组、或者多数组。它和 list 的帆桐区别就是 多了一个多维数组的封装。
出现场景:
ro 中没有,只有三个单 List。
三者的声明头如下:
list_array_tt 源码部分如下:
cache_t 的结构体定义:
buckets 的内部是一个连续的存储空间,存储是一个散列表。
开辟声明的函数调用的是 calloc
当 msgSend 的时候,就会调用 fillCache 进行方法的缓存,存储的涉及 cls sel 和 imp
bucket_t 的结构体很有意思,arm64 和 i386 的两个值的顺序是反着的。
arm64 的时候是 :
armv7* , i386 和 x86_64 的时候是:
源码注释:
初始的 capacity 是 4。
源码中 cache_t::insert(cls, sel, imp, reveiver) 方法调用的时候,判断扩容。
fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)
也就是说当大于四分之三的时候,就会进行扩容操作,每次 double 扩容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
当然不是无限制的扩容,有一个最大容量的限制:
MAX_CACHE_SIZE = 1 << 16
这个类型应该是执行最多次的,看一些文章说一秒钟iOS中执行几百万次
explicit_atomic用来给catchT缓存方法用,核心是原子性和线程安全。
weak弱引用的散列表
扩展: non-fragile structs 是什么?OC 1.0 (iOS自始至终都是2.0起的,Mac最开始是1.0)桐轿缺译器生成了一个 ivar 布局,显示了在类中从哪可局辩以访问 ivars ,对 ivar 的访问就可以通过 对象地址 + ivar偏移字节 的方法。苹果更新了NSObject类,例如增加一些属性,这个又是静态库,发布新版本的系统,这个时候布局就出错了,就不得不重新编译子类来恢复兼容性。(那如果是在线上运行的app,升级系统后就没办法运行了)
使用 Non Fragile ivars 时,程序进行检测来调整类中新增的 ivar 的偏移量。 这样就可以通过 对象地址 + 基类大小 + ivar偏移字节 的方法来计算出 ivar 相应的地址,并访问到相应的 ivar。(即使升级iOS系统,之前的app也能正常运行)
扩展再扩展: 为什么OC类不能动态添加成员变量? runtime函数中,确实有一个class_addIvar()函数用于给类添加成员变量,但是文档中特别说明: This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported. 这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用objc_registerClassPair之后才可以被使用,同样没有机会再添加成员变量。
理论上说,我还是认为可以添加,只是为什么一定不可以,就不得而知了。