v2版本的MapReduce作业中,作业JOB_SETUP_COMPLETED事件的发生,即作业SETUP阶段完成事件,会触发作业由SETUP状态转换到RUNNING状态,而作业状态转换中涉及作业信息的处理,是由SetupCompletedTransition来完成的,它主要做了四件事:

1、通过设置作业Job的成员变量setupProgress为1,标记作业setup已完成;

2、调度作业Job的Map Task;

3、调度作业的JobReduce Task;

4、如果没有task了,则生成JOB_COMPLETED事件并交由作业的事件处理器eventHandler进行处理。

本文,我们就将研究作业Job中Task是如何被调度的。

首先看下SetupCompletedTransition中transition()方法关于作业Job中Task调度的代码,如下:

  1. // 调度作业Job的Map Task
  2. job.scheduleTasks(job.mapTasks, job.numReduceTasks == 0);
  3. // 调度作业Job的Reduce Task
  4. job.scheduleTasks(job.reduceTasks, true);

它实际上是通过Job,也就是JobImpl的scheduleTasks()完成的,这个方法需要两个参数,第一个是作业Job待调度任务的任务ID集合taskIDs,第二个参数是表示是否恢复任务输出的标志位recoverTaskOutput,对于Map-Only型作业中Map任务和所有类型作业的Reduce任务,都需要恢复,标志位recoverTaskOutput为true,具体代码如下:

  1. protected void scheduleTasks(Set<TaskId> taskIDs,
  2. boolean recoverTaskOutput) {
  3. // 遍历传入的任务集合taskIDs中的每个TaskId,对taskID做以下处理:
  4. for (TaskId taskID : taskIDs) {
  5. // 根据taskID从集合completedTasksFromPreviousRun中移除对应元素,并获取被移除的元素TaskInfo实例taskInfo
  6. TaskInfo taskInfo = completedTasksFromPreviousRun.remove(taskID);
  7. if (taskInfo != null) {// 若存在taskID对应任务信息TaskInfo实例taskInfo
  8. // 构造T_RECOVER类型任务恢复事件TaskRecoverEvent,交给eventHandler处理,标志位recoverTaskOutput表示是否恢复任务的输出,
  9. // 对于Map-Only型Map任务和所有的Reduce任务,都需要恢复,标志位recoverTaskOutput为true
  10. eventHandler.handle(new TaskRecoverEvent(taskID, taskInfo,
  11. committer, recoverTaskOutput));
  12. } else {
  13. // 否则,构造T_SCHEDULE类型任务调度事件TaskEvent,交给eventHandler处理
  14. eventHandler.handle(new TaskEvent(taskID, TaskEventType.T_SCHEDULE));
  15. }
  16. }
  17. }

scheduleTasks()方法遍历传入的任务集合taskIDs中的每个TaskId实例taskID,对taskID做以下处理:

1、根据taskID从集合completedTasksFromPreviousRun中移除对应元素,并获取被移除的元素TaskInfo实例taskInfo;

2、若存在taskID对应任务信息TaskInfo实例taskInfo,构造T_RECOVER类型任务恢复事件TaskRecoverEvent,交给eventHandler处理,标志位recoverTaskOutput表示是否恢复任务的输出,对于Map-Only型Map任务和所有的Reduce任务,都需要恢复,标志位recoverTaskOutput为true;

3、否则,构造T_SCHEDULE类型任务调度事件TaskEvent,交给eventHandler处理。

我们先看T_SCHEDULE类型任务调度事件TaskEvent的处理,它是交由Job的eventHandler来处理的,而这个eventHandler是在Job被创建时(即构造JobImpl实例时)由MRAppMaster的dispatcher来赋值的,而在MRAppMaster中,dispatcher被创建后就会注册任务事件的处理器TaskEventDispatcher实例,代码如下:

  1. dispatcher.register(TaskEventType.class, new TaskEventDispatcher());

而这个任务事件处理器TaskEventDispatcher中处理任务事件TaskEvent的handle()方法定义如下:

  1. private class TaskEventDispatcher implements EventHandler<TaskEvent> {
  2. @SuppressWarnings("unchecked")
  3. @Override
  4. public void handle(TaskEvent event) {
  5. Task task = context.getJob(event.getTaskID().getJobId()).getTask(
  6. event.getTaskID());
  7. ((EventHandler<TaskEvent>)task).handle(event);
  8. }
  9. }

它实际上是通过作业Job中相关任务Task的handle()方法来处理的,而这个任务Task的实现则是TaskImpl,其中对于各种任务事件的处理,也是类似作业Job,由一个任务Task的状态机进行处理,关于任务Task的状态机,我们会有专门的文章进行介绍,这里,您只需要知道在TaskImpl中,对于上述两种任务状态机中任务状态的转换、触发事件及事件处理者定义如下:

  1. private static final StateMachineFactory
  2. <TaskImpl, TaskStateInternal, TaskEventType, TaskEvent>
  3. stateMachineFactory
  4. = new StateMachineFactory<TaskImpl, TaskStateInternal, TaskEventType, TaskEvent>
  5. (TaskStateInternal.NEW)
  6. // 省略部分代码
  7. .addTransition(TaskStateInternal.NEW, TaskStateInternal.SCHEDULED,
  8. TaskEventType.T_SCHEDULE, new InitialScheduleTransition())
  9. // 省略部分代码
  10. .addTransition(TaskStateInternal.NEW,
  11. EnumSet.of(TaskStateInternal.FAILED,
  12. TaskStateInternal.KILLED,
  13. TaskStateInternal.RUNNING,
  14. TaskStateInternal.SUCCEEDED),
  15. TaskEventType.T_RECOVER, new RecoverTransition())
  16. // 省略部分代码

由此可见,对于T_RECOVER类型任务恢复事件TaskRecoverEvent,Task状态机指定由RecoverTransition处理,并且任务Task的状态会由NEW转换为RUNNING、FAILED、KILLED、SUCCEEDED等,而对于T_SCHEDULE类型任务调度事件TaskEvent,则由Task状态机指定为InitialScheduleTransition处理,并且任务Task的状态会由NEW转换为SCHEDULED。下面,我们挨个进行分析。

一、T_SCHEDULE类型任务调度事件TaskEvent

由InitialScheduleTransition进行处理,任务Task的状态会由NEW转换为SCHEDULED,InitialScheduleTransition代码如下:

  1. private static class InitialScheduleTransition
  2. implements SingleArcTransition<TaskImpl, TaskEvent> {
  3. @Override
  4. public void transition(TaskImpl task, TaskEvent event) {
  5. // 添加并调度任务运行尝试TaskAttempt,Avataar.VIRGIN表示它是第一个Attempt,
  6. // 而剩余的Avataar.SPECULATIVE表示它是为拖后腿任务开启的一个Attempt,即推测执行原理
  7. task.addAndScheduleAttempt(Avataar.VIRGIN);
  8. // 设置任务的调度时间scheduledTime为当前时间
  9. task.scheduledTime = task.clock.getTime();
  10. // 发送任务启动事件
  11. task.sendTaskStartedEvent();
  12. }
  13. }

InitialScheduleTransition的处理逻辑比较简单,大体如下:

1、调用addAndScheduleAttempt()方法,添加并调度任务运行尝试TaskAttempt,Avataar.VIRGIN表示它是第一个Attempt,而剩余的Avataar.SPECULATIVE表示它是为拖后腿任务开启的一个Attempt,即推测执行原理;

2、设置任务的调度时间scheduledTime为当前时间;

3、发送任务启动事件。

其中,1中的addAndScheduleAttempt()方法实现如下:

  1. // This is always called in the Write Lock
  2. private void addAndScheduleAttempt(Avataar avataar) {
  3. // 调用addAttempt()方法,创建一个任务运行尝试TaskAttempt实例attempt,
  4. // 并将其添加到attempt集合attempts中,还会设置attempt的Avataar属性
  5. TaskAttempt attempt = addAttempt(avataar);
  6. // 将attempt的id添加到正在执行的attempt集合inProgressAttempts中
  7. inProgressAttempts.add(attempt.getID());
  8. //schedule the nextAttemptNumber
  9. // 调度TaskAttempt
  10. // 如果集合failedAttempts大小大于0,说明该Task之前有TaskAttempt失败过,此次为重新调度,
  11. // TaskAttemp事件类型为TA_RESCHEDULE,
  12. if (failedAttempts.size() > 0) {
  13. eventHandler.handle(new TaskAttemptEvent(attempt.getID(),
  14. TaskAttemptEventType.TA_RESCHEDULE));
  15. } else {
  16. // 否则为TaskAttemp事件类型为TA_SCHEDULE
  17. eventHandler.handle(new TaskAttemptEvent(attempt.getID(),
  18. TaskAttemptEventType.TA_SCHEDULE));
  19. }
  20. }

addAndScheduleAttempt()方法处理逻辑如下:

1、调用addAttempt()方法,创建一个任务运行尝试TaskAttempt实例attempt,并将其添加到attempt集合attempts中,还会设置attempt的Avataar属性;

2、将attempt的id添加到正在执行的attempt集合inProgressAttempts中;

3、调度TaskAttempt:如果集合failedAttempts大小大于0,说明该Task之前有TaskAttempt失败过,此次为重新调度,TaskAttemp事件类型为TA_RESCHEDULE,否则为TaskAttemp事件类型为TA_SCHEDULE。

而addAttempt()方法实现如下:

  1. private TaskAttemptImpl addAttempt(Avataar avataar) {
  2. / 调用createAttempt()方法创建任务运行尝试TaskAttemptImpl实例attempt
  3. TaskAttemptImpl attempt = createAttempt();
  4. // 设置attempt的Avataar属性
  5. attempt.setAvataar(avataar);
  6. // 记录debug级别日志信息:Created attempt ... ...
  7. if (LOG.isDebugEnabled()) {
  8. LOG.debug("Created attempt " + attempt.getID());
  9. }
  10. // 将创建的任务运行尝试TaskAttemptImpl实例attempt与其ID的对应关系添加到TaskImpl的任务运行尝试集合attempts中,
  11. // attempts先被初始化为Collections.emptyMap()
  12. // this.attempts = Collections.emptyMap();
  13. switch (attempts.size()) {
  14. case 0:
  15. // 如果attempts大小为0,即为Collections.emptyMap(),则将其更换为Collections.singletonMap(),并加入该TaskAttemptImpl实例attempt
  16. attempts = Collections.singletonMap(attempt.getID(),
  17. (TaskAttempt) attempt);
  18. break;
  19. case 1:
  20. // 如果attempts大小为1,即为Collections.singletonMap(),则将其替换为LinkedHashMap,并加入之前和现在的TaskAttemptImpl实例attempt
  21. Map<TaskAttemptId, TaskAttempt> newAttempts
  22. = new LinkedHashMap<TaskAttemptId, TaskAttempt>(maxAttempts);
  23. newAttempts.putAll(attempts);
  24. attempts = newAttempts;
  25. attempts.put(attempt.getID(), attempt);
  26. break;
  27. default:
  28. // 如果attempts大小大于1,说明其实一个LinkedHashMap,直接put吧
  29. attempts.put(attempt.getID(), attempt);
  30. break;
  31. }
  32. // 累加TaskAttempt计数器nextAttemptNumber
  33. ++nextAttemptNumber;
  34. // 返回TaskAttemptImpl实例attempt
  35. return attempt;
  36. }

其处理逻辑如下:

1、调用createAttempt()方法创建任务运行尝试TaskAttemptImpl实例attempt;

2、设置attempt的Avataar属性;

3、记录debug级别日志信息:Created attempt ... ...;

4、将创建的任务运行尝试TaskAttemptImpl实例attempt与其ID的对应关系添加到TaskImpl的任务运行尝试集合attempts中,attempts先被初始化为Collections.emptyMap():

4.1、如果attempts大小为0,即为Collections.emptyMap(),则将其更换为Collections.singletonMap(),并加入该TaskAttemptImpl实例attempt;

4.2、如果attempts大小为1,即为Collections.singletonMap(),则将其替换为LinkedHashMap,并加入之前和现在的TaskAttemptImpl实例attempt;

4.3、如果attempts大小大于1,说明其实一个LinkedHashMap,直接put吧;

5、累加TaskAttempt计数器nextAttemptNumber;

6、返回TaskAttemptImpl实例attempt。

继续往下追踪createAttempt()方法,其在TaskImpl中代码如下:

  1. protected abstract TaskAttemptImpl createAttempt();

这是一个抽象方法,由其子类实现,而它的子类有两个,表示Map任务的MapTaskImpl和表示Reduce任务的ReduceTaskImpl,其createAttempt()方法分别实现如下:

1、MapTaskImpl.createAttempt()

  1. @Override
  2. protected TaskAttemptImpl createAttempt() {
  3. return new MapTaskAttemptImpl(getID(), nextAttemptNumber,
  4. eventHandler, jobFile,
  5. partition, taskSplitMetaInfo, conf, taskAttemptListener,
  6. jobToken, credentials, clock, appContext);
  7. }

生成一个MapTaskAttemptImpl实例,传入表示Attempt序号的nextAttemptNumber、事件处理器eventHandler、作业文件jobFile、分区信息partition、分片元数据信息taskSplitMetaInfo等关键变量。

2、ReduceTaskImpl.createAttempt()

  1. @Override
  2. protected TaskAttemptImpl createAttempt() {
  3. return new ReduceTaskAttemptImpl(getID(), nextAttemptNumber,
  4. eventHandler, jobFile,
  5. partition, numMapTasks, conf, taskAttemptListener,
  6. jobToken, credentials, clock, appContext);
  7. }

生成一个ReduceTaskAttemptImpl实例,除不需要分片元数据信息taskSplitMetaInfo,和需要一个Map任务数numMapTasks外,其他与MapTaskAttemptImpl基本相同。

TaskAttempt生成了,接下来就应该进行调度执行了。我们再折回去看看addAndScheduleAttempt()方法中,发送的TA_SCHEDULE或TA_RESCHEDULE类型的TaskAttemptEvent,其与JobImpl、TaskImpl一样,是由TaskAttempt状态机负责处理的,如下所示:

  1. // 在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,
  2. // TaskAttempt的状态由NEW转换成UNASSIGNED
  3. .addTransition(TaskAttemptStateInternal.NEW, TaskAttemptStateInternal.UNASSIGNED,
  4. TaskAttemptEventType.TA_SCHEDULE, new RequestContainerTransition(false))
  5. // 在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,
  6. // TaskAttempt的状态由NEW转换成UNASSIGNED
  7. .addTransition(TaskAttemptStateInternal.NEW, TaskAttemptStateInternal.UNASSIGNED,
  8. TaskAttemptEventType.TA_RESCHEDULE, new RequestContainerTransition(true))
  9. // 上述二者的区别是RequestContainerTransition传入的标志位rescheduled,前者为false,后者为true

在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,TaskAttempt的状态由NEW转换成UNASSIGNED;在事件TaskAttemptEventType.TA_SCHEDULE的触发下,经过RequestContainerTransition的处理,TaskAttempt的状态由NEW转换成UNASSIGNED;上述二者的区别是RequestContainerTransition传入的标志位rescheduled,前者为false,后者为true。

我们再看下RequestContainerTransition的实现,代码如下:

  1. @SuppressWarnings("unchecked")
  2. @Override
  3. public void transition(TaskAttemptImpl taskAttempt,
  4. TaskAttemptEvent event) {
  5. // Tell any speculator that we're requesting a container
  6. // taskAttempt的事件处理器eventHandler处理SpeculatorEvent事件,告诉所有的speculator,此时正在申请一个容器
  7. taskAttempt.eventHandler.handle
  8. (new SpeculatorEvent(taskAttempt.getID().getTaskId(), +1));
  9. //request for container
  10. // 申请容器
  11. if (rescheduled) {// Task的Attempt重新调度
  12. // 构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,
  13. // 这个eventHandler实际上是MRAppMaster中的dispatcher,依次经过TaskImpl、TaskAttemptImpl的创建传递过来的,
  14. taskAttempt.eventHandler.handle(
  15. ContainerRequestEvent.createContainerRequestEventForFailedContainer(
  16. taskAttempt.attemptId,
  17. taskAttempt.resourceCapability));
  18. } else {// Task的Attempt第一次调度
  19. // 构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,
  20. taskAttempt.eventHandler.handle(new ContainerRequestEvent(
  21. taskAttempt.attemptId, taskAttempt.resourceCapability,
  22. taskAttempt.dataLocalHosts.toArray(
  23. new String[taskAttempt.dataLocalHosts.size()]),
  24. taskAttempt.dataLocalRacks.toArray(
  25. new String[taskAttempt.dataLocalRacks.size()])));
  26. }
  27. // 两者创建的ContainerRequestEvent事件的区别是,rescheduled时,不需要考虑Node和Lock位置属性,因为此时Attempt之前已经失败过,此时应当能够以完成Attempt为首要任务,
  28. // 同时,两者的事件类型都是ContainerAllocator.EventType.CONTAINER_REQ,
  29. // MRAppMaster中的dispatcher针对该事件ContainerAllocator.EventType注册的事件处理器是LocalContainerAllocator或RMContainerAllocator
  30. }

RequestContainerTransition的transition()方法处理逻辑如下:

1、TaskAttempt的事件处理器eventHandler处理SpeculatorEvent事件,告诉所有的speculator,此时正在申请一个容器;

2、申请容器:

2.1、如果是Task的Attempt重新调度,构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理,这个eventHandler实际上是MRAppMaster中的dispatcher,依次经过TaskImpl、TaskAttemptImpl的创建传递过来的;

2.2、否则如果是Task的Attempt第一次调度,构造容器申请事件ContainerRequestEvent,并交由taskAttempt的事件处理器eventHandler处理。

两者创建的ContainerRequestEvent事件的区别是,rescheduled时,不需要考虑Node和Lock位置属性,因为此时Attempt之前已经失败过,此时应当能够以完成Attempt为首要任务,同时,两者的事件类型都是ContainerAllocator.EventType.CONTAINER_REQ,MRAppMaster中的dispatcher针对该事件ContainerAllocator.EventType注册的事件处理器是LocalContainerAllocator或RMContainerAllocator。

关于Yarn容器等资源申请与分配RMContainerAllocator的介绍,我会在以后的文章中为大家讲解,这里,你只需要了解其执行的大体流程即可:

1、RMContainerAllocator首先间接继承自AbstractService,它是Hadoop中的一种服务,有服务初始化serviceInit()及服务启动serviceStart()方法要执行;

2、RMContainerAllocator针对容器请求分配事件,是一个双重生产者-消费者模式,第一层生产者通过其handle()方法,将容器请求分配ContainerAllocatorEvent加入其内部eventQueue队列,第一层消费者通过其内部事件处理线程eventHandlingThread,不断的从事件队列eventQueue中take事件进行消费,而消费的方式是做为第二层生产者,将事件按照任务类型放入调度请求列表scheduledRequests、pendingReduces中,scheduledRequests是一个复杂的区分Map和Reduce任务的会立即被调度的请求列表,而pendingReduces只是存储等待被调度的Reduce任务请求的列表,其会根据Yarn中资源情况和Map任务完成情况确定是将事件移送至(即rampUp)scheduledRequests,还是从scheduledRequests移回Reduce任务调度请求至pendingReduces(即rampDown),而第二层的消费者则是RMContainerAllocator祖先父类RMCommunicator中的心跳线程allocatorThread,它周期性的调用heartbeat()方法,从Yarn的RM中获取可用资源,然后消费scheduledRequests列表中的请求,进行容器分配;

3、RMContainerAllocator中,对于Map任务来说,它经历的数据结构,或者生命周期为scheduled->assigned->completed,而Reduce任务则是pending->scheduled->assigned->completed;

4、经过一些的复杂逻辑后,包括综合判断资源情况、任务本地性、优先调度失败任务、Map任务完成比例、针对拖后退的任务进行推测执行等,无论是Map任务还是Reduce任务,最终在分配到容器Container后,都会发送一个TaskAttemptContainerAssignedEvent事件,交由TaskAttemptImpl的状态机中ContainerAssignedTransition进行处理,而其方法则最终会构造ContainerRemoteLaunchEvent事件,进行Container远程加载,在远程或本机或本进程Container中Launch任务尝试进行任务的执行。

关于RMContainerAllocator,因为其结构、处理逻辑比较复杂,我会专门写文章进行分析,敬请期待!

二、T_RECOVER类型任务恢复事件TaskRecoverEvent

未完待续!敬请关注后续文章!

Yarn源码分析之MapReduce作业中任务Task调度整体流程(一)的更多相关文章

  1. Yarn源码分析之如何确定作业运行方式Uber or Non-Uber?

    在MRAppMaster中,当MapReduce作业初始化时,它会通过作业状态机JobImpl中InitTransition的transition()方法,进行MapReduce作业初始化相关操作,而 ...

  2. Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(二)

    本文继<Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)>,接着讲述MapReduce作业在MRAppMaster上处理总流程,继上篇讲到作业初始化之后的作 ...

  3. Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)

    我们知道,如果想要在Yarn上运行MapReduce作业,仅需实现一个ApplicationMaster组件即可,而MRAppMaster正是MapReduce在Yarn上ApplicationMas ...

  4. Android多线程之(一)View.post()源码分析——在子线程中更新UI

    提起View.post(),相信不少童鞋一点都不陌生,它用得最多的有两个功能,使用简便而且实用: 1)在子线程中更新UI.从子线程中切换到主线程更新UI,不需要额外new一个Handler实例来实现. ...

  5. Hbase源码分析:Hbase UI中Requests Per Second的具体含义

    Hbase源码分析:Hbase UI中Requests Per Second的具体含义 让运维加监控,被问到Requests Per Second(见下图)的具体含义是什么?我一时竟回答不上来,虽然大 ...

  6. [源码分析] 分布式任务队列 Celery 之 发送Task & AMQP

    [源码分析] 分布式任务队列 Celery 之 发送Task & AMQP 目录 [源码分析] 分布式任务队列 Celery 之 发送Task & AMQP 0x00 摘要 0x01 ...

  7. Hadoop2源码分析-MapReduce篇

    1.概述 前面我们已经对Hadoop有了一个初步认识,接下来我们开始学习Hadoop的一些核心的功能,其中包含mapreduce,fs,hdfs,ipc,io,yarn,今天为大家分享的是mapred ...

  8. ABP源码分析二:ABP中配置的注册和初始化

    一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...

  9. 【源码分析】- 在SpringBoot中你会使用REST风格处理请求吗?

    ​ 目录 前言 1.什么是 REST 风格 1.1  资源(Resources) 1.2  表现层(Representation) 1.3  状态转化(State Transfer) 1.4  综述 ...

随机推荐

  1. 【棋盘DP】【OpenJudge7614】最低通行费

    最低通行费 总时间限制: 1000ms 内存限制: 65536kB [描述] 一个商人穿过一个 N*N 的正方形的网格,去参加一个非常重要的商务活动.他要从网格的左上角进,右下角出.每穿越中间1个小方 ...

  2. ext js layout and tree

    数据     <configuration> <configSections> <section name="hibernate-configuration&q ...

  3. VB6的UTF8编码解码

    'UTF-8编码  Public Function UTF8Encode(ByVal szInput As String) As String     Dim wch  As String     D ...

  4. Git服务器分类

    目录(?)[-] 服务器上的 Git 协议 本地协议 优点 缺点 SSH 协议 优点 缺点 Git 协议 优点 缺点 HTTPS 协议 优点 缺点 在服务器部署 Git 将纯目录转移到服务器 小型安装 ...

  5. subscription group permisson

  6. netty4 ServerBootstrap.bind(port) debug

    代码是netty4自带的例子 我们在bind的行前加个断电,下面是ServerBootstrap.bind(port)方法所经历的一些步骤. 在AbstractBootstrap.initAndReg ...

  7. Win7如何开启管理员账户

    打开运行对话框,在LUSRMGR.MSC里,左边点用户,在右边栏里右击Administrator选择属性,去掉账户已禁用这个选项前面的勾.我也问过这个问题,确实如版主说的这样可解决这个问题,但有个问题 ...

  8. Android下的数据存储与訪问 --- 以文件的形式

    Android下的数据存储与訪问 --- 以文件的形式 1.1 储存文件存放在手机内存中: // *** 储存数据到 /data/data/包名/files/jxn.txt文件里 String dat ...

  9. Js中数组的追加

    Concat arrayObject.concat(arrayX,arrayX,......,arrayX) 常用于 加载更多 ,数组的追加.

  10. .Net之路(十五)图解LoadRunner压力測试

    在项目编码阶段结束后,就须要进行软件測试. 成为软件开发过程中一个不可缺少的环节.而自己主动化測试也是将逐步取代人工繁杂的測试.压力測试就是软件測试对软件性能评估的一个方面,以下就简介我在使用load ...