凌峰创科服务平台

android 上传文件到服务器

目录

  1. 核心概念:HTTP 上传
  2. 主流上传方案
    • 使用 HttpURLConnection (原生,无需库)
    • 使用 OkHttp (强烈推荐,现代 Android 开发标准)
    • 使用 Retrofit (基于 OkHttp,适合 RESTful API)
  3. 关键步骤与代码示例 (以 OkHttp 为例)
    • 步骤 1:添加依赖
    • 步骤 2:获取文件与创建请求体
    • 步骤 3:构建并发起请求
    • 步骤 4:处理响应
  4. 高级功能与最佳实践
    • 上传进度监听
    • 多线程/分块上传
    • 断点续传
    • 权限处理
    • 错误处理
    • 后台执行 (使用 WorkManager)

核心概念:HTTP 上传

在 Web 世界中,文件上传通常通过 HTTP 协议的 POST 请求来完成,最常用的两种格式是:

android 上传文件到服务器-图1
(图片来源网络,侵删)
  • multipart/form-data

    • 这是最通用、最标准的文件上传方式。
    • 它允许你在一个 POST 请求中同时发送文件数据和表单字段(文件名、描述、用户ID等)。
    • 请求体被分割成多个部分(part),每个部分之间由一个特定的边界字符串分隔。
    • 服务器端(如 PHP, Java Spring, Node.js)都有成熟的库来解析这种格式。
  • application/octet-stream

    • 这种方式只发送文件二进制流本身,不包含任何额外的表单数据。
    • 通常用于简单的、只上传文件的场景,灵活性较差。

对于绝大多数 Android 应用,你都应该使用 multipart/form-data


主流上传方案

使用 HttpURLConnection (原生)

这是 Java 标准库自带的类,无需添加任何第三方依赖。

android 上传文件到服务器-图2
(图片来源网络,侵删)
  • 优点:无依赖,轻量级。
  • 缺点
    • API 较为繁琐,代码量大。
    • 处理流、线程、回调等需要手动编写较多代码。
    • 不支持现代的 HTTP/2。
    • 处理上传进度比较麻烦。

适用场景:对项目依赖有严格限制,或上传逻辑极其简单的场景。不推荐用于新项目。

使用 OkHttp (强烈推荐)

OkHttp 是目前 Android 和 Java 平台上最流行的 HTTP 客户端。

  • 优点
    • 性能卓越:支持 HTTP/2、连接池,能有效减少延迟。
    • API 简洁:代码可读性高,易于使用。
    • 功能强大:内置拦截器、缓存、超时控制等。
    • 社区活跃:文档完善,问题容易解决。
    • 易于扩展:可以方便地添加拦截器来实现进度监听、日志等功能。

适用场景:几乎所有需要网络请求的 Android 应用,是现代 Android 开发的首选。

使用 Retrofit

Retrofit 是一个类型安全的 HTTP 客户端,它底层就是 OkHttp。

android 上传文件到服务器-图3
(图片来源网络,侵删)
  • 优点
    • 接口定义:通过 Java 接口来定义 API,代码非常清晰。
    • 自动序列化/反序列化:自动将 JSON/XML 等数据转换为 Java 对象,无需手动解析。
    • 与 OkHttp 无缝集成:可以轻松利用 OkHttp 的所有功能(如拦截器)。
    • 非常适合 RESTful API:如果你的服务器 API 是 REST 风格的,Retrofit 能让你如虎添翼。

适用场景:当你的应用不仅需要上传文件,还需要与 RESTful API 进行复杂的交互(GET, POST, PUT, DELETE 等)时,Retrofit 是最佳选择。


关键步骤与代码示例 (以 OkHttp 为例)

下面我们以最推荐的 OkHttp 为例,演示如何实现一个完整的文件上传功能。

步骤 1:添加依赖

在项目的 app/build.gradle 文件中添加 OkHttp 依赖:

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // 使用最新版本
}

步骤 2:获取文件与创建请求体

假设我们要从设备存储中选择一个图片文件进行上传。

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
// 1. 获取要上传的文件
val file = File("/path/to/your/image.jpg") // 替换为你的实际文件路径
// 2. 检查文件是否存在
if (!file.exists()) {
    // 处理文件不存在的情况
    return
}
// 3. 创建 MultipartBody.Part
// "file" 是服务器端接收文件时使用的字段名
// "upload.jpg" 是提供给服务器的文件名
val requestBody = MultipartBody.Part.createFormData(
    "file", 
    "upload.jpg", 
    RequestBody.create("image/jpeg".toMediaType(), file)
)
// (可选) 添加其他表单字段
val description = RequestBody.create("text/plain".toMediaType(), "This is a test image")

解释

  • MultipartBody.Part:代表了 multipart/form-data 中的一个“部分”,专门用于上传文件。
  • RequestBody:代表了请求体的一个部分,对于文件,我们使用 RequestBody.create() 来创建它,并指定文件的 MediaTypeimage/jpeg),对于普通的文本字段,也使用 RequestBody

步骤 3:构建并发起请求

val client = OkHttpClient()
// 4. 构建请求
val request = Request.Builder()
    .url("https://your-api-endpoint.com/upload") // 替换为你的服务器地址
    .post(
        MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addPart(requestBody) // 添加文件部分
            .addPart("description", description) // 添加文本部分
            .build()
    )
    .build()
// 5. 发起请求并处理响应
client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // 请求失败,例如网络错误、超时等
        e.printStackTrace()
        // 在主线程更新 UI
        runOnUiThread {
            Toast.makeText(this@MainActivity, "上传失败: ${e.message}", Toast.LENGTH_SHORT).show()
        }
    }
    override fun onResponse(call: Call, response: Response) {
        // 服务器成功返回响应
        if (response.isSuccessful) {
            val responseBody = response.body?.string()
            // 在主线程更新 UI
            runOnUiThread {
                Toast.makeText(this@MainActivity, "上传成功: $responseBody", Toast.LENGTH_LONG).show()
            }
        } else {
            // 服务器返回了错误状态码,如 400, 404, 500 等
            runOnUiThread {
                Toast.makeText(this@MainActivity, "上传失败: ${response.code}", Toast.LENGTH_SHORT).show()
            }
        }
    }
})

解释

  • 我们使用 MultipartBody.Builder 来构建完整的 multipart/form-data 请求体。
  • client.newCall(request).enqueue() 是异步执行网络请求的标准方式,它会自动在后台线程执行,并通过回调返回结果。
  • 注意:网络请求不能在主线程(UI线程)中执行,否则会抛出 NetworkOnMainThreadExceptionenqueue 方法已经帮我们处理了线程切换,但在回调中操作 UI 时,需要切回主线程,这里使用了 runOnUiThread(在 Activity 中)。

高级功能与最佳实践

上传进度监听

OkHttp 本身不直接提供上传进度回调,但我们可以通过 拦截器 来实现。

class ProgressListener(private val progressListener: (Long, Long) -> Unit) : Callback {
    // ... 其他代码保持不变 ...
    override fun onResponse(call: Call, response: Response) {
        // ... 原有代码 ...
    }
    // 创建一个包装了 ProgressRequestBody 的 MultipartBody.Part
    fun createProgressPart(file: File, fieldName: String, filename: String): MultipartBody.Part {
        val progressRequestBody = ProgressRequestBody(file, progressListener)
        return MultipartBody.Part.createFormData(fieldName, filename, progressRequestBody)
    }
}
// 自定义 RequestBody 来追踪上传进度
class ProgressRequestBody(
    private val file: File,
    private val progressListener: (Long, Long) -> Unit
) : RequestBody() {
    override fun contentType(): MediaType? {
        return "application/octet-stream".toMediaType()
    }
    override fun contentLength(): Long {
        return file.length()
    }
    override fun writeTo(sink: BufferedSink) {
        val fileLength = file.length()
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        var uploaded: Long = 0
        val inputStream = FileInputStream(file)
        inputStream.use { input ->
            var read: Int
            while (input.read(buffer).also { read = it } != -1) {
                sink.write(buffer, 0, read)
                uploaded += read
                progressListener(uploaded, fileLength)
            }
        }
    }
}

然后在构建请求时使用 createProgressPart,并传入一个 lambda 表达式来更新 UI 进度条。

多线程/分块上传 & 断点续传

  • 分块上传:将大文件分割成多个小块(5MB 一块),然后并发上传,这可以显著提高大文件的上传速度,服务器端需要支持接收并合并这些分块。
  • 断点续传:记录已上传的字节数,如果上传中断,下次可以从已上传的位置继续,这需要客户端和服务器端协同工作,通常需要记录上传的会话 ID 和已上传的字节数。

这两个功能都比较复杂,通常用于上传超大文件(如视频),实现它们需要与服务器端 API 紧密配合。

权限处理

如果文件来自设备存储(如相册或下载文件夹),你必须在 AndroidManifest.xml 中声明权限,并在运行时动态请求(针对 Android 6.0+)。

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 如果需要写入,例如保存下载的文件 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 对于 Android 11 (API 30) 及以上,推荐使用 MANAGE_EXTERNAL_STORAGE 或更精确的权限 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

在代码中请求权限:

// 在 Activity 或 Fragment 中
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
    != PackageManager.PERMISSION_GRANTED) {
    // 权限未授予,请求权限
    ActivityCompat.requestPermissions(
        this,
        arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
        MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
    )
} else {
    // 权限已授予,可以执行文件操作
    startFileUpload()
}

错误处理

  • 网络错误IOException,可能是无网络、DNS 解析失败等。
  • 服务器错误:检查 response.code(),处理 4xx (客户端错误) 和 5xx (服务器错误)。
  • 业务逻辑错误:即使 HTTP 状态码是 200,服务器也可能返回 JSON 格式的错误信息,需要解析后判断。

后台执行 (使用 WorkManager)

如果上传任务需要在用户离开应用或关闭屏幕后继续进行,你应该使用 Android Jetpack 的 WorkManager

  • 优点
    • 可靠:即使应用或系统重启,WorkManager 也能保证任务最终执行。
    • 灵活:可以设置约束(如仅在 Wi-Fi 下执行)、延迟、重试策略等。
    • 生命周期感知:与应用的生命周期解耦。

你需要将 OkHttp 的上传逻辑封装到一个 WorkerCoroutineWorker 中,然后通过 WorkManager 来调度它。


方案 优点 缺点 推荐度
HttpURLConnection 无依赖 API 熨琐,功能弱 ⭐☆☆☆☆
OkHttp 性能好、API简洁、功能强大、社区活跃 - ⭐⭐⭐⭐⭐ (首选)
Retrofit 接口化、类型安全、适合RESTful API 依赖 OkHttp ⭐⭐⭐⭐⭐ (复杂API首选)

对于新的 Android 项目,强烈推荐使用 OkHttp 进行文件上传,如果你的应用有复杂的 API 交互,再结合 Retrofit 使用,理解 multipart/form-data 的原理是成功上传的关键,不要忘记处理权限、进度、错误和后台执行等高级场景。

分享:
扫描分享到社交APP
上一篇
下一篇