热门标签 | HotTags
当前位置:  开发笔记 > 数据库 > 正文

AndroidPaging库使用详解(小结)

这篇文章主要介绍了AndroidPaging库使用详解(小结),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

Android分页包能够更轻易地在RecyclerView里面缓慢且优雅地加载数据.

许多应用从数据源消耗数据, 数据源里面有大量的数据, 但是一次却只展示一小部分.

分页包帮助应用观测和展示大量数据的合理数目的子集. 这个功能有如下几个优势:

  • 数据请求消耗更少的网络带宽和系统资源.
  • 即使在数据更新期间, 应用依然对用户输入响应迅速.

添加分页依赖

按照如下代码添加依赖:

dependencies {
  def paging_version = "1.0.0"

  implementation "android.arch.paging:runtime:$paging_version"

  // alternatively - without Android dependencies for testing
  testImplementation "android.arch.paging:common:$paging_version"

  // optional - RxJava support, currently in release candidate
  implementation "android.arch.paging:rxjava2:1.0.0-rc1"
}

备注: 分页包帮助开发者在UI的列表容器中顺畅地展示数据, 而不管是使用设备内部的数据库还是从应用后端拉取数据.

库架构

分页库的核心构件是PagedList类, 它是一个集合, 用于异步加载应用数据块或者数据页. 该类在应用的其它架构之间充当中介.

Data

每一个PagedList实例从DataSource中加载最新的应用数据. 数据从应用后端或者数据库流入PagedList对象. 分页包支持多样的应用架构, 包括脱机数据库和与后台服务器通讯的数据库.

UI

PagedList类通过PagedListAdapter加载数据项到RecyclerView里面. 在加载数据的时候, 这些类协同工作, 拉取数据并展示内容, 包括预取看不见的内容并在内容改变时加载动画.

支持不同的数据架构

分页包支持应用架构, 包括应用拉取数据的地方是从后台服务器, 还是本机数据库, 还是两者的结合.

只有网络

要展示后台数据, 需要使用Retrofit的同步版本, 加载信息到自定义的DataSource对象中.
备注: 分页包的DataSource对象并没有提供任何错误处理机制, 因为不同的应用需要用不同的方式处理和展示UI错误. 如果错误发生了, 顺从结果的回调, 然后稍后重试.

只有数据库

要设置RecyclerView观测本地存储, 偏向于使用Room持久化库. 用这种方式, 无论任何时候数据库数据插入或者修改, 这些改变会自动地在负责展示这些数据的RecyclerView展示出来.

网络+数据库

在开始观测数据库之后, 你能够通过使用PagedList.BoundaryCallback来监听数据库什么时候过期. 之后, 你可能从网络拉取更多的数据, 并把它们插入到数据库中. 如果UI正在展示数据库, 以上就是你所需要做的全部.

下面的代码片断展示了BoundaryCallback的使用实例:

class ConcertViewModel {
  fun search(query: String): ConcertSearchResult {
    val boundaryCallback =
        ConcertBoundaryCallback(query, myService, myCache)
    // Error-handling not shown in this snippet.
    val networkErrors = boundaryCallback.networkErrors
  }
}

class ConcertBoundaryCallback(
    private val query: String,
    private val service: MyService,
    private val cache: MyLocalCache
) : PagedList.BoundaryCallback() {
  override fun onZeroItemsLoaded() {
    requestAndSaveData(query)
  }

  override fun onItemAtEndLoaded(itemAtEnd: Concert) {
    requestAndSaveData(query)
  }
}

处理网络错误

在使用网络拉取或者分页的数据, 而这些数据正在使用分页包展示的时候, 不总是把网络分为要么"可用"要么"不可能"是很重要的, 因为许多连接是间歇性或者成片的:

  • 特定的服务器可能不能响应网络请求;
  • 设备可能联接了慢的或者弱的网络;

应用应该检查每一个请求是否成功, 并且在网络不可用的情形下, 尽可能快地恢复. 比如, 你可以为用户提供一个"重试"按钮, 如果数据没有刷新成功的话. 如果在数据分页期间发生错误, 最好自动地重新分页请求.

更新已有应用

如果应用已经从网络或者数据库消费数据, 很大可能可以直接升级到分页库提供的功能.

自定义分页解决方案

如果你使用了自定义功能加载数据源中的小的数据集, 你可以使用PagedList类取代这个逻辑. PagedList类实例提供了内建的连接, 到通用的数据源. 这些实例也提供了在应用中引用的RecyclerView的适配器.

使用列表而非分页加载的数据

如果你使用内存里的列表作为UI适配器的后备数据结构, 考虑使用PagedList类观测数据更新, 如果列表中数据项变得很多的话. PagedList实例既可以使用LiveData也可以使用Observable对UI传递数据更新, 同时最小化了加载时间和内存使用. 然而, 应用中使用PagedList对象代替List并不要求对UI结构和数据更新逻辑作任何改变.

使用CursorAdapter将数据cursor与列表视图联系起来

应用也许会使用CursorAdapter将数据从Cursor跟ListView连接起来. 在这种情况下, 通常需要从ListView迁移到RecyclerView, 然后使用Room或者PositionalDataSource构件代替Cursor, 当然, 这主要依据于Cursor实例能否访问SQLite数据库.

在一些情况下, 比如使用Spinner实例的时候, 你仅仅提供了Adapter本身. 然后一个库使用了加载进adapter中的数据, 并展示了数据. 在这些情况下, 把adapter数据类型转化为LiveData, 之后在尝试使用将这些数据项在UI中填充起来之前, 将这个列表在ArrayAdapter对象中包裹起来.

使用AsyncListUtil异步加载内容

如果你在使用AsyncListUtil对象异步地加载和展示分组信息的话, 分页包将会使得加载数据更加方便:

  • 数据并不需要定位. 分页包让你直接从后台使用网络提供的键加载数据.
  • 数据量太大. 使用分页包可以将数据加载分页直到没有任何数据留下.
  • 更方便地观测数据. 分页包能够展示应用在可观测数据结构中持有的ViewModel.

 数据库例子

 使用LiveData观测分页数据

下面的示例代码展示了所有一起工作的碎片. 当演唱会事件在数据库中添加, 删除或者修改的修改的时候, RecyclerView中的内容自动且高效地更新:

@Dao
interface ConcertDao {
  // The Integer type parameter tells Room to use a PositionalDataSource
  // object, with position-based loading under the hood.
  @Query("SELECT * FROM user ORDER BY concert DESC")
  fun concertsByDate(): DataSource.Factory
}

class MyViewModel(concertDao: ConcertDao) : ViewModel() {
  val concertList: LiveData> = LivePagedListBuilder(
      concertDao.concertsByDate(),
      /* page size */ 20
  ).build()
}

class MyActivity : AppCompatActivity() {
  public override fun onCreate(savedState: Bundle?) {
    super.onCreate(savedState)
    val viewModel = ViewModelProviders.of(this)
        .get(MyViewModel::class.java!!)
    val recyclerView = findViewById(R.id.concert_list)
    val adapter = ConcertAdapter()
    viewModel.concertList.observe(this, { pagedList ->
        adapter.submitList(pagedList) })
    recyclerView.setAdapter(adapter)
  }
}

class ConcertAdapter() :
    PagedListAdapter(DIFF_CALLBACK) {
  fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
    val cOncert= getItem(position)
    if (concert != null) {
      holder.bindTo(concert)
    } else {
      // Null defines a placeholder item - PagedListAdapter automatically
      // invalidates this row when the actual object is loaded from the
      // database.
      holder.clear()
    }
  }

  companion object {
    private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() {
      // Concert details may have changed if reloaded from the database,
      // but ID is fixed.
      override fun areItemsTheSame(oldConcert: Concert,
          newConcert: Concert): Boolean =
          oldConcert.id == newConcert.id

      override fun areContentsTheSame(oldConcert: Concert,
          newConcert: Concert): Boolean =
          oldCOncert== newConcert
    }
  }
}

使用RxJava2观测分页数据

如果你偏爱使用RxJava2而非LiveData, 那么你可以创建Observable或者Flowable对象:

 class MyViewModel(concertDao: ConcertDao) : ViewModel() {
   val concertList: Flowable> = RxPagedListBuilder(
       concertDao.concertsByDate(),
       /* page size */ 50
   ).buildFlowable(BackpressureStrategy.LATEST)
 }

之后你可以按照如下代码开始和停止观测数据:

class MyActivity : AppCompatActivity() {
  private lateinit var adapter: ConcertAdapter
  private lateinit var viewModel: MyViewModel

  private val disposable = CompositeDisposable()

  public override fun onCreate(savedState: Bundle?) {
    super.onCreate(savedState)
    val recyclerView = findViewById(R.id.concert_list)
    viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java!!)
    adapter = ConcertAdapter()
    recyclerView.setAdapter(adapter)
  }

  override fun onStart() {
    super.onStart()
    disposable.add(viewModel.concertList.subscribe({
        flowableList -> adapter.submitList(flowableList)
    }))
  }

  override fun onStop() {
    super.onStop()
    disposable.clear()
  }
}

基于RxJava2解决方案的ConcertDao和ConcertAdapter代码, 和基于LiveData解决方案的代码是一样的.

UI构件及其出发点

将UI和视图模型联接起来 

你可以按照如下方式, 将LiveData实例跟PagedListAdapter联系起来:

private val adapter = ConcertPagedListAdapter()
private lateinit var viewModel: ConcertViewModel

override fun onCreate(savedInstanceState: Bundle?) {
  viewModel = ViewModelProviders.of(this)
      .get(ConcertViewModel::class.java)
  viewModel.concerts.observe(this, adapter::submitList)
}

当数据源提供一个新PagedList实例的时候, activity会将这些对象改善给adapter. PagedListAdapter实现, 定义了更新如何计算, 自动地处理分页和列表不同. 由此, 你的ViewHolder只需要绑定到特定的提供项:

class ConcertPagedListAdapter() : PagedListAdapter(
    object : DiffUtil.ItemCallback() {
  // The ID property identifies when items are the same.
  override fun areItemsTheSame(oldItem: Concert, newItem: Concert)
      = oldItem.id = newItem.id

  // Use the "==" operator (or Object.equals() in Java-based code) to know
  // when an item's content changes. Implement equals(), or write custom
  // data comparison logic here.
  override fun areContentsTheSame(oldItem: Concert, newItem: Concert) =
      oldItem.name == newItem.name && oldItem.date == newItem.date
  }
) {
  override fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
    val concert: Concert? = getItem(position)

    // Note that "concert" is a placeholder if it's null
    holder.bind(concert)
  }
}

PagedListAdapter使用PagedList.Callback对象处理分页加载事件. 当用户滑动时, PagedListAdapter调用PagedList.loadAround()方法将从DataSource中拉聚拢数据项提示提供给基本的PagedList.
备注: PageList是内容不可变的. 这意味着, 尽管新内容能够被加载到PagedList实例中, 但已加载项一旦加载完成便不能发生改变. 由此, 如果PagedList中的内容发生改变, PagedListAdapter对象将会接收到一个包含已更新信息的全新的PagedList.

实现diffing回调

先前的代码展示了areContentsTheSame()的手动实现, 它比较了对象的相关的域. 你也可以使用Java中的Object.equals()方法或者Kotlin中的==操作符. 但是要确保要么实现了对象中的equals()方法或者使用了kotlin中的数据对象.

使用不同的adapter类型进行diffing

如果你选择不从PagedListAdapter继承--比如你在使用一个提供了自己的adapter的库的时候--你依然可以通过直接使用AsyncPagedListDiffer对象使用分页包adapter的diffing功能.

在UI中提供占位符

在应用完成拉取数据之前, 如果你想UI展示一个列表, 你可以向用户展示占位符列表项. RecyclerView通过将列表项临时地设置为null来处理这个情况.

备注: 默认情况下, 分页包开启了占位符行为.

占位符有如下好处:

  • 支持scrollbar. PagedList向PagedListAdapter提供了大量的列表项. 这个信息允许adapter绘制一个表示列表已满的scrollbar. 当新的页加载时, scrollbar并不会跳动, 因为列表是并不没有改变它的size.
  • 不需要"正在加载"旋转指针. 因为列表大小已知, 没必要提醒用户有更多的数据项正在加载. 占位符本身表达了这个信息.

在添加占位符的支持之前, 请牢记以下先置条件:

  • 要求集合中数据可数. 来自Room持久化库的DataSource实例能够高效地计算数据项. 然而, 如果你在用自定义本地存储方案或者只有网络的数据架构, 想了解数据集中有多少数据项可能代价很高, 甚至不可能.
  • 要求adapter负责未加载数据项. 你正在使用的adapter或者展示机制来准备填充列表, 需要处理null列表项. 比如, 当将数据绑定到ViewHolder的时候, 你需要提供默认值表示未加载数据.
  • 要求数据相同数量的item view. 如果列表项数目能够基于内容发生改变, 比如, 社交网络更新, 交叉淡入淡出看起来并不好. 在这种情况下, 强烈推荐禁掉占位符.

数据构件及其出发点

构建可观测列表

通常情况下, UI代码观测LiveData对象(或者, 如果你在使用RxJava2, 是Flowable/Observable对象), 这个对象存在于应用的ViewModel中. 这个可观测对象形成了应用列表数据内容和展示的连接.

要创建这么一个可观测PagedList对象, 需要将DataSource.Factory实例传给LivePageListBuilder/RxPagedListBuilder对象. 一个DataSource对象对单个PagedList加载分页. 这个工厂类为内容更新创建PagedList实例, 比如数据库表验证, 网络刷新等. Room持久化库能够提供DataSource.Factory, 或者自定义.

如下代码展示了如何在应用的ViewModel类中使用Room的DataSource.Factory构建能力创建新的LiveData实例:

ConcertDao.kt:

interface ConcertDao {
   // The Integer type parameter tells Room to use a PositionalDataSource
   // object, with position-based loading under the hood.
   @Query("SELECT * FROM concerts ORDER BY date DESC")
   public abstract DataSource.Factory concertsByDate()
 }

ConcertViewModel.kt:

// The Integer type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory =
    concertDao.concertsByDate()

val myPagedList = LivePagedListBuilder(myConcertDataSource, /* page size */ 20)
    .build()

定义分页配置

要想为复杂情形更深入地配置LiveData, 你也可以定义自己的分页配置. 尤其是, 你可以定义如下属性:

  • 页大小: 每一页的数据量.
  • 预取距离: 给定UI中最后可见项, 超过该项之后多少项, 分页包要尝试提前提取数据. 这个值应该比page size大几倍.
  • 占位符展示: 决定了UI是否会为还没有完成加载的数据项展示占位符.

如果你想要对分布包从数据库加载中设置更多的控件, 要像下面的代码一样, 传递自定义的Executor对象给LivePagedListBuilder:

EventViewModel.kt:

val myPagingCOnfig= PagedList.Config.Builder()
    .setPageSize(50)
    .setPrefetchDistance(150)
    .setEnablePlaceholders(true)
    .build()

// The Integer type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory =
    concertDao.concertsByDate()

val myPagedList = LivePagedListBuilder(myConcertDataSource, myPagingConfig)
    .setFetchExecutor(myExecutor)
    .build()

选择正确的数据源类型

连接更最好地处理源数据结构的数据源很重要:

  • 如果加载的页嵌套了之前/之后页的key的话, 使用PageKeyDataSource. 比如, 比如你正在从网络中拉取社交媒体博客, 你也许需要传递从一次加载向下一次加载的nextPage token.
  • 如果需要使用每N项数据项的数据拉取每N+1项的话, 使用ItemKeyedDataSource. 比如, 你在为一个讨论型应用拉取螺纹评论, 你可能需要传递最后一条评论的ID来获取下一条评论的内容.
  • 如果你需要从数据商店中的任意位置拉取分页数据的话, 使用PositionalDataSource. 这个类支持请求任意位置开始的数据集. 比如, 请求也许返回从位置1200开始的20条数据.

通知数据非法

在使用分页包时, 在表或者行数据变得陈腐时, 取决于数据层来通知应用的其它层. 要想这么做的话, 需要从DataSource类中调用invalidate()方法.

备注: UI也可以使用"滑动刷新"模式来触发数据非法功能.

构建自己的数据源

如果你使用了自定义的数据解决方案, 或者直接从网络加载数据, 你可以实现一个DataSource子类. 下面的代码展示了数据源从给定的concert起始时间切断:

class ConcertTimeDataSource(private val concertStartTime: Date) :
    ItemKeyedDataSource() {
  override fun getKey(item: Concert) = item.startTime

  override fun loadInitial(
      params: LoadInitialParams,
      callback: LoadInitialCallback) {
    val items = fetchItems(concertStartTime, params.requestedLoadSize)
    callback.onResult(items)
  }

  override fun loadAfter(
      params: LoadParams,
      callback: LoadCallback) {
    val items = fetchItemsAfter(
      date = params.key,
      limit = params.requestedLoadSize)
    callback.onResult(items)
  }
}

通过创建真实的DataSource.Factory子类, 你之后能够加载自定义的数据到PagedList对象. 下面的代码展示了如何创建在之前代码中定义的自定义数据源:

class ConcertTimeDataSourceFactory(private val concertStartTime: Date) :
    DataSource.Factory() {
  val sourceLiveData = MutableLiveData()
  override fun create(): DataSource {
    val source = ConcertTimeDataSource(concertStartTime)
    sourceLiveData.postValue(source)
    return source
  }
}

考虑内容更新

当你构建可观测PagedList对象的时候, 考虑一下内容是如何更新的. 如果你直接从Room数据库中加载数据, 更新会自动地推送到UI上面.

如果你在使用分页的网络API, 通常你会有用户交互, 比如"滑动刷新", 把它作为信号去验证当前DataSource非法并请求一个新的. 这个行为出行在下面的代码中:

class ConcertActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    ...

    concertViewModel.refreshState.observe(this, Observer {
      swipeRefreshLayout.isRefreshing =
          it == NetworkState.LOADING
    })
    swipeRefreshLayout.setOnRefreshListener {
      concertViewModel.invalidateDataSource()
    }
  }
}

提供数据表现之间的映射

对于DataSource加载的数据, 分页包支持基于数据项和基于页的转换.

下面的代码中, concert名和日期的联合被映射成包含姓名和日期的字符串:

class ConcertViewModel : ViewModel() {
  val concertDescriptions : LiveData>
    init {
      val factory = database.allConcertsFactory()
          .map { concert ->
              concert.name + " - " + concert.date
          }
      cOncerts= LivePagedListBuilder(factory, 30).build()
    }
  }
}

如果在数据加载之后, 想要包裹, 转换或者准备item, 这将非常有用. 因为这个工作是在获取执行器中完成的, 你可以在其中执行花销巨大的工作, 比如, 从硬盘中读取, 查询数据库等.

备注: JOIN查询总是比作为map()一部分的查询要高效.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 如何基于ggplot2构建相关系数矩阵热图以及一个友情故事
    本文介绍了如何在rstudio中安装ggplot2,并使用ggplot2构建相关系数矩阵热图。同时,通过一个友情故事,讲述了真爱难觅的故事背后的数据量化和皮尔逊相关系数的概念。故事中的小伙伴们在本科时参加各种考试,其中有些沉迷网络游戏,有些热爱体育,通过他们的故事,展示了不同兴趣和特长对学习和成绩的影响。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
author-avatar
black李曼_827
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有