我本来以为这是程序员职业的某种被动特性,但我意识到很多同行对程序触发事件和执行时序并不太关注。在这篇文章中,我想解释为什么我们作为程序员要关注这些东西,这样做有什么好处。
#程序何时被触发
假如你不慎从网络上下载下来一个病毒,它现在已经被保存到你的本地磁盘上了。现在事情完蛋了吗?还没有,它还没被执行呢,把它删了就行(最好用那种文件粉碎工具)。我们的电脑是冯·诺伊曼结构,在硬盘(与内存)中,程序与数据被同等看待并存储,存在于硬盘上的程序还没有被执行。我猜做网络安全的朋友们对这个比较熟悉,一次成功的代码执行漏洞攻击中必然包含恶意载荷的成功执行。
你一定被那种整蛊Flash游戏吓到过吧(没有?好吧,可能这个例子太老了)。如果你在jump scare场景触发前关闭游戏,你就安全了,无事发生。Flash游戏已经运行在你的浏览器里了(即,这个游戏的所有代码已经存在于内存中),但这并不代表jump scare场景(一段特定的代码)一定会被你的操作触发。
#程序触发事件和执行时序
“程序触发事件”这个名词是我自己胡诌的,指“触发一段程序执行的事件”,很简单的定义,但我没找到现成的符合定义的术语。也许在合适的语境下(比如“事件驱动框架”),单“事件”一词便体现了这个含义,我们下文也用“事件”代替这个词。
在图形用户界面应用中,用户操作是事件;在服务器中,外部请求是事件;在计划执行系统中,系统作为触发定时任务的事件。不同的程序主体可以链式地触发彼此,比如著名的面试问题——“当你在浏览器的地址框中输入 google.com,然后按回车键时会发生什么?”,哇,真的是好大一串。我觉得从这个角度看,待在内存里的程序有点像神经系统:它是活的(一部分持久运行的逻辑在主动地时刻监听着事件),但是处于一种待机状态。事件将激活它的(与事件所对应的一部分)处理逻辑,待逻辑执行完,它便又回到待机状态。
抽象一点看,链式调用中的每个节点都是一个 input–process–output 模型。让我们以一个更细微的层级观察同一个进程中的链式调用,比如以函数的角度。你一定对调用堆栈不陌生,它记录了函数间相互调用的顺序,谁调用谁一清二楚,这就是函数层次的程序执行时序。上文的面试问题中包含了多个层级的执行时序。
#好处都有啥
在编程和设计程序时,应该时刻惦记着程序的事件和执行时序,举几个例子:
- 接到新需求,第一个想到的事不应该是如何实现需求,而是什么事件能触发这段新的逻辑。再花里胡哨的实现,没有事件触发,还是呆坐在内存里的一段死代码。
- 你需要在已有逻辑中加入一段新逻辑,在哪里插入对新逻辑的调用最合适?画一下数据流图和执行时序,在新逻辑所需要的数据(函数参数)凑齐后的第一时间调用新逻辑。
- 程序没有崩溃,但是输出结果中的一些字段的值不正确,你需要debug发现错误,什么工具能够利用“一些输出字段的值不正确”这个信息?数据流分析,从对那些错误字段的输出逻辑向上朔源,直到回到声明它们的时候,错误就在这之间。
- 你想为一个游戏编写修改原有逻辑和数据的外挂,修改原有逻辑收到的数据,或取消调用本应调用的逻辑,哪里是理想的插入点?就像大坝一样,你新写的代码必须处于你想修改的逻辑之前,最好是正正好紧挨在原有逻辑之前,避免修改的副作用扩散到其他你不想修改的地方。(一些Minecraft mod的原理便是这种思路,当然,它们不会做坏事。)
广告环节!
minecraft-access是一个帮助视障者游玩Minecraft的mod。目前它的活跃开发者只有我一个人,而新的需求和bug又是那么多。如果你想通过开发mod来了解Minecraft运行逻辑的话,这mod绝对是你最棒的选择。如果你感兴趣,期望我们在PR页面和issue页面再见。