博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
BUAA_OO第二单元总结性博客作业——多线程电梯架构
阅读量:4938 次
发布时间:2019-06-11

本文共 13350 字,大约阅读时间需要 44 分钟。

一、设计策略

  在第一次作业时,我刚第一次接触多线程这个东西……于是乎对于第一次VIP直上直下一次只接一个人的电梯,我借鉴了指导书中为我们提供的架构,设计了一个输入线程和一个电梯线程,并设置了一个中间类RequestQueue,开一个队列来存放异步输入的请求,并保证这个类是线程安全的。mian类之下的整体架构为“Getrequest-RequestQueue-Elevator”。在main类中仅仅只做了new了这几个类并且启动输入线程以及电梯线程的工作。当电梯运行时,GetRequest线程读入请求,并存放到RequestQueue类中的队列中,Eletavor线程run方法中的while循环一直检测队列中的请求,若有请求则取出第一条请求执行(sleep相应的与移动楼层有关的时间,输出开门、进人、关门的语句,再sleep相应时间,再输出与出人有关的语句)。当GetRequest中读到为null的请求时,将Request中的flag置flase。当Elevator检测到RequestQueue中的flag为flase,并且队列为空,则中断while循环,接下来线程也就结束运行,程序结束。

  第二次作业相较于第一次作业增加了需要捎带的需求,按道理在第二次作业就加入调度器模块是比较符合课程组要求的,但是……我在写第二次作业时为了更充分地利用第一次作业的架构,并没有对“输入-队列-电梯”这个三层的架构做过多的更改(除了加入了wait()与notify()避免暴力轮询带来的CPU时间过长)。整体架构依旧为“Getrequest-RequestQueue-Elevator”。于是我对Elevator线程中的runelevator方法做了一些修改,在类中增加了一个名为inelevator的arraylist(不需要线程安全),在RequestQueue中加入了遍历队列并移出匹配出发楼层的请求并返回的方法(名为getfromthisfrom)。对于runelevator方法,原本的暴力直接sleep移动速度乘以移动楼层的方法不可用了,我只能一层一层地移动电梯,输出"ARRIVE-x"。我仍然使用了第一次电梯作业中取出队列的第一条指令进入runelevator方法运行的机制,并且在第二次作业中可以按照指导书的说法将这条指令看作是主请求。(虽然我不怎么愿意这么叫它,因为它的作用仅仅是确定电梯中没有人时需要运行的方向,其他跟捎带指令并没有区别),在电梯开始移动后,在到达每一层时均调用RequestQueue中的判断是否有能上电梯的人加入inelevetor队列(这就是所谓的捎带请求)或是inelevator队列中是否有需要出电梯的人(这一步中主请求与捎带请求并无区别,只要到达楼层与当前楼层相等,就让他出来)。

  在这种情况下电梯运行状态中很重要的一项就是运行方向的判断。在我的电梯线程中我设置了一个direction标志位,若为true则电梯接下来上升一层,若为false则下降一层。如果要按照原本的主请求设计模式,对于类似“1-FROM-10-TO-1;2-FROM-11-TO-1”这样的指令。电梯会先上到10层,接上乘客1,先把“1”送到1层,再去11层接“2”。而实际上的最优的策略是,在一次上升中,就接上10层的“1”与11层的“2”。这样一来一去差了将近一倍的时间。为了实现这个接送策略,我在每一层判断电梯运行方向之前,都对请求队列以及电梯中的inelevator数组进行一次遍历,得到所有未上电梯乘客的出发楼层以及所有已经上了电梯的乘客的到达楼层的最大值maxfloor与最小值minfloor,若电梯上行(direction=true)且当前楼层>maxfloor,则置direction为false;若电梯下行(direction=false)且当前楼层<minfloor,则置direction为true。在每次电梯移动前先调用setdirection()方法进行direction的判断,再根据direction的真值进行电梯的移动,输出"ARRIVE-x"。

  第三次作业其实我还是没怎么大改先前两次作业留下来的架构,只是在RequestQueue层与Elevator层之间增加了一个Scheduler调度层,负责将指令拆分以及指令分配,最后整体架构为“Getrequest-RequestQueue-Scheduler-Elevator”。前面两层我基本没有动过,在这次作业中我做了改动的只有Scheduler线程以及Elevator对象。对于三个电梯线程来说,Scheduler扮演了先前Getrequest以及RequestQueue的角色。由Scheduler来newA、B、C三个电梯线程,并在run方法中分别启动这三部电梯,并执行dietributePerson()方法。在Scheduler中有三个arraylist,负责存放3个电梯需要运送的请求,若RequestQueue中有请求,Scheduler从中逐条取出并调用distributePerson()进行分析,若能够被某个电梯直接送达,则直接放到这个电梯的请求等待队列里。至于具体要投递到哪一部电梯,我的策略就比较简单粗暴了。A电梯最快,先判断A电梯,A能送先投递给A吧;C电梯能到的楼层最少,第二步考虑C电梯;A与C都送不了?B电梯你来试试吧……但这样就有可能出现,同时来很多条不止一部电梯能够直接送达的指令,但我全部塞给了某一部电梯。那好……我就在前面加上等待电梯的人是否超过电梯最大允许人数的判断吧,所以我处理能够直达请求的代码长这样:

  (对没错,if-else解君愁)

  

  若这个请求不能被任何一个电梯直接送达,则要进入到step2,进行拆分处理。我没有想到一些很高大上的转运策略,我设计的拆分策略是:

  先根据请求的出发楼层找到一个能接上这个请求的电梯,找第一部电梯的策略类似step1中的暴力策略,并根据这个电梯的能够到达的楼层以及请求的到达楼层,找到第二部能够从第一部电梯的到达楼层接上请求,并送到请求的到达楼层的电梯;第一部电梯的到达楼层(也是就是第二部电梯接到这个请求的楼层)即为拆分的中介楼层。根据请求的出发楼层,到达楼层以及中介楼层,使用新版接口中提供的personrequest构造函数,将请求拆分成两个personid相同的新请求,并分别投递到这两部电梯的请求等待队列中。

  最终负责处理拆分的代码长成了这个样子……

  

  这时就存在一个问题,我需要设置好这两个请求的执行顺序,确保在第一个请求到达后,也就是这个person被第一部电梯送到中间楼层后,才能被第二部电梯接走,也就是说,虽然拆分请求的第二段请求已经找到了能够运送它的对应电梯,但在第一段请求完成前,第二段请求暂时还不能被其对应电梯接走并执行。为了解决这个先后问题,我思考过两个方法:

  一、再设置一个专门存放拆分后的第二段请求的数组,当第一段请求被执行完后,由调度器主动将第二段请求放入对应电梯的请求等待队列中,也就是说,电梯只负责不断地取出自己的请求队列中的请求并执行,不考虑请求之间的先后关系。

  二、让电梯间进行通信,来判断电梯是否能够将某个请求从请求等待队列中取出。拆分请求在拆分完成后立即将两段请求放入对应电梯的请求队列中,但是,只有第一段请求能够直接被电梯从请求等待队列中取出,第二段请求在第一段请求完成之前,即使电梯到达了中介楼层,它也不能被接入电梯,当第一段请求完成后,通知第二段请求所在电梯,第二段请求方可被接走。

  说实话我考虑这两种方式想了挺久的。根据面向对象的思想,每个对象只应该管自己该管的事儿。电梯只负责从请求队列中接人、送人,请求什么时候被执行,什么时候被放入电梯对应的请求队列应该都是调度器的事儿。电梯老铁说:“你不让我接你就别往我这扔呗,调度器兄弟你能不能别把我不能接的指令扔到我这来。” 调度器兄弟说:“老铁,你已经是一个成熟的电梯了,哪个person让进你心里没数吗?我要一条一条从RequestQueue中取指令再拆分,再塞给你们,管的事已经够多了,我再新增一个数组,存一些我自己都不知道怎么冒出来的请求,臣妾做不到。” 我舍友说:“没有什么事情是一个flag解决不了的,如果有的话,那就来两个flag吧。”最终我还是采取了第二种策略,在将请求拆分后立即放入两部协作电梯的请求等待队列中,由电梯来判断第二段请求能不能被接走。

  为了解决电梯间通信问题,我封装了一个Person类,来代替之前的PersonRequest。Person类与接口中为我们提供的PersonRequest的主要区别是,新增了一个Flag对象以及一个boolean型变量inthisfloor,并新增以及重载了相应的构造器。Flag对象主要用于两个Person对象(同一个personId被拆分后分别放入两个电梯请求队列的的两个分身)之间的通信,inthisfloor用来判断这个Person是第一段请求还是第二段请求:inthisfloor为true,则这个Person是被拆分后的第一段请求,可以直接被电梯接走;inthisfloor为false,则这个Person是第二段请求,需要第一段请求到达设置Flag中的值后,才能被带走。

  

  而在将Person类加入两个电梯的请求等待队列中时,对Person类的处理是这样子的,主要是new了一个canNotBePutFlag对象并传入两个不同的Person类中,用于两个Person间信息的传递,也就是说,第一个Person(inthisfoor == true)在到达后,主动修改Flag中的值,使得第二个Person(inthisfloor == false)能够被电梯转运走。

  

  在电梯到达每一层遍历相应的请求队列时,需要对Person的Flag状态以及inthisfloor的值进行判断,若getNotPutFlag()的值为真且inthisfloor的值为false,则不能接上这个Person,其他情况这个Person均能被从请求队列中取出。

  

  而在电梯将某个Person送到到达楼层时,需要对这个Person的getNotPutFlag()进行判断,若为真,则说明这是一个被拆分后的请求的第一段,需要调用setPutFlag()方法对Flag状态进行修改,并notify其他两部电梯,使得这个第二段指令能够被接走。

  

  同样地,在每次判断队列是否为空、遍历电梯外的请求等待队列以及遍历电梯内的inelevator队列时,也都要忽略getNotPutFlag()的值为真且inthisfloor的值为false的Person对象。

  在第三次作业为了完成诸多复杂的楼层判断之类的事,我封装了一个typecheck方法类,里面包含了几个方法,本意是想简化写法的,后来发现好像没什么改进……

  以上就是我处理第三次作业时调度器中的指令分配、拆分策略。至于电梯线程,除了为了配合上面的拆分线程所做的改动之外,其他的步骤,包括开关门,进出乘客,电梯的移动等,我都是继承的之前的代码……先前写的架构虽然菜,但是稳定好用,线程安全也有保障orz……

  

二、程序结构度量

第一次作业:

 类图:

  

各类方法:

  

时序图:

  

复杂度分析:

  (1)method

method ev(G) iv(G) v(G)
elevatorfirst.Elevator.calculatemovefloor(int,int) 2.0 1.0 2.0
elevatorfirst.Elevator.close(int) 1.0 1.0 1.0
elevatorfirst.Elevator.Elevator(RequestQueue) 1.0 1.0 1.0
elevatorfirst.Elevator.elevatorwait(long) 1.0 2.0 2.0
elevatorfirst.Elevator.inperson(int,int) 1.0 1.0 1.0
elevatorfirst.Elevator.open(int) 1.0 1.0 1.0
elevatorfirst.Elevator.outperson(int,int) 1.0 1.0 1.0
elevatorfirst.Elevator.run() 3.0 4.0 5.0
elevatorfirst.Elevator.runelevator() 1.0 1.0 1.0
elevatorfirst.ElevatorMain.main(String[]) 1.0 1.0 1.0
elevatorfirst.GetRequest.GetRequest(RequestQueue) 1.0 1.0 1.0
elevatorfirst.GetRequest.run() 3.0 4.0 4.0
elevatorfirst.RequestQueue.getFlag() 1.0 1.0 1.0
elevatorfirst.RequestQueue.isEmpty() 1.0 1.0 1.0
elevatorfirst.RequestQueue.offer(PersonRequest) 1.0 1.0 1.0
elevatorfirst.RequestQueue.poll() 1.0 1.0 1.0
elevatorfirst.RequestQueue.RequestQueue() 1.0 1.0 1.0
elevatorfirst.RequestQueue.setFlag() 1.0 1.0 1.0
Total 23.0 25.0 27.0
Average 1.2777777777777777 1.3888888888888888 1.5

  (2)class

class OCavg WMC
elevatorfirst.Elevator 1.4444444444444444 13.0
elevatorfirst.ElevatorMain 1.0 1.0
elevatorfirst.GetRequest 2.0 4.0
elevatorfirst.RequestQueue 1.0 6.0
Total   24.0
Average 1.3333333333333333 6.0

第二次作业:

类图:

  

各类方法:

  

时序图:

  

复杂度分析:

  (1)method

    

method ev(G)  iv(G) v(G) 
elevatorsecond.Elevator.doorclose() 1.0 2.0 2.0
elevatorsecond.Elevator.dooropen() 1.0 2.0 2.0
elevatorsecond.Elevator.Elevator(RequestQueue) 1.0 1.0 1.0
elevatorsecond.Elevator.elevatormove() 1.0 1.0 8.0
elevatorsecond.Elevator.elevatorwait(long) 1.0 2.0 2.0
elevatorsecond.Elevator.inperson(PersonRequest) 1.0 1.0 1.0
elevatorsecond.Elevator.max(int,int) 2.0 1.0 2.0
elevatorsecond.Elevator.min(int,int) 2.0 1.0 2.0
elevatorsecond.Elevator.outperson(PersonRequest) 1.0 1.0 1.0
elevatorsecond.Elevator.run() 3.0 3.0 4.0
elevatorsecond.Elevator.runelevator() 6.0 6.0 10.0
elevatorsecond.Elevator.setDirection() 1.0 1.0 3.0
elevatorsecond.Elevator.setDirection(int,int) 1.0 1.0 2.0
elevatorsecond.Elevator.takeinput() 1.0 2.0 2.0
elevatorsecond.ElevatorMain.main(String[]) 1.0 1.0 1.0
elevatorsecond.Monitor.Monitor(RequestQueue) 1.0 1.0 1.0
elevatorsecond.Monitor.run() 3.0 4.0 4.0
elevatorsecond.RequestQueue.getfromthisfloor(int) 4.0 3.0 4.0
elevatorsecond.RequestQueue.getMaxfloor() 2.0 3.0 4.0
elevatorsecond.RequestQueue.getMinfloor() 2.0 3.0 4.0
elevatorsecond.RequestQueue.hasMoreRequest() 1.0 1.0 1.0
elevatorsecond.RequestQueue.isEmpty() 1.0 1.0 1.0
elevatorsecond.RequestQueue.noMoreRequest() 1.0 1.0 1.0
elevatorsecond.RequestQueue.offer(PersonRequest) 1.0 1.0 1.0
elevatorsecond.RequestQueue.poll() 1.0 3.0 3.0
elevatorsecond.RequestQueue.RequestQueue() 1.0 1.0 1.0
Total 42.0 48.0 68.0
Average 1.6153846153846154 1.8461538461538463 2.6153846153846154

  (2)class

class OCavg WMC
elevatorsecond.Elevator 2.5714285714285716 36.0
elevatorsecond.ElevatorMain 1.0 1.0
elevatorsecond.Monitor 2.0 4.0
elevatorsecond.RequestQueue 2.111111111111111 19.0
Total   60.0
Average 2.3076923076923075 15.0

第三次作业:

 类图:

    

各类方法:

    

时序图:

  

SOLID原则分析:

  单一责任原则:电梯、调度器、请求获取线程都只管自己的事,电梯之间通过Person中的Flag、各个Monitor传递信息,责任划分比较清楚。

  开放封闭原则:电梯类可以new出更多的要求不同的实体。但是Typecheck之类的均只针对这三部电梯的特点确定。

  里氏替换原则:并不满足……没有根据这个原则设计。

  接口分离原则:并没有使用interface来构造代码。

  依赖倒置原则:基本符合,程序结构分明。

复杂度分析:

      (1)method

method ev(G) iv(G) v(G)
elevatorthird.Elevator.doorclose() 1.0 2.0 2.0
elevatorthird.Elevator.dooropen() 1.0 2.0 2.0
elevatorthird.Elevator.Elevator(String,ArrayList,Typecheck,Object,Flag,Object,Object) 1.0 2.0 3.0
elevatorthird.Elevator.elevatormove() 1.0 1.0 4.0
elevatorthird.Elevator.elevatorwait(long) 1.0 2.0 2.0
elevatorthird.Elevator.getDirection() 1.0 1.0 1.0
elevatorthird.Elevator.getFloor() 1.0 1.0 1.0
elevatorthird.Elevator.getInSize() 1.0 1.0 1.0
elevatorthird.Elevator.getMaxfloor() 1.0 7.0 7.0
elevatorthird.Elevator.getMinfloor() 1.0 7.0 7.0
elevatorthird.Elevator.getWaitSize() 1.0 1.0 1.0
elevatorthird.Elevator.inperson(Person) 1.0 1.0 1.0
elevatorthird.Elevator.NoPersonToTake() 4.0 3.0 5.0
elevatorthird.Elevator.outperson(Person) 1.0 1.0 1.0
elevatorthird.Elevator.run() 3.0 3.0 4.0
elevatorthird.Elevator.runelevator() 4.0 6.0 7.0
elevatorthird.Elevator.sendoutperson() 2.0 4.0 5.0
elevatorthird.Elevator.setDirection() 1.0 1.0 3.0
elevatorthird.Elevator.setDirection(int,int) 1.0 1.0 5.0
elevatorthird.Elevator.takeinperson() 2.0 6.0 7.0
elevatorthird.Elevator.waitPersonFull() 1.0 1.0 1.0
elevatorthird.ElevatorMain.main(String[]) 1.0 1.0 1.0
elevatorthird.Flag.Flag() 1.0 1.0 1.0
elevatorthird.Flag.Flag(boolean) 1.0 1.0 1.0
elevatorthird.Flag.getFlag() 1.0 1.0 1.0
elevatorthird.Flag.setFlag() 1.0 1.0 1.0
elevatorthird.Flag.setFlag(boolean) 1.0 1.0 1.0
elevatorthird.Getrequest.Getrequest(RequestQueue) 1.0 1.0 1.0
elevatorthird.Getrequest.run() 3.0 4.0 4.0
elevatorthird.Person.cangoinelevator(String) 3.0 3.0 3.0
elevatorthird.Person.getDirection() 1.0 1.0 1.0
elevatorthird.Person.getFromFloor() 1.0 1.0 1.0
elevatorthird.Person.getNotPutFlag() 1.0 1.0 1.0
elevatorthird.Person.getPersonId() 1.0 1.0 1.0
elevatorthird.Person.getToFloor() 1.0 1.0 1.0
elevatorthird.Person.hashCode() 1.0 1.0 1.0
elevatorthird.Person.inthisFloor() 1.0 1.0 1.0
elevatorthird.Person.Person(int,int,int) 1.0 1.0 1.0
elevatorthird.Person.Person(int,int,int,Flag,Boolean) 1.0 1.0 1.0
elevatorthird.Person.Person(PersonRequest) 1.0 1.0 1.0
elevatorthird.Person.setPutFlag() 1.0 1.0 1.0
elevatorthird.Person.toString() 1.0 1.0 1.0
elevatorthird.RequestQueue.hasMoreRequest() 1.0 1.0 1.0
elevatorthird.RequestQueue.isEmpty() 1.0 1.0 1.0
elevatorthird.RequestQueue.noMoreRequest() 1.0 1.0 1.0
elevatorthird.RequestQueue.offer(Person) 1.0 1.0 1.0
elevatorthird.RequestQueue.poll() 2.0 3.0 3.0
elevatorthird.RequestQueue.RequestQueue() 1.0 1.0 1.0
elevatorthird.Scheduler.addtoelevator(String,Person) 1.0 4.0 4.0
elevatorthird.Scheduler.checkcanbesend(Person) 1.0 1.0 1.0
elevatorthird.Scheduler.checkcanbetake(Person) 1.0 1.0 1.0
elevatorthird.Scheduler.ChooseAnotherElevator(Person,String) 1.0 3.0 3.0
elevatorthird.Scheduler.cutRequestandAdd(String,String,int,Person) 1.0 1.0 1.0
elevatorthird.Scheduler.distrubutePerson() 8.0 19.0 22.0
elevatorthird.Scheduler.endThread() 1.0 1.0 1.0
elevatorthird.Scheduler.FindElevatorandAdd(Person,String,int[],int,int) 7.0 7.0 7.0
elevatorthird.Scheduler.notifyElevator() 1.0 1.0 1.0
elevatorthird.Scheduler.run() 3.0 4.0 4.0
elevatorthird.Scheduler.Scheduler(RequestQueue) 1.0 1.0 1.0
elevatorthird.Typecheck.canbeTakeandSendbyType(String,int,int) 3.0 6.0 6.0
elevatorthird.Typecheck.canbeTakebyOtherTypeBut(String,int,int) 10.0 13.0 16.0
elevatorthird.Typecheck.isAtype(int) 1.0 1.0 1.0
elevatorthird.Typecheck.isBtype(int) 1.0 1.0 1.0
elevatorthird.Typecheck.isCtype(int) 1.0 1.0 1.0
elevatorthird.Typecheck.istype(String,int) 15.0 2.0 19.0
Total 120.0 155.0 194.0
Average 1.8461538461538463 2.3846153846153846 2.9846153846153847

      (2)class

class OCavg WMC
elevatorthird.Elevator 2.761904761904762 58.0
elevatorthird.ElevatorMain 1.0 1.0
elevatorthird.Flag 1.0 5.0
elevatorthird.Getrequest 2.0 4.0
elevatorthird.Person 1.1538461538461537 15.0
elevatorthird.RequestQueue 1.1666666666666667 7.0
elevatorthird.Scheduler 3.4545454545454546 38.0
elevatorthird.Typecheck 5.166666666666667 31.0
Total   159.0
Average 2.4461538461538463 19.875

      个人分析:使用typecheck封装方法类并没有降低程序负责度。Schduler的架构过于简单,导致代码复杂度偏高。

三、bug分析

   三次作业的中强测以及互测都没有发现bug,主要是在自己编写代码以及通过弱测的过程中由于线程不安全以及对于暴力轮询的细节处理而导致的real time error以及CPU time limited。

  在第一次作业中,我一开始使用了ArrayBlockingQueue这个线程安全的阻塞队列,但是由于对它的阻塞方式以及各种方法的返回值没有很清楚的认识,导致了各种奇奇怪怪的bug的出现。后来我改用了CurrentlinkedQueue来存放请求,并在对它的所有操作都加上了锁,确保了线程的安全……

  在第二次作业中,我沿用了第一次作业的成熟架构,主要的bug出现在对于while轮询的处理上,我在调用RequestQueue的poll方法时,若队列为空,则将其elevator线程wait掉,若有新的指令进入或是读到输入结束信号,则进行一个notifyall操作,唤醒沉睡的电梯线程,以此来打破轮询机制。

  在第三次作业中,线程安全以及轮询方面我没有发现什么bug,主要是在调度器处理的细节问题上,发生过几次没有及时return造成的重复拆分指令并塞入队列的操作,造成了很莫名奇妙的输出,还好这种bug很容易发现……

  总而言之,写完这一单元的作业,我深深地体会到了一个成熟地可拓展的架构的重要性,每一次沿用上一次作业的架构以及思想,避过上一次作业踩到的坑,大大提高了写代码的效率以及代码的安全性。回想第一单元作业的重构,一言难尽……

四、测试策略

  编写代码时对自己代码的测试主要包括功能测试以及稳定性测试两个方面。

  拿第三次代码来说,功能测试主要是将一些自己希望实现的功能,比如2-3层的转运,通过在代码中的相应位置使用timeoutput接口手动输出相应的想知道的数据来进行调试,修改代码。

  而稳定性测试,就是构造一堆比较奇怪的代码,包括相同的重复请求,大量需要转运的请求等,同时输入到程序中,观察输出结果是否与预期相符。

  至于互测中对房间里代码的测试,也都是在观察别人代码的架构后,拿自己的测试数据去测试别人的代码。这一单元的互测我觉得主要是一个学习别人代码的机会,包括第6次作业的wait-notify机制、第7次作业的多部电梯协同,都是我在这前一次的互测中阅读了房间里大佬的代码之后学习到的。

五、心得体会

  第一次写多线程的时候一头雾水,被线程安全、阻塞之类的机制绕晕了。但是第二次作业写起来就比较得心应手了,对于线程安全的处理在刚敲代码时就已经有意识地去注意,避免了很多错误。到了第三次作业,就已经把考虑的重点放在了电梯整体实现架构的调整上。所以架构很重要,很重要,很重要……第一单元最后一次作业的惨不忍睹,就是败在了架构的不成熟、不合理上面。从这一单元开始我才开始渐渐认识了面向对象思想,逐级应用在代码里头,而不是每一次AC了就完事,不去管代码的质量。也逐渐认识到了老师和助教大大们的良苦用心……老师学长辛苦了……

 

 

  

转载于:https://www.cnblogs.com/hkywwr/p/10742375.html

你可能感兴趣的文章
HDOJ1002 A+B Problem II
查看>>
ADB server didn't ACK(adb不能开启
查看>>
Python基础(三)
查看>>
Continuous integration
查看>>
前端知识点总结
查看>>
github 在ubuntu 使用--常用命令
查看>>
hl7 V2中Message Control ID的含义及应用
查看>>
IOS 4个容易混淆的属性(textAligment contentVerticalAlignment contentHorizontalAlignment contentMode)...
查看>>
iOS 修改textholder的颜色
查看>>
【资料】wod地城掉落
查看>>
C# FTPHelper(搬运)
查看>>
C#HttpHelper类1.3正式版教程与升级报告
查看>>
【转】Android 语言切换过程分析
查看>>
jpa 多对多关系的实现注解形式
查看>>
Android开发——View绘制过程源码解析(一)
查看>>
Quartz和TopShelf Windows服务作业调度
查看>>
让ie9之前的版本支持canvas
查看>>
排序规则
查看>>
percent的用法
查看>>
中文词频统计
查看>>