/** Spark SQL源码分析系列文章*/

接上一篇文章Spark SQL Catalyst源码分析之Physical Plan,本文将介绍Physical Plan的toRDD的具体实现细节:

我们都知道一段sql,真正的执行是当你调用它的collect()方法才会执行Spark Job,最后计算得到RDD。

  1. lazy val toRdd: RDD[Row] = executedPlan.execute()

Spark Plan基本包含4种操作类型,即BasicOperator基本类型,还有就是Join、Aggregate和Sort这种稍复杂的。

如图:

一、BasicOperator

1.1、Project

  Project 的大致含义是:传入一系列表达式Seq[NamedExpression],给定输入的Row,经过Convert(Expression的计算eval)操作,生成一个新的Row。
  Project的实现是调用其child.execute()方法,然后调用mapPartitions对每一个Partition进行操作。
  这个f函数其实是new了一个MutableProjection,然后循环的对每个partition进行Convert。
  1. case class Project(projectList: Seq[NamedExpression], child: SparkPlan) extends UnaryNode {
  2. override def output = projectList.map(_.toAttribute)
  3. override def execute() = child.execute().mapPartitions { iter => //对每个分区进行f映射
  4. @transient val reusableProjection = new MutableProjection(projectList)
  5. iter.map(reusableProjection)
  6. }
  7. }

通过观察MutableProjection的定义,可以发现,就是bind references to a schema 和 eval的过程:

  将一个Row转换为另一个已经定义好schema column的Row。
  如果输入的Row已经有Schema了,则传入的Seq[Expression]也会bound到当前的Schema。
  1. case class MutableProjection(expressions: Seq[Expression]) extends (Row => Row) {
  2. def this(expressions: Seq[Expression], inputSchema: Seq[Attribute]) =
  3. this(expressions.map(BindReferences.bindReference(_, inputSchema))) //bound schema
  4. private[this] val exprArray = expressions.toArray
  5. private[this] val mutableRow = new GenericMutableRow(exprArray.size) //新的Row
  6. def currentValue: Row = mutableRow
  7. def apply(input: Row): Row = {
  8. var i = 0
  9. while (i < exprArray.length) {
  10. mutableRow(i) = exprArray(i).eval(input)  //根据输入的input,即一个Row,计算生成的Row
  11. i += 1
  12. }
  13. mutableRow //返回新的Row
  14. }
  15. }

1.2、Filter

 Filter的具体实现是传入的condition进行对input row的eval计算,最后返回的是一个Boolean类型,
 如果表达式计算成功,返回true,则这个分区的这条数据就会保存下来,否则会过滤掉。
  1. case class Filter(condition: Expression, child: SparkPlan) extends UnaryNode {
  2. override def output = child.output
  3. override def execute() = child.execute().mapPartitions { iter =>
  4. iter.filter(condition.eval(_).asInstanceOf[Boolean]) //计算表达式 eval(input row)
  5. }
  6. }

1.3、Sample

  Sample取样操作其实是调用了child.execute()的结果后,返回的是一个RDD,对这个RDD调用其sample函数,原生方法。
  1. case class Sample(fraction: Double, withReplacement: Boolean, seed: Long, child: SparkPlan)
  2. extends UnaryNode
  3. {
  4. override def output = child.output
  5. // TODO: How to pick seed?
  6. override def execute() = child.execute().sample(withReplacement, fraction, seed)
  7. }

1.4、Union

  Union操作支持多个子查询的Union,所以传入的child是一个Seq[SparkPlan]
  execute()方法的实现是对其所有的children,每一个进行execute(),即select查询的结果集合RDD。
  通过调用SparkContext的union方法,将所有子查询的结果合并起来。
  1. case class Union(children: Seq[SparkPlan])(@transient sqlContext: SQLContext) extends SparkPlan {
  2. // TODO: attributes output by union should be distinct for nullability purposes
  3. override def output = children.head.output
  4. override def execute() = sqlContext.sparkContext.union(children.map(_.execute())) //子查询的结果进行union
  5. override def otherCopyArgs = sqlContext :: Nil
  6. }

1.5、Limit

  Limit操作在RDD的原生API里也有,即take().
  但是Limit的实现分2种情况:
  第一种是 limit作为结尾的操作符,即select xxx from yyy limit zzz。 并且是被executeCollect调用,则直接在driver里使用take方法。
  第二种是 limit不是作为结尾的操作符,即limit后面还有查询,那么就在每个分区调用limit,最后repartition到一个分区来计算global limit.
  1. case class Limit(limit: Int, child: SparkPlan)(@transient sqlContext: SQLContext)
  2. extends UnaryNode {
  3. // TODO: Implement a partition local limit, and use a strategy to generate the proper limit plan:
  4. // partition local limit -> exchange into one partition -> partition local limit again
  5. override def otherCopyArgs = sqlContext :: Nil
  6. override def output = child.output
  7. override def executeCollect() = child.execute().map(_.copy()).take(limit) //直接在driver调用take
  8. override def execute() = {
  9. val rdd = child.execute().mapPartitions { iter =>
  10. val mutablePair = new MutablePair[Boolean, Row]()
  11. iter.take(limit).map(row => mutablePair.update(false, row)) //每个分区先计算limit
  12. }
  13. val part = new HashPartitioner(1)
  14. val shuffled = new ShuffledRDD[Boolean, Row, Row, MutablePair[Boolean, Row]](rdd, part) //需要shuffle,来repartition
  15. shuffled.setSerializer(new SparkSqlSerializer(new SparkConf(false)))
  16. shuffled.mapPartitions(_.take(limit).map(_._2)) //最后单独一个partition来take limit
  17. }
  18. }

1.6、TakeOrdered

  TakeOrdered是经过排序后的limit N,一般是用在sort by 操作符后的limit。
  可以简单理解为TopN操作符。
  1. case class TakeOrdered(limit: Int, sortOrder: Seq[SortOrder], child: SparkPlan)
  2. (@transient sqlContext: SQLContext) extends UnaryNode {
  3. override def otherCopyArgs = sqlContext :: Nil
  4. override def output = child.output
  5. @transient
  6. lazy val ordering = new RowOrdering(sortOrder) //这里是通过RowOrdering来实现排序的
  7. override def executeCollect() = child.execute().map(_.copy()).takeOrdered(limit)(ordering)
  8. // TODO: Terminal split should be implemented differently from non-terminal split.
  9. // TODO: Pick num splits based on |limit|.
  10. override def execute() = sqlContext.sparkContext.makeRDD(executeCollect(), 1)
  11. }

1.7、Sort

  Sort也是通过RowOrdering这个类来实现排序的,child.execute()对每个分区进行map,每个分区根据RowOrdering的order来进行排序,生成一个新的有序集合。
  也是通过调用Spark RDD的sorted方法来实现的。
  1. case class Sort(
  2. sortOrder: Seq[SortOrder],
  3. global: Boolean,
  4. child: SparkPlan)
  5. extends UnaryNode {
  6. override def requiredChildDistribution =
  7. if (global) OrderedDistribution(sortOrder) :: Nil else UnspecifiedDistribution :: Nil
  8. @transient
  9. lazy val ordering = new RowOrdering(sortOrder) //排序顺序
  10. override def execute() = attachTree(this, "sort") {
  11. // TODO: Optimize sorting operation?
  12. child.execute()
  13. .mapPartitions(
  14. iterator => iterator.map(_.copy()).toArray.sorted(ordering).iterator, //每个分区调用sorted方法,传入<span style="font-family: Arial, Helvetica, sans-serif;">ordering排序规则,进行排序</span>
  15. preservesPartitioning = true)
  16. }
  17. override def output = child.output
  18. }

1.8、ExistingRdd

ExistingRdd是
  1. object ExistingRdd {
  2. def convertToCatalyst(a: Any): Any = a match {
  3. case o: Option[_] => o.orNull
  4. case s: Seq[Any] => s.map(convertToCatalyst)
  5. case p: Product => new GenericRow(p.productIterator.map(convertToCatalyst).toArray)
  6. case other => other
  7. }
  8. def productToRowRdd[A <: Product](data: RDD[A]): RDD[Row] = {
  9. data.mapPartitions { iterator =>
  10. if (iterator.isEmpty) {
  11. Iterator.empty
  12. } else {
  13. val bufferedIterator = iterator.buffered
  14. val mutableRow = new GenericMutableRow(bufferedIterator.head.productArity)
  15. bufferedIterator.map { r =>
  16. var i = 0
  17. while (i < mutableRow.length) {
  18. mutableRow(i) = convertToCatalyst(r.productElement(i))
  19. i += 1
  20. }
  21. mutableRow
  22. }
  23. }
  24. }
  25. }
  26. def fromProductRdd[A <: Product : TypeTag](productRdd: RDD[A]) = {
  27. ExistingRdd(ScalaReflection.attributesFor[A], productToRowRdd(productRdd))
  28. }
  29. }
 

二、 Join Related Operators

HashJoin:

  在讲解Join Related Operator之前,有必要了解一下HashJoin这个位于execution包下的joins.scala文件里的trait。
  Join操作主要包含BroadcastHashJoinLeftSemiJoinHashShuffledHashJoin均实现了HashJoin这个trait.
  主要类图如下:
  
  
  HashJoin这个trait的主要成员有:
  buildSide是左连接还是右连接,有一种基准的意思。
  leftKeys是左孩子的expressions, rightKeys是右孩子的expressions。
  left是左孩子物理计划,right是右孩子物理计划。
  buildSideKeyGenerator是一个Projection是根据传入的Row对象来计算buildSide的Expression的。
  streamSideKeyGenerator是一个MutableProjection是根据传入的Row对象来计算streamSide的Expression的。
  这里buildSide如果是left的话,可以理解为buildSide是左表,那么去连接这个左表的右表就是streamSide。
  
  HashJoin关键的操作是joinIterators,简单来说就是join两个表,把每个表看着Iterators[Row].
  方式:
  1、首先遍历buildSide,计算buildKeys然后利用一个HashMap,形成 (buildKeys, Iterators[Row])的格式。
  2、遍历StreamedSide,计算streamedKey,去HashMap里面去匹配key,来进行join
  3、最后生成一个joinRow,这个将2个row对接。
  见代码注释:
  1. trait HashJoin {
  2. val leftKeys: Seq[Expression]
  3. val rightKeys: Seq[Expression]
  4. val buildSide: BuildSide
  5. val left: SparkPlan
  6. val right: SparkPlan
  7. lazy val (buildPlan, streamedPlan) = buildSide match {  //模式匹配,将physical plan封装形成Tuple2,如果是buildLeft,那么就是(left,right),否则是(right,left)
  8. case BuildLeft => (left, right)
  9. case BuildRight => (right, left)
  10. }
  11. lazy val (buildKeys, streamedKeys) = buildSide match { //模式匹配,将expression进行封装<span style="font-family: Arial, Helvetica, sans-serif;">Tuple2</span>
  12. case BuildLeft => (leftKeys, rightKeys)
  13. case BuildRight => (rightKeys, leftKeys)
  14. }
  15. def output = left.output ++ right.output
  16. @transient lazy val buildSideKeyGenerator = new Projection(buildKeys, buildPlan.output) //生成buildSideKey来根据Expression来计算Row返回结果
  17. @transient lazy val streamSideKeyGenerator = //<span style="font-family: Arial, Helvetica, sans-serif;">生成</span><span style="font-family: Arial, Helvetica, sans-serif;">streamSideKeyGenerator</span><span style="font-family: Arial, Helvetica, sans-serif;">来根据Expression来计算Row返回结果</span>
  18. () => new MutableProjection(streamedKeys, streamedPlan.output)
  19. def joinIterators(buildIter: Iterator[Row], streamIter: Iterator[Row]): Iterator[Row] = { //把build表的Iterator[Row]和streamIterator[Row]进行join操作返回Join后的Iterator[Row]
  20. // TODO: Use Spark's HashMap implementation.
  21. val hashTable = new java.util.HashMap[Row, ArrayBuffer[Row]]() //匹配主要使用HashMap实现
  22. var currentRow: Row = null
  23. // Create a mapping of buildKeys -> rows
  24. while (buildIter.hasNext) { //目前只对build Iterator进行迭代,形成rowKey,Rows,类似wordCount,但是这里不是累加Value,而是Row的集合。
  25. currentRow = buildIter.next()
  26. val rowKey = buildSideKeyGenerator(currentRow) //计算rowKey作为HashMap的key
  27. if(!rowKey.anyNull) {
  28. val existingMatchList = hashTable.get(rowKey)
  29. val matchList = if (existingMatchList == null) {
  30. val newMatchList = new ArrayBuffer[Row]()
  31. hashTable.put(rowKey, newMatchList) //(rowKey, matchedRowList)
  32. newMatchList
  33. } else {
  34. existingMatchList
  35. }
  36. matchList += currentRow.copy() //返回matchList
  37. }
  38. }
  39. new Iterator[Row] { //最后用streamedRow的Key来匹配buildSide端的HashMap
  40. private[this] var currentStreamedRow: Row = _
  41. private[this] var currentHashMatches: ArrayBuffer[Row] = _
  42. private[this] var currentMatchPosition: Int = -1
  43. // Mutable per row objects.
  44. private[this] val joinRow = new JoinedRow
  45. private[this] val joinKeys = streamSideKeyGenerator()
  46. override final def hasNext: Boolean =
  47. (currentMatchPosition != -1 && currentMatchPosition < currentHashMatches.size) ||
  48. (streamIter.hasNext && fetchNext())
  49. override final def next() = {
  50. val ret = buildSide match {
  51. case BuildRight => joinRow(currentStreamedRow, currentHashMatches(currentMatchPosition)) //右连接的话,streamedRow放左边,匹配到的key的Row放到右表
  52. case BuildLeft => joinRow(currentHashMatches(currentMatchPosition), currentStreamedRow) //左连接的话,相反。
  53. }
  54. currentMatchPosition += 1
  55. ret
  56. }
  57. /**
  58. * Searches the streamed iterator for the next row that has at least one match in hashtable.
  59. *
  60. * @return true if the search is successful, and false if the streamed iterator runs out of
  61. *         tuples.
  62. */
  63. private final def fetchNext(): Boolean = {
  64. currentHashMatches = null
  65. currentMatchPosition = -1
  66. while (currentHashMatches == null && streamIter.hasNext) {
  67. currentStreamedRow = streamIter.next()
  68. if (!joinKeys(currentStreamedRow).anyNull) {
  69. currentHashMatches = hashTable.get(joinKeys.currentValue) //streamedRow从buildSide里的HashTable里面匹配rowKey
  70. }
  71. }
  72. if (currentHashMatches == null) {
  73. false
  74. } else {
  75. currentMatchPosition = 0
  76. true
  77. }
  78. }
  79. }
  80. }
  81. }
joinRow的实现,实现2个Row对接:
实际上就是生成一个新的Array,将2个Array合并。
  1. class JoinedRow extends Row {
  2. private[this] var row1: Row = _
  3. private[this] var row2: Row = _
  4. .........
  5. def copy() = {
  6. val totalSize = row1.size + row2.size
  7. val copiedValues = new Array[Any](totalSize)
  8. var i = 0
  9. while(i < totalSize) {
  10. copiedValues(i) = apply(i)
  11. i += 1
  12. }
  13. new GenericRow(copiedValues) //返回一个新的合并后的Row
  14. }

2.1、LeftSemiJoinHash

 left semi join,不多说了,hive早期版本里替代IN和EXISTS 的版本。
 将右表的join keys放到HashSet里,然后遍历左表,查找左表的join key是否能匹配。
  1. case class LeftSemiJoinHash(
  2. leftKeys: Seq[Expression],
  3. rightKeys: Seq[Expression],
  4. left: SparkPlan,
  5. right: SparkPlan) extends BinaryNode with HashJoin {
  6. val buildSide = BuildRight //buildSide是以右表为基准
  7. override def requiredChildDistribution =
  8. ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
  9. override def output = left.output
  10. def execute() = {
  11. buildPlan.execute().zipPartitions(streamedPlan.execute()) { (buildIter, streamIter) => //右表的物理计划执行后生成RDD,利用zipPartitions对Partition进行合并。然后用上述方法实现。
  12. val hashSet = new java.util.HashSet[Row]()
  13. var currentRow: Row = null
  14. // Create a Hash set of buildKeys
  15. while (buildIter.hasNext) {
  16. currentRow = buildIter.next()
  17. val rowKey = buildSideKeyGenerator(currentRow)
  18. if(!rowKey.anyNull) {
  19. val keyExists = hashSet.contains(rowKey)
  20. if (!keyExists) {
  21. hashSet.add(rowKey)
  22. }
  23. }
  24. }
  25. val joinKeys = streamSideKeyGenerator()
  26. streamIter.filter(current => {
  27. !joinKeys(current).anyNull && hashSet.contains(joinKeys.currentValue)
  28. })
  29. }
  30. }
  31. }

2.2、BroadcastHashJoin

 名约: 广播HashJoin,呵呵。
  是InnerHashJoin的实现。这里用到了concurrent并发里的future,异步的广播buildPlan的表执行后的的RDD。
  如果接收到了广播后的表,那么就用streamedPlan来匹配这个广播的表。
  实现是RDD的mapPartitions和HashJoin里的joinIterators最后生成join的结果。
  1. case class BroadcastHashJoin(
  2. leftKeys: Seq[Expression],
  3. rightKeys: Seq[Expression],
  4. buildSide: BuildSide,
  5. left: SparkPlan,
  6. right: SparkPlan)(@transient sqlContext: SQLContext) extends BinaryNode with HashJoin {
  7. override def otherCopyArgs = sqlContext :: Nil
  8. override def outputPartitioning: Partitioning = left.outputPartitioning
  9. override def requiredChildDistribution =
  10. UnspecifiedDistribution :: UnspecifiedDistribution :: Nil
  11. @transient
  12. lazy val broadcastFuture = future {  //利用SparkContext广播表
  13. sqlContext.sparkContext.broadcast(buildPlan.executeCollect())
  14. }
  15. def execute() = {
  16. val broadcastRelation = Await.result(broadcastFuture, 5.minute)
  17. streamedPlan.execute().mapPartitions { streamedIter =>
  18. joinIterators(broadcastRelation.value.iterator, streamedIter) //调用joinIterators对每个分区map
  19. }
  20. }
  21. }

2.3、ShuffleHashJoin

ShuffleHashJoin顾名思义就是需要shuffle数据,outputPartitioning是左孩子的的Partitioning。
会根据这个Partitioning进行shuffle。然后利用SparkContext里的zipPartitions方法对每个分区进行zip。
这里的requiredChildDistribution,的是ClusteredDistribution,这个会在HashPartitioning里面进行匹配。
关于这里面的分区这里不赘述,可以去org.apache.spark.sql.catalyst.plans.physical下的partitioning里面去查看。
  1. case class ShuffledHashJoin(
  2. leftKeys: Seq[Expression],
  3. rightKeys: Seq[Expression],
  4. buildSide: BuildSide,
  5. left: SparkPlan,
  6. right: SparkPlan) extends BinaryNode with HashJoin {
  7. override def outputPartitioning: Partitioning = left.outputPartitioning
  8. override def requiredChildDistribution =
  9. ClusteredDistribution(leftKeys) :: ClusteredDistribution(rightKeys) :: Nil
  10. def execute() = {
  11. buildPlan.execute().zipPartitions(streamedPlan.execute()) {
  12. (buildIter, streamIter) => joinIterators(buildIter, streamIter)
  13. }
  14. }
  15. }

未完待续 :)

 

原创文章,转载请注明:

转载自:OopsOutOfMemory盛利的Blog,作者: OopsOutOfMemory

本文链接地址:http://blog.csdn.net/oopsoom/article/details/38274621

注:本文基于署名-非商业性使用-禁止演绎 2.5 中国大陆(CC BY-NC-ND 2.5 CN)协议,欢迎转载、转发和评论,但是请保留本文作者署名和文章链接。如若需要用于商业目的或者与授权方面的协商,请联系我。

转自:http://blog.csdn.net/oopsoom/article/details/38274621

第七篇:Spark SQL 源码分析之Physical Plan 到 RDD的具体实现的更多相关文章

  1. 【Spark SQL 源码分析系列文章】

    从决定写Spark SQL源码分析的文章,到现在一个月的时间里,陆陆续续差不多快完成了,这里也做一个整合和索引,方便大家阅读,这里给出阅读顺序 :) 第一篇 Spark SQL源码分析之核心流程 第二 ...

  2. 第十一篇:Spark SQL 源码分析之 External DataSource外部数据源

    上周Spark1.2刚发布,周末在家没事,把这个特性给了解一下,顺便分析下源码,看一看这个特性是如何设计及实现的. /** Spark SQL源码分析系列文章*/ (Ps: External Data ...

  3. 第六篇:Spark SQL Catalyst源码分析之Physical Plan

    /** Spark SQL源码分析系列文章*/ 前面几篇文章主要介绍的是spark sql包里的的spark sql执行流程,以及Catalyst包内的SqlParser,Analyzer和Optim ...

  4. 第一篇:Spark SQL源码分析之核心流程

    /** Spark SQL源码分析系列文章*/ 自从去年Spark Submit 2013 Michael Armbrust分享了他的Catalyst,到至今1年多了,Spark SQL的贡献者从几人 ...

  5. 第十篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 query

    /** Spark SQL源码分析系列文章*/ 前面讲到了Spark SQL In-Memory Columnar Storage的存储结构是基于列存储的. 那么基于以上存储结构,我们查询cache在 ...

  6. 第九篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 cache table

    /** Spark SQL源码分析系列文章*/ Spark SQL 可以将数据缓存到内存中,我们可以见到的通过调用cache table tableName即可将一张表缓存到内存中,来极大的提高查询效 ...

  7. Spark SQL 源代码分析之Physical Plan 到 RDD的详细实现

    /** Spark SQL源代码分析系列文章*/ 接上一篇文章Spark SQL Catalyst源代码分析之Physical Plan.本文将介绍Physical Plan的toRDD的详细实现细节 ...

  8. Spark SQL源码解析(二)Antlr4解析Sql并生成树

    Spark SQL原理解析前言: Spark SQL源码剖析(一)SQL解析框架Catalyst流程概述 这一次要开始真正介绍Spark解析SQL的流程,首先是从Sql Parse阶段开始,简单点说, ...

  9. Spark SQL源码解析(四)Optimization和Physical Planning阶段解析

    Spark SQL原理解析前言: Spark SQL源码剖析(一)SQL解析框架Catalyst流程概述 Spark SQL源码解析(二)Antlr4解析Sql并生成树 Spark SQL源码解析(三 ...

随机推荐

  1. Spring数据访问和事务

    1.模型 2.解耦 3.实现 3.1 核心接口 3.2 代码分析 3.2.1 事务管理 3.2.2 数据访问 4.使用 4.1 编程模式 4.2 配置模式 4.2.1 声明式配置方式 4.2.2 注解 ...

  2. jumpserver(0.3.2版本)开源跳板机系统部署

    1. 介绍 JumpServer亮点: 集成Ansible,批量执行命令: 支持WebTerminal wiki地址:https://github.com/jumpserver/jumpserver/ ...

  3. java基础02

    使用 myeclipse写第一个java程序 之后  右键 点击 src  创建 Class /** * package:包的声明! * 代表我们当前的java源文件在项目中的位置! * 必须位于除了 ...

  4. 转:docker的核心技术深度剖析

    一.docker是什么 Docker的英文本意是码头工人,也就是搬运工,这种搬运工搬运的是集装箱(Container),集装箱里面装的可不是商品货物,而是任意类型的App,Docker把App(叫Pa ...

  5. JSON 序列化与反序列化(二)使用TypeReference 构建类型安全的异构容器

    1. 泛型通常用于集合,如Set和Map等.这样的用法也就限制了每个容器只能有固定数目的类型参数,一般来说,这也确实是我们想要的. 然而有的时候我们需要更多的灵活性,如数据库可以用任意多的Column ...

  6. Java中二叉树存储结构实现

    一.二叉树 二叉树指的是每个节点最多只能有两个子树的有序树.通常左边的子树被称为“左子树”(left subtree),右边的子树被称为右子树. 二叉树的每个节点最多只有2棵子树,二叉树的子树次序不能 ...

  7. jmeter 请求发送加密参数(加密接口测试一)

    最近在做http加密接口,请求头的uid参数及body的请求json参数都经过加密再发送请求,加密方式为:ase256.所以,jmeter发送请求前也需要对uid及json参数进行加密.我这里是让开发 ...

  8. vim设置tab键默认为4个空格

    有两种方法 1.vim /etc/vimrc set ts=4 set sw=4 2.vim /etc/vimrc set ts=4 set expandtab set autoindent 推荐使用 ...

  9. 微信小程序组件toast

    操作反馈toast:官方文档 Demo Code: var toastNum = 2 var pageData = {} pageData.data = {} for(var i = 0; i < ...

  10. 转:centos彻底删除文件夹、文件命令

    转自:http://www.cnblogs.com/kluan/p/4458296.html centos彻底删除文件夹.文件命令(centos 新建.删除.移动.复制等命令: 1.新建文件夹 mkd ...