当前位置:首页 » 操作系统 » 解析算法源码

解析算法源码

发布时间: 2023-10-21 11:31:57

A. XXL admin 源码解析

xxl-job 的 admin 服务是 xxl-job 的调度中心,负责管理和调度注册的 job,关于 xxl-job 的使用,可以阅读 “参考阅读” 中的《XXL-JOB分布式调度框架全面详解》,这里主要是介绍 admin 中的源码。

admin 服务除了管理页面上的一些接口外,还有一些核心功能,比如:

1、根据 job 的配置,自动调度 job;

2、接收 executor 实例的请求,实现注册和下线;

3、监视失败的 job,进行重试;

4、结束一些异常的 job;

5、清理和统计日志;

这些功能都是在 admin 服务启动后,在后台自动运行的,下面将详细介绍 admin 服务这些功能敏罩的实现。

XxlJobAdminConfig 是 admin 服务的配置类,在 admin 服务启动时,它除了配置 admin 服务的一些参数外,还会启动 admin 服务的所有后台线程。

该类的属性主要分为5类:

1、配置文件中的参数,比如 accessToken;

2、DAO 层各个数据表的 mapper;

3、Spring 容器中的一些 Bean,比如 JobAlarmer、DataSource 等;

4、私有变量 XxlJobScheler 对象;

5、私有静态变量 adminConfig,指向实例自身。

该类有两个重要方法,分别实现自接口 InitializingBean、DisposableBean,作用如下:

这两个方法分别调用了 XxlJobScheler 对象的 init 、 destroy 方法,源码如下:

XxlJobAdminConfig 作为 admin 服务的配置类,作用就是在 Spring 容器启动时,调用 XxlJobScheler 的初始化方法,来初始化和启动 admin 服务的功能。

XxlJobScheler 的作用就是调用各个辅助类(xxxHelper)来启动和结束不同的线程和功能,初始化方法 init 的代码如下:

下面我们主要介绍 init 中各个类及其作用,最后再简单一下介绍 destroy 的作用纤拿森。

当 admin 服务向 executor 实例发出一个调度请求来执行 job 时,会调用 XxlJobTrigger.trigger() 方法把要传输的参数(比如 job_id、jobHandler、job_log_id、阻塞策略等,包装成 TriggerParam 对象)传给 ExecutorBiz 对象来执行一次调度。

xxl-job 对调度过程做了两个优化:

JobTriggerPoolHelper 在 toStart 方法中初始化了它的两个线程池属性,代码如下:

每次有调度请求时,就会在这两个线程池中创建线程,创建线程的逻辑在 addTrigger 方法中。

不同 job 存在执行时长的差异,为了避免不同耗时 job 之间相互阻塞,xxl-job 根据 job 的响应时间,对 job 进行了区分,主要体现在:

如果快 job 与调用频繁的慢 job 在同一个线程池中创建线程,慢 job 会占用大量的线程,导致快 job 线程不能及时运行,降低了线程池和线程的利用率。xxl-job 通过快慢隔离,避免了这个问题。

不能,因为慢 job 还是会占用大量线程,抢占了快 job 的线程资源;增加线程池中的线程数不但没有提升利用率,还会导致大量线程看空闲,利用率反而降低了。最好的方法还是用两个线程池把两者隔离,可以合理地使用各自线程池的资源。

为了记录慢 job 的超时次毁亩数,代码中使用一个 map(变量 jobTimeoutCountMap )来记录一分钟内 job 超时次数,key 值是 job_id,value 是超时次数。在调用 XxlJobTrigger.trigger() 方法之前,会先判断 map 中,该 job_id 的超时次数是否大于 10,如果大于10,就是使用 slowTriggerPool,代码如下:

调用 XxlJobTrigger.trigger() 方法后,根据两个值来更新 jobTimeoutCountMap 的值:

和上面的代码相结合,一个 job 在一分钟内有10次调用超过 500 毫秒,就认为该 job 是一个 频繁调度且耗时的 job。

代码如下:

在该类中,属性变量 minTim 和 jobTimeoutCountMap 都使用 volatile 来修饰,保证了并发调用 addTrigger 时数据的一致性和可见性。

admin 服务发起 job 调度请求时,是在静态方法 public static void trigger() 中调用静态变量 private static JobTriggerPoolHelper helper 的 addTrigger 方法来发起请求的。minTim 和 jobTimeoutCountMap 虽然不是 static 修饰的,但可以看做是全局唯一的(因为持有它们的对象是全局唯一的),因此这两个参数维护的是 admin 服务全局的调度时间和超时次数,为了避免记录的数据量过大,需要每分钟清空一次数据的操作。

admin 服务提供了接口给 executor 来注册和下线,另外,当 executor 长时间(90秒)没有发心跳时,要把 executor 自动下线。前一个功能通过暴露一个接口来接收请求,后一个功能需要开启一个线程,定时更新过期 executor 的状态。

xxl-job 为了提升 admin 服务的性能,在前一个功能的接口接收到 executor 的请求时,不是同步执行,而是在线程池中开启一个线程,异步执行 executor 的注册和下线请求。

JobRegistryHelper 类就负责管理这个线程池和定时线程的。

线程池的定义和初始化代码如下:

executor 实例在发起注册和下线请求时,会调用 AdminBizImpl 类的对应方法,该类的方法如下:

可以看到,AdminBizImpl 类的两个方法都是调用了 JobRegistryHelper 方法来实现,其中 JobRegistryHelper.registry 方法代码如下(registryRemove 代码与之相似):

这两个方法是通过在线程池 registryOrRemoveThreadPool 中创建线程来异步执行请求,然后把数据更新或新建到数据表 xxl_job_registry 中。

当 executor 注册到 admin 服务后(数据入库到 xxl_job_registry 表),是不会在页面上显示的,需要要用户手动添加 job_group 数据(添加到 xxl_job_group 表),admin 服务会自动把用户添加的 job_group 数据与 xxl_job_registry 数据关联。这就需要 admin 定时从 xxl_job_group 表读取数据,关联 xxl_job_registry 表和 xxl_job_group 表的数据。

这个功能是与 “executor 自动下线” 功能在同一个线程中实现,该线程的主要逻辑是:

相关代码如下:

从这里可以看出,如果是对外接口(接收请求等)的功能,使用线程池和异步线程来实现;如果是一些自动任务,则是通过一个线程来定时执行。

如果一个 Job 调度后,没有响应返回,需要定时重试。作为一种“自动执行”的任务,很显然可以像前面 JobRegistryHelper 一样,使用一个线程定时重试。

在这个类中,定义了一个监视线程,以每10 秒一次的频率运行,对失败的 job 进行重试。如果 job 剩余的重试次数大于0,就会 job 进行重试,并把发送告警信息。线程的定义如下:

在这个线程中,它利用 “数据库执行 UPDATE 语句时会加上互斥锁” 的特性,使用了 “基于数据库的分布式锁”,代码如下所示:

在这个语句中,会把 jobLog 的状态设置为 -1,这是一个无效状态值,当其他线程通过有效状态值来搜索失败记录时,会略过该记录,这样该记录就不会被其他线程重试,达到的分布式锁的功能(这个锁是一个行锁)。或者说,-1状态类似于 java 中的对象头的锁标志位,表明该记录已经被加锁了,其他线程会“忽略”该记录。

在 try 代码块中加锁和解锁,如果加锁后重试时抛出异常,会导致该记录永远无法解锁。所以,应该在 finnally 块中执行解锁操作,或者使用 redis 给锁加一个过期时间来实现分布式锁。

从失败的日志中取出 jobId,查询出对应的 jobInfo 数据,如果日志中的剩余重试次数大于 0,就执行重试。代码如下:

调度任务使用的就是前面介绍的 JobTriggerPoolHelper.trigger 方法,最后更新 jobLog 的 alarm_status 值,有两个作用:

这个类与 JobRegistryHelper 类似,都有一个线程池、一个线程,通过前面 JobRegistryHelper 的学习,可以大胆猜测:

实际上,该类中线程池和线程的作用就是用来 “完成” 一个 job。

当 executor 接收到 admin 的调度请求后,会异步执行 job,并立刻返回一个回调。

admin 接受到回调后,和前面的 “注册、下线” 一样,在线程池中创建线程来处理回调,主要是更新 job 和日志。

当有回调请求时, public callback 方法(该方法被 AdminBizImpl 调用)会在线程池中创建一个线程,遍历回调请求的参数列表,依次处理回调参数,代码如下:

从代码可以看出,最后调用 XxlJobCompleter.updateHandleInfoAndFinish 方法完成回调逻辑。

如果一个 job 较长时间前被调度,但是一直处于 “运行中” 且它所属的 executor 已经超过 90 秒没有心跳了,那么可以认为该 job 已经丢失了,需要把该 job 结束掉。这个就是线程 monitorThread 的主要功能。

monitorThread 会以 60秒 一次的频率,从 xxl_job_log 表中找出 10分钟前调度、仍处于”运行中“状态、executor 已经下线 的 job,然后调用 XxlJobCompleter.updateHandleInfoAndFinish 来更新 handler 的信息和结束 job,代码如下:

从代码可以看出,上面的两个功能最后都调用了 XxlJobCompleter.updateHandleInfoAndFinish 方法,关于该方法的介绍,可以看后面 XxlJobCompleter 部分的介绍,这里不详细展开。

如果去看 XxlJobTrigger.triger 方法,会发现每次调度 job 时,都会先新增一个 jobLog 记录,这也是为什么 JobFailMonitorHelper 中的线程在重试时,先查询 jobLog 的原因。

JobLog 作为 job 的调度记录,还可以用来统计一段时间内 job 的调度次数、成功数等;另外,会清理超出有效期(配置的参数 logretentiondays )的日志,避免日志数据过大。很显然,这又是一个 ”自动任务“,可以使用一个线程定时完成。

该类持有一个线程变量,线程以 每分钟一次的频率,执行两个操作:

在线程 run 方法的前半部分,线程会统计 3 天内,每天的调度次数、运行次数、成功运行数、失败次数;然后更新或新增 xxl_job_log_report 表的数据。

在线程 run 方法的后半部分,线程按天对日志进行清理,如果当前时间与上次清理的时间相隔超过一天,就会清理日志记录,代码如下:

如果不使用参数 lastCleanLogTime 来记录上次清理的时间,只是清理一天前创建的数据记录。那么该线程每分钟执行一次时,都会删除前天当前时刻的数据,导致前一年的数不完整。

使用参数 lastCleanLogTime 来记录上次清理的时间,并且与当前时间相差超过一天时才清理,能保证前一天的日志是完整的。

不明白为什么清理日志时,不是一次性删除全部的过期日志,而是每次删除 1000条。按理说,这些旧的日志数据应该已经不在 buffer pool 中了,trigger_time 字段又是普通索引,那么 DELETE 操作会先更新到 change buffer 中,之后再合并。现在先查询再删除,相当于多了一次 IO 且没有使用到 change buffer。

admin 服务是用来管理和调度 job 的,用户也可以在它的管理后台新建一个 job,配置 CRON 和 JobHandler,然后 admin 服务就会按照配置的参数来调度 job。很显然,这种“自动化工作”也是由线程定时执行的。

1、如果使用线程调度 Job,存在的第一个问题是:如果某个 Job 在调度时比较耗时,就可能阻塞后续的 Job,导致后续 job 的执行有延迟,怎么解决这个问题?

在前面 JobTriggerPoolHelper 我们已经知道,admin 在调度 job 时是 ”使用线程池、线程“ 异步执行调度任务,避免了主线程的阻塞。

2、使用线程定时调度 job,存在的第二个问题是:怎么保证 job 在指定的时间执行,而不会出现大量延迟?

admin 使用 ”预读“ 的方式,提前读取在未来一段时间内要执行的 job,提前取到内存中,并使用 “时间轮算法” 按时间分组 job,把未来要执行的 job 下一个时间段执行。

3、还隐藏第三个问题:admin 服务是可以多实例部署的,在这种情况下该怎么避免一个 job 被多个实例重复调度?

admin 把一张数据表作为 “分布式锁” 来保证只有一个 admin 实例能执行 job 调度,又通过随机 sleep 线程一段时间,来降低线程之间的竞争。

下面我们就通过代码来了解 xxl-job 是怎么解决上述问题的。

在该类中,定义了一个调度线程,用来调度要执行的 job 和已经过期一段时间的 job,定义代码如下:

该线程会预读出 “下次执行时间 <= now + 5000 毫秒内” 的部分 job,根据它们下一次执行时间划分成三段,执行三种不同的逻辑。

1、下次执行时间在 (- , now - 5000) 范围内

说明过期时间已经大于 5000 毫秒,这时如果过期策略要求调度,就调度一次。代码如下:

2、下次执行时间在 [now - 5000, now) 范围内

说明过期时间小于5000毫秒,只能算是延迟不能算是过期,直接调度一次,代码如下:

如果 job 的下一次执行时间在 5000 毫秒以内,为了省下下次预读的 IO 耗时,这里会记录下 job id,等待后面的调度。

3、下次执行时间在 [now, now + 5000) 范围内

说明还没到执行时间,先记录下 job id, 等待后面的调度 ,代码如下:

上面的3个步骤结束后,会更新 jobInfo 的 trigger_last_time、trigger_next_time、trigger_status 字段:

可以看到,通过预读,一方面会把过期一小段时间的 job 执行一遍,另一方面会把未来一小段时间内要执行的 job 取出,保存进一个 map 对象 ringData 中,等待另一个线程调度。这样就避免了某些 job 到了时间还没执行。

因为 admin 是可以多实例部署的,所以在调度 job 时,需要考虑怎么避免 job 被多次调度。

xxl-job 在前面 JobFailMonitorHelper 中遍历失败的 job 时,会对每个 job 设置一个无效的状态作为 ”分布式行锁“,如果设置失败就跳过。而在这里,如果还使用该方法,有可能出现,一个 job 被设置为无效状态后,线程就崩溃了,导致该 job 永远无法被调度。因此,要尽量避免对 job 状态的修改。

在这里,admin 服务使用一张表 xxl_job_lock 作为分布式锁,每个 admin 实例都要先尝试获取该表的锁,获取成功才能继续执行;同时,为了降低不同实例之间的竞争,会在线程开始执勤随机 sleep 一段时间。

如何获取分布式锁?

在线程中会开启一个事务,设置为手动提交,然后对表 xxl_job_lock 执行 FOR UPDATE 查询。如果该线程执行语句成功,其他实例的线程就会排队等待该表的锁,实现了分布式锁功能。代码如下:

怎么降低锁的竞争?

为了降低锁竞争,在线程开始前会先 sleep 4000 5000 毫秒的随机值(不能大于 5000 毫秒,5000 毫秒是预读的时间范围);在线程结束当前循环时,会根据耗时和是否有预读数据,选择不同的 sleep 策略:

代码如下:

在前面的线程中,对即将要开始的 job,不是立刻调度,而是按照执行的时刻(秒),把 job id 保存进一个 map 中,然后由 ringThread 线程按时刻进行调度,这只典型的“时间轮算法”。代码如下:

每次轮询调度时,只取出当前时刻(秒)、前一秒内的 job,不会去调度与现在相隔太久的 job。

在执行轮询调度前,有一个时间在 0 1000 毫秒范围内的 sleep。如果没有这个 sleep,该线程会一直执行,而 ringData 中当前时刻(秒)的数据可能已经为空,会导致大量无效的操作;增加了这个 sleep 之后,可以避免这种无效的操作。之所以 sleep 时间在 1000 毫秒以内,是因为调度时刻最小精确到秒,一秒的 sleep 可以避免 job 的延迟。

因为在前面的 scheleThread 线程中,最后一个操作是把 job 的 next_trigger_time 值更新为大于 now + 5000 毫秒,其他 admin 实例 scheleThread 线程的查询条件是:next_trigger_time < now + 5000,不会查询出这里调度的 job,所以不需要加分布式锁。

至此,XxlJobScheler-init 方法的作用我们介绍完毕,下面我们简单介绍一下 XxlJobScheler-destroy 方法

destroy 方法很简单,就是销毁前面初始化的线程池和线程,它销毁的顺序与前面启动的顺序相反。

代码如下:

因为各个 toStop 方法都很相似,所以我们只介绍 JobScheleHelper 的 toStop 方法。

该方法的步骤如下:

1、设置停止标志位为 true;

2、sleep 一段时间,让出 CPU 时间片给线程执行任务;

3、如果线程不是终止状态(线程正在 sleep),中断它;

4、线程执行 join 方法,直到线程结束,执行最后一次。

代码如下:

至此,JobScheleHelper 的主要功能就介绍完了,可以看出, admin 服务在启动时,启动了多个线程池和线程,异步执行任务和异步响应 executor 的请求。

下面,我们介绍前面涉及到的 XxlJobTrigger 和 XxlJobCompleter。

XxlJobTrigger 是调度 job 时的封装类,它主要工作就是接受传入的 jobId、调度参数等,查询对应的 jobGroup、jobInfo,然后调用 ExecutorBiz 对象来执行调度(run 方法)。

该类中三个核心方法及其调用关系如下: trigger -> processTrigger -> runExecutor ,

该方法的功能比较简单,就是根据传入的参数查询 jobGroup 和 jobInfo 对象,设置相关的字段值,然后调用 processTrigger 方法。

该方法的主要工作分为以下几步:

1、保存一条调度日志;

2、从 jobInfo、jobGroup 中取出字段值,构造 TriggerParam 对象;

3、根据 jobInfo 的路由策略,从 jobGroup 中取出要调度的 executor 地址;

4、调用 runExecutor 方法执行调度;

5、保存调度参数、设置调度信息、更新日志。

这里不会修改 jobInfo、jobGroup 对象的字段值,只取出字段值来使用,对这两个对象字段的修改,是在前一步 trigger 方法中进行的。

该方法会执行调度,并返回调度结果,它的核心代码如下:

这里使用 XxlJobScheler 类取出 ExecutorBiz 对象,以 “懒加载” 的方式给每个 address 创建一个 ExecutorBiz 对象,代码如下:

可以看出,该类中的三个方法其实可以归类为:pre -> execute -> post,在执行前、执行时、执行后做一些前置和收尾工作。

该类在前面 JobCompleteHelper 中被使用,最终 job 的完成就是在该类中执行的,该类有两个主要方法:

下面主要介绍 finishJob 方法。

finishJob 的主要功能是:如果当前任务执行成功了,就调度它的所有子任务,最后把子任务的调度消息添加到当前 job 的日志中。代码如下:

需要注意的是:

1、这里依赖于 JobTriggerPoolHelper 来调度 job,所以在 JobCompleteHelper 的监视线程开始时,有一个 50 秒的等待,就是等待 JobTriggerPoolHelper 启动完成;

2、在 finishJob 方法中,调度子任务的时候,默认子任务的调度结果是成功,注意,这里是指 “调度” 这个行为是成功的,而不是指子任务执行是成功的。

1、XxlJobAdminConfig 作为 admin 服务的启动入口,要尽可能保持简洁,作用类似于一个仓库,来管理和持有所有的类和对象,并不会去启动具体的线程,它只需要“按下启动器的按钮”就可以了;

2、XxlJobScheler 是 admin 服务的启动器类,它会调用各个辅助类(xxxHelper)来启动对应的线程;

3、对外的接口,比如调度 job、接收注册或下线等,都是使用线程池 + 线程 的异步方式实现,避免 job 对主线程的阻塞;

4、对“自动任务“类的功能,都是使用线程定时执行;

XXL-JOB分布式调度框架全面详解: https://juejin.cn/post/6948397386926391333

时间轮算法:https://spongecaptain.cool/post/widget/timingwheel

一个开源的时间轮算法介绍:https://spongecaptain.cool/post/widget/timingwheel2

B. Android TV开发焦点移动源码分析

点可以理解为选中态,在Android TV上起很重要的作用。一个视图控件只有在获得焦点的状态下,才能响应按键的Click事件。
相对于手机上用手指点击屏幕产生的Click事件, 在TV中通过点击遥控器的方向键来控制焦点的移动。当焦点移动到目标控件上之后,按下遥控器的确定键,才会触发一个Click事件,进而去做下一步的处理
在处理焦点的时候,有一些基础的用法需要知道。
首先,一个控件isFocusable()需要为true才有资格可以获取到焦点。如果想要在触摸模式下获取焦点,需要通过setFocusableInTouchMode(boolean)来设置。也可以直接在xml布局文件中指定:

keyEvent 分发过程:

而当按下遥控器的按键时,会产生一个按键事件,就是KeyEvent,包含“上”,“下”,“左”,“右”,“返回”,“确定”等指令。焦点的处理就在KeyEvent的分发当中完成。
首先,KeyEvent会流转到ViewRootImpl中开始进行处理,具体方法是内部类 ViewPostImeInputStage 中的 processKeyEvent :

接下来先看一下KeyEvent在view框架中的分发:

这里也是可以做焦点控制,最好是在 event.getAction() == KeyEvent.ACTION_DOWN 进行.
因为android 的 ViewRootlmpl 的 processKeyEvent 焦点搜索与请求的地方 进行了判断if (event.getAction() == KeyEvent.ACTION_DOWN)

• 首先ViewGroup会一层一层往上执行父类的dispatchKeyEvent方法,如果返回true那么父类的dispatchKeyEvent方法就会返回true,也就代表父类消费了该焦点事件,那么焦点事件自然就不会往下进行分发。
• 然后ViewGroup会悔李键判断mFocused这个view是否为空,如果为空就会return false,焦点继续往下传递;如果不为空,那就会return mFocused的dispatchKeyEvent方法返回的结果。这个mFocused其实是扰歼ViewGroup中当前获取焦点的子View

发现执行了onKeyListener中的onKey方法,如果onKey方法返回true,那么dispatchKeyEvent方法也会返回true
如果想要修改ViewGroup焦点事件的分发
• 重写view的dispatchKeyEvent方法
• 给某个子view设置onKeyListener监听

下面再来看一下如果一个页面第一次进入,系统是如何确定焦点是定位在哪个view上的

由于DecorView继承自FrameLayout,这里调用的是ViewGroup的requestFocus

descendantFocusability:
• FOCUS_AFTER_DESCENDANTS:先分发给Child View进行处理,如果所有的Child View都没有处理,则自己再处理
• FOCUS_BEFORE_DESCENDANTS:ViewGroup先对焦碧巧点进行处理,如果没有处理则分发给child View进行处理
• FOCUS_BLOCK_DESCENDANTS:ViewGroup本身进行处理,不管是否处理成功,都不会分发给ChildView进行处理
因为 PhoneWindow 给 DecoreView 初始化时设置 了 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS),所以这里默认是FOCUS_AFTER_DESCENDANTS

到此第一次请求焦点的过程基本告一个段落

焦点移动的时候,默认的情况下,会按照一种算法去找在指定移动方向上最近的邻居。在一些情况下,焦点的移动可能跟开发者的意图不符,这时开发者可以在布局文件中使用下面这些XML属性来指定下一个焦点对象:

在KeyEvent分发中已经知道如果分发过程中event没有被消耗,就会根据方向搜索以及请求焦点View

流程一:查找用户指定的下一个焦点

流程二:获取搜索方向上所有可以获取焦点的view,使用算法查找下一个view
addFocusables() 获取搜索方向上可获得焦点的view

descendantFocusability属性决定了ViewGroup和其子view的聚焦优先级
• FOCUS_BLOCK_DESCENDANTS:viewgroup会覆盖子类控件而直接获得焦点
• FOCUS_BEFORE_DESCENDANTS:viewgroup会覆盖子类控件而直接获得焦点
• FOCUS_AFTER_DESCENDANTS:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
addFocusables 的第一个参数views是由root决定的。在ViewGroup的focusSearch方法中传进来的root是DecorView,也可以主动调用FocusFinder的findNextFocus方法,在指定的ViewGroup中查找焦点。
FocusFinder.findNextFocus 查找焦点

C. 如何用java计算一个圆的面积和周长

一、数学公式:

圆周长=2*π*半径

面积=π*半径²

二、算法分析:

周长和面积都依赖半径,所以要先输入半径值,然后套用公式,计算周长和面积。 最终输出结果即可。

三、参考代码:

代码如下

#include"stdio.h"

#definePi3.14

voidmain()

{

floatr,c,area;

printf("请输入圆的半径:");

scanf("%f",&r);

c=2*Pi*r;

area=Pi*r*r;

printf("该圆的周长是%.2f,面积是%.2f ",c,area);

}

D. Android TV 焦点原理源码解析

相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行。不管是学技术还是学习其他知识,都要学习和理解其中原理,碰到问题我们才能得心应手。下面就来探一探Android的焦点分发的过程。

Android焦点事件的分发是从ViewRootImpl的processKeyEvent开始的,源码如下:

源码比较长,下面我就慢慢来讲解一下具体的每一个细节。

dispatchKeyEvent方法返回true代表焦点事件被消费了。

ViewGroup的dispatchKeyEvent()方法的源码如下:

(2)ViewGroup的dispatchKeyEvent执行流程

(3)下面再来瞧瞧view的dispatchKeyEvent方法的具体的执行过程

惊奇的发现执行了onKeyListener中的onKey方法,如果onKey方法返回true,那么dispatchKeyEvent方法也会返回true

可以得出结论:如果想要修改ViewGroup焦点事件的分发,可以这么干:

注意:实际开发中,理论上所有焦点问题都可以通过给dispatchKeyEvent方法增加监听来来拦截来控制。

(1)dispatchKeyEvent方法返回false后,先得到按键的方向direction值,这个值是一个int类型参数。这个direction值是后面来进行焦点查找的。

(2)接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。
ViewGroup的findFocus方法如下:

View的findFocus方法

说明:判断view是否获取焦点的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的。

其中isFocused()方法的作用是判断view是否已经获取焦点,如果viewGroup已经获取到了焦点,那么返回本身即可,否则通过mFocused的findFocus()方法来找焦点。mFocused其实就是ViewGroup中获取焦点的子view,如果mView不是ViewGourp的话,findFocus其实就是判断本身是否已经获取焦点,如果已经获取焦点了,返回本身。

(3)回到processKeyEvent方法中,如果findFocus方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。

(4)focusSearch方法的具体实现。

focusSearch方法的源码如下:

可以看出focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。

(5)FocusFinder是什么?

它其实是一个实现 根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android框架焦点的查找最终都是通过FocusFinder类来实现的。

(6)FocusFinder是如何通过findNextFocus方法寻找焦点的。

下面就来看看FocusFinder类是如何通过findNextFocus来找焦点的。一层一层往下看,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,且isFocusable = true && isInTouchMode() = true的话,FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。

(7)findNextFocus会优先根据XML里设置的下一个将获取焦点的View ID值来寻找将要获取焦点的View。

看看View的findUserSetNextFocus方法内部都干了些什么,OMG不就是通过我们xml布局里设置的nextFocusLeft,nextFocusRight的viewId来找焦点吗,如果按下Left键,那么便会通过nextFocusLeft值里的View Id值去找下一个获取焦点的View。

可以得出以下结论:

1. 如果一个View在XML布局中设置了focusable = true && isInTouchMode = true,那么这个View会优先获取焦点。

2. 通过设置nextFocusLeft,nextFocusRight,nextFocusUp,nextFocusDown值可以控制View的下一个焦点。

Android焦点的原理实现就这些。总结一下:

为了方便同志们学习,我这做了张导图,方便大家理解~

E. c语言的两种排序

1、选择排序法

要求输入10个整数,从大到小排序输出

输入:2 0 3 -4 8 9 5 1 7 6

输出:9 8 7 6 5 3 2 1 0 -4

代码:

#include&lt;stdio.h&gt;

int main(int argc,const char*argv[]){

int num[10],i,j,k,l,temp;

//用一个数组保存输入的数据

for(i=0;i&lt;=9;i++)

{

scanf("%d",&num&lt;i&gt;);

}

//用两个for嵌套循环来进行数据大小比较进行排序

for(j=0;j&lt;9;j++)

{

for(k=j+1;k&lt;=9;k++)

{

if(num[j]&lt;num[k])//num[j]&lt;num[k]

{

temp=num[j];

num[j]=num[k];

num[k]=temp;

}

}

}

//用一个for循环来输出数组中排序好的数据

for(l=0;l&lt;=9;l++)

{

printf("%d",num[l]);

}

return 0;

}

2、冒泡排序法

要求输入10个整数,从大到小排序输出

输入:2 0 3-4 8 9 5 1 7 6

输出:9 8 7 6 5 3 2 1 0-4

代码:

#include&lt;stdio.h&gt;

int main(int argc,const char*argv[]){

//用一个数组来存数据

int num[10],i,j,k,l,temp;

//用for来把数据一个一个读取进来

for(i=0;i&lt;=9;i++)

{

scanf("%d",&num&lt;i&gt;);

}

//用两次层for循环来比较数据,进行冒泡

for(j=0;j&lt;9;j++)

{

for(k=0;k&lt;9-j;k++)

{

if(num[k]&lt;num[k+1])//num[k]&lt;num[k+1]

{

temp=num[k];

num[k]=num[k+1];

num[k+1]=temp;

}

}

}

//用一个for循环来输出数组中排序好的数据

for(l=0;l&lt;=9;l++)

{

printf("%d",num[l]);

}

return 0;

}

(5)解析算法源码扩展阅读:

return 0代表程序正常退出。return是C++预定义的语句,它提供了终止函数执行的一种方式。当return语句提供了一个值时,这个值就成为函数的返回值。

return语句用来结束循环,或返回一个函数的值。

1、return 0,说明程序正常退出,返回到主程序继续往下执行。

2、return 1,说明程序异常退出,返回主调函数来处理,继续往下执行。return 0或return 1对程序执行的顺序没有影响,只是大家习惯于使用return(0)退出子程序而已。

F. Android socket源码解析(三)socket的connect源码解析

上一篇文章着重的聊了socket服务端的bind,listen,accpet的逻辑。本文来着重聊聊connect都做了什么?

如果遇到什么问题,可以来本文 https://www.jianshu.com/p/da6089fdcfe1 下讨论

当服务端一切都准备好了。客户端就会尝试的通过 connect 系统调用,尝试的和服务端建立远程连接。

首先校验当前socket中是否有正确的目标地址。然后获取IP地址和端口调用 connectToAddress 。

在这个方法中,能看到有一个 NetHooks 跟踪socket的调用,也能看到 BlockGuard 跟踪了socket的connect调用。因此可以hook这两个地方跟踪socket,不过很少用就是了。

核心方法是 socketConnect 方法,这个方法就是调用 IoBridge.connect 方法。同理也会调用到jni中。

能看到也是调用了 connect 系统调用。

文件:/ net / ipv4 / af_inet.c

在这个方法中做的事情如下:

注意 sk_prot 所指向的方法是, tcp_prot 中 connect 所指向的方法,也就是指 tcp_v4_connect .

文件:/ net / ipv4 / tcp_ipv4.c

本质上核心任务有三件:

想要能够理解下文内容,先要明白什么是路由表。

路由表分为两大类:

每个路由器都有一个路由表(RIB)和转发表 (fib表),路由表用于决策路由,转发表决策转发分组。下文会接触到这两种表。

这两个表有什么区别呢?

网上虽然给了如下的定义:

但实际上在Linux 3.8.1中并没有明确的区分。整个路由相关的逻辑都是使用了fib转发表承担的。

先来看看几个和FIB转发表相关的核心结构体:

熟悉Linux命令朋友一定就能认出这里面大部分的字段都可以通过route命令查找到。

命令执行结果如下:

在这route命令结果的字段实际上都对应上了结构体中的字段含义:

知道路由表的的内容后。再来FIB转发表的内容。实际上从下面的源码其实可以得知,路由表的获取,实际上是先从fib转发表的路由字典树获取到后在同感加工获得路由表对象。

转发表的内容就更加简单

还记得在之前总结的ip地址的结构吗?

需要进行一次tcp的通信,意味着需要把ip报文准备好。因此需要决定源ip地址和目标IP地址。目标ip地址在之前通过netd查询到了,此时需要得到本地发送的源ip地址。

然而在实际情况下,往往是面对如下这么情况:公网一个对外的ip地址,而内网会被映射成多个不同内网的ip地址。而这个过程就是通过DDNS动态的在内存中进行更新。

因此 ip_route_connect 实际上就是选择一个缓存好的,通过DDNS设置好的内网ip地址并找到作为结果返回,将会在之后发送包的时候填入这些存在结果信息。而查询内网ip地址的过程,可以成为RTNetLink。

在Linux中有一个常用的命令 ifconfig 也可以实现类似增加一个内网ip地址的功能:

比如说为网卡eth0增加一个IPV6的地址。而这个过程实际上就是调用了devinet内核模块设定好的添加新ip地址方式,并在回调中把该ip地址刷新到内存中。

注意 devinet 和 RTNetLink 严格来说不是一个存在同一个模块。虽然都是使用 rtnl_register 注册方法到rtnl模块中:

文件:/ net / ipv4 / devinet.c

文件:/ net / ipv4 / route.c

实际上整个route模块,是跟着ipv4 内核模块一起初始化好的。能看到其中就根据不同的rtnl操作符号注册了对应不同的方法。

整个DDNS的工作流程大体如下:

当然,在tcp三次握手执行之前,需要得到当前的源地址,那么就需要通过rtnl进行查询内存中分配的ip。

文件:/ include / net / route.h

这个方法核心就是 __ip_route_output_key .当目的地址或者源地址有其一为空,则会调用 __ip_route_output_key 填充ip地址。目的地址为空说明可能是在回环链路中通信,如果源地址为空,那个说明可能往目的地址通信需要填充本地被DDNS分配好的内网地址。

在这个方法中核心还是调用了 flowi4_init_output 进行flowi4结构体的初始化。

文件:/ include / net / flow.h

能看到这个过程把数据中的源地址,目的地址,源地址端口和目的地址端口,协议类型等数据给记录下来,之后内网ip地址的查询与更新就会频繁的和这个结构体进行交互。

能看到实际上 flowi4 是一个用于承载数据的临时结构体,包含了本次路由操作需要的数据。

执行的事务如下:

想要弄清楚ip路由表的核心逻辑,必须明白路由表的几个核心的数据结构。当然网上搜索到的和本文很可能大为不同。本文是基于LInux 内核3.1.8.之后的设计几乎都沿用这一套。

而内核将路由表进行大规模的重新设计,很大一部分的原因是网络环境日益庞大且复杂。需要全新的方式进行优化管理系统中的路由表。

下面是fib_table 路由表所涉及的数据结构:

依次从最外层的结构体介绍:

能看到路由表的存储实际上通过字典树的数据结构压缩实现的。但是和常见的字典树有点区别,这种特殊的字典树称为LC-trie 快速路由查找算法。

这一篇文章对于快速路由查找算法的理解写的很不错: https://blog.csdn.net/dog250/article/details/6596046

首先理解字典树:字典树简单的来说,就是把一串数据化为二进制格式,根据左0,右1的方式构成的。

如图下所示:

这个过程用图来展示,就是沿着字典树路径不断向下读,比如依次读取abd节点就能得到00这个数字。依次读取abeh就能得到010这个数字。

说到底这种方式只是存储数据的一种方式。而使用数的好处就能很轻易的找到公共前缀,在字典树中找到公共最大子树,也就找到了公共前缀。

而LC-trie 则是在这之上做了压缩优化处理,想要理解这个算法,必须要明白在 tnode 中存在两个十分核心的数据:

这负责什么事情呢?下面就简单说说整个lc-trie的算法就能明白了。

当然先来看看方法 __ip_dev_find 是如何查找

文件:/ net / ipv4 / fib_trie.c

整个方法就是通过 tkey_extract_bits 生成tnode中对应的叶子节点所在index,从而通过 tnode_get_child_rcu 拿到tnode节点中index所对应的数组中获取叶下一级别的tnode或者叶子结点。

其中查找index最为核心方法如上,这个过程,先通过key左移动pos个位,再向右边移动(32 - bits)算法找到对应index。

在这里能对路由压缩算法有一定的理解即可,本文重点不在这里。当从路由树中找到了结果就返回 fib_result 结构体。

查询的结果最为核心的就是 fib_table 路由表,存储了真正的路由转发信息

文件:/ net / ipv4 / route.c

这个方法做的事情很简单,本质上就是想要找到这个路由的下一跳是哪里?

在这里面有一个核心的结构体名为 fib_nh_exception 。这个是指fib表中去往目的地址情况下最理想的下一跳的地址。

而这个结构体在上一个方法通过 find_exception 获得.遍历从 fib_result 获取到 fib_nh 结构体中的 nh_exceptions 链表。从这链表中找到一模一样的目的地址并返回得到的。

文件:/ net / ipv4 / tcp_output.c

G. 阿里sentinel源码解析

sentinel是阿里巴巴开源的流量整形(限流、熔断)框架,目前在github拥有15k+的star,sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

我们以sentinel的主流程入手,分析sentinel是怎么搜集流量指标,完成流量整形的。

首先我们先看一个sentinel的简单使用demo,只需要调用SphU.entry获取到entry,然后在完成业务方法之后调用entry.exit即可。

SphU.entry会调用Env.sph.entry,将name和流量流向封装成StringResourceWrapper,然后继续调用entry处理。

进入CtSph的entry方法,最终来到entryWithPriority,调用InternalContextUtil.internalEnter初始化ThreadLocal的物锋Context,然后调用lookProcessChain初始化责任链,最终调用chain.entry进入责任链进行处理。

InternalContextUtil.internalEnter会调用trueEnter方法,主要是生成DefaultNode到contextNameNodeMap,然后生成Context设置到contextHolder的过程。

lookProcessChain已经做过优化,支持spi加载自定义的责任链bulider,如果没有定义则使用默认的DefaultSlotChainBuilder进行加载。默认加载的slot和顺序可见镇楼图,不再细说。

最后来到重头戏chain.entry进入责任链进行处理,下面会按照顺序分别对每个处理器进行分析。
首先来到NodeSelectorSlot,主要是获取到name对应的DefaultNode并缓存起来,设置为context的当前节点,然后通知下一个节点。

下一个节点是ClusterBuilderSlot,继续对DefaultNode设置ClusterNode与OriginNode,然颂咐后通知下一节点。

下一个节点是LogSlot,只是单纯的打印日志,不再细说。

下一个节点是StatisticSlot,是一个后置节点,先通知下一个节点处理完后,
1.如果没有报错,则对node、clusterNode、originNode、ENTRY_NODE的线程数、通过请求数进行增加。
2.如果报错是PriorityWaitException,则只对线程数进行增加。
3.如果报错是BlockException,设置报错到node,然后对阻挡请求数进行增加。
4.如果是其他报错,设置报错到node即可。

下一个节点是FlowSlot,这个节点就是重要的限流处理节点,进入此节点是调用checker.checkFlow进行限流处理。

来到FlowRuleChecker的checkFlow方法,调用ruleProvider.apply获取到资源对应的FlowRule列表,然后遍历FlowRule调用canPassCheck校验限流规则。

canPassCheck会根据rule的限流模式,选择集群限流或者本地限流,这里分别作出分析。

passLocalCheck是本地限流的入口,首先会调用选出限流的node,然后调用canPass进行校验。罩樱晌

会根据以下规则选中node。
1.strategy是STRATEGY_DIRECT。
1.1.limitApp不是other和default,并且等于orgin时,选择originNode。
1.2.limitApp是other,选择originNode。
1.3.limitApp是default,选择clusterNode。
2.strategy是STRATEGY_RELATE,选择clusterNode。
3.strategy是STRATEGY_CHAIN,选择node。

选择好对应的node后就是调用canPass校验限流规则,目前sentinel有三种本地限流规则:普通限流、匀速限流、冷启动限流。

普通限流的实现是DefaultController,就是统计当前的线程数或者qps加上需要通过的数量有没有大于限定值,小于等于则直接通过,否则阻挡。

匀速限流的实现是RateLimiterController,使用了AtomicLong保证了latestPassedTime的原子增长,因此停顿的时间是根据latestPassedTime-currentTime计算出来,得到一个匀速的睡眠时间。

冷启动限流的实现是WarmUpController,是sentinel中最难懂的限流方式,其实不太需要关注这些复杂公式的计算,也可以得出冷启动的限流思路:
1.当qps已经达到温热状态时,按照正常的添加令牌消耗令牌即可。
2.当qps处于过冷状态时,会添加令牌使得算法继续降温。
3.当qps逐渐回升,大于过冷的边界qps值时,不再添加令牌,慢慢消耗令牌使得逐渐增大单位时间可通过的请求数,让算法继续回温。
总结出一点,可通过的请求数跟令牌桶剩余令牌数量成反比,以达到冷启动的作用。

接下来是集群限流,passClusterCheck是集群限流的入口,会根据flowId调用clusterSerivce获取指定数量的token,然后根据其结果判断是否通过、睡眠、降级到本地限流、阻挡。

接下来看一下ClusterService的处理,会根据ruleId获取到对应的FlowRule,然后调用ClusterFlowChecker.acquireClusterToken获取结果返回。ClusterFlowChecker.acquireClusterToken的处理方式跟普通限流是一样的,只是会将集群的请求都集中在一个service中处理,来达到集群限流的效果,不再细说。

FlowSlot的下一个节点是DegradeSlot,是熔断处理器,进入时会调用performChecking,进而获取到CircuitBreaker列表,然后调用其tryPass校验是否熔断。

来到AbstractCircuitBreaker的tryPass方法,主要是判断熔断器状态,如果是close直接放行,如果是open则会校验是否到达开启halfopen的时间,如果成功将状态cas成halfopen则继续放行,其他情况都是阻拦。

那怎么将熔断器的状态从close变成open呢?怎么将halfopen变成close或者open呢?sentinel由两种熔断器:错误数熔断器ExceptionCircuitBreaker、响应时间熔断器ResponseTimeCircuitBreaker,都分析一遍。
当业务方法报错时会调用Tracer.traceEntry将报错设置到entry上。

当调用entry.exit时,会随着责任链来到DegradeSlot的exit方法,会遍历熔断器列表调用其onRequestComplete方法。

ExceptionCircuitBreaker的onRequestComplete会记录错误数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果没有报错,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较错误数/错误比例是否大于限定值,如果大于则直接转换成open。

ExceptionCircuitBreaker的onRequestComplete会记录慢响应数和总请求数,然后调用继续处理。
1.当前状态是open时,不应该由熔断器底层去转换状态,直接退出。
2.当前状态是halfopen时,如果当前响应时间小于限定值,则将halfopen变成close,否则将halfopen变成open。
3.当前状态时close时,则根据是否总请求达到了最低请求数,如果达到了话再比较慢请求数/慢请求比例是否大于限定值,如果大于则直接转换成open。

下一个节点是AuthoritySlot,权限控制器,这个控制器就是看当前origin是否被允许进入请求,不允许则报错,不再细说。

终于来到最后一个节点SystemSlot了,此节点是自适应处理器,主要是根据系统自身负载(qps、最大线程数、最高响应时间、cpu使用率、系统bbr)来判断请求是否能够通过,保证系统处于一个能稳定处理请求的安全状态。

尤其值得一提的是bbr算法,作者参考了tcp bbr的设计,通过最大的qps和最小的响应时间动态计算出可进入的线程数,而不是一个粗暴的固定可进入的线程数,为什么能通过这两个值就能计算出可进入的线程数?可以网上搜索一下tcp bbr算法的解析,十分巧妙,不再细说。

H. 面试中的网红Vue源码解析之虚拟DOM,你知多少呢深入解读diff算法

众所周知,在前端的面试中,面试官非常爱考dom和diff算法。比如,可能会出现在以下场景

滴滴滴,面试官发来一个面试邀请。接受邀请📞

我们都知道, key 的作用在前端的面试是一道很普遍的题目,但是呢,很多时候我们都只浮于知识的表面,而没有去深挖其原理所在,这个时候我们的竞争力就在这被拉下了。所以呢,深入学习原理对于提升自身的核心竞争力是一个必不可少的过程。

在接下来的这篇文章中,我们将讲解面试中很爱考的虚拟DOM以及其背后的diff算法。 请认真阅读本文~文末有学习资源免费共享!!!

虚拟DOM是用JavaScript对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性。本质上是JS 和 DOM 之间的一个映射缓存。

要点:虚拟 DOM 是 JS 对象;虚拟 DOM 是对真实 DOM 的描述。

diff发生在虚拟DOM上。diff算法是在新虚拟DOM和老虚拟DOM进行diff(精细化比对),实现最小量更新,最后反映到真正的DOM上。

我们前面知道diff算法发生在虚拟DOM上,而虚拟DOM是如何实现的呢?实际上虚拟DOM是有一个个虚拟节点组成。

h函数用来产生虚拟节点(vnode)。虚拟节点有如下的属性:
1)sel: 标签类型,例如 p、div;
2)data: 标签上的数据,例如 style、class、data-*;
3)children :子节点;
4) text: 文本内容;
5)elm:虚拟节点绑定的真实 DOM 节点;

通过h函数的嵌套,从而得到虚拟DOM树。

我们编写了一个低配版的h函数,必须传入3个参数,重载较弱。

形态1:h('div', {}, '文字')
形态2:h('div', {}, [])
形态3:h('div', {}, h())

首先定义vnode节点,实际上就是把传入的参数合成对象返回。

[图片上传失败...(image-7a9966-1624019394657)]
然后编写h函数,根据第三个参数的不同进行不同的响应。

当我们进行比较的过程中,我们采用的4种命中查找策略:
1)新前与旧前:命中则指针同时往后移动。
2)新后与旧后:命中则指针同时往前移动。
3)新后与旧前:命中则涉及节点移动,那么新后指向的节点,移到 旧后之后
4)新前与旧后:命中则涉及节点移动,那么新前指向的节点,移到 旧前之前

命中上述4种一种就不在命中判断了,如果没有命中,就需要循环来寻找,移动到旧前之前。直到while(新前<=新后&&旧前<=就后)不成立则完成。

如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和旧后指针中间的节点),说明他们是要被删除的节点。

如果是旧节点先循环完毕,说明新节点中有要插入的节点。

1.什么是Virtual DOM 和Snabbdom
2.手写底层源码h函数
3.感受Vue核心算法之diff算法
4.snabbdom之核心h函数的工作原理

1、零基础入门或者有一定基础的同学、大中院校学生
2、在职从事相关工作1-2年以及打算转行前端的朋友
3、对前端开发有兴趣人群

热点内容
易语言静态编译后软件位置 发布:2025-01-23 01:05:38 浏览:465
剪力墙压脚筋大小怎么配置 发布:2025-01-23 00:50:53 浏览:534
腾讯云cos云服务器 发布:2025-01-23 00:46:47 浏览:63
如何给安卓平板刷上MIUI系统 发布:2025-01-23 00:45:51 浏览:73
2开方算法 发布:2025-01-23 00:27:21 浏览:16
如何看自己steam服务器 发布:2025-01-23 00:07:21 浏览:710
armlinux命令 发布:2025-01-23 00:01:08 浏览:137
战地4亚洲服务器为什么被攻击 发布:2025-01-22 23:45:42 浏览:671
javascript反编译 发布:2025-01-22 23:37:57 浏览:432
夏天来了你的巴氏奶存储对吗 发布:2025-01-22 23:37:56 浏览:206