Android 文件上传

在 Android 开发中,文件上传是网络编程中的常见需求,通常通过 HTTP POST 请求以 multipart/form-data 格式将文件(如图片、视频、文档等)发送到服务器。Android 提供了多种方式实现文件上传,包括原生的 HttpURLConnection 和第三方库如 OkHttpRetrofit。以下是 Android 文件上传的详细学习指南,涵盖实现方法、代码示例、注意事项和实践建议。


一、文件上传概述

  • 目标:将本地文件(如存储在设备上的图片、音频或文本文件)通过 HTTP 请求上传到服务器。
  • 常见场景
  • 上传用户头像或照片。
  • 提交表单数据(如包含文件的表单)。
  • 上传大文件(如视频),可能需要断点续传。
  • 协议:通常使用 HTTP POST 请求,Content-Typemultipart/form-data
  • 工具
  • HttpURLConnection:原生,支持简单上传。
  • OkHttp:功能强大,支持异步和拦截器。
  • Retrofit:基于 OkHttp,简化 API 调用,适合 RESTful 上传。
  • 关键点
  • 文件以二进制流形式发送。
  • 支持同时上传多个文件或文件与表单数据。
  • 异步处理,避免阻塞主线程。
  • 处理进度反馈(大文件上传)。

二、准备工作

  1. 权限声明
    AndroidManifest.xml 中添加网络和存储权限:
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
   <!-- Android 9 及以下需要 -->
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • Android 6.0+:动态请求存储权限。
  • Android 10+:使用 Scoped Storage,优先通过 MediaStoreACTION_GET_CONTENT 访问文件。
  1. 文件来源
  • 本地文件:如 /sdcard/image.jpg 或通过 Intent 从相册选择。
  • 示例文件路径:/storage/emulated/0/Pictures/image.jpg
  1. 服务器要求
  • 服务器需支持 multipart/form-data 上传。
  • 确认上传接口的 URL、参数名(如 file)和认证方式(如 Token)。
  1. 依赖添加(如使用 OkHttp 或 Retrofit):
    build.gradle(app 模块)中添加:
   // OkHttp
   implementation 'com.squareup.okhttp3:okhttp:4.12.0'
   // Retrofit
   implementation 'com.squareup.retrofit2:retrofit:2.11.0'

三、文件上传方法详解与代码示例

以下是使用 HttpURLConnection、OkHttp 和 Retrofit 实现文件上传的详细代码示例。

1. 使用 HttpURLConnection

HttpURLConnection 是 Android 原生的 HTTP 客户端,适合简单文件上传。

  • 代码示例(上传单个文件):
  import java.io.DataOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.InputStream;
  import java.net.HttpURLConnection;
  import java.net.URL;

  public class FileUploader {
      public static String uploadFile(String serverUrl, String filePath) {
          String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
          String lineEnd = "\r\n";
          String twoHyphens = "--";
          HttpURLConnection conn = null;
          DataOutputStream dos = null;
          FileInputStream fis = null;
          StringBuilder response = new StringBuilder();

          try {
              // 1. 创建连接
              URL url = new URL(serverUrl);
              conn = (HttpURLConnection) url.openConnection();
              conn.setRequestMethod("POST");
              conn.setDoOutput(true);
              conn.setDoInput(true);
              conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
              conn.setRequestProperty("Authorization", "Bearer your_token_here");

              // 2. 写入文件数据
              dos = new DataOutputStream(conn.getOutputStream());
              // 写入 multipart 头部
              dos.writeBytes(twoHyphens + boundary + lineEnd);
              dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + new File(filePath).getName() + "\"" + lineEnd);
              dos.writeBytes("Content-Type: application/octet-stream" + lineEnd);
              dos.writeBytes(lineEnd);

              // 写入文件内容
              fis = new FileInputStream(filePath);
              byte[] buffer = new byte[1024];
              int bytesRead;
              while ((bytesRead = fis.read(buffer)) != -1) {
                  dos.write(buffer, 0, bytesRead);
              }
              dos.writeBytes(lineEnd);
              dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
              dos.flush();

              // 3. 获取响应
              int responseCode = conn.getResponseCode();
              if (responseCode == HttpURLConnection.HTTP_OK) {
                  InputStream inputStream = conn.getInputStream();
                  BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                  String line;
                  while ((line = reader.readLine()) != null) {
                      response.append(line);
                  }
                  reader.close();
                  inputStream.close();
              } else {
                  response.append("Error: ").append(responseCode);
              }
              return response.toString();
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          } finally {
              try {
                  if (fis != null) fis.close();
                  if (dos != null) dos.close();
                  if (conn != null) conn.disconnect();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
  }
  • 使用示例(异步调用):
  new Thread(() -> {
      String filePath = "/sdcard/Pictures/image.jpg";
      String result = FileUploader.uploadFile("https://api.example.com/upload", filePath);
      runOnUiThread(() -> {
          Log.d("Upload", result);
      });
  }).start();
  • 说明
  • 使用 multipart/form-data 格式,设置 boundary 分隔符。
  • 手动构造 multipart 头部和文件内容。
  • 适合简单上传,但代码较冗长。

2. 使用 OkHttp

OkHttp 是一个功能强大的第三方库,简化文件上传并支持异步处理。

  • 代码示例(上传单个文件和表单数据):
  import okhttp3.MediaType;
  import okhttp3.MultipartBody;
  import okhttp3.OkHttpClient;
  import okhttp3.Request;
  import okhttp3.RequestBody;
  import okhttp3.Response;
  import java.io.File;

  public class OkHttpUploader {
      public static String uploadFile(String serverUrl, String filePath, String paramName, String paramValue) {
          OkHttpClient client = new OkHttpClient();
          File file = new File(filePath);

          // 构建 multipart 请求体
          RequestBody requestBody = new MultipartBody.Builder()
                  .setType(MultipartBody.FORM)
                  .addFormDataPart("param", paramValue) // 表单数据
                  .addFormDataPart("file", file.getName(),
                          RequestBody.create(file, MediaType.parse("application/octet-stream")))
                  .build();

          // 构建请求
          Request request = new Request.Builder()
                  .url(serverUrl)
                  .post(requestBody)
                  .addHeader("Authorization", "Bearer your_token_here")
                  .build();

          try (Response response = client.newCall(request).execute()) {
              if (response.isSuccessful()) {
                  return response.body() != null ? response.body().string() : "Success";
              } else {
                  return "Error: " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }
  }
  • 使用示例(异步调用):
  new Thread(() -> {
      String filePath = "/sdcard/Pictures/image.jpg";
      String result = OkHttpUploader.uploadFile(
              "https://api.example.com/upload",
              filePath,
              "username",
              "Alice"
      );
      runOnUiThread(() -> {
          Log.d("Upload", result);
      });
  }).start();
  • 异步调用(使用 OkHttp 的回调):
  OkHttpClient client = new OkHttpClient();
  File file = new File("/sdcard/Pictures/image.jpg");
  RequestBody requestBody = new MultipartBody.Builder()
          .setType(MultipartBody.FORM)
          .addFormDataPart("file", file.getName(),
                  RequestBody.create(file, MediaType.parse("application/octet-stream")))
          .build();
  Request request = new Request.Builder()
          .url("https://api.example.com/upload")
          .post(requestBody)
          .build();
  client.newCall(request).enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
          e.printStackTrace();
          runOnUiThread(() -> Log.e("Upload", "Exception: " + e.getMessage()));
      }

      @Override
      public void onResponse(Call call, Response response) throws IOException {
          String result = response.body() != null ? response.body().string() : "Success";
          runOnUiThread(() -> Log.d("Upload", result));
      }
  });
  • 说明
  • 使用 MultipartBody 构建请求体,简化 multipart/form-data 格式。
  • 支持异步回调,适合大文件上传。
  • 可通过拦截器添加全局头或监控进度(见下文)。

3. 使用 Retrofit

Retrofit 是基于 OkHttp 的高级封装,适合 RESTful API 文件上传。

  • 定义接口
  import okhttp3.MultipartBody;
  import okhttp3.RequestBody;
  import retrofit2.Call;
  import retrofit2.http.Multipart;
  import retrofit2.http.POST;
  import retrofit2.http.Part;

  public interface UploadService {
      @Multipart
      @POST("upload")
      Call<ResponseBody> uploadFile(
              @Part MultipartBody.Part file,
              @Part("username") RequestBody username
      );
  }
  • 配置 Retrofit
  import okhttp3.ResponseBody;
  import retrofit2.Retrofit;

  public class RetrofitUploader {
      public static String uploadFile(String serverUrl, String filePath, String username) {
          Retrofit retrofit = new Retrofit.Builder()
                  .baseUrl(serverUrl)
                  .build();
          UploadService service = retrofit.create(UploadService.class);

          File file = new File(filePath);
          RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));
          MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), fileBody);
          RequestBody usernameBody = RequestBody.create(username, MediaType.parse("text/plain"));

          try {
              Call<ResponseBody> call = service.uploadFile(filePart, usernameBody);
              Response<ResponseBody> response = call.execute();
              if (response.isSuccessful()) {
                  return response.body() != null ? response.body().string() : "Success";
              } else {
                  return "Error: " + response.code();
              }
          } catch (Exception e) {
              e.printStackTrace();
              return "Exception: " + e.getMessage();
          }
      }
  }
  • 使用示例(Kotlin 协程):
  interface UploadService {
      @Multipart
      @POST("upload")
      suspend fun uploadFile(
          @Part file: MultipartBody.Part,
          @Part("username") username: RequestBody
      ): Response<ResponseBody>
  }

  suspend fun uploadFile(serverUrl: String, filePath: String, username: String): String = withContext(Dispatchers.IO) {
      val retrofit = Retrofit.Builder()
          .baseUrl(serverUrl)
          .build()
      val service = retrofit.create(UploadService::class.java)

      val file = File(filePath)
      val fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"))
      val filePart = MultipartBody.Part.createFormData("file", file.getName(), fileBody)
      val usernameBody = RequestBody.create(username, MediaType.parse("text/plain"))

      try {
          val response = service.uploadFile(filePart, usernameBody)
          if (response.isSuccessful) {
              response.body()?.string() ?: "Success"
          } else {
              "Error: ${response.code()}"
          }
      } catch (e: Exception) {
          "Exception: ${e锟大屏蔽 e.message}"
      }
  }

  // 调用
  lifecycleScope.launch {
      val result = uploadFile("https://api.example.com/", "/sdcard/Pictures/image.jpg", "Alice")
      Log.d("Upload", result)
  }
  • 说明
  • 使用 @Multipart@Part 注解简化上传。
  • 结合协程,异步处理更优雅。
  • 支持复杂表单数据和文件上传。

四、上传进度监听(OkHttp 示例)

对于大文件上传,显示上传进度是常见需求。OkHttp 支持通过自定义 RequestBody 实现进度监听。

  • 代码示例
  public class ProgressRequestBody extends RequestBody {
      private final File file;
      private final MediaType mediaType;
      private final ProgressListener listener;

      public interface ProgressListener {
          void onProgress(long bytesWritten, long totalBytes);
      }

      public ProgressRequestBody(File file, MediaType mediaType, ProgressListener listener) {
          this.file = file;
          this.mediaType = mediaType;
          this.listener = listener;
      }

      @Override
      public MediaType contentType() {
          return media);

      @Override
      public long contentLength() {
          return file.length();
      }

      @Override
      public void writeTo(BufferedSink sink) throws IOException {
          Source source = Okio.source(file);
          long totalBytes = contentLength();
          long bytesWritten = 0;
          Buffer buffer = new Buffer();
          while (source.read(buffer, 8192) != -1) {
              bytesWritten += sink.write(buffer);
              listener.onProgress(bytesWritten, totalBytes);
          }
          source.close();
      }
  }

  public void uploadFileWithProgress(String serverUrl, String filePath) {
      OkHttpClient client = new OkHttpClient();
      File file = new File(filePath);

      ProgressRequestBody progressBody = new ProgressRequestBody(
              file,
              MediaType.parse("application/octet-stream"),
              (bytesWritten, totalBytes) -> {
                  int progress = (int) (100 * bytesWritten / totalBytes);
                  runOnUiThread(() -> Log.d("Upload", "Progress: " + progress + "%"));
              }
      );

      RequestBody requestBody = new MultipartBody.Builder()
              .setType(MultipartBody.FORM)
              .addFormDataPart("file", file.getName(), progressBody)
              .build();

      Request request = new Request.Builder()
              .url(serverUrl)
              .post(requestBody)
              .build();

      client.newCall(request).enqueue(new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
              runOnUiThread(() -> Log.e("Upload", "Exception: " + e.getMessage()));
          }

          @Override
          public void onResponse(Call call, Response response) throws IOException {
              String result = response.body() != null ? response.body().string() : "Success";
              runOnUiThread(() -> Log.d("Upload", result));
          }
      });
  }
  • 使用示例
  new Thread(() -> {
      uploadFileWithProgress("https://api.example.com/upload", "/sdcard/Pictures/image.jpg");
  }).start();

五、注意事项

  1. 异步处理
  • 文件上传必须在子线程执行,避免 NetworkOnMainThreadException.
  • 使用 Thread、AsyncTask(已废弃)、OkHttp 回调或 Kotlin 协程。
  1. 权限管理
  • Android 6.0+:动态请求存储权限。
  • Android 10+:使用 ACTION_GET_CONTENTMediaStore 获取文件 URI。
  • 示例(获取文件 URI):
    java Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); startActivityForResult(intent, 100);
  1. 错误处理
  • 处理 IOException 和 HTTP 错误状态码(4xx、5xx)。
  • 示例:
    java if (responseCode >= 400) { InputStream errorStream = conn.getErrorStream(); String error = new BufferedReader(new InputStreamReader(errorStream)).lines().collect(Collectors.joining("\n")); Log.e("Error", error); }
  1. 资源释放
  • 关闭 InputStreamOutputStreamHttpURLConnection
  • OkHttp 和 Retrofit 自动管理连接。
  1. 文件大小与性能
  • 大文件上传需监控进度,考虑分片上传或断点续传。
  • 使用 Gzip 压缩(如果服务器支持):
    java conn.setRequestProperty("Content-Encoding", "gzip");
  1. 安全性
  • 使用 HTTPS 确保数据安全。
  • 验证服务器证书,避免信任所有证书。
  • 添加认证头(如 Authorization: Bearer token)。

六、学习建议与实践

  1. 学习路径
  • 了解 multipart/form-data 格式和 HTTP POST 请求。
  • 掌握 HttpURLConnection 手动构造 multipart 请求。
  • 学习 OkHttp 的 MultipartBody 和异步回调。
  • 使用 Retrofit 简化上传,结合 Kotlin 协程。
  • 实现上传进度监听(OkHttp)。
  1. 实践项目
  • 简单项目:开发一个图片上传应用,选择相册图片并上传到服务器。
  • 进阶项目:实现多文件上传,附加表单数据(如用户名)。
  • 高级项目:支持大文件分片上传和进度显示。
  1. 调试工具
  • 使用 Postman 测试上传 API,验证请求格式。
  • 使用 Charles 或 Fiddler 抓包,分析 multipart 数据。
  • 使用 Android Studio 的 Network Profiler 查看请求详情。
  1. 推荐资源
  • OkHttp 文档:https://square.github.io/okhttp/
  • Retrofit 文档:https://square.github.io/retrofit/
  • Android 官方文档:https://developer.android.com/training/basics/network-ops
  • 实践 API:使用 Mocky.io 或自建服务器测试上传。

七、总结

  • 上传方法
  • HttpURLConnection:手动构造 multipart,适合简单上传。
  • OkHttp:使用 MultipartBody,支持异步和进度监听。
  • Retrofit:基于 OkHttp,简化 API 调用,适合 RESTful 上传。
  • 核心步骤:配置 multipart 请求、写入文件流、处理响应、释放资源。
  • 注意事项:异步处理、权限管理、错误处理、资源释放、HTTPS 安全。
  • 推荐:新项目优先使用 Retrofit + OkHttp,结合 Kotlin 协程。
  • 实践:实现图片上传,验证服务器响应,添加进度显示。

如果需要更详细的代码示例(如多文件上传、分片上传、断点续传)或特定场景的讲解,请告诉我!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注