精华内容
下载资源
问答
  • 近一段大家逐步从各个不同的角度切入到这个主题, 比如从相关到因果, 从感知到认知, 重新结合符号主义, 结合脑科学,发展基础数学等, 而2020的智源大会,就是分别从这些不同角度进行跨学科交流的一次盛会。...

    来源:混沌巡洋舰

    导读:智源大会2020 聚焦AI的未来,大家都知道, 2010是深度学习的时代, 我们经历了AI从巅峰进入到瓶颈的过程。那么什么是2020这个十年AI的中心?

    近一段大家逐步从各个不同的角度切入到这个主题, 比如从相关到因果, 从感知到认知, 重新结合符号主义, 结合脑科学,发展基础数学等, 而2020的智源大会,就是分别从这些不同角度进行跨学科交流的一次盛会。

    1 因果角度

    一个当下机器学习界的共识是,机器学习需要从感知过度到认知,但是对于如何引导这次革命,大家的答案是不一样的, 而一个重要的观点在于当下机器没有因果推理的能力。

    这次会议有幸请到Jude Pearl, 因果被称为是第二次数学科学革命。因为对于我们需要回答的大部分问题 ,都密切依赖我们对因果关系的建模, 这些问题包含对行为结果的分析和反事实推理。

    比如当下如果我要达到目标A, 我如何构建中间条件, 如果我没有做某事今天会怎样(反事实)等。而这些能力是当下深度学习所不具备的, Jude Pearl的演讲深入浅出的引入了这个问题, 并用几个生动的例子提出了指出一些可能的解决方法。

    因果理论的核心,就是do calculus, 它提到我们的模型如果要包含因果,就要能够模拟由行为产生的结果的不同, 这和我们之前物理的mechanic model 是相通的。你有现象模型可以预测数据,但是这是观测数据的联系,而非基于某个行为产生一定变化的因果模型。

    为了建立这样的模型,一些必要的数学工具是必不可少的, 比如概率图模型,独立因果分析框架, 有关反事实的数学模型等。

    2 认知和脑网络角度

    让人工智能具有认知能力那么最直接的解决方法无疑是直接模拟人类的理解和思维能力。

    2.1 毕彦超 人类的平行知识表示系统

    我们的知识一种是从经验提取的,一种是存在人脑中的先验知识。对于神经网络,对于同样的事物,比如香蕉和苹果,红色和黄色, 我们既可以形成类似CNN的物体表示, 又可以生成类似语义网络那样的符号结构(知识图谱)。这两种系统的认知基础是什么?

    毕研超团队通过研究先天盲人和健康人对类似的颜色等概念的表征,证实了平行的知识表示系统的存在。例如盲人和正常人都可以形成对不同颜色的概念表示,但是这些表示通过FMRI 显示背后活跃的脑区是有区别的。而盲人的概念表示更接近语义网络, 正常人却相似CNN经过ImageNet训练得到的一般表示。

    我们知道大脑最终学习概念需要学习得到从感知到抽象符号的关系, 并通过概念之间的关联来定位一个特定概念。那么是否这两种表示系统需要以某种形式耦合得到人类的知识表示, 这是一个非常值得思考的方向。

    PPT :blog.csdn.net/qq_411858

    方方 Maps and Functions of Humbian Attention

    注意地图和功能地图 , 大脑资源有限, 动态切换注意点是非常必要的。这就涉及注意力这个AI和脑科学都是很核心的问题。

    虽然当下的注意力模型已经是AI最重要的组成部分, 但是它和大脑真实使用的注意力仍有不小的差距。我们说大脑真实的注意力有两种不同的机制。一个是自下而上的salientcy map,另一个是自上而下与自下而上的priority map 。 

    这就好像当平静的草原突然出现一匹马,那么你的注意力可能会突然响应,这就是salientcy mapping, 反过来,你可能因为你的目标,比如击中飞来的网球来锁定注意力,它就是priority map。

    事实上我们发现salientcy map 从V1 就有很强的表达 ,说明这种自下而上的注意力是很基础的。而这类attention model 也不同于我们AI的模型,而是一个动态图, 始终在搜索图片最具有信息的地方作为关注点, 然后跳到下一个点。构建一个动态的attnetion model 。

    2.2 刘嘉 From representation to computation : the cognitve neurointelligence

    通过AI研究大脑, 通过大脑启发AI, 这个讲座告诉大家如何比较深度神经网络和大脑的”思考“方式, 如果有一天两者开始对话,他们能够互相理解对方吗?

    刘教授从心理学的角度来讲解如何对比深度神经网络和人脑两个信息处理的黑箱到底能否直接进行对比, 假定我们了解了人脑认知的结构, 那么是否可以用来启发人工智能系统 。

    我们说当下的深度网络是一个黑箱,事实上心理学也经常把我们的大脑比喻成黑箱。黑箱和黑箱进行比较,似乎是不可能的任务, 然而刘嘉团队的方法 -逆向相关化使这个问题变成可能。

    这里介绍人脸识别(此处以性别识别为例)的工作,无论对人还是计算机这都是一个很重要的工作。然而我们并不清楚无论是人还是机器是怎么完成这个任务的。

    这里我们可以想法得到女性和男性的平均脸(无数面孔的平均)。然后在这个平均脸上加上各类人为生成的奇怪噪声然后并让机器(一个训练好的VGG网络)分类是男是女, 这样我们就可以得到一组“男性”噪声 和 “女性” 噪声,我们分别把两组噪声再此分别叠加平均后就得到两个奇怪的斑图,我们可以称为feature 图。

    然后我们把“男噪声” 和“女噪声”两个相减得到一个差值的feature图, 我们会发现这时候一个非常清晰的图案从噪声里涌现出来,比如我们看到眼角,嘴角,和下巴轮廓等。这就是机器用来分辨人类的特征图,或者说机器识别人脸的因子图。

    然后团队对人做了相同的分析,也得到人对性别识别的这个因子图, 这时候我们可以分析人和计算机识别人脸的相关性高达0.73 , 说明人和计算机学习到的性格学习方法高度同质化。同时这也说明随机噪声其实是有结构的, 我们对这个特征图做低频和高频的傅里叶分解,可以看到低频部分人和VGG(vgg face)相关性更高。这让机器和人对人脸的表示相关性极高且集中于低频。

    这里讨论比较认知科学和机器学习的关联的时候,我们最好引入David Marr的三个层次语言体系, 也就是计算层, 表示层,硬件层, 人和VGG模型的计算目标(最高层)是相同的,在硬件层显然大相径庭, 这个研究表明在第二层次表征层两者却是高度相通的。

    刚刚用到的网络是专门为人脸训练的。我们也可以用迁移学习转移一个从ImageNet训练的通用CNN,然后训练读出层,用迁移学习来识别人脸,这时候两者的表征是否相通呢?实验表明两者表征居然依然是类似的。

    最后研究团队用sparse coding来认识这件事。sparse coding的原理表明, 随着网络层级的提高,网络表示的稀疏性随层级增加, 这将导致对人脸识别的关键特征会呈现disentagle的离散化形式,也就是可能只有少数神经元编码一个特征(这是神经编码sparse coding和symbolic 表示的内在联系, 最终的表示是一个稀疏因子图) 

    事实上高层越稀疏, 分类效果越好, 比如alexnet。人脑和深度网络的共同选择。心理学可以用来理解深度学习, 得到一个可解释的神经网络。

    *** 从这个讲座一个有意思的延申, 似乎我们个人识别美丑的特征正是这些用于性别识别的特征。

    2.3 吴思 The cross talk between biology and computer vision

    我们所熟知的深度网络泛化能力有限, 容易受到噪声干扰, 其关键问题是不能抓住全局信息。所谓的全局信息在图像处理的重要体现就是拓扑信息。人对图片的认知从来是从宏观到局部认知的, 所谓top-down-processing。

    一个重要的假设是我们对图象的理解是从图象整体的拓扑结构一步步展开的:首先人的认知从拓扑开始的,人的图像理解是一个猜测认证的过程,在一个不确定的图片里, 人要通过物体的整体轮廓对可能的情况进行猜测(sub-cortical pathway 比如天空中飞翔的鹰的影子),然后再提取出和这个整体轮廓相关的细节(ventral pathway,从细节到整体),鹰爪,鹰眼·, 补充对整体的认知,经过一个循环得到对视觉事物的认知 。人,先验的知识, 经验,记忆决定了我们要看到什么。从高级到初级视皮层的。

    深度学习只模拟了从细节到整体的ventral pathway, 而没有那个事先提炼整体的通路subcortical pathway。实验表明人脑能够非常快速的探测图像的拓扑信息, 这似乎揭示这个拓扑的认知并非那么困难而是存在于比较基础的脑区。

    事实上这个对图象的宏观拓扑特征提取的机制可能从视网膜就开始了, 人眼里的RGC cell通过电突触gap junction相连(正反馈),使得这部分神经元可以快速的同步化,同时兴奋和抑制神经元间存在旁侧抑制。

    这种正负反馈并存的一个后果是对图象宏观结构的自然分割,比如白色背景(天空)中的一个黑色局部(老鹰)。这种同步可以帮助大脑很方便的提取低频信息,如同在原野上燃烧的一把火,它会自然的把易燃的草地和岩石给分隔,从而抓住拓扑特征。

    我们能否利用人类对视觉图像理解的这种两通路设计来构建一个人工神经网络呢?第一阶段利用subcortical pathway 来实现整体的轮廓提取, 第二阶段再利用ventral pathway的原理做细致的分类。 

    动力学上看第一个回路的启动速度要快于第二个回路, 在第一阶段通过拓扑提取回路的同步和正反馈来抓住大类。第二阶段, 则通过负反馈来去掉大类平均强化对细节的注意,如此反复循环,这将实现非常具有鲁棒性的对视觉物体的理解!

    吴思最终在讲座中指出, 当人工智能发展下去到认知,人脑和AI会越来越近。

    ** 这个讲座告诉我们当下的深度学习和人类认知的很大区别在于缺乏人脑的丰富动力学机制, 加入这些机制后一些对机器很困难的事情可能是自然而然的。

    2.4 余山 From Brain Network to brain-like computation

    脑和AI很大差异, 如何从计算原理的层面理解这种本质并利用脑启发改善AI?

    我们首先看细胞和突触层面,神经细胞信息传递靠突触,上游脉冲, 神经递质不总有效, 有随机性的, 30%会发放当上游信号到达。这样随机的发放如何能够支持非常稳健和鲁棒的智能?事实上这不是单纯是生物限制,而是一种主动进化。因为它可以提高系统的泛化性。

    类似启发在深度学习的体现就是dropout的技术,dropout可以模拟某些细胞和突触发放的随机性,却可以在几乎任何任务减少泛化误差。

    另一个重要的生物启发是临界,我们知道生物神经网络的动力学处在稳定和混沌的边缘,所谓一个临界的状态。从图网络传播的角度,这个时候的网络内部信息具有长程关联,又不至于信息爆炸。

    利用这个原理我们可以设计蓄水池网络,这是一种在临界态处理信息的RNN网络因为这个时候的工作效率最高。有意思的是,这个混沌和秩序边缘的状态非常像一个finetune的精细调参结果。大脑不可能调参,它是如何维持临界态的?

    这时候我们必须拿出自组织临界的重要原理,通过引入负反馈, 我们可以使得临界态处于一个系统的收敛稳定点状态,从而在非常广的参数范围内,实现临界。很多人认为批量正则化BN使用了类似的原理让神经网络处于稳定的状态。

    余山的另一个部分讲座讲解了如何设计能够根据情境调节的神经网络。我们知道人工神经网络不能进行情景化处理(也就是依据不同情境调节自己的认知状态),而这是大脑的本质特点。

    背后的重要原理是大脑是个平行通路的系统,从sensory cortex进来的信息, 一方面直接通过视皮层层层抽象, 并输入给运动皮层作动作决策(下意识动作),另一方面通过cortex进行认知推理,得到关于情景的高层信息, 再往运动皮层推(理解之上的动作), 类似于我们说的系统一和系统二。

    我们能否根据这个原理设计一个神经网络?因此我们可以在CNN中加入一个平行于前馈网络context modulation 模块(yu 2019)。这个模块可以调控感知信息的处理过程。

    事实上这个模块更加类似一个学习的调控器,它可以根据识别到的情景对在网络内传递的梯度进行调整,从而显著缓解灾难遗忘改善连续学习。因为学习新内容的过程无疑会擦掉以往权重,这导致神经网络很难持续不断的学习到新的内容,而刚刚加入的情景调控模块引入一个OWM算法, 也叫正交调节器。就是当这个模块识别到情景的变化,就把权重的学习投影到与之前情景正交的子平面上。

    Yu Nature Intelligence 2019

    ** 余山的讲座提到的核心点与吴思有相通之处,就是充分利用人脑的平行回路原理。

    脑与认知模块总结:

    如何利用脑启发改善AI的模块是我本次大会最喜欢的模块, 从这里,可以看到大家达成共识的点是 

    1,大脑是个动力学系统, 很多动力学的基本原理对AI目前的致命缺陷有重大补充, 因果或逻辑可能蕴含在动力学里。临界很重要。 

    2. 人脑的稀疏表示与符号注意AI和因果有重要联系 

    3, 充分利用平行回路设计系统

    3 Consciousness AI

    大会也涉及了当下认知科学和AI交叉领域的最hot topic - 意识,虽然只是一小部分、

    会议邀请了研究意识问题的计算机科学家Manuel Blum 和 Lenore Blum两个专家(夫妇)。

    首先, 两个科学家介绍了意识作为科学的发展史。1988 年 科学家首次发现了意识的实验证据 (FMRI evidences) , 之后人们从不同领域进入意识的研究, 比如神经科学, 哲学, 计算机。

    之后Blum指出如果要建立一个意识的模型,它需要具备的特点。首先意识的模型并不等价于全脑模型,但是它必须符合涌现性的原理(复杂科学最重要的概念, 复杂从简单中脱颖而出的一种跳跃变化), 而且构成意识的神经组织和非意识的组织应该是共享的而非隔离的(同样脑区可以既处于有意识也处于无意识状态)。

    然后Blum指出了如何让意识模型和计算机模型对应。它提出意识的功能是一个从长期记忆力提取短期记忆内容的提取器(类似一个pointer)。因为我们的长期记忆事实上处于无意识状态, 这个内容的数量浩如星海。

    而短期工作记忆却是你我都可以意识到的, 但它通常只有几个bits。这个信息瓶颈可能就是需要意识来实现,它需要根据当下的任务和情景把和任务最相关的因子提取出来。 

    这正是著名的(conscious turning machine) Baar's global workspace model 。这样存在的意识可以迅速的把长期记忆的关键因素抽取到working memory里,方便当下任务的执行,大大加强了系统的灵活性。

    然后它指出意识可能的具体形态需要包含

    1, inner voice 

    2 inner image 

    3 feeling 

    4 thought

    非常有意思的是, 意识在取出很多脑区后依然存在。最后Blum指出意识可能必要的模型组成成分: 

    1. Inner-dialogue processor  

        2  model-of-the-world processor

    4 图网络专场

    4.1 唐建 如何借助图网络构建认知推理模型

    深度学习需要认知推理已经是人所共知,但是如何实现,图网络是一个重要的中间步骤 。大家可能熟悉当下红红火火的图网络,但是不一定熟悉它的前身条件随机场。唐建老师认为, 这两个模型分别对应人脑中的系统一和系统二认知。所谓的系统一我们可以认为是system

    I(graph network) 和system II(conditional random field) 认知模型。

    基于图的conditional random field模型可以对概率进行计算和推理。为此我们定义一个potential function, 用条件概率乘积形式来表示不同节点间的依赖关系 , 但是这一定义形式往往过于复杂,优化也很困难。这种学习类似于人类较为缓慢的总结关系规律的系统二推理。

    而图网络则相反,利用message passing的思想, 得到node representation, 这中间我们并没有直接模拟node和node间的统计关系, 但是我们可以通过拟合node上的label来获取node之间的基本关系。因此这样的学习更加类似于人类直觉思考的形式。

    可否把两者的优势结合呢?

    这就是最后要介绍的,如何把图网络和条件随机场的优势结合起来做推理。

    整个框架类似于一个EM算法,我们由一个已知部分节点和边的信息的图网络出发, 在E步骤我们需要由图网络的直觉做inference 推测 , 这样我们会得到大量新的标签, 而在M步骤这些新增加的标签会辅助条件随机场模型更好的算概率, 更新连接图,我们得到一个循环。


    如此的方法可以被用于知识图谱推理。我们用马尔科夫logic net 求解所有fact的joint distribution。然后再用图网络学习每个逻辑三元组的权重 有了三元组就可以预测每个事实是对的还是错的。

    整个思路就是首先可以基于system I (图网络)提供初始预测, 然后用system II基于这些比较初始的预测得到更加具有逻辑的结果,关系推理最终由一个概率图来执行。

    4.2 何向南 用图神经网络做推荐

    这其实是一个非常有趣的topic因为推荐系统本质上就是一个图(由用户和商品构成的bi-partite graph)。我们知道早期的推荐系统协同过滤主要是计算相似度。而当下的图网络则本质上用高阶连接性建立协同过滤的信号。

    通过信息传递得到推荐网络的表示, 在此基础上做推荐 。因此我们需要先定义从一个物品到用户的message,然后通过节点和邻居的连接度矩阵做message passing,得到任意两个节点的相似度,这是我们可以预测某个未知商品和用户关系的基础。

    在此之上,我们还可以加入用户意图。因为一个人和物品产生交互的时候, 背后的意图是不一样的则推荐的机制不同。如何能够对不同用户的意图下都学习一个表示就成为关键。这里我们可以巧妙的借鉴attention的机制, 学习一个和用户意图相关的权重。

    最终我们还需要考虑推荐本身是一个行为而非预测,因为推荐的结果会反过来影响用户的行为(反身性), 很大程度阻碍模型发现用户的真正兴趣。现有的方法需要给一个用于修正这种偏差的bias, 比如降低popular 物体的权重 第二个是用对不那么popular物品的 随机曝光,

    4.3 沈华伟 GNN的局限性

    图神经网络内核处在停滞不前的状态。我们说深度学习是炼金术,而事实上, 现在开始流行的GNN也是这样一个炼金术, 和深度学习类似。

    当年大家分析CNN为什么能work , 那么现在我们必须明白为什么GNN也work。大家看到GNN这么强大的表达能力, 做推理, 做认知,做分子式,做物理,其核心都是围绕图 G = (V, E, E, X)做节点分类,边分类和图分类。

    我们分析机器学习模型的表达能力, 通常是看它可以分开的模式数量。而GNN的表达能力能否用类似的角度研究呢?首先, 我们可以把问题定义为GNN模型能够分析的网络种类。对于不同的网络, 其主要特征就是网络的拓扑结构。

    对于GNN其核心部分是Message passing,当经过一次消息传递,看起来无区别的节点就带上其邻居的信息, 当节点和节点的对外连接不同,这个信息就变得不一样,因此在节点分类的角度, 经过一次信息传递,节点已经形成了根据自己邻域拓扑特征形成的聚类。

    当然对于比较复杂的图谱,一次传递可能是不够的,经过两次传递后, 关于网络的更深层特征就会进入到每个节点里,从而使得节点的聚类体现更长程的邻域结构。这样的方法对节点的分类很有效但对图整体的分类却没有那么有效。

    我们可以把所有节点的表示向量合起来作为图的整体表示,经过信息传递一些不同的图结构明显的被分类了, 而得到根据网络相似度表达的metric,而且信息传递的次数越多似乎这个表征能力越强。

    然而按照这样的思想传递N次后我们是否可以区分任何网络结构呢?答案确实是否定的。我们可以证明对于特定的网络结构传递多少次信息都无法区分其拓扑结构, 因此靠加大图网络深度提高其表示能力是有局限性的。这个方法揭示了单纯靠信息传递得到结果的图网络方法的局限性。

    所使用的分析方法叫做 WLtest区分,类似于把整个节点传播过程用树展开。最后看子树的分布。一阶WL_tes通常认为是 GNN表达能力的上界

    这个分析从根本上揭示出GNN的局限性,它似乎只是对图的结构做了一个平滑 。

    参考文献:

    Can you design a layer-by-layer network to achieve more powerful expressiveity than existing GNNs

    How powerful are grpah neural nets ICLR2019

    ** 这个talk让我想到用GNN做认知推理真的可靠吗?目前来看, 似乎还有差距。看起来它依然只是一个模式识别的工具。感知 = 模式识别 ,而模式识别本身是够不上认知的。

    4.4 wang min jie 新的图网络工具箱

    图神经网络本质基于消息传播模式-message passing paradim,这里面关键的是一个消息传递函数,一个累加函数。当我们假定消息传递函数是线性的,累加函数是average pooling, 且这些函数和特定网络局域结构无关, 我们得到GCN。

    如果你观察这种结构表达方程,你会发现大名鼎鼎的page rank和GCN公式几乎是同质的,都符合这个paradim。这说明我们可以设计一个更加灵活的工具箱,帮助我们设计新的图网络框架。

    消息传递,累加, 都都可以扩展成为更复杂的工具,甚至lstm。编辑图神经网络,可能用到已有神经网络的模块。新的面向图神经网络系统,需要和现有的深度学习平台结合。这是deep graph library项目动机,它搭建了张量世界和图世界的桥梁。

    **这个讲座有趣的地方不在于介绍工具箱而是对图网络精辟的总结。

    5 知识智能专题

    5.1 孙一舟 如何在深度学习里加入符号知识

    符号主义AI在当下的精髓在于knowledge graph, 知识图谱里由无数的triplet(head, relation,tail)组成, 作为知识的计算机的主体。但是单纯知识图谱是无法做认知推理的, 为了完成这样的任务, 我们需要把它和概率图模型连接在一起。

    6 强化学习部分

    6.1 Statinder singh 如何让强化学习做discovery

    discovery 是强化学习非常重要的发现和探索过程。谈到强化学习的探索,大家可能都很熟悉epsilon-greedy这一类简单的随机搜索策略。而一个大家往往不会注意的地方是奖励函数。

    奖励函数不仅是一个有关游戏目标的标量,事实上也可以存储更多游戏有关的知识,比如和探索有关的,这部分奖励又称为内部奖励(intrinsic reward),这类似于你并非因为外在的物质奖励而是由于内在求知兴趣去探索发现的过程。

    我们发现加入intrinsic reward 不仅可以,能够发现一些不易发现的不变性 ,解决类似于于non-stationary 问题的探索问题。

    ** 其它一些有趣的点1 general value function ,generalize to any feature of states

    2 引入question network 辅助task

    PPT :blog.csdn.net/qq_411858

    6.2 俞扬 更好的环境模型,更好的强化学习

    强化学习在游戏类任务里取得了史诗级的成功几乎在所有高难度任务里击败人类。如果问这个胜利的根源,不是网络的巨大,而在于环境。监督学习的原料是数据集, 强化学习则是它的环境。

    比如围棋游戏的境法则固定, 因而可以无限量的取得数据。所以可以取得优秀的成绩。强化学习agent的在环境中通过一定策略采集数据,学好策略,回到环境,验证策略有没有更好。如果环境规定固定,agent就拥有无限多的稳定数据,如同CNN拥有一个ImageNet。

    为什么环境规则必须固定?因为与监督学习针对固定数据分布的情形不同的是,强化学习面对的数据不符合这一设定, 因为游戏的数据采集是按照一定策略来的, 当我们的策略发生变化,数据会发生变化。

    监督学习通过的训练集和测试集符合同一分布假设, 而强化学习用于未来训练的数据获却永远来自历史学习策略(如同我们看着后视镜驶向未来),造成训练不稳定。在封闭固定环境下随着训练的进行这种当下策略和未来策略的差距会逐步收敛。而在一个开放环境下则变得非常困难。

    什么办法可以相对缩小真实环境和游戏的区别呢?模拟器,模拟器实际等价于真实世界的缩影,可以预测在当下的agent做出选择后,真实环境可能给以的反馈。

    我们知道在很多领域如机器人控制 ,物流,流体动力学,我们已经在使用模拟器。虽然模拟器可以做, 但是精度可能不足,尤其是当环境更加复杂。一旦出现误差这种误差可能迅速随时间扩大, 那么如何解决这个问题?

    我们可以从数据中学习一个模拟器,用监督学习的方法?从当下状态S,和行为 a 得到下一个时刻的状态 S‘和奖励R‘, 这是模拟器的本质, 这样的模型可以有效解决误差问题。有了模拟器对于真实环境的强化学习意义重大,因为我们不再依赖真实世界,而可以通过想象中的世界训练我们的值函数。

    有关世界模型的关键是模型的可迁移能力。因为随着策略的更新数据采样的区域很快放大(非iid分布)。我们因此必须不停获取数据更新世界模型。除此外我们还可以想办法加强模型的鲁棒性。

    比如 我们可以把预测分解为M(s'|s,a)* p(s,a)来假定状态转移矩阵和状态之间的独立性来增强可迁移性。还有比如利用类似对抗学习的优化方法,可以把长程优化误差迅速缩小。

    可学习世界模型的思想可以用于商品设计, 把顾客买家当成世界模型,学习顾客这个model。可以用于网约车, 可以学习一个司机的虚拟模型-模拟器。可以用于商户拣货问题, 学习工人和商品派单系统 world & agent,甚至学习一个砍价机器人, 从而提高所有电商, 推荐, 网约车等等商业问题的效率,非常有前景。

    最后总结人类决策技术的进步史:专家决策 - 专家设计模拟器决策- 专家设计可以学习的模拟器来学习决策 - AGI?模型是强化学习进入真实世界的必经之路

    **此处联想世界模型和因果的联系,理想的世界模型是一个包含do 的因果model, 而且模型动力学和数据分布独立-便于因子化factorization。如同粒子状态和作用力独立(相互作用关系模型)。当这种假定是合理的, 结果将迅速提升。

    PPT:blog.csdn.net/qq_411858

    6.3 张伟楠 Model base reinforcement learning & bidirectional model

    深度强化学习和无模型的强化学习是一对绝配,因为无模型强化学习非常适合在深度神经网络的框架里来进行,然而离开游戏场景无模型强化学习就会产生数据效率低效的问题,如果试错无法从环境中得到有效信息, 试错将毫无意义。为了解决这个问题, 提出model base 是必须的。

    基于模型的学习高效的一大原因是一旦模型学习到,可以直接在模型里perform on-policy learning,从而极大提高数据的适用效率(on policy下数据采样的集合和实施策略的集合是最匹配的)

    后面的讲座主要强调基于模型学习的优化方法, 一类所谓黑箱模型,例如dyna-Q, MPC 。模型本身也是未知的神经网络。一类是白盒模型,即模型完全已知。

    模型的加入可以提高数据适用效率,但是也会引入一定的bias。以黑盒算法 Q-planning为例。当学好一个模型后, 我们可以从模型中采样得到一个action sequence(想象中的行为) ,从而计算得到accumulative rewards。

    用术语说就是采样一个轨迹 sample a trajectory, 如果模型本身是不准确的(主观和客观的差距)那么这个差距会在整个轨迹放大。因此我们知道我们一方面要控制模型的精度,一方面要控制这个想象轨迹的长度。当前者模型误差越小,后者轨迹可以抽样的时间就越长 。

    能不能有效改进这个方法呢?此处引入一个叫bidirectional model的模型改进 。什么意思, 就是既考虑前向预测, 也考虑往历史。以往的想象轨迹只考虑未来n步发生的事情,现在我还会推测之前发生的事情,以及如果之前采取其它动作,可能到达的状态。这样时间上的双向性会缩小由于模型偏差带来的误差。

    **此处是否想到因果的counter-factual呢?

    PPT: blog.csdn.net/qq_411858

    以上是我总结的内容,会议全部内容名单请见:2020.baai.ac.cn/

    7 最终总结-世界模型

    这次会议是特别有趣的, 不同领域的人根据自己的经验提出了不同的approach在不同角度告诉我们什么可能是达到AGI的最短通路 。那么如果我总结什么可能是一个最短的到达下一个AI阶段的通路呢?

    其实大会最终采访LSTM之父施密特的对话很有意思。在对施密特的采访中,他兴高采烈的介绍了LSTM的崛起, 和他的贡献,并认为这是一个由大脑启发的能够解决大量不同实际任务的成功典范。

    那么我们回顾下LSTM为什么如此成功,首先,它是一个动力学模型, 与CNN那些前馈网络不同,LSTM模型事实上可以对一个动力过程进行模拟。而我们的物理学尝试告诉我们,世界的本质就是一台动力学机器,每个齿轮的转动拉动了整个宇宙向前。LSTM具有对宇宙齿轮的模拟能力,因此,它可以做不同的任务,学习不同的领域。

    这台机器具体包含哪些部分?它有一个记忆memory, 有一个中央动力处理单元h, 是一个神经元互相反馈的通用图灵机, 还有一个执行单元o,输出对未来的预测和动作。

    这个机器像大脑却不是, 专家最后说施密特未来的神经网络,lstm的接任者需要有什么特征。施密特很快坚定的说它需要更像大脑, 需要具有自我发现和探索的好奇心。

    然后它需要一个能够预测世界的,尤其是自我行为对世界影响的world model, 以及一个能够根据这些 知识做出行为决策的行为的action model, 这两个model组成一个循环的反馈系统。这样的模型可以像小孩子一样从真实世界里通过探索学习得到抽象的知识。

    我们从这个角度出发,将看到此次大会的所有topic 的联系。这里的第一个关键词是world model,它围绕奖励而来,却时时刻刻满足着自己的好奇心,用来discover真实世界的抽象规律。

    因果学派说的世界如同一个概率图模型,每个节点之间的关系可以由节点的扰动(do)和它的影响刻画。说的不正是这个充满好奇的world model需要做的事情,根据自己的行为或自己感测到的外界变化预测未来未知世界的关键?

    而具体的这个world model的抽象能力, 不正是类似于在真实的世界中提取这样一个核心变量组成的图网络?

    因此图网络学派可能说对了世界模型的一个重要组成成分, 它却没有涵盖这个图是怎么产生的这个最核心的议题,如何产生这张图,这让我们想到认知科学启发专题的若干讲座,大脑是如何完成这个工作的。

    而world model不是关注世界所有的内容,而只关心和智能体完成任务最相关的信息,这就是强化学习的观点, 或者叫做以行为为中心,以任务为导向

    当我们有了world model和action model, 而且world变得越来越复杂不能用一个模型来运转的时候,是不是我们可以在此基础上加一个超级观察者或者叫self model,这个self model可以预测哪些部分的world model 需要进入到action model供action 决策,同时预测action根据这些信息的决策结果?这个self model是不是就是我们要的意识模型呢?

    由此看, 这几个流派已经贯穿成一体。最终我们要实现上述的任何步骤,无疑都需要向我们的大脑取经。因为自然设计已经给了我们一份完美的解决上述通用智能问题的框架,当我们让机器执行的任务和人越来越接近,无疑将会参考这份完美答案。

    希望更多的人能够对这个问题感兴趣并加入到研究的队伍里(可加微信XUTie0609)。

    未来智能实验室的主要工作包括:建立AI智能系统智商评测体系,开展世界人工智能智商评测;开展互联网(城市)云脑研究计划,构建互联网(城市)云脑技术和企业图谱,为提升企业,行业与城市的智能水平服务。

      如果您对实验室的研究感兴趣,欢迎加入未来智能实验室线上平台。扫描以下二维码或点击本文左下角“阅读原文”

    展开全文
  • 本系列专题以提炼要点的方式总结知识点,而不做具体展开,对应的会附上个人较喜欢的文章链接供大家详细了解和学习。第一个板块 开发艺术 是对任老师的《Android开发艺术探索》著作的学习和扩展,此书的目录也是该...

    本系列专题以提炼要点的方式总结知识点,而不做具体展开,对应的会附上个人较喜欢的文章链接供大家详细了解和学习。第一个板块开发艺术 是对任老师的《Android开发艺术探索》著作的学习和扩展,此书的目录也是该板块的主线,那么就先从我们熟悉的Activity开始吧~

    在之前的Activity篇主要学习Activity的生命周期、创建和页面跳转的实现,本篇将深入了解Activity,学习清单:

    • 生命周期全解析
    • 四种启动模式
    • IntentFilter匹配规则

    一.生命周期全解析

    1.Q:典型情况下Activity生命周期
    A:
    a.onCreate():

    • 状态:Activity 正在创建
    • 任务:做初始化工作,如setViewContent界面资源、初始化数据
    • 注意:此方法的传参Bundle为该Activity上次被异常情况销毁时保存的状态信息。

    b.onStart():

    • 状态:Activity 正在启动,这时Activity 可见但不在前台,无法和用户交互。

    c.onResume():

    • 状态:Activity 获得焦点,此时Activity 可见且在前台并开始活动。

    d.onPause():

    • 状态: Activity 正在停止
    • 任务:可做 数据存储、停止动画等操作。
    • 注意:Activity切换时,旧Activity的onPause会先执行,然后才会启动新的Activity。

    e.onStop():

    • 状态:Activity 即将停止
    • 任务:可做稍微重量级回收工作,如取消网络连接、注销广播接收器等。
    • 注意:新Activity是透明主题时,旧Activity都不会走onStop。

    f.onDestroy():

    • 状态:Activity 即将销毁
    • 任务:做回收工作、资源释放

    g.onRestart():

    • 状态:Activity 重新启动,Activity由后台切换到前台,由不可见到可见。

    onStart()和onResume()、onPause()和onStop()的区别: onStart与onStop是从Activity是否可见这个角度调用的,onResume和onPause是从Activity是否显示在前台这个角度来回调的,在实际使用没其他明显区别。

    推荐阅读:对Activity生命周期方法的感性理解

    2.Activity生命周期的切换过程
    ①启动一个Activity:

    • onCreate()–>onStart()–>onResume()

    ②打开一个新Activity:

    • 旧Activity的onPause() –>新Activity的onCreate()–>onStart()–>onResume()–>旧Activity的onStop()

    ③返回到旧Activity:

    • 新Activity的onPause()–>旧Activity的onRestart()–>onStart()–>onResume()–>新Activity的onStop()–>onDestory();

    ④Activity1上弹出对话框Activity2:

    • Activity1的onPause()–>Activity2的onCreate()–>onStart()–>onResume()

    ⑤关闭屏幕/按Home键:

    • Activity2的onPause()–>onStop()–>Activity1的onStop()

    ⑥点亮屏幕/回到前台:

    • Activity2的onRestart()–>onStart()–>Activity1的onRestart()–>onStart()–>Activity2的onResume()

    ⑦关闭对话框Activity2:

    • Activity2的onPause()–>Activity1的onResume()–>Activity2的onStop()–>onDestroy()

    ⑧销毁Activity1:

    • onPause()–>onStop()–>onDestroy()

    参考文章:实际体验Activity生命周期

    3.Q:生命周期的各阶段
    A:
    a.完整生命周期
    Activity在onCreate()和onDestroy()之间所经历的。
    在onCreate()中完成各初始化操作,在onDestroy()中释放资源。

    b.可见生命周期
    Activity在onStart()和onStop()之间所经历的。
    活动对于用户是可见的,但仍无法与用户进行交互。

    c.前台生命周期
    Activity在onResume()和onPause()之间所经历的。
    活动可见,且可交互。

    4.onSaveInstanceState和onRestoreInstanceState
    a.出现时机:异常 情况下Activity 重建,非用户主动去销毁

    b.系统异常终止时,调用onSavaInstanceState来保存状态。该方法调用在onStop之前,但和onPause没有时序关系。

    onSaveInstanceState与onPause的区别:前者适用于对临时性状态的保存,而后者适用于对数据的持久化保存。

    c.Activity被重新创建时,调用onRestoreInstanceState(该方法在onStart之后),并将onSavaInstanceState保存的Bundle对象作为参数传到onRestoreInstanceState与onCreate方法。

    可通过onRestoreInstanceState(Bundle savedInstanceState)和onCreate((Bundle savedInstanceState)来判断Activity是否被重建,并取出数据进行恢复。但需要注意的是,在onCreate取出数据时一定要先判断savedInstanceState是否为空。另外,谷歌更推荐使用onRestoreInstanceState进行数据恢复。

    推荐阅读onSaveInstanceState和onRestoreInstanceState详解

    5.Activity异常情况下生命周期分析

    a.由于资源相关配置发生改变,导致Activity被杀死和重新创建
    例如屏幕发生旋转:当竖屏切换到横屏时,会先调用onSaveInstanceState来保存切换时的数据,接着销毁当前的Activity,然后重新创建一个Activity,再调用onRestoreInstanceState恢复数据。

    • onSaveInstanceState–>onPause(不定)–>onStop–>
      onDestroy–>onCreate–>onStart–>onRestoreInstanceState–>onResume

    为了避免由于配置改变导致Activity重建,可在AndroidManifest.xml中对应的Activity中设置android:configChanges=”orientation|keyboardHidden|screenSize”。此时再次旋转屏幕时,该Activity不会被系统杀死和重建,只会调用onConfigurationChanged。因此,当配置程序需要响应配置改变,指定configChanges属性,重写onConfigurationChanged方法即可。

    b.由于系统资源不足,导致优先级低的Activity被回收
    ①Activity优先级排序:

    • 前台可见Activity>前台可见不可交互Activity(前台Activity弹出Dialog)>后台Activity(用户按下Home键、切换到其他应用)

    ②当系统内存不足时,会按照Activity优先级从低到高去杀死目标Activity所在的进程。

    ③若一个进程没有四大组件在执行,那么这个进程将很快被系统杀死。

    推荐阅读异常情况下的生命周期分析


    二.Activity四种启动模式

    1.Q:设置Activity启动模式的方法
    A:
    a.在AndroidManifest.xml中给对应的Activity设定属性android:launchMode=”standard|singleInstance|single Task|singleTop”。
    b.通过标记位设定,方法是intent.addFlags(Intent.xxx)

    2.Q:Activity的四种LaunchMode
    A:
    a.standard:标准模式、默认模式
    - 含义:每次启动一个Activity就会创建一个新的实例。
    - 注意:使用ApplicationContext去启动standard模式Activity就会报错。因为standard模式的Activity会默认进入启动它所属的任务栈,但是由于非Activity的Context没有所谓的任务栈。

    b.singleTop:栈顶复用模式

    • 含义:如果新Activity已经位于任务栈的栈顶,就不会重新创建,并回调onNewIntent(intent)方法。

    c.singleTask:栈内复用模式

    • 含义:只要该Activity在一个任务栈中存在,都不会重新创建,并回调onNewIntent(intent)方法。如果不存在,系统会先寻找是否存在需要的栈,如果不存在该栈,就创建一个任务栈,并把该Activity放进去;如果存在,就会创建到已经存在的栈中。

    d.singleInstance:单实例模式

    • 含义: 具有此模式的Activity只能单独位于一个任务栈中,且此任务栈中只有唯一一个实例。

    标识Activity任务栈名称的属性:android:taskAffinity,默认为应用包名。

    推荐阅读:Activity的四种LaunchMode详解

    3.常用的可设定Activity启动模式的标记位

    FLAG_ACTIVITY_SINGLE_TOP:对应singleTop启动模式。
    FLAG_ACTIVITY_NEW_TASK :对应singleTask模式。


    三.IntentFilter匹配规则

    原则:
    ①一个intent只有同时匹配某个Activity的intent-filter中的action、category、data才算完全匹配,才能启动该Activity。
    ② 一个Activity可以有多个 intent-filter,一个 intent只要成功匹配任意一组 intent-filter,就可以启动该Activity。

    a. action匹配规则:

    • 要求intent中的action 存在且必须和intent-filter中的其中一个 action相同。
    • 区分大小写。

    b. category匹配规则:

    • intent中的category可以不存在,这是因为此时系统给该Activity 默认加上了< category android:name=”android.intent.category.DEAFAULT” />属性值。
    • 除上述情况外,有其他category,则要求intent中的category和intent-filter中的所有 category 相同。

    c. data匹配规则:

    • 如果intent-filter中有定义data,那么Intent中也必须也要定义date。
    • data主要由mimeType(媒体类型)和URI组成。在匹配时通过intent.setDataAndType(Uri data, String type)方法对date进行设置。
    • 要求和action相似:如果没有指定URI,默认值为content和file; 有多组data规则时,匹配其中一组即可。

    采用隐式方式启动Activity时,可以用PackageManager的resolveActivity方法或者Intent的resolveActivity方法判断是否有Activity匹配该隐式Intent。

    推荐阅读:Intent和IntentFilter


    希望这篇文章对你有帮助~

    展开全文
  • 上文我们完成了引擎的初步设计,本文进行引擎提炼的第一次迭代,从炸弹人游戏中提炼引擎类,搭建引擎的整体框架。 名词解释 用户 “用户”是个相对的概念,指使用的一方法,默认指游戏开发者。 当相对于某个引擎类...

    前言

    上文我们完成了引擎的初步设计,本文进行引擎提炼的第一次迭代,从炸弹人游戏中提炼引擎类,搭建引擎的整体框架。

    名词解释

    • 用户

    “用户”是个相对的概念,指使用的一方法,默认指游戏开发者。
    当相对于某个引擎类时,“用户”就是指引擎类的使用方;当相对于整个引擎时,“用户”就是指引擎的使用方。

    • 用户逻辑

    引擎使用方的逻辑,默认指与具体游戏相关的业务逻辑。

    本文目的

    1、参考引擎初步领域模型,从炸弹人参考模型中提炼出对应的通用类,搭建引擎框架。
    2、将炸弹人游戏改造为基于引擎实现。

    本文主要内容

    炸弹人的参考模型

    112119302284476.jpg

    引擎初步领域模型

    120708044156625.jpg

    开发策略

    按照引擎初步领域模型从左往右的顺序,确定要提炼的引擎类,从炸弹人参考模型对应的炸弹人类中提炼出通用的引擎类。
    本文迭代步骤
    112120301961563.jpg
    迭代步骤说明

    • 确定要提炼的引擎类

    按照“引擎初步领域模型”从左往右的顺序依次确定要提炼的引擎类。
    每次迭代提炼一个引擎类,如果有强关联的引擎类,也一并提出。

    • 提炼引擎类

    从炸弹人参考模型中确定对应的炸弹人类,从中提炼出可复用的、通用的引擎类。

    • 重构提炼的引擎类

    如果提炼的引擎类有坏味道,或者包含了用户逻辑,就需要进行重构。

    • 炸弹人改造为基于引擎实现

    对应修改炸弹人类,使用提炼的引擎类。
    这样能够站在用户角度发现引擎的改进点,得到及时反馈,从而马上重构。

    • 通过游戏的运行测试

    通过运行测试,修复由于修改炸弹人代码带来的bug。

    • 重构引擎

    如果有必要的话,对引擎进行相应的重构。
    下面的几个原因会导致重构引擎:
    1、提炼出新的引擎类后,与之关联的引擎类需要对应修改。
    2、得到了新的反馈,需要改进引擎。
    3、违反了引擎设计原则。
    4、处理前面遗留的问题

    • 通过游戏的运行测试

    通过运行测试,修复炸弹人和引擎的bug。

    • 编写并通过引擎的单元测试

    因为引擎是从炸弹人代码中提炼出来的,所以引擎的单元测试代码可以参考或者直接复用炸弹人的单元测试代码。
    炸弹人代码只进行运行测试,不进行单元测试。因为本系列的重点是提炼引擎,不是二次开发炸弹人,这样做可以节省精力,专注于引擎的提炼。

    • 完成本次迭代

    进入新一轮迭代,确定下一个要提炼的引擎类。

    思考
    1、为什么先提炼引擎,后进行引擎的单元测试?
    在炸弹人开发中,我采用TDD的方式,即先写测试,再进行开发,然而这里不应该采用这种方式,这是因为:
    (1)我已经有了一定的游戏开发经验了,可以先进行一大步的开发,然后再写对应的测试来覆盖开发。
    (2)在提炼引擎类前我只知道引擎类的大概的职责,不能确定引擎类的详细设计。在提炼的过程中,引擎类会不停的变化,如果先写了引擎类的单元测试代码,则需要不停地修改,浪费很多时间。
    2、为什么要先进行游戏的运行测试,再进行引擎的单元测试?
    因为:
    (1)炸弹人游戏并不复杂,如果运行测试失败,也能比较容易地定位错误
    (2)先进行游戏的运行测试,不用修改单元测试代码就能直接修复发现的引擎bug,这样之后进行的引擎单元测试就能比较顺利的通过,节省时间。

    不讨论测试

    因为测试并不是本系列的主题,所以本系列不会讨论专门测试的过程,“本文源码下载”中也没有单元测试代码!
    您可以在最新的引擎版本中找到引擎完整的单元测试代码: YEngine2D

    引擎使用方式的初步研究

    开篇介绍中,给出了引擎的三种使用方式:直接使用引擎类提供的API、继承重写、实例重写,现在来研究下后两种使用方式。

    继承重写应用了模板模式,由引擎类搭建框架,将变化点以钩子方法、虚方法和抽象成员的形式提供给用户子类实现。

    实例重写也是应用了模板模式的思想,引擎类也提供钩子方法供用户类重写,不过用户类并不是继承复用引擎类,而是委托复用引擎类。
    继承重写与实例重写的区别,实际上就是继承与委托的区别。

    继承重写和实例重写的比较
    共同点
    (1)都是单向关联,即用户类依赖引擎类,引擎类不依赖用户类。
    (2)用户都可以插入自己的逻辑到引擎中。

    不同点
    (1)继承重写通过继承的方式实现引擎类的使用,实例重写通过委托的方式实现引擎类的使用
    (2)继承重写不仅提供了钩子方法,还提供了虚方法、抽象成员供用户重写,实例重写则只提供了钩子方法。

    实例重写的优势主要在于用户类与引擎类的关联性较弱,用户类只与引擎类实例的钩子方法耦合,不会与整个引擎类耦合。
    继承重写的优势主要在于父类和子类代码共享,提高代码的重用性。

    什么时候用继承重写
    当用户类与引擎类同属于一个概念,引擎类是精心设计用于被继承的类时,应该用继承重写。

    什么时候用实例重写
    当用户类需要插入自己的逻辑到引擎类中而又不想与引擎类紧密耦合时,应该用实例重写。

    本文选用的方式
    因为引擎Main和Director是从炸弹人Main、Game中提出来的,不是设计为可被继承的类,所以引擎Main、Director采用实例重写的方式,
    (它们的使用方式会在第二次迭代中修改)

    引擎Layer和Sprite是从炸弹人Layer、Sprite中提出来的,都是抽象基类,本身就是设计为被继承的类,所以引擎Layer和Sprite采用继承重写的方式。

    其它引擎类不能被重写,而是提供API,供引擎类或用户类调用。
    (第二次迭代会将引擎Scene改为继承重写的方式)

    修改namespace方法,提出Tool

    修改namespace方法

    引擎使用命名空间来组织,引擎的顶级命名空间为YE。
    在炸弹人开发中,我使用工具库YTool的namespace方法来定义命名空间。
    分析YTool的namespace方法:

    var YToolConfig = {
        topNamespace: "YYC",    //指定了顶级命名空间为YYC
        toolNamespace: "Tool"
    };
    ...
    
    namespace: function (str) {
        var parent = window[YToolConfig.topNamespace], 
        parts = str.split('.'),
            i = 0,
            len = 0;
    
        if (str.length == 0) {
            throw new Error("命名空间不能为空");
        }
        if (parts[0] === YToolConfig.topNamespace) {
            parts = parts.slice(1);
        }
    
        for (i = 0, len = parts.length; i < len; i++) {
            if (typeof parent[parts[i]] === "undefined") {
                parent[parts[i]] = {};
            }
            parent = parent[parts[i]];
        }
    
        return parent;
    },

    该方法指定了顶级命名空间为YYC,不能修改,这显然不符合引擎的“顶级命名空间为YE”的需求。
    因此将其修改为不指定顶级命名空间,并设为全局方法:

    (function(){
        var extend = function (destination, source) {
            var property = "";
    
            for (property in source) {
                destination[property] = source[property];
            }
            return destination;
        };
    
        (function () {
    
            /**
             * 创建命名空间。
             示例:
             namespace("YE.Collection");
             */
            var global = {
                namespace: function (str) {
                    var parent = window,
                        parts = str.split('.'),
                        i = 0,
                        len = 0;
    
                    if (str.length == 0) {
                        throw new Error("命名空间不能为空");
                    }
    
                    for (i = 0, len = parts.length; i < len; i++) {
                        if (typeof parent[parts[i]] === "undefined") {
                            parent[parts[i]] = {};
                        }
                        parent = parent[parts[i]];  //递归增加命名空间
                    }
    
                    return parent;
                }
            };
    
            extend(window, global);
        }());
    }());

    提出Tool类

    不应该直接修改YTool的namespace方法,而应该将修改后的方法提取到引擎中,因为:
    (1)导致引擎依赖工具库YTool
    YTool中的很多方法引擎都使用不到,如果将修改后的namespace方法放到YTool中,在使用引擎时就必须引入YTool。
    这样做会增加引擎的不稳定性,增加整个引擎文件的大小,违反引擎设计原则“尽量减少引擎依赖的外部文件”。
    (2)导致大量关联代码修改
    我的很多代码都使用了YTool,如果修改了YTool的namespace方法,那么使用了YTool的namespace方法的相关代码可能都需要进行修改。

    所以,引擎增加Tool类,负责放置引擎内部使用的通用方法,将修改后的namespace方法放在Tool类中,从而将引擎的依赖YTool改为依赖自己的Tool。
    同理,在后面的提炼引擎类时,将引擎类依赖的YTool的方法也全部转移到Tool类中。
    引擎Tool的命名空间为YE.Tool。
    因为引擎Tool类仅供引擎内部使用,所以炸弹人仍然依赖YTool,而不依赖引擎Tool类。

    领域模型

    112128140094693.jpg

    提出ImgLoader

    按照从左到右的提炼顺序,首先要提炼引擎初步领域模型中的LoaderResource。

    提炼引擎类

    领域类LoaderResource负责加载各种资源,对应炸弹人PreLoadImg类,该类本身就是一个独立的图片预加载组件(参考发布我的图片预加载控件YPreLoadImg v1.0),可直接提炼到引擎中。

    我将其重命名为ImgLoader,加入到命名空间YE中,代码如下:
    引擎ImgLoader

    namespace("YE").ImgLoader = YYC.Class({
        Init: function (images, onstep, onload) {
            this._checkImages(images);
    
            this.config = {
                images: images || [],
                onstep: onstep || function () {
                },
                onload: onload || function () {
                }
            };
            this._imgs = {};
            this.imgCount = this.config.images.length;
            this.currentLoad = 0;
            this.timerID = 0;
    
            this.loadImg();
        },
        Private: {
            _imgs: {},
    
            _checkImages: function (images) {
                var i = null;
    
                for (var i in images) {
                    if (images.hasOwnProperty(i)) {
                        if (images[i].id === undefined || images[i].url === undefined) {
                            throw new Error("应该包含id和url属性");
                        }
                    }
                }
            }
        },
        Public: {
            imgCount: 0,
            currentLoad: 0,
            timerID: 0,
    
            get: function (id) {
                return this._imgs[id];
            },
            loadImg: function () {
                var c = this.config,
                    img = null,
                    i,
                    self = this,
                    image = null;
    
                for (i = 0; i < c.images.length; i++) {
                    img = c.images[i];
                    image = this._imgs[img.id] = new Image();
                    
                    image.onload = function () {
                        this.onload = null;   
                        YYC.Tool.func.bind(self, self.onload)();
                    };
                    image.src = img.url;
    
                    this.timerID = (function (i) {
                        return setTimeout(function () {
                            if (i == self.currentLoad) {
                                image.src = img.url;
                            }
                        }, 500);
                    })(i);
                }
            },
            onload: function (i) {
                clearTimeout(this.timerID);
                this.currentLoad++;
                this.config.onstep(this.currentLoad, this.imgCount);
                if (this.currentLoad === this.imgCount) {
                    this.config.onload(this.currentLoad);
                }
            },
            dispose: function () {
                var i, _imgs = this._imgs;
                for (i in _imgs) {
                    _imgs[i].onload = null;
                    _imgs[i] = null;
                }
                this.config = null;
            }
        }
    });

    炸弹人使用提炼的引擎类

    对应修改炸弹人Main,改为使用引擎ImgLoader加载图片:
    炸弹人Main修改前

            init: function () {
                window.imgLoader = new YYC.Control.PreLoadImg(…);
            },

    炸弹人Main修改后

            init: function () {
                window.imgLoader = new YE.ImgLoader(...);
            },

    领域模型

    112127287753875.jpg

    提出Main

    接着就是提炼依赖LoadResource的Main。

    提炼引擎类

    领域类Main负责启动游戏,对应炸弹人Main。
    先来看下相关代码:
    炸弹人Main

    var main = (function () {
        var _getImg = function () {
            var urls = [];
            var i = 0, len = 0;
    
            var map = [
                { id: "ground", url: getImages("ground") },
                { id: "wall", url: getImages("wall") }
            ];
            var player = [
                { id: "player", url: getImages("player") }
            ];
            var enemy = [
                { id: "enemy", url: getImages("enemy") }
            ];
            var bomb = [
                { id: "bomb", url: getImages("bomb") },
                { id: "explode", url: getImages("explode") },
                { id: "fire", url: getImages("fire") }
            ];
    
            _addImg(urls, map, player, enemy, bomb);
    
            return urls;
        };
        var _addImg = function (urls, imgs) {
            var args = Array.prototype.slice.call(arguments, 1),
                i = 0,
                j = 0,
                len1 = 0,
                len2 = 0;
    
            for (i = 0, len1 = args.length; i < len1; i++) {
                for (j = 0, len2 = args[i].length; j < len2; j++) {
                    urls.push({ id: args[i][j].id, url: args[i][j].url });
                }
            }
        };
    
        var _hideBar = function () {
            $("#progressBar").css("display", "none");
        };
    
        return {
            init: function () {
                //使用引擎ImgLoader加载图片
                window.imgLoader = new YE.ImgLoader(_getImg(), function (currentLoad, imgCount) {
                    $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));     //调用进度条插件
                }, YYC.Tool.func.bind(this, this.onload));
            },
            onload: function () {
                _hideBar();
    
                var game = new Game();
                game.init();
                game.start();
            },
        };
    
        window.main = main;
    }());

    炸弹人Main负责以下的逻辑:
    (1)定义要加载的图片数据
    (2)创建ImgLoader实例,加载图片
    (3)完成图片加载后,启动游戏
    (4)提供入口方法,由页面调用

    可以将第4个逻辑提到引擎Main中,由引擎搭建一个框架,炸弹人Main负责填充具体的业务逻辑。

    引擎Main:

    (function () {
        var _instance = null;
    
        namespace("YE").Main = YYC.Class({
            Init: function () {
            },
            Public: {
                init: function () {
                    this. loadResource ();
                },
                    //* 钩子
    
                    loadResource: function () {
                    }
            },
            Static: {
                getInstance: function () {
                    if (instance === null) {
                        _instance = new this();
                    }
                    return _instance;
                }
            }
        });
    }());

    分析引擎Main

    • 使用方式为实例重写

    提供了loadResource钩子方法供用户重写。

    • 单例

    因为游戏只有一个入口类,因此引擎Main为单例类。

    • 框架设计

    页面调用引擎Main的init方法进入游戏,init方法调用钩子方法loadResource,该钩子方法由炸弹人Main重写,从而实现在引擎框架中插入用户逻辑。

    炸弹人使用提炼的引擎类

    炸弹人Main通过重写引擎Main的loadResource钩子方法来插入用户逻辑。
    炸弹人Main

    (function () {
        var main = YE.Main.getInstance();
    
    
        var _getImg = function () {
            ...
        };
    
        var _addImg = function (urls, imgs) {
            ...
        };
    
        var _hideBar = function () {
            ...
        };
    
        var _onload = function(){
            …
        };
    
        //重写引擎Main的loadResource钩子
        main.loadResource =function () {
            window.imgLoader = new YE.ImgLoader(_getImg(), function (currentLoad, imgCount) {
                $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));
            }, YYC.Tool.func.bind(this,_onload));
        }
    }());

    修改页面,调用引擎Main的init方法进入游戏:
    页面修改前

    <script type="text/javascript">
    (function () {
        //调用炸弹人Main的init方法
            main.init();
        })();
    </script>

    页面修改后

    <script type="text/javascript">
        (function () {
            YE.Main.getInstance().init();
        })();
    </script>

    重构引擎

    引擎Main应该负责加载图片,对用户隐藏引擎ImgLoader

    炸弹人应该只负责定义要加载的图片和在加载图片过程中要插入的用户逻辑,不需要知道如何加载图片。这个工作应该交给引擎Main,由它封装引擎ImgLoader,向用户提供操作图片的方法和在加载图片的过程中插入用户逻辑的钩子方法。

    1、重构引擎ImgLoader

    由于引擎ImgLoader设计僵化,需要先进行重构。
    现在来看下引擎ImgLoader的构造函数

    Init: function (images, onstep, onload) {
            this._checkImages(images);
    
            this.config = {
                images: images || [],
                onstep: onstep || function () {
                },
                onload: onload || function () {
                }
            };
            this._imgs = {};
            this.imgCount = this.config.images.length;
            this.currentLoad = 0;
            this.timerID = 0;
    
            this.loadImg();
        },

    “设置加载图片的回调函数”和“加载图片”的逻辑和ImgLoader构造函数绑定在了一起,创建ImgLoader实例时会执行这两项任务。
    需要将其从构造函数中分离出来,由用户自己决定何时执行这两个任务。
    因此进行下面的重构:
    (1)将回调函数onstep重命名为onloading,将onload、onloading从构造函数中提出,作为钩子方法。
    (2)将图片数据images的设置和检查提取到新增的load方法中。
    (3)提出done方法,负责调用_loadImg方法加载图片。

    引擎ImgLoader修改后

    namespace("YE").ImgLoader = YYC.Class({
            Init: function () {
            },
            Private: {
                _images: [],
                _imgs: {},
    
                //修改了原来的_checkImages方法,现在传入的图片数据可以为单个数据,也可为数组形式的多个数据
                _checkImages: function (images) {
                    var i = 0,
                        len = 0;
    
                    if (YYC.Tool.judge.isArray(images)) {
                        for (len = images.length; i < len; i++) {
                            if (images[i].id === undefined || images[i].url === undefined) {
                                throw new Error("应该包含id和url属性");
                            }
                        }
                    }
                    else {
                        if (images.id === undefined || images.url === undefined) {
                            throw new Error("应该包含id和url属性");
                        }
                    }
                },
                //将onload改为私有方法
                _onload: function (i) {
                    …
    
                    //调用钩子
    
                    this.onloading(this.currentLoad, this.imgCount);
    
                    if (this.currentLoad === this.imgCount) {
                        this.onload(this.imgCount);
                    }
                },
                //改为私有方法
                _loadImg: function () {
                    …
                }
            }
        },
        Public: {
    …
    
            done: function () {
                this._loadImg();
            },
            //负责检查和保存图片数据
            load: function (images) {
                this._checkImages(images);
        
                if (YYC.Tool.judge.isArray(images)) {
                    this._images = this._images.concat(images);
                }
                else {
                    this._images.push(images);
                }
                this.imgCount = this._images.length;
            },
    …
    
            //*钩子
        
            onloading: function (currentLoad, imgCount) {
            },
            onload: function (imgCount) {
            }
        }
    });

    2、重构引擎Main

    现在回到引擎Main的重构,通过下面的重构来实现封装引擎ImgLoader,向用户提供钩子方法和操作图片的方法:
    (1)构造函数中创建ImgLoader实例
    (2)init方法中调用ImgLoader的done方法加载图片
    (3)提供getImg和load方法来操作图片数据
    (4)增加onload、onloading钩子,将其与ImgLoader的onload、onloading钩子绑定到一起。

    绑定钩子的目的是为了让炸弹人Main只需要知道引擎Main的钩子,从而达到引擎Main封装引擎ImgLoader的目的。
    这个方案并不是很好,在第二次迭代中会修改。

    引擎Main修改后

    (function () {
        var _instance = null;
    
        namespace("YE").Main = YYC.Class({
            Init: function () {
                this._imgLoader = new YE.ImgLoader();
            },
            Private: {
                _imgLoader: null,
    
                _prepare: function () {
                    this.loadResource();
    
                    this._imgLoader.onloading = this.onloading;
                    this._imgLoader.onload = this.onload;
                }
            },
            Public: {
                init: function () {
                    this._prepare();
                    this._imgLoader.done();
                },
                getImg: function (id) {
                    return this._imgLoader.get(id);
                },
                load: function (images) {
                    this._imgLoader.load(images);
                },
    
                //* 钩子
    
                loadResource: function () {
                },
                onload: function () {
                },
                onloading: function (currentLoad, imgCount) {
                }
            },
    …
        });
    }());

    3、修改炸弹人Main

    炸弹人Main在重写的引擎Main的loadResource方法中重写引擎Main的onload、onloading钩子方法,这相当于重写了imgLoader的onload、onloading钩子方法,从而在加载图片的过程中插入用户逻辑。

    炸弹人Main

    (function () {
       …
    
        main.loadResource = function () {
            this.load(_getImg());
        };
        main.onloading = function (currentLoad, imgCount) {
            $("#progressBar_img_show").progressBar(parseInt(currentLoad * 100 / imgCount, 10));
        };
        main.onload = function () {
            _hideBar();
    
            var game = new Game();
            game.init();
            game.start();
        };
    }());

    将依赖的YTool方法移到Tool

    修改后的引擎ImgLoader需要调用YTool的isArray方法,将其移到引擎Tool中。
    引擎Tool

        namespace("YE.Tool").judge = {
            isArray: function (val) {
                return Object.prototype.toString.call(val) === "[object Array]";
            }
        };

    对应修改ImgLoader,将YYC.Tool调用改为YE.Tool
    ...
    if (YE.Tool.judge.isArray(images)) {

    }

    领域模型

    112127468211988.jpg

    提出Director

    继续往右提炼Director。

    提炼引擎类

    领域类Director负责游戏的统一调度,对应炸弹人的Game类
    炸弹人Game

     (function () {
        //初始化游戏全局状态
        window.gameState = window.bomberConfig.game.state.NORMAL;
    
        var Game = YYC.Class({
            Init: function () {
                window.subject = new YYC.Pattern.Subject();
            },
            Private: {
                _createLayerManager: function () {
                    this.layerManager = new LayerManager();
                    this.layerManager.addLayer("mapLayer", layerFactory.createMap());
                    this.layerManager.addLayer("enemyLayer", layerFactory.createEnemy(this.sleep));
                    this.layerManager.addLayer("playerLayer", layerFactory.createPlayer(this.sleep));
                    this.layerManager.addLayer("bombLayer", layerFactory.createBomb());
                    this.layerManager.addLayer("fireLayer", layerFactory.createFire());
                },
                _addElements: function () {
                    var mapLayerElements = this._createMapLayerElement(),
                        playerLayerElements = this._createPlayerLayerElement(),
                        enemyLayerElements = this._createEnemyLayerElement();
    
                    this.layerManager.addSprites("mapLayer", mapLayerElements);
                    this.layerManager.addSprites("playerLayer", playerLayerElements);
                    this.layerManager.addSprites("enemyLayer", enemyLayerElements);
                },
                _createMapLayerElement: function () {
                    var i = 0,
                        j = 0,
                        x = 0,
                        y = 0,
                        row = bomberConfig.map.ROW,
                        col = bomberConfig.map.COL,
                        element = [],
                        mapData = mapDataOperate.getMapData(),
                        img = null;
    
                    for (i = 0; i < row; i++) {
                        y = i * bomberConfig.HEIGHT;
    
                        for (j = 0; j < col; j++) {
                            x = j * bomberConfig.WIDTH;
                            img = this._getMapImg(i, j, mapData);
                            element.push(spriteFactory.createMapElement({ x: x, y: y }, bitmapFactory.createBitmap({ img: img, width: bomberConfig.WIDTH, height: bomberConfig.HEIGHT })));
                        }
                    }
    
                    return element;
                },
                _getMapImg: function (i, j, mapData) {
                    var img = null;
    
                    switch (mapData[i][j]) {
                        case 1:
                            img = YE.Main.getInstance().getImg("ground");
                            break;
                        case 2:
                            img = YE.Main.getInstance().getImg("wall");
                            break;
                        default:
                            break
                    }
    
                    return img;
                },
                _createPlayerLayerElement: function () {
                    var element = [],
                        player = spriteFactory.createPlayer();
    
                    player.init();
                    element.push(player);
    
                    return element;
                },
                _createEnemyLayerElement: function () {
                    var element = [],
                        enemy = spriteFactory.createEnemy(),
                        enemy2 = spriteFactory.createEnemy2();
    
                    enemy.init();
                    enemy2.init();
                    element.push(enemy);
                    element.push(enemy2);
    
                    return element;
                },
                _initLayer: function () {
                    this.layerManager.initLayer();
                },
                _initEvent: function () {
                    //监听整个document的keydown,keyup事件
                    keyEventManager.addKeyDown();
                    keyEventManager.addKeyUp();
                },
                _judgeGameState: function () {
                    switch (window.gameState) {
                        case window.bomberConfig.game.state.NORMAL:
                            break;
                        case window.bomberConfig.game.state.OVER:
                            this.gameOver();
                            return "over";
                            break;
                        case window.bomberConfig.game.state.WIN:
                            this.gameWin();
                            return "over";
                            break;
                        default:
                            throw new Error("未知的游戏状态");
                    }
                    return false;
                }
            },
            Public: {
                sleep: 0,
                layerManager: null,
                mainLoop: null,
    
                init: function () {
                    this.sleep = Math.floor(1000 / bomberConfig.FPS);
                    this._createLayerManager();
                    this._addElements();
                    this._initLayer();
                    this._initEvent();
    
                    window.subject.subscribe(this.layerManager.getLayer("mapLayer"), this.layerManager.getLayer("mapLayer").changeSpriteImg);
                },
                start: function () {
                    var self = this;
    
                    this.mainLoop = window.setInterval(function () {
                        self.run();
                    }, this.sleep);
                },
                run: function () {
                    if (this._judgeGameState() === "over") {
                        return;
                    }
    
                    this.layerManager.run();
                    this.layerManager.change();
                },
                gameOver: function () {
                    YYC.Tool.asyn.clearAllTimer(this.mainLoop);
                    alert("Game Over!");
                },
                gameWin: function () {
                    YYC.Tool.asyn.clearAllTimer(this.mainLoop);
                    alert("You Win!");
                }
            }
        });
    
        window.Game = Game;
    }());

    炸弹人Game负责游戏的统一调度,包括以下的逻辑:
    (1)初始化场景
    (2)调度layerManager
    (3)控制主循环
    (4)计算帧率fps
    (5)管理游戏状态

    其中控制主循环、调度layerManager、计算fps的逻辑可以提取到引擎Director中:
    引擎Director

    (function () {
        var _instance = null;
        var GameStatus = {
            NORMAL: 0,
            STOP: 1
    };
    var STARTING_FPS = 60;
    
        namespace("YE").Director = YYC.Class({
            Private: {
                _startTime: 0,
                _lastTime: 0,
    
                _fps: 0,
    
                _layerManager: null,
                //内部游戏状态
                _gameState: null,
    
    
                _getTimeNow: function () {
                    return +new Date();
                },
                _run: function (time) {
                    var self = this;
    
                    this._loopBody(time);
    
                    if (this._gameState === GameStatus.STOP) {
                        return;
                    }
    
                    window.requestNextAnimationFrame(function (time) {
                        self._run(time);
                    });
                },
                _loopBody: function (time) {
                    this._tick(time);
    
                    this.onStartLoop();
    
                    this._layerManager.run();
                    this._layerManager.change();
    
                    this.onEndLoop();
                },
                _tick: function (time) {
                    this._updateFps(time);
                    this.gameTime = this._getTimeNow() - this._startTime;
                    this._lastTime = time;
                },
                _updateFps: function (time) {
                    if (this._lastTime === 0) {
                        this._fps =STARTING_FPS;
                    }
                    else {
                        this._fps = 1000 / (time - this._lastTime);
                    }
                }
            },
            Public: {
                gameTime: null,
    
                start: function () {
                    var self = this;
    
                    this._startTime = this._getTimeNow();
    
                    window.requestNextAnimationFrame(function (time) {
                        self._run(time);
                    });
                },
                setLayerManager: function (layerManager) {
                    this._layerManager = layerManager;
                },
                getFps: function () {
                    return this._fps;
                },
                stop: function () {
                    this._gameState = GameStatus.STOP;
                },
    
    
                //*钩子
    
                init: function () {
                },
                onStartLoop: function () {
                },
                onEndLoop: function () {
                }
    
            },
            Static: {
                getInstance: function () {
                    if (_instance === null) {
                        _instance = new this();
                    }
                    return _instance;
                }
            }
        });
    }()); 

    分析引擎Director

    使用方式为实例重写

    引擎Director提供了init、onStartLoop、onEndLoop钩子方法供用户重写。
    引擎会在加载完图片后调用钩子方法init,用户可以通过重写该钩子,插入初始化游戏的用户逻辑。
    onStartLoop、onEndLoop钩子分别在每次主循环开始和结束时调用,插入用户逻辑:
    引擎Director

                _loopBody: function (time) {
                    this._tick(time);
                    
                    this.onStartLoop();
    
    …
    
                    this.onEndLoop();
                },

    单例

    因为全局只有一个Director,因此为单例。

    主循环

    使用requestAnimationFrame实现主循环

    炸弹人Game中使用setInterval方法,而引擎Director使用requestAnimationFrame方法实现主循环。这是因为可以通过setTimeout和setInterval方法在脚本中实现动画,但是这样效果可能不够流畅,且会占用额外的资源。
    参考《HTML5 Canvas核心技术:图形、动画与游戏开发》中的论述:

    它们有如下的特征:
    1、即使向其传递毫秒为单位的参数,它们也不能达到ms的准确性。这是因为javascript是单线程的,可能会发生阻塞。
    2、没有对调用动画的循环机制进行优化。
    3、没有考虑到绘制动画的最佳时机,只是一味地以某个大致的事件间隔来调用循环。
    其实,使用setInterval或setTimeout来实现主循环,根本错误就在于它们抽象等级不符合要求。我们想让浏览器执行的是一套可以控制各种细节的api,实现如“最优帧速率”、“选择绘制下一帧的最佳时机”等功能。但是如果使用它们的话,这些具体的细节就必须由开发者自己来完成。

    requestAnimationFrame不需要使用者指定循环间隔时间,浏览器会基于当前页面是否可见、CPU的负荷情况等来自行决定最佳的帧速率,从而更合理地使用CPU。
    需要注意的时,不同的浏览器对于requestAnimationFrame、cancelNextRequestAnimationFrame的实现不一样,因此需要定义通用的方法,放到引擎Tool类中。
    引擎Tool

    /**
    * 来自《HTML5 Canvas核心技术:图形、动画与游戏开发》
    */
     window.requestNextAnimationFrame = (function () {
                var originalWebkitRequestAnimationFrame = undefined,
                    wrapper = undefined,
                    callback = undefined,
                    geckoVersion = 0,
                    userAgent = navigator.userAgent,
                    index = 0,
                    self = this;
    
                // Workaround for Chrome 10 bug where Chrome
                // does not pass the time to the animation function
    
                if (window.webkitRequestAnimationFrame) {
                    // Define the wrapper
    
                    wrapper = function (time) {
                        if (time === undefined) {
                            time = +new Date();
                        }
                        self.callback(time);
                    };
    
                    // Make the switch
    
                    originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;
    
                    window.webkitRequestAnimationFrame = function (callback, element) {
                        self.callback = callback;
    
                        // Browser calls the wrapper and wrapper calls the callback
    
                        originalWebkitRequestAnimationFrame(wrapper, element);
                    }
                }
    
                // Workaround for Gecko 2.0, which has a bug in
                // mozRequestAnimationFrame() that restricts animations
                // to 30-40 fps.
    
                if (window.mozRequestAnimationFrame) {
                    // Check the Gecko version. Gecko is used by browsers
                    // other than Firefox. Gecko 2.0 corresponds to
                    // Firefox 4.0.
    
                    index = userAgent.indexOf('rv:');
    
                    if (userAgent.indexOf('Gecko') != -1) {
                        geckoVersion = userAgent.substr(index + 3, 3);
    
                        if (geckoVersion === '2.0') {
                            // Forces the return statement to fall through
                            // to the setTimeout() function.
    
                            window.mozRequestAnimationFrame = undefined;
                        }
                    }
                }
    
                return  window.requestAnimationFrame ||
                    window.webkitRequestAnimationFrame ||
                    window.mozRequestAnimationFrame ||
                    window.oRequestAnimationFrame ||
                    window.msRequestAnimationFrame ||
    
                    function (callback, element) {
                        var start,
                            finish;
    
                        window.setTimeout(function () {
                            start = +new Date();
                            callback(start);
                            finish = +new Date();
    
                            self.timeout = 1000 / 60 - (finish - start);
    
                        }, self.timeout);
                    };
            }());
    
    
    window.cancelNextRequestAnimationFrame = window.cancelRequestAnimationFrame
    || window.webkitCancelAnimationFrame
    || window.webkitCancelRequestAnimationFrame
    || window.mozCancelRequestAnimationFrame
    || window.oCancelRequestAnimationFrame
    || window.msCancelRequestAnimationFrame
    || clearTimeout;
    控制主循环

    主循环的逻辑封装在_run方法中。
    start方法负责启动主循环。

    退出主循环的机制
    为了能够退出主循环,增加内部游戏状态_gameState。用户可调用引擎Director的stop方法来设置内部游戏状态为STOP,然后Director会在主循环中的_run方法中判断内部游戏状态,如果为STOP状态,则退出主循环。

    引擎Director

                _run: function (time) {
                    var self = this;
    
                    this._loopBody(time);
    
                    if (this._gameState === GameStatus.STOP) {
                        //退出主循环
                        return; 
                    }
    
                    window.requestNextAnimationFrame(function (time) {
                        self._run(time);
                    });
                },
    …
                stop: function () {
                    this._gameState = GameStatus.STOP;
                },

    这里有同学可能会问为什么stop方法不直接调用cancelNextRequestAnimationFrame方法来结束主循环?
    参考代码如下所示:

    引擎Director
                _run: function (time) {
                    var self = this;
    
                    this._loopBody(time);
                    
                    //删除游戏状态的判断
    
                    this._loopId = window.requestNextAnimationFrame(function (time) {
                        self._run(time);
                    });
                },
    …
                stop: function () {
                    //直接在stop方法中结束主循环
                    window.cancelNextRequestAnimationFrame(this._loopId);
                }

    这是因为:
    如果用户是在引擎的钩子中调用stop方法,由于引擎的钩子方法都是在主循环中调用的(_loopBody方法中调用),所以不能结束主循环!

                //该方法包含了主循环逻辑,所有的钩子方法都是在该方法中调用 
                _loopBody: function (time) {
                    this._tick(time);
    
                    this._scene.onStartLoop();
                    this._scene.run();
                    this._scene.onEndLoop();
                },

    只有当用户在引擎主循环外部调用stop方法时,才可以结束主循环。
    详见《深入理解requestAnimationFrame》中的“为什么在callback内部执行cancelAnimationFrame不能取消动画”

    调度layerManager

    目前LayerManager为炸弹人类,用户通过调用引擎Director的setLayerManager方法将其注入到引擎Director中。

    领域模型
    112126203684109.jpg

    引擎Director在主循环中调用layerManager实例的run和change方法,执行炸弹人LayerManager的主循环逻辑。

    • 为什么要由用户注入LayerManager实例,而不是直接在引擎Director中创建LayerManager实例?

    (1)根据引擎设计原则“引擎不应该依赖用户,用户应该依赖引擎”,LayerManager为用户类,引擎不应该依赖用户。

    (2)这样会降低引擎Director的通用性
    引擎Director应该操作抽象角色,而不应该直接操作具体的“层管理”类,这样会导致具体的“层管理”类变化时,引擎Director也会受到影响。

    因此,此处采用“由用户注入”的设计更加合理。

    • 为什么由引擎Director调用炸弹人LayerManager的change方法?

    LayerManager的change方法负责调用每个层的change方法,设置画布的状态(主循环中会判断画布状态,决定是否更新画布):
    炸弹人LayerManager

                change: function () {
                    this.__iterator("change");
                }

    change方法的调用有两个选择:
    (1)由用户调用
    用户可在重写引擎Director提供的钩子方法中(如onEndLoop),调用炸弹人LayerManager的change方法
    (2)由引擎调用
    引擎Director主循环在调用layerManager的run方法后调用layerManager的change方法。

    因为:
    (1)设置画布状态的逻辑属于通用逻辑
    (2)引擎对什么时候设置画布状态有最多的知识

    所以应该由引擎Director调用。

    计算帧率fps

    引擎Director的_updateFps方法负责根据上一次主循环执行时间计算fps:

                //time为当前主循环的开始时间(从1970年1月1日到当前所经过的毫秒数)
                //lastTime为上一次主循环的开始时间
                _updateFps: function (time) {
                    if (this._lastTime === 0) {
                        this._fps = STARTING_FPS;
                    }
                    else {
                        this._fps = 1000 / (time - this._lastTime);
                    }
                }

    其中引擎Director的STARTING_FPS定义了初始的fps,“time-this._lastTime”计算的是上次主循环的执行时间。
    如果为第一次主循环,lastTime为0,fps为初始值;
    否则,fps为上次主循环执行时间的倒数。

    炸弹人使用提炼的引擎类

    修改炸弹人Game

    炸弹人Game改为只负责初始化场景和管理游戏状态,其它逻辑委托引擎实现。

    炸弹人Game

    (function () {
        //获得引擎Director实例,从而可实例重写。
        var director = YE.Director.getInstance();
    
        var Game = YYC.Class({
            Init: function () {
            },
            Private: {
                    ...
                _gameOver: function () {
                    director.stop();    //结束主循环
                    alert("Game Over!");
                },
                _gameWin: function () {
                    director.stop();    //结束主循环
                    alert("You Win!");
                }
            },
            Public: {
    …
    
                init: function () {
                    //初始化游戏全局状态
                    window.gameState = window.bomberConfig.game.state.NORMAL;
    
                    window.subject = new YYC.Pattern.Subject();
    
                //调用引擎Director的getFps方法获得fps
                    this.sleep = 1000 / director.getFps();
    …
                },
                judgeGameState: function () {
    …
                }
            }
        });
    
    
        var game = new Game();
    
        //重写引擎Director的init钩子
        director.init = function () {
            game.init();
    
            //设置场景
            this.setLayerManager(game.layerManager);
        };
    
        //重写引擎Director的onStartLoop钩子
        director.onStartLoop = function () {
            game.judgeGameState();
        };
    }()); 

    重构炸弹人Game

    • 移动逻辑

    将Game中属于“初始化场景”职责的“初始化游戏全局状态”和“创建Subject实例”逻辑提到Game的 init方法中。

    • 将gameOver、gameWin设为私有方法,judgeGameState设为公有方法

    因为只有Game调用这两个方法,因此将其设为私有方法。
    而judgeGameState方法被director的钩子方法调用,因此将其设为公有方法。

    炸弹人Game实例重写引擎Director

    • 重写引擎Director的init钩子

    在init钩子中,炸弹人插入了Game的初始化场景的逻辑,注入了Game创建的layerManager实例。

    • 删除start和run方法

    这部分职责已经移到引擎Director中了,所以Game删除start和run方法,由引擎负责控制主循环。

    • 重写了Director的onStartLoop钩子,实现了炸弹人游戏的结束机制

    修改了Game的gameOver、gameWin方法,改为调用director.stop方法来结束主循环。
    将Game的run方法的“关于全局游戏状态判断”的逻辑移到Director的onStartLoop钩子中,引擎会在每次主循环开始时判断一次全局游戏状态,决定是否调用Game的gameOver或gameWin方法结束游戏。

    修改炸弹人Main

    为了能通过游戏的运行测试,先修改炸弹人Main重写引擎Main的onload钩子,改为调用引擎Director的init和start方法来执行游戏初始化并启动主循环。
    炸弹人Main修改前

        main.onload = function () {
    …
            var game = new Game();
            game.init();
            game.start();
        };

    炸弹人Main修改后

        main.onload = function () {
    …
                  var director = YE.Director.getInstance();
                    director.init();
                    director.start();
        };

    重构引擎

    • 将炸弹人Main的“执行游戏初始化并启动主循环”的逻辑移到引擎Main中

    因为:
    (1)“执行游戏初始化”的逻辑具体是调用Director的钩子方法init,而钩子方法应该由引擎调用。
    (2)“执行游戏初始化”和“启动主循环”的逻辑应该由入口类负责,也就是说可以由引擎Main或炸弹人Main负责。因为该逻辑与引擎更相关,并且考虑到引擎设计原则“尽量减少用户负担”,所以应该由引擎Main负责。

    所以应该由引擎Main负责该逻辑。

    因此修改引擎ImgLoader,增加onload_game钩子;然后在引擎Main中重写ImgLoader的onload_game钩子,实现“执行游戏初始化并启动主循环”的逻辑;最后修改炸弹人Main重写引擎Main的onload钩子,不再调用引擎Director的init和start方法。

    为什么引擎ImgLoader要增加onload_game钩子?
    因为现在引擎ImgLoader的钩子是供炸弹人Main重写的,引擎Main无法重写引擎ImgLoader的钩子来执行“执行游戏初始化并启动主循环”逻辑,所以引擎ImgLoader增加内部钩子onload_game,供引擎Main重写,而炸弹人Main则负责在重写的引擎ImgLoader的onload钩子中实现“加载图片完成到执行游戏初始化并启动主循环”之间的用户逻辑。

    相关代码
    引擎ImgLoader

            _onload: function (i) {
    ...
    
                if (this.currentLoad === this.imgCount) {
                    //图片加载完成后调用onload和onload_game钩子
                    this.onload(this.imgCount);
                    this.onload_game();
                }
            },
    ...
    
            //*内部钩子
    
            onload_game: function () {
            },
    ...
        }

    引擎Main

    _prepare: function () {
        this.loadResource();
    
        this._imgLoader.onloading = this.onloading;
        this._imgLoader.onload = this.onload;
    
        this._imgLoader.onload_game = function () {
            var director = YE.Director.getInstance();
    
            director.init();
            director.start();
        }
    }

    炸弹人Main

                main.onload = function () {
                    //隐藏资源加载进度条
                    _hideBar();
                };

    待重构点

    引擎ImgLoader的onload钩子和onload_game钩子重复了,两者都是在加载图片完成后调用。
    提出onload_game钩子只是一个临时的解决方案,在第二次迭代中会删除它。

    领域模型

    112127124465319.jpg

    提出Scene和Hash

    现在应该提出Scene领域类,使引擎Director依赖引擎Scene,而不是依赖炸弹人LayerManager。
    由于Scene继承于Hash,因此将Hash也一起提出。

    提炼引擎类

    领域类Scene负责管理场景,对应炸弹人LayerManager;领域类Hash为哈希结构的集合类,对应炸弹人Hash。
    炸弹人LayerManager是一个容器类,负责层的管理,属于通用类,可直接提取到引擎中,重命名为Scene。
    炸弹人Hash是一个独立的抽象类,可直接提取到引擎中

    引擎Hash

    (function () {
        namespace("YE").Hash = YYC.AClass({
            Private: {
                //容器
                _childs: {}
            },
            Public: {
                getChilds: function () {
                    return this._childs;
                },
                getValue: function (key) {
                    return this._childs[key];
                },
                add: function (key, value) {
                    this._childs[key] = value;
                    return this;
                }
            }
        });
    }());

    引擎Scene

    (function () {
        namespace("YE").Scene = YYC.Class(YE.Hash, {
            Private: {
                __iterator: function (handler, args) {
                    var args = Array.prototype.slice.call(arguments, 1),
                        i = null,
                        layers = this.getChilds();
    
                    for (i in layers) {
                        if (layers.hasOwnProperty(i)) {
                            layers[i][handler].apply(layers[i], args);
                        }
                    }
                },
                __getLayers: function () {
                    return this.getChilds();
                }
            },
            Public: {
                addLayer: function (name, layer) {
                    this.add(name, layer);
                    return this;
                },
                getLayer: function (name) {
                    return this.getValue(name);
                },
                addSprites: function (name, elements) {
                    this.getLayer(name).appendChilds(elements);
                },
                initLayer: function () {
                    this.__iterator("setCanvas");
                    this.__iterator("init", this.__getLayers());
                },
                run: function () {
                    this.__iterator("run");
                },
                change: function () {
                    this.__iterator("change");
                }
            }
        });
    }());

    炸弹人使用提炼的引擎类

    重构炸弹人Game,改为依赖引擎Scene

    因为炸弹人LayerManager重构为引擎Scene了,因此炸弹人Game也要对应修改为依赖引擎Scene。

    领域模型
    112129037285217.jpg

    将Game的layerMangaer属性重命名为scene,并重命名_createLayerManager方法为_createScene,改为创建引擎Scene实例。
    炸弹人Game

     _createScene: function () {
        this.scene = new YE.Scene();
        this.scene.addLayer("mapLayer", layerFactory.createMap());
        this.scene.addLayer("enemyLayer", layerFactory.createEnemy(this.sleep));
        this.scene.addLayer("playerLayer", layerFactory.createPlayer(this.sleep));
        this.scene.addLayer("bombLayer", layerFactory.createBomb());
        this.scene.addLayer("fireLayer", layerFactory.createFire());
    },
    _addElements: function () {
    …
        this.scene.addSprites("mapLayer", mapLayerElements);
        this.scene.addSprites("playerLayer", playerLayerElements);
        this.scene.addSprites("enemyLayer", enemyLayerElements);
    },
    …
    _initLayer: function () {
        this.scene.initLayer();
    },
    …
    init: function () {
    …
        this._createScene(); 
    …
    }

    重构引擎

    因为引擎Director依赖引擎Scene了,所以应该将_layerManager属性重命名为scene,将setLayerManager方法重命名为setScene。
    引擎Director

                _scene: null,
    
    …
                _loopBody: function (time) {
    …
    
                    this._scene.run();
                    this._scene.change();
    …
                },
    …
                setScene: function (scene) {
                    this._scene = scene;
                },

    对应修改Game,改为调用setScene方法:
    炸弹人Game

    director.init = function () {
    …
    
        //设置场景
        this.setScene(game.scene);
    };

    领域模型

    112127561656108.jpg

    提出Layer和Collection

    现在应该提出Layer领域类,使引擎Scene依赖引擎Layer。
    由于Layer继承于Collection类,因此将Collection也一起提出。

    提炼引擎类

    领域类Layer负责层内精灵的统一管理,对应炸弹人的Layer。
    领域类Collection为线性结构的集合类,对应炸弹人Collection.
    炸弹人Layer是一个抽象类,负责精灵的管理,具有通用性,直接提取到引擎中。
    炸弹人Collection是一个独立的类,可直接提取到引擎中
    引擎Layer

    (function () {
        namespace("YE").Layer = YYC.AClass(Collection, {
            Init: function () {
            },
            Private: {
                __state: bomberConfig.layer.state.CHANGE,
    
                __getContext: function () {
                    this.P_context = this.P_canvas.getContext("2d");
                }
            },
            Protected: {
                P_canvas: null,
                P_context: null,
    
                P_isChange: function () {
                    return this.__state === bomberConfig.layer.state.CHANGE;
                },
                P_isNormal: function () {
                    return this.__state === bomberConfig.layer.state.NORMAL;
                },
                P_iterator: function (handler) {
                    var args = Array.prototype.slice.call(arguments, 1),
                        nextElement = null;
    
                    while (this.hasNext()) {
                        nextElement = this.next();
                        nextElement[handler].apply(nextElement, args);  //要指向nextElement
                    }
                    this.resetCursor();
                },
                P_render: function () {
                    if (this.P_isChange()) {
                        this.clear();
                        this.draw();
                        this.setStateNormal();
                    }
                }
            },
            Public: {
                remove: function (sprite) {
                    this.base(function (e, obj) {
                        if (e.x === obj.x && e.y === obj.y) {
                            return true;
                        }
                        return false;
                    }, sprite);
                },
                setStateNormal: function () {
                    this.__state = bomberConfig.layer.state.NORMAL;
                },
                setStateChange: function () {
                    this.__state = bomberConfig.layer.state.CHANGE;
                },
                Virtual: {
                    init: function () {
                        this.__getContext();
                    },
                    clear: function (sprite) {
                        if (arguments.length === 0) {
                            this.P_iterator("clear", this.P_context);
                        }
                        else if (arguments.length === 1) {
                            sprite.clear(this.P_context);
                        }
                    }
                }
            },
            Abstract: {
                setCanvas: function () {
                },
                change: function () {
                },
                draw: function () {
                },
                //游戏主循环调用的方法
                run: function () {
                }
            }
        });
    }());

    引擎Collecton

    (function () {
        //*使用迭代器模式
    
        var IIterator = YYC.Interface("hasNext", "next", "resetCursor");
    
    
        namespace("YE").Collection = YYC.AClass({Interface: IIterator}, {
            Private: {
                //当前游标
                _cursor: 0,
                //容器
                _childs: []
            },
            Public: {
                getChilds: function () {
                    return YYC.Tool.array.clone(this._childs);
                },
                getChildAt: function (index) {
                    return this._childs[index];
                },
                appendChild: function (child) {
                    this._childs.push(child);
    
                    return this;
                },
                appendChilds: function (childs) {
                    var i = 0,
                        len = 0;
    
                    for (i = 0, len = childs.length; i < len; i++) {
                        this.addChild(childs[i]);
                    }
                },
                removeAll: function () {
                    this._childs = [];
                },
                hasNext: function () {
                    if (this._cursor === this._childs.length) {
                        return false;
                    }
                    else {
                        return true;
                    }
                },
                next: function () {
                    var result = null;
    
                    if (this.hasNext()) {
                        result = this._childs[this._cursor];
                        this._cursor += 1;
                    }
                    else {
                        result = null;
                    }
    
                    return result;
                },
                resetCursor: function () {
                    this._cursor = 0;
                },
                Virtual: {
                    remove: function (func, child) {
                        this._childs.remove(func, child);
                    }
                }
            }
        });
    }());

    分析

    将引擎Collection依赖YTool的clone方法提到引擎Tool中。

    引擎Tool

     namespace("YE.Tool").array = {
            /*返回一个新的数组,元素与array相同(地址不同)*/
            clone: function (array) {
                var new_array = new Array(array.length);
                for (var i = 0, _length = array.length; i < _length; i++) {
                    new_array[i] = array[i];
                }
                return new_array;
            }
        };

    对应修改引擎Collection

              getChilds: function () {
                    return YE.Tool.array.clone(this._childs);
                },

    重构提炼的引擎类

    重构Collection

    引擎Collection重命名appendChild、appendChilds为addChild、addChilds:

    引擎Collection

    addChild: function (child) {
    …
    },
    addChilds: function (childs) {
    …
    },

    重构Layer

    现在引擎Layer依赖炸弹人Config定义的枚举值State:
    引擎Layer

            Private: {
                __state: bomberConfig.layer.state.CHANGE,
            …
            Protected: {
            …
                P_isChange: function () {
                    return this.__state === bomberConfig.layer.state.CHANGE;
                },
                P_isNormal: function () {
                    return this.__state === bomberConfig.layer.state.NORMAL;
                },
            …
    
            Public: {
            …
                setStateNormal: function () {
                    this.__state = bomberConfig.layer.state.NORMAL;
                },
                setStateChange: function () {
                    this.__state = bomberConfig.layer.state.CHANGE;
                },

    因为引擎Layer不应该依赖用户类,因此应该将枚举值State移到引擎类中。又因为State为画布状态,与引擎Layer相关,因此将其提出来直接放到引擎Layer中,解除引擎Layer对炸弹人Config的依赖。

    引擎Layer

        //定义State枚举值
        var State = {
            NORMAL: 0,
            CHANGE: 1
        };
    
        namespace("YE").Layer = YYC.AClass(YE.Collection, {
            Init: function () {
            },
            Private: {
                __state: State.CHANGE, 
    …
            Protected: {
    … 
                P_isChange: function () {
                    return this.__state === State.CHANGE;
                },
                P_isNormal: function () {
                    return this.__state === State.NORMAL;
                },
    …
            Public: {
    …
                setStateNormal: function () {
                    this.__state = State.NORMAL;
                },
                setStateChange: function () {
                    this.__state = State.CHANGE;
                },

    炸弹人使用提炼的引擎类

    炸弹人层类改为继承引擎Layer

    由于引擎Layer的使用方式为继承重写,所以修改炸弹人BombLayer、CharacterLayer、FireLayer、MapLayer、PlayerLayer,继承引擎Layer:

    var BombLayer = YYC.Class(YE.Layer, {
    …
    
    var CharacterLayer = YYC.Class(YE.Layer, {
    …
    
    var FireLayer = YYC.Class(YE.Layer, {
    …
    
    var MapLayer = YYC.Class(YE.Layer, {
    …
    
    var PlayerLayer = YYC.Class(YE.Layer, {

    领域模型

    112127358066621.jpg

    提出Sprite、Config和collision

    现在应该提出Sprite类,使引擎Layer依赖引擎Sprite。

    提炼引擎类

    领域类Sprite为精灵类,对应炸弹人的Sprite。
    炸弹人Sprite作为抽象类,提炼了炸弹人精灵类的共性,具有通用性,因此将其直接提取到引擎中。
    引擎Sprite

    (function () {
        namespace("YE").Sprite = YYC.AClass({
            Init: function (data, bitmap) {
                this.bitmap = bitmap;
    
                if (data) {
                    //初始坐标
                    this.x = data.x;
                    this.y = data.y;
    
                    this.defaultAnimId = data.defaultAnimId;
                    this.anims = data.anims;
                }
            },
            Private: {
                //更新帧动画
                _updateFrame: function (deltaTime) {
                    if (this.currentAnim) {
                        this.currentAnim.update(deltaTime);
                    }
                }
            },
            Public: {
                //bitmap实例
                bitmap: null,
    
                //精灵的坐标
                x: 0,
                y: 0,
    
                //精灵动画集合
                anims: null,
                //默认的动画id
                defaultAnimId: null,
    
                //当前的Animation.
                currentAnim: null,
    
                //设置当前动画
                setAnim: function (animId) {
                    this.currentAnim = this.anims[animId];
                },
                //重置当前帧
                resetCurrentFrame: function (index) {
                    this.currentAnim && this.currentAnim.setCurrentFrame(index);
                },
                //取得精灵的碰撞区域,
                getCollideRect: function () {
                    var obj = {
                        x: this.x,
                        y: this.y,
                        width: this.bitmap.width,
                        height: this.bitmap.height
                    };
    
                    return YE.collision.getCollideRect(obj);
                },
                Virtual: {
                    init: function () {
                        //初始化时显示默认动画
                        this.setAnim(this.defaultAnimId);
                    },
                    // 更新精灵当前状态.
                    update: function (deltaTime) {
                        this._updateFrame(deltaTime);
                    },
                    //获得坐标对应的方格坐标(向下取值)
                    getCellPosition: function (x, y) {
                        return {
                            x: Math.floor(x / YE.Config.WIDTH),
                            y: Math.floor(y / YE.Config.HEIGHT)
                        }
                    },
                    draw: function (context) {
                        context.drawImage(this.bitmap.img, this.x, this.y, this.bitmap.width, this.bitmap.height);
                    },
                    clear: function (context) {
                        //直接清空画布区域
                        context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT);
                    }
                }
            }
        });
    }());

    重构提炼的引擎类

    提出Config

    现在引擎Sprite引用了炸弹人Config类定义的“方格大小”和“画布大小”:
    引擎Sprite

                    getCellPosition: function (x, y) {
                        return {
                            x: Math.floor(x / bomberConfig.Config.WIDTH),
                            y: Math.floor(y / bomberConfig.Config.HEIGHT)
                        }
                    },
    …
                    clear: function (context) {
                        context.clearRect(0, 0, bomberConfig.Config.canvas.WIDTH, bomberConfig.Config.canvas.HEIGHT);
                    }

    有下面几个问题:
    1、引擎Sprite依赖了炸弹人Config,违背了引擎设计原则“不应该依赖用户”。
    2、“方格大小”和“画布大小”与精灵无关,因此不应该像引擎Layer的枚举值State一样放在Sprite中
    因此,引擎提出Config配置类,将“方格大小”和“画布大小”放在其中,使引擎Sprite依赖引擎Config。
    引擎Config

    namespace("YE").Config = {
        //方格宽度
        WIDTH: 30,
        //方格高度
        HEIGHT: 30,
    
        //画布
        canvas: {
            //画布宽度
            WIDTH: 600,
            //画布高度
            HEIGHT: 600
        }

    对应修改引擎Sprite,依赖引擎Config
    引擎Sprite

                    getCellPosition: function (x, y) {
                        return {
                            x: Math.floor(x / YE.Config.WIDTH),
                            y: Math.floor(y / YE.Config.HEIGHT)
                        }
                    },
    …
                    clear: function (context) {
                        context.clearRect(0, 0, YE.Config.canvas.WIDTH, YE.Config.canvas.HEIGHT);
                    }

    待重构点

    引擎Config应该放置与引擎相关的、与用户逻辑无关的配置属性,而“方格大小”和“画布大小”与具体的游戏逻辑相关,属于用户逻辑,不应该放在引擎Config中。
    另外,引擎Sprite访问了“方格大小”和“画布大小”,混入了用户逻辑。因此引擎Sprite还需要进一步提炼和抽象。
    这个重构放到第二次迭代中进行。

    炸弹人和引擎都有Config配置类,两者有什么区别?

    炸弹人Config放置与用户逻辑相关的配置属性,引擎Config放置与引擎相关的配置属性,炸弹人类应该只访问炸弹人的Config类,而引擎类应该只访问引擎Config类。

    提出collision

    引擎Sprite使用了炸弹人collision的getCollideRect方法来获得碰撞区域数据:
    引擎Sprite

                getCollideRect: function () {
    …
                    return YYC.Tool.collision.getCollideRect(obj);
                },

    考虑到炸弹人collision是一个碰撞算法类,具有通用性,因此将其提取到引擎中。
    引擎collision

    namespace("YE").collision = (function () {
        return {
            //获得精灵的碰撞区域,
            getCollideRect: function (obj) {
                return {
                    x1: obj.x,
                    y1: obj.y,
                    x2: obj.x + obj.width,
                    y2: obj.y + obj.height
                }
            },
            //矩形和矩形间的碰撞
            col_Between_Rects: function (obj1, obj2) {
                var rect1 = this.getCollideRect(obj1);
                var rect2 = this.getCollideRect(obj2);
    
                if (rect1 && rect2 && !(rect1.x1 >= rect2.x2 || rect1.y1 >= rect2.y2 || rect1.x2 <= rect2.x1 || rect1.y2 <= rect2.y1)) {
                    return true;
                }
                return false;
            }
        };
    }());

    对应修改引擎Sprite,依赖引擎collision

                getCollideRect: function () {
    …
                    return YE.collision.getCollideRect(obj);
                },

    炸弹人使用提炼的引擎类

    炸弹人精灵类改为继承引擎Sprite

    由于引擎Sprite的使用方式为继承重写,所以修改炸弹人的具体精灵类BombSprite、FireSprite、MapElementSprite、MoveSprite,继承引擎Sprite类

    var BombSprite= YYC.Class(YE.Sprite, {
    …
    
    var FireSprite = YYC.Class(YE.Sprite, {
    …
    
    var MapElementSprite = YYC.Class(YE.Sprite, {
    …
    
    var MoveSprite = YYC.Class(YE.Sprite, {
    …

    炸弹人改为依赖引擎collision

    因为炸弹人collision提取到引擎中了,因此炸弹人改为依赖引擎的collision。
    炸弹人BombSprite

    collideFireWithCharacter: function (sprite) {
    …
        if (YE.collision.col_Between_Rects(fire, obj2)) {
            return true;
        }

    炸弹人EnemySprite

    collideWithPlayer: function (sprite2) {
    …
        if (YE.collision.col_Between_Rects(obj1, obj2)) {
            throw new Error();
        }

    领域模型

    112128052596686.jpg

    提出Factory

    现在提炼Factory类。

    思考

    有两个问题需要思考:
    1、哪些引擎类需要工厂。
    2、用哪种方式实现工厂。

    对于第1个问题,目前我认为抽象类不需要工厂(第二次迭代中抽象类Scene、Layer、Sprite也会加上工厂方法create,使得用户可直接使用这些引擎类),其它非单例的类都统一用工厂创建实例。

    对于第2个问题,有以下两个选择:
    1、与炸弹人代码一样,提出工厂类LayerFactory、SpriteFactory,分别负责创建引擎Layer、Sprite的实例
    2、直接在类中提出create静态方法,负责创建自身的实例

    考虑到工厂只需要负责创建实例,没有复杂的逻辑,因此采用第二个选择,引擎所有的非单例类都提出create静态方法。

    修改引擎类

    目前只有引擎ImgLoader需要增加create方法
    引擎ImgLoader

        Static: {
            create: function(){
                return new this();
            }
        }

    对应修改引擎Main,使用引擎ImgLoader的create方法创建它的实例

                getInstance: function () {
                    if (_instance === null) {
                        _instance = new this();
                        _instance.imgLoader = YE.ImgLoader.create();
                    }
                    return _instance;
                },

    提出Animation

    提炼Animation类,使引擎Sprite依赖引擎Animation。

    提炼引擎类

    领域类Animation负责控制帧动画的播放,对应炸弹人Animation类。
    该类负责帧动画的控制,具有通用性,因此将其提取到引擎中
    引擎Animation

     (function () {
        namespace("YE").Animation = YYC.Class({
            Init: function (config) {
                this._frames = YE.Tool.array.clone(config);
                this._init();
            },
            Private: {
                //帧数据
                _frames: null,
                _frameCount: -1,
                _img: null,
                _currentFrame: null,
                _currentFrameIndex: -1,
                _currentFramePlayed: -1,
    
                _init: function () {
                    this._frameCount = this._frames.length;
    
                    this.setCurrentFrame(0);
                }
            },
            Public: {
                setCurrentFrame: function (index) {
                    this._currentFrameIndex = index;
                    this._currentFrame = this._frames[index];
                    this._currentFramePlayed = 0;
                },
                /**
                 * 更新当前帧
                 * @param deltaTime 主循环的持续时间
                 */
                update: function (deltaTime) {
                    //如果没有duration属性(表示动画只有一帧),则返回(因为不需要更新当前帧)
                    if (this._currentFrame.duration === undefined) {
                        return;
                    }
    
                    //判断当前帧是否播放完成
                    if (this._currentFramePlayed >= this._currentFrame.duration) {
                        //播放下一帧
    
                        if (this._currentFrameIndex >= this._frameCount - 1) {
                            //当前是最后一帧,则播放第0帧
                            this._currentFrameIndex = 0;
                        } else {
                            //播放下一帧
                            this._currentFrameIndex++;
                        }
                        //设置当前帧
                        this.setCurrentFrame(this._currentFrameIndex);
    
                    } else {
                        //增加当前帧的已播放时间.
                        this._currentFramePlayed += deltaTime;
                    }
                },
                getCurrentFrame: function () {
                    return this._currentFrame;
                }
            },
    
            Static: {
                create: function(config){
                    return new this(config);
                }
            }
    
        });
    }());

    炸弹人使用提炼的引擎类

    炸弹人改为创建引擎Animation实例

    修改炸弹人SpriteData,改为创建引擎Animation实例
    炸弹人SpriteData

    anims: {
        "stand_right": YE.Animation.create(getFrames("player", "stand_right")),
    …

    重构引擎

    引擎Animation改为依赖引擎Tool的clone方法
    引擎Animation

            Init: function (config) {
                this._frames = YE.Tool.array.clone(config);
    …
            },

    领域模型

    112126430874282.jpg

    提出AI

    现在提炼AI类。

    提炼引擎类

    领域类AI负责实现人工智能算法,对应炸弹人使用的碰撞算法和寻路算法。碰撞算法已经提炼到引擎中了(提炼为引擎collision),寻路算法对应炸弹人FindPath类,它实现了A*寻路算法,属于通用的算法,应该将其提取到引擎中。
    然而“FindPath”这个名字范围太大了,应该重命名为实际采用的寻路算法的名字,因此将其重命名为AStar。
    引擎AStar

    (function () {
    …
        function aCompute(mapData, begin, end) {
    …
        //8方向寻路
            if (bomberConfig.algorithm.DIRECTION == 8) {
    …
        //4方向寻路
            if (bomberConfig.algorithm.DIRECTION == 4) {
    …
    }
    …
    
    
        namespace("YE").AStar = {
            aCompute: function (terrainData, begin, end) {
    …
                return aCompute(terrainData, begin, end);
            }
        };
    }());

    重构提炼的引擎类

    用户能够设置寻路方向数为4或者为8

    现在引擎AStar直接读取炸弹人Config中配置的寻路方向数algorithm.Director,导致引擎AStar依赖用户类,违反了引擎设计原则。
    因此,引擎AStar增加setDirection方法,由用户调用该方法来设置寻路方向数,并删除炸弹人Config的algorithm属性。

    引擎AStar

    …
            DIRECTION = 4;  //默认为4方向寻路
    …
    if (DIRECTION == 8) {
    …
    if (DIRECTION == 4) {
    …
        namespace("YE").AStar = {
    …
            /**
             * 设置寻路方向
             * @param direction 4或者8
             */
            setDirection: function (direction) {
                DIRECTION = direction;
            }
    }

    炸弹人使用提炼的引擎类

    修改炸弹人EnemySprite,在构造函数中设置寻路的方向数为4,并改为调用引擎AStar的aCompute方法来寻路。
    炸弹人EnemySprite

            Init: function (data, bitmap) {
    …
                YE.AStar.setDirection(4);
    …
            },
            Private: {
                ___findPath: function () {
                    return YE.AStar.aCompute(window.terrainData, this.___computeCurrentCoordinate(), this.___computePlayerCoordinate()).path
                },

    领域模型

    112126544627990.jpg

    提出EventManager和Event

    现在提炼EventManager类。

    提炼引擎类

    领域类EventManager负责事件的监听和移除,与炸弹人KeyCodeMap、KeyState以及KeyEventManager对应。

    炸弹人KeyCodeMap、KeyState以及KeyEventManager都在KeyEventManager.js文件中,先来看下这个文件:
    KeyEventManager.js

     (function () {
        //枚举值
        var keyCodeMap = {
            LEFT: 65, // A键
            RIGHT: 68, // D键
            DOWN: 83, // S键
            UP: 87, // W键
            SPACE: 32   //空格键
        };
        //按键状态
        var keyState = {
        };
    
        keyState[keyCodeMap.LEFT] = false;
        keyState[keyCodeMap.RIGHT] = false;
        keyState[keyCodeMap.UP] = false;
        keyState[keyCodeMap.DOWN] = false;
        keyState[keyCodeMap.SPACE] = false;
    
        //键盘事件管理类
        var KeyEventManager = YYC.Class({
            Private: {
                _keyDown: function () {
                },
                _keyUp: function () {
                }
            },
            Public: {
                addKeyDown: function () {
                    this._keyDown = YYC.Tool.event.bindEvent(this, function (e) {
                        keyState[e.keyCode] = true;
    
                        e.preventDefault();
                    });
    
                    YYC.Tool.event.addEvent(document, "keydown", this._keyDown);
                },
                removeKeyDown: function () {
                    YYC.Tool.event.removeEvent(document, "keydown", this._keyDown);
                },
                addKeyUp: function () {
                    this._keyUp = YYC.Tool.event.bindEvent(this, function (e) {
                        keyState[e.keyCode] = false;
                    });
    
                    YYC.Tool.event.addEvent(document, "keyup", this._keyUp);
                },
                removeKeyUp: function () {
                    YYC.Tool.event.removeEvent(document, "keyup", this._keyUp);
                }
            }
        });
    
        window.keyCodeMap = keyCodeMap;
        window.keyState = keyState;
        window.keyEventManager = new KeyEventManager();
    }());

    提出KeyCodeMap

    KeyCodeMap是键盘按键的枚举值,因为所有浏览器中的键盘按键值都一样,因此具有通用性,可以将其提取到引擎中。

    不提出KeyState

    炸弹人KeyState是存储当前按键状态的容器类,与用户逻辑相关,因此不提取到引擎中。

    从KeyEventManager中提出EventManager

    炸弹人KeyEventManager负责键盘事件的监听和移除,可以从中提出一个通用的、负责所有事件的监听和移除的引擎类EventManager。
    另外,将事件类型(如"keydown"、"keyup")提取为枚举值EventType,从而对用户隔离具体的事件类型的变化。

    提出事件枚举类Event

    引擎增加Event类,放置KeyCodeMap和EventType枚举值。

    引擎EventManager

    (function () {
        var _keyListeners = {};
    
        namespace("YE").EventManager = {
            _getEventType: function (event) {
                var eventType = "",
                    e = YE.Event;
    
                switch (event) {
                    case e.KEY_DOWN:
                        eventType = "keydown";
                        break;
                    case e.KEY_UP:
                        eventType = "keyup";
                        break;
                    case e.KEY_PRESS:
                        eventType = "keypress";
                        break;
                    default:
                        throw new Error("事件类型错误");
                }
    
                return eventType;
            },
            addListener: function (event, handler) {
                var eventType = "";
    
                eventType = this._getEventType(event);
    
                YYC.Tool.event.addEvent(window, eventType, handler);
                this._registerEvent(eventType, handler);
            },
            _registerEvent: function (eventType, handler) {
                if (_keyListeners[eventType] === undefined) {
                    _keyListeners[eventType] = [handler];
                }
                else {
                    _keyListeners[eventType].push(handler);
                }
            },
            removeListener: function (event) {
                var eventType = "";
    
                eventType = this._getEventType(event);
    
                if (_keyListeners[eventType]) {
                    _keyListeners[eventType].forEach(function (e, i) {
                        YYC.Tool.event.removeEvent(window, eventType, e);
                    })
                }
    
            }
        };
    }());

    引擎Event

    namespace("YE").Event = {
        //事件枚举值
        KEY_DOWN: 0,
        KEY_UP: 1,
        KEY_PRESS: 2,
    
        //按键枚举值
        KeyCodeMap: {
            LEFT: 65, // A键
            RIGHT: 68, // D键
            DOWN: 83, // S键
            UP: 87, // W键
            SPACE: 32   //空格键
        }
    };

    待重构点

    目前引擎只支持键盘事件,以后可以通过“增加Event事件枚举值,并对应修改EventManager的_getEventType方法”的方式来增加更多的事件支持。

    重构提炼的引擎类

    将依赖的YTool方法移到Tool

    引擎类依赖了YTool事件操作方法addEvent和removeEvent,考虑到YTool的event中的事件操作方法都具有通用性,因此将其提取到引擎Tool类中
    又因为YTool的event对象依赖YTool的judge对象的方法,所以将judge对象的相关的方法提取到引擎Tool中。
    引擎Tool

    namespace("YE.Tool").judge = {
    …
        /**
         * 判断是否为jQuery对象
         */
        isjQuery: function (ob) {
            …
        },
        /**
         * 检查宿主对象是否可调用
         *
         * 任何对象,如果其语义在ECMAScript规范中被定义过,那么它被称为原生对象;
         环境所提供的,而在ECMAScript规范中没有被描述的对象,我们称之为宿主对象。
    
         该方法用于特性检测,判断对象是否可用。用法如下:
    
         MyEngine addEvent():
         if (Tool.judge.isHostMethod(dom, "addEventListener")) {    //判断dom是否具有addEventListener方法
                dom.addEventListener(sEventType, fnHandler, false);
                }
         */
        isHostMethod: (function () {
            …
        }())
    };
    namespace("YE.Tool").event = (function () {
        return {
            bindEvent: function (object, fun) {
                …
            },
            /* oTarget既可以是单个dom元素,也可以是jquery集合。
             如:
             Tool.event.addEvent(document.getElementById("test_div"), "mousedown", _Handle);
             Tool.event.addEvent($("div"), "mousedown", _Handle);
             */
            addEvent: function (oTarget, sEventType, fnHandler) {
                …
            },
            removeEvent: function (oTarget, sEventType, fnHandler) {
                …
            },
            wrapEvent: function (oEvent) {
                …
            },
            getEvent: function () {
                …
            }
        }
    }()); 

    提炼通用的KeyCodeMap

    现在引擎KeyCodeMap的枚举变量与用户逻辑有关,定死了上下左右移动对应的按键keyCode值(如左对应A键,右对应D键):
    引擎Event

        KeyCodeMap: {
            LEFT: 65, // A键
            RIGHT: 68, // D键
            DOWN: 83, // S键
            UP: 87, // W键
            SPACE: 32   //空格键
        }

    然而对于不同的游戏,它的上下左右对应的按键可能不同。
    因此KeyCodeMap应该只定义按键对应的keyCode值,由用户来决定上下左右移动对应的按键。
    引擎Event

        KeyCodeMap: {
            A: 65,
            D: 68,
            S: 83,
            W: 87,
            SPACE: 32 
        }

    炸弹人使用提炼的引擎类

    修改炸弹人Game的初始化事件逻辑

    修改前
    炸弹人实现了监听事件的逻辑:
    炸弹人Game

                _initEvent: function () {
                    //监听整个document的keydown,keyup事件
                    keyEventManager.addKeyDown();
                    keyEventManager.addKeyUp();
                },

    炸弹人KeyEventManager

        addKeyDown: function () {
                    this._keyDown = YYC.Tool.event.bindEvent(this, function (e) {
                        keyState[e.keyCode] = true;
    
                        e.preventDefault();
                    });
    
                    YYC.Tool.event.addEvent(document, "keydown", this._keyDown);
                },
                addKeyUp: function () {
                    this._keyUp = YYC.Tool.event.bindEvent(this, function (e) {
                        keyState[e.keyCode] = false;
                    });
    
                    YYC.Tool.event.addEvent(document, "keyup", this._keyUp);
                },

    修改后
    炸弹人调用引擎EventManager API和传入键盘事件的枚举值来监听键盘事件:
    炸弹人Game

                _initEvent: function () {
                    //调用引擎EventManager的addListener绑定事件,传入引擎Event定义的事件类型枚举值,并定义事件处理方法
                    YE.EventManager.addListener(YE.Event.KEY_DOWN, function (e) {
                        window.keyState[e.keyCode] = true;
    
                        e.preventDefault();
                    });
                    YE.EventManager.addListener(YE.Event.KEY_UP, function (e) {
                        window.keyState[e.keyCode] = false;
                    });
                }

    删除炸弹人KeyEventManager.js文件中的KeyCodeMap和KeyEventManager,并将该文件重命名为KeyState

    因为炸弹人KeyEventManager.js中的KeyCodeMap和KeyEventManager已经移到引擎中了,所以删除它们,只保留keyState,并重命名文件为KeyState.js。

    炸弹人KeyState

    (function () {
        //按键状态
        var keyState = {
        };
    
        keyState[keyCodeMap.LEFT] = false;
        keyState[keyCodeMap.RIGHT] = false;
        keyState[keyCodeMap.UP] = false;
        keyState[keyCodeMap.DOWN] = false;
        keyState[keyCodeMap.SPACE] = false;
    
        window.keyState = keyState;
    }());

    炸弹人改为使用引擎Event的KeyCodeMap

    如对应修改炸弹人KeyState和PlayerLayer
    炸弹人KeyState

        keyState[YE.Event.KeyCodeMap.A] = false;
        keyState[YE.Event.KeyCodeMap.D] = false;
        keyState[YE.Event.KeyCodeMap.W] = false;
        keyState[YE.Event.KeyCodeMap.S] = false;
        keyState[YE.Event.KeyCodeMap.SPACE] = false;

    炸弹人PlayerLayer

                ___keyDown: function () {
                    if (keyState[YE.Event.KeyCodeMap.A] === true || keyState[YE.Event.KeyCodeMap.D] === true
                        || keyState[YE.Event.KeyCodeMap.W] === true || keyState[YE.Event.KeyCodeMap.S] === true) {
                        return true;
                    }
                    else {
                        return false;
                    }
                },

    领域模型

    112127209004769.jpg

    提出DataOperator

    现在提炼DataOperator类。

    提炼引擎类

    领域类DataOperator负责对数据进行读、写操作,对应炸弹人数据操作层的类,具体为MapDataOperate、GetPath、TerrainDataOperate、GetSpriteData、GetFrames。
    这些数据操作类都与具体的业务逻辑相关,没有可提炼的。

    提出Data

    现在提炼Data类。

    提炼引擎类

    领域类Data负责保存游戏数据,对应炸弹人的数据层的类,具体为MapData、Bitmap、ImgPathData、TerrainData、SpriteData、FrameData。
    其中Bitmap是图片的包装类,包含与图片本身密切相关的属性和方法,但不包含与游戏相关的具体图片,因此具有通用性,可提取到引擎中。

    引擎Bitmap

    (function () {
        namespace("YE").Bitmap = YYC.Class({
            Init: function (data) {
                this.img = data.img;
                this.width = data.width;
                this.height = data.height;
            },
            Private: {
            },
            Public: {
                img: null,
                width: 0,
                height: 0
            }
        });
    }()); 

    炸弹人使用提炼的引擎类

    修改炸弹人BitmapFactory,改为创建引擎的Bitmap实例
    炸弹人BitmapFactory

    (function () {
        var bitmapFactory = {
            createBitmap: function (data) {
    …
                return new YE.Bitmap(bitmapData);
            }
        }
    
        window.bitmapFactory = bitmapFactory;
    }());

    领域模型

    112127036503299.jpg

    本文最终领域模型

    112126334319131.jpg

    此处炸弹人省略了与引擎类无关的类。

    高层划分

    包图

    112128203375526.jpg

    对应领域模型

    • 核心包
      放置引擎的核心类。
      • Main
      • Director
      • Scene
      • Layer
      • Sprite
    • 算法包
      放置通用的算法类。
      • AStar
      • collision
    • 动画包
      放置游戏动画的相关类。
      • Animation
    • 加载包
      负责游戏资源的加载和管理。
      • ImgLoader
    • 数据结构包
      放置引擎的基础结构类。
      • Bitmap
    • 集合包
      放置引擎集合类。
      • Collection
      • Hash
    • 通用工具包
      放置引擎通用的方法类。
      • Tool
    • 配置包
      放置引擎配置类。
      • Config
    • 事件管理包
      负责事件的管理。
      • Event
      • EventManager

    引擎集合类也属于数据结构,为什么不放在数据结构包中,而是放在单独的集合包中?
    因为引擎集合类的使用方式为继承,而数据结构包中的引擎Bitmap的使用方式为委托,两者使用方式不同,因此不能放到一个包中。

    总结

    本文将炸弹人通用的类提炼到了引擎中,搭建了引擎的整体框架。但是现在引擎还很粗糙,包含了很多炸弹人逻辑,不具备通用性。因此,在下文中,我会进行第二次迭代,对引擎进行进一步的抽象和提炼。

    本文源码下载

    GitHub

    参考资料

    炸弹人游戏系列

    上一篇博文

    提炼游戏引擎系列:初步设计引擎

    下一篇博文

    提炼游戏引擎系列:第二次迭代(上)

    转载于:https://www.cnblogs.com/chaogex/p/4158580.html

    展开全文
  • 进程模式 序列化 Serializable接口 Parcelable接口 Binder机制 IPC方式 Bundle 文件共享 AIDL Messager ContentProvider Socket Binder连接池 一、IPC基础及概念 1.进程模式 a.进程&线程 进程:

    在上一篇Window里提及过IPC,本篇将详细总结IPC,知识点如下:

    • IPC基础及概念
      • 多进程模式
      • 序列化
        • Serializable接口
        • Parcelable接口
      • Binder机制
    • IPC方式
      • Bundle
      • 文件共享
      • AIDL
      • Messager
      • ContentProvider
      • Socket
    • Binder连接池

    一、IPC基础及概念

    1.多进程模式

    a.进程&线程

    • 进程:一般指一个执行单元,在PC和移动设备上指一个程序应用
    • 线程:CPU调度的最小单元。线程是一种有限的系统资源。

    两者关系: 一个进程可包含多个线程,即一个应用程序上可以同时执行多个任务。

    • 主线程(UI线程):UI操作
    • 有限个子线程:耗时操作

    注意:不可在主线程做大量耗时操作,会导致ANR(应用无响应)。

    b.开启多进程模式的方式

    • (不常用)通过JNI在native层fork一个新的进程。
    • (常用)在AndroidMenifest中给四大组件指定属性android:process,进程名的命名规则:
      • 默认进程:没有指定该属性则运行在默认进程,其进程名就是包名
      • 以“:”开头的进程:
        • 省略包名,如android:process=":remote",表示进程名为com.example.myapplication:remote
        • 属于当前应用的私有进程,其他进程的组件不能和他跑在同一进程中。
      • 完整命名的进程:
        • android:process="com.example.myapplication.remote"
        • 属于全局进程,其他应用可以通过ShareUID方式和他跑在用一个进程中。

    UID&ShareUID:

    • Android系统为每个应用分配一个唯一UID,具有相同UID的应用才能共享数据。
    • 两个应用通过ShareUID跑在同一进程的条件:ShareUID相同且签名也相同。
      • 满足上述条件的两个应用,无论是否跑在同一进程,它们可共享data目录,组件信息。
      • 若跑在同一进程,它们除了可共享data目录、组件信息,还可共享内存数据。它们就像是一个应用的两个部分。

    c.查看进程信息的方法

    • 通过DDMS视图查看进程信息。
    • 通过shell查看,命令为:adb shell ps|grep 包名

    d.需要进程间通信的必要性:所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。

    原因:由于Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。

    e.多进程造成的影响,总结为以下四方面:

    ①静态变量和单例模式失效。

    • 由独立的虚拟机造成。

    ②线程同步机制失效。

    • 原因同上。

    ③SharedPreference的不可靠下降。

    • SharedPreferences不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失。

    ④Application多次创建。

    • Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次。

    推荐阅读关于Android多进程


    2.序列化

    a.序列化的介绍

    • 含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
    • 场景:需要通过Intent和Binder等传输类对象就必须完成对象的序列化过程。
    • 两种方式:实现Serializable/Parcelable接口。

    b.Serializable接口和Parcelable接口的比较:

    image

    c.serialVersionUID

    • 含义:是Serializable接口中用来辅助序列化和反序列化过程。
    • 注意:原则上序列化后的数据中的serialVersionUID要和当前类的serialVersionUID 相同才能正常的序列化。

    注意:两种变量不会参与序列化过程:

    • 静态成员变量属于类,不属于对象。
    • transient关键字标记的成员变量。

    推荐阅读序列化Serializable和Parcelable的理解和区别


    3.IPC简介

    a.IPC(Inter-Process Communication,跨进程通信):指两个进程之间进行数据交换的过程。

    b.任何一个操作系统都有对应的IPC机制。

    • Windows:通过剪切板、管道、油槽等进行进程间通讯。
    • Linux:通过命名空间、共享内容、信号量等进行进程间通讯。
    • Android:没有完全继承Linux,比如,其独具特色的通讯方式有Binder、Socket等等。

    c.IPC的使用场景:

    • 由于某些原因,应用自身需要采用多进程模式来实现。可能原因有:
      • 某些模块因特殊原因要运行在单独进程中;
      • 为加大一个应用可使用的内存,需通过多进程来获取多份内存空间。
    • 当前应用需要向其它应用获取数据

    d.Android的进程架构:每一个Android进程都是独立的,且都由两部分组成,一部分是用户空间,另一部分是内核空间,如下图:

    如此设计的优点:

    • 稳定性、安全性高:每一个Android进程都拥有自己独立的虚拟地址空间,一方面可以限制其他进程访问自己的虚拟地址空间;另一方面,当一个进程崩溃时不至于“火烧连营”。
    • 便于复用与管理:内核共享有助于系统维护和并发操作、节省空间。

    4.Binder机制

    a.概念:

    • 从API角度:是一个类,实现IBinder接口。
    • 从IPC角度:是Android中的一种跨进程通信方式。
    • 从Framework角度:是ServiceManager连接各种Manager和相应ManagerService的桥梁。
    • 从应用层:是客户端和服务端进行通信的媒介。客户端通过它可获取服务端提供的服务或者数据。

    b.Android是基于Linux内核基础上设计的,却没有把管道/消息队列/共享内存/信号量/Socket等一些IPC通信手段作为Android的主要IPC方式,而是新增了Binder机制,其优点有:

    • 传输效率高、可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较:
    方式 拷贝次数 操作难度
    Binder 1 简易
    消息队列 2 简易
    Socket 2 简易
    管道 2 简易
    共享内存 0 复杂

    从Android进程架构角度分析:对于消息队列、Socket和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:

    image

    而对于Binder来说,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程,如图:

    image

    由于共享内存操作复杂,综合来看,Binder的传输效率是最好的。

    • 实现C/S架构方便:Linux的众IPC方式除了Socket以外都不是基于C/S架构,而Socket主要用于网络间的通信且传输效率较低。Binder基于C/S 架构 ,Server端与Client端相对独立,稳定性较好。

    • 安全性高:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Binder机制为每个进程分配了UID/PID且在Binder通信时会根据UID/PID进行有效性检测。

    c.Binder框架定义了四个角色:Server,Client,ServiceManager和Binder驱动。

    其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间。关系如图:

    image

    下面简单介绍这四个角色:

    • ServiceManager:服务的管理者,将Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用。流程如图:

    image

    • Binder驱动
      • 与硬件设备没有关系,其工作方式与设备驱动程序是一样的,工作于内核态。
      • 提供open()mmap()poll()ioctl()等标准文件操作。
      • 以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。
      • 负责进程之间binder通信的建立,传递,计数管理以及数据的传递交互等底层支持。
      • 驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,由于ioctl()灵活、方便且能够一次调用实现先写后读以满足同步交互,因此不必分别调用write()和read()接口。
      • 其代码位于linux目录的drivers/misc/binder.c中。
    • Server&Client:服务器&客户端。在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。

    d.代理模式Proxy:给某个对象提供一个代理对象,并由代理对象控制对原对象的访问。如图:

    image

    代理模式的组成:

    • Abstarct Subject(抽象主题):声明Real Subject和Proxy的共同接口,这样在任何可以使用Real Subject的地方都可以使用Proxy。
    • Real Subject(真实主题):定义了Proxy所代表的Real Subject。
    • Proxy Subject(代理主题):
      • 内部含有Real Subject的引用,可在任何时候操作目标对象;
      • 提供一个与Real Subject相同的接口,可在任何时候替代目标对象。

    推荐阅读代理模式

    e.Binder 工作原理

    • 服务器端:在服务端创建好了一个Binder对象后,内部就会开启一个线程用于接收Binder驱动发送的消息,收到消息后会执行onTranscat(),并按照参数执行不同的服务端代码。
    • Binder驱动:在服务端成功Binder对象后,Binder驱动也会创建一个mRemote对象(也是Binder类),客户端可借助它调用transcat()即可向服务端发送消息。
    • 客户端:客户端要想访问Binder的远程服务,就必须获取远程服务的Binder对象在Binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binder对象的暴露给客户端的方法。

    后面会通过AIDL和Messager更深刻地体会这一工作原理。

    推荐阅读Android - Binder驱动Binder设计与实现Binder系列


    三、IPC方式

    image

    由上图可以看到,其他一些IPC方式实际都是通过Binder来实现,只不过封装方式不同。接下来分别总结其他六种IPC方式:

    1.使用Bundle

    a.Bundle:支持在Activity、Service和Receiver之间通过Intent.putExtra()传递Bundle数据。

    Intent intent = new Intent();
    Bundle bundle = new Bundle();
    bundle.putString("xxx","xxx");
    intent.putExtra("data", bundle);

    b.原理:Bundle实现Parcelable接口,它可方便的在不同的进程中传输。

    c.注意:Bundle不支持的数据类型无法在进程中被传递。

    思考下面这种情况:
    Q:在A进程进行计算后的结果不是Bundle所支持的数据类型,该如何传给B进程?
    A:将在A进程进行的计算过程转移到B进程中的一个Service里去做,这样可成功避免进程间的通信问题。

    推荐阅读通过Bundle在Android Activity间传递数据


    2.使用文件共享

    a.文件共享:两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。

    b.适用情况:对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。

    c.虽然SharedPreferences也是文件存储的一种,但不建议采用。

    • 原因:系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。

    3.使用AIDL

    a.AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。

    b.可支持的数据类型:

    • 基本数据类型:byte,int,long,float,double,boolean,char
    • String类型
    • CharSequence类型
    • ArrayList、HashMap且里面的每个元素都能被AIDL支持
    • 实现Parcelable接口的对象
    • 所有AIDL接口本身

    注意:除了基本数据类型,其它类型的参数必须标上方向:in、out或inout,用于表示在跨进程通信中数据的流向。

    • in
      • 表示数据只能由客户端流向服务端。
      • 服务端将会接收到这个对象的完整数据,但在服务端修改它不会对客户端输入的对象产生影响。
    • out
      • 表示数据只能由服务端流向客户端。
      • 服务端将会接收到这个对象的的空对象,但在服务端对接收到的空对象有任何修改之后客户端将会同步变动。
    • inout
      • 表示数据可在服务端与客户端之间双向流通。
      • 服务端将会接收到客户端传来对象的完整信息,且客户端将会同步服务端对该对象的任何变动。

    c.两种AIDL文件:

    • 用于定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。
    • 用于定义方法接口,以供系统使用来完成跨进程通信的。

    注意:

    • 自定义的Parcelable对象必须把java文件和自定义的AIDL文件显式的import进来,无论是否在同一包内。
    • AIDL文件用到自定义Parcelable的对象,必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。

    d.AIDL的本质是系统提供了一套可快速实现Binder的工具。关键类和方法:

    • AIDL接口:继承IInterface
    • Stub类:Binder的实现类,服务端通过这个类来提供服务。
    • Proxy类:服务器的本地代理,客户端通过这个类调用服务器的方法。
    • asInterface():客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。返回对象:
      • 若客户端和服务端位于同一进程,则直接返回Stub对象本身;
      • 否则,返回的是系统封装后的Stub.proxy对象。
    • asBinder():根据当前调用情况返回代理Proxy的Binder对象。
    • onTransact():运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
    • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

    原理图

    通过此处实例具体了解AIDL实现IPC的流程:

    推荐阅读Android中AIDL的工作原理

    e.实现方法

    • 服务端:
      • 创建一个aidl文件
      • 创建一个Service,实现AIDL的接口函数并暴露AIDL接口。
    • 客户端:
      • 通过bindService绑定服务端的Service;
      • 绑定成功后,将服务端返回的Binder对象转化成AIDL接口所属的类型,进而调用相应的AIDL中的方法。

    总结:服务端里的某个Service给和它绑定的特定客户端进程提供Binder对象,客户端通过AIDL接口的静态方法asInterface() 将Binder对象转化成AIDL接口的代理对象,通过这个代理对象就可以发起远程调用请求。

    实例使用AIDL进行进程间通信

    f.可能产生ANR的情形:

    • 对于客户端,且假设在主线程调用方法:
      • 调用服务端的方法是运行在服务端的Binder线程池中,若所调用的方法里执行了较耗时的任务,同时会导致客户端线程长时间阻塞,易导致客户端ANR。
      • onServiceConnected()onServiceDisconnected()里直接调用服务端的耗时方法,易导致客户端ANR。
    • 对于服务端:
      • 服务端的方法本身就运行在服务端的Binder线程中,可在其中执行耗时操作,而无需再开启子线程。
      • 回调客户端Listener的方法是运行在客户端的Binder线程中,若所调用的方法里执行了较耗时的任务,易导致服务端ANR。

    g.解决客户端频繁调用服务器方法导致性能极大损耗的办法:实现观察者模式。即当客户端关注的数据发生变化时,再让服务端通知客户端去做相应的业务处理。

    比如:每个客户端的请求Listener传递给服务端,服务端用一个list保存,当数据变化时服务器再依次通知,此时客户端就用Listener进行回调处理。注意要用Handler切换到主线程。

    h.AIDL 解注册失败

    • 原因:Binder进行对象传输实际是通过序列化和反序列化进行,即Binder会把客户端传递过来的对象重新转化并生成一个新的对象,虽然在注册和解注册的过程中使用的是同一个客户端对象,但经过Binder传到服务端后会生成两个不同的对象。另外,多次跨进程传输的同一个客户端对象会在服务端生成不同的对象,但它们在底层的Binder对象是相同的。
    • 解决办法:当客户端解注册的时候,遍历服务端所有的Listener,找到和解注册Listener具有相同的Binder对象的服务端Listener,删掉即可。

    需要用到RemoteCallBackList:Android系统专门提供的用于删除跨进程listener的接口。其内部自动实现了线程同步的功能。

    推荐文章Android:学习AIDL,这一篇文章就够了


    4.使用Messager

    a.Messenger:轻量级的IPC方案,通过它可在不同进程中传递Message对象。

    Messenger.send(Message);

    相关记忆:

    • Handler:主要进行线程间的数据通信。
    • Messenger:进程间的数据通信。

    b.特点

    • 底层实现是AIDL,即对AIDL进行了封装,更便于进行进程间通信。
    • 其服务端以串行的方式来处理客户端的请求,不存在并发执行的情形,故无需考虑线程同步的问题。
    • 可在不同进程中传递Message对象,Messager可支持的数据类型即Messenge可支持的数据类型。
      • arg1、arg2、what字段:int型数据
      • obj字段:Object对象,支持系统提供的Parcelable对象
      • setData:Bundle对象
    • 有两个构造函数,分别接收Handler对象和Binder对象。

    c.实现方法

    • 服务端:
      • 创建一个Service用于提供服务;
      • 其中创建一个Handler用于接收客户端进程发来的数据;
      • 利用Handler创建一个Messenger对象;
      • 在Service的onBind()中返回Messenger对应的Binder对象。
    • 客户端:
      • 通过bindService绑定服务端的Service;
      • 通过绑定后返回的IBinder对象创建一个Messenger,进而可向服务器端进程发送Message数据。(至此只完成单向通信)
      • 在客户端创建一个Handler并由此创建一个Messenger,并通过Message的replyTo字段传递给服务器端进程。服务端通过读取Message得到Messenger对象,进而向客户端进程传递数据。(完成双向通信)

    image

    实例使用Messenger实现IPC

    d.Message的缺点:

    • 主要作用是传递 Message,难以实现远程方法调用。
    • 以串行的方式处理客户端发来的消息的,不适合高并发的场景。

    解决办法:考虑使用AIDL实现IPC。

    推荐阅读超简单的Binder,AIDL和Messenger的原理及使用流程


    5.使用ContentProvider

    a.ContentProvider:是Android提供的专门用来进行不同应用间数据共享的方式。

    底层同样是通过Binder实现的。

    b.注意:

    • 除了onCreat()运行在UI线程中,其他的query()、update()、insert()、delete()和getType()都运行在Binder线程池中。
    • CRUD四大操作存在多线程并发访问,要注意在方法内部要做好线程同步
    • 一个SQLiteDatabase内部对数据库的操作有同步处理,但多个SQLiteDatabase之间无法同步。

    基础篇组件篇之ContentProvider


    6.使用Socket

    a.Socket(套接字):不仅可跨进程,还可以跨设备通信。

    image

    b.使用类型

    • 流套接字:基于TCP协议,采用流的方式提供可靠的字节流服务。
    • 数据报套接字:基于UDP协议,采用数据报文提供数据打包发送的服务。

    c.实现方法:TCP/UDP

    • 服务端:
      • 创建一个Service,在线程中建立TCP服务、监听相应的端口等待客户端连接请求;
      • 与客户端连接时,会生成新的Socket对象,利用它可与客户端进行数据传输;
      • 与客户端断开连接时,关闭相应的Socket并结束线程。
    • 客户端:
      • 开启一个线程、通过Socket发出连接请求;
      • 连接成功后,读取服务端消息;
      • 断开连接,关闭Socket。

    image

    d.注意:使用Socket进行通信

    • 需要声明权限
    <uses-permission android:name="android.permission.INTERNET" />  
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  
    • 能在主线程中访问网络

    推荐阅读:这是一份很详细的Socket使用攻略

    综上,以上六种IPC方式的优缺点和使用场景见下图:

    image


    四.Binder连接池

    a.背景:有多个业务模块都需要AIDL来进行IPC,此时需要为每个模块创建特定的aidl文件,那么相应的Service就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。

    b.作用:将每个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。

    c.工作原理:每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用了。流程如图:

    image

    d.实现方式:

    • 为每个业务模块创建AIDL接口并具体实现;
    • 为Binder连接池创建AIDL接口IBinderPool.aidl并具体实现;
    • 远程服务BinderPoolService的实现,在onBind()返回实例化的IBinderPool实现类对象;
    • Binder连接池的具体实现,来绑定远程服务。
    • 客户端的调用。

    实例细说Binder连接池

    现在可以回答以下问题:

    Q:在Android开发中提高开发效率的方法?
    A:使用Binder连接池,避免反复创建Service,统一管理和维护AIDL。

    推荐阅读Android的IPC机制Android跨进程通信IPC


    希望这篇文章对你有帮助~

    展开全文
  • onStart与onStop是从Activity是否可见这个角度调用的,onResume和onPause是从Activity是否显示在前台这个角度来回调的,在实际使用没其他明显区别。   Q: Activity的切换过程 (1) Activty 第一次...
  • 作者介绍 **梁铭图,**新炬网络首席架构师,10年以上数据库运维、数据分析、数据库设计以及系统规划建设...SRE首先是一套方法论,它从传统运维中与稳定性相关的工作内容提炼出来进行升华,构建了SRE的方法论体系。冗
  • 社会化媒体中的垂直社区是移动互联网时代的“宠儿”,沉淀有大量的优质且专业的内容,因而吸聚了大批用户,随之而来的是海量的UGC,这给Social Listening提供了可供挖掘的矿藏,从中提炼出改进产品、提升品牌价值的...
  • 而卡通渲染(Toon Rendering)作为一种特殊形式的非真实感渲染方法,近年来倍受关注。 通过阅读这篇文章,你将对非真实感渲染技术的以下要点有所了解: 非真实感渲染的基本思想和相关领域 卡通渲染 轮廓描边的几种...
  • 评论的描述对象涵盖商家、商品、配送等类型,首先需要识别出描述对象的类型,如果描述对象的类型是商品,需要进一步识别描述的实体是订单中的哪个商品,以及具体的主题角度等等。片段抽取模型以基于评论数据训练的...
  • 人们常说“千言万语不及一图”,运维人员经常会通过各系统的运维数据进行统计分析并生成各类实时报表,对各类运维数据 (如应用日志、交易日志、系统日志)进行多维度、多角度深入分析、预测及可视化展现,将他们...
  • 会议活动的核心是使品牌者与参会者互相了解、提升品牌认知度。...整体而言,会议活动的创意首先是围绕公司主题(品牌理念/品牌核心卖点)的一个创意点,追求的是与目标人群的心理契合度; 接着是战术上的形式创意...
  • 如果两个相邻的三角形朝着不同的方向,它们将以不同的角度反射光线,因此他妈妈将以不同的方式着色。对于弯曲物体,这是一个问题:显而易见的是,几何形状由平面三角形构成。 为了避免这个问题,光线在三角形...
  • 在建设领导驾驶舱之前,首要是调研、规划、提炼指标,一般是建立在对全公司指标体系的基础上,围绕驾驶舱的主题,罗列出核心指标展示。 但是如何建设指标体系也是一门学问,足够花一万字来叙述。指标体系建设的常用...
  • 作者 | Mitchell A. Gordon 译者 | 孙薇 出品 | AI科技大本营(ID:rgznai100) 模型压缩可减少受训神经网络的冗余——...本篇列出了作者所见过的BERT压缩论文,以下表格中对不同方法进行了分类。 论文1:Compr...
  • 人们常说“千言万语不及一图”,运维人员经常会通过各系统的运维数据进行统计分析并生成各类实时报表,对各类运维数据(如应用日志、交易日志、系统日志)进行多维度、多角度深入分析、预测及可视化展现,将他们分析...
  • 高效的学习方法

    热门讨论 2018-01-25 21:46:37
    学习方法
  • 第一,概念,我们可以去研究一个概念,比如《合法性概念的滥用与重述》,我们还可以对几个概念进行辨析,比如我们可以对教育和教学这两个概念进行辨析,再比如我们还可以对教学的很概念,像混合式教学,翻转课堂,...
  • 本系列文字是一位创业者的投稿《面向NLP的AI产品方法论》,老曹尽量不做变动和评价,尽量保持系列文章的原貌,这是第2篇。设计语音技能跟软件开发一样集体协作完成,本文主要讨论,产品经理在业...
  • 用户研究的系统方法

    千次阅读 2019-08-05 23:35:43
    它是一种理解用户,将他们的目标、需求与产品所具有的商业宗旨相匹配的方法。在不了解用户的情况下设计出的产品通常不会符合用户的使用习惯,不能满足用户的体验需求。用户研究的深度和质量直接关系到产品设计的成败...
  • 最后就可以对这些重建起来的路径进行风格化笔划渲染,其中,这些笔划本身可以用很多方法来进行风格化处理,包括变细、火焰、摆动、淡化等效果,同时还有深度和距离信息。如下图。   图14 使用混合轮廓描边方法生成...
  • 沟通能力之PPT综合能力方法

    千次阅读 2017-09-11 16:22:20
    一、主题提炼与逻辑架构能力  历史观(横向和纵向对比)  语言表达与逻辑思维  内容逻辑,逻辑大纲 提炼重点、架构逻辑  内容层面: 主题提炼与逻辑架构能力(想清楚讲什么?用什么逻辑顺序讲?)  在内容层面要...
  • 本文主题:网络推广的关键在于系统性策划 在言及网络推广时,我们首先要考虑的就是确认目标,推什么,是最先要考虑的,因为目标是引导行为的根本,失去了目标的网络推广就变成了一团糟,结果事倍功半。 30-BRAND...
  • 数据仓库常见建模方法与大数据领域建模实例综述

    千次阅读 多人点赞 2021-05-01 14:01:51
    为什么需要数据建模? 为什么要进行数据仓库建模?...大数据的数仓建模是通过建模的方法更好的组织、存储数据,以便在 性能、成本、效率和数据质量之间找到最佳平衡点。一般主要从下面四点考虑 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,110
精华内容 2,444
关键字:

多角度提炼主题的方法