- Android 客户端:选择图片、压缩图片、构建请求、发送到服务器。
- 服务器端:接收 HTTP 请求、解析上传的文件、保存到服务器。
- 网络通信:客户端和服务器之间使用的协议(通常是 HTTP/HTTPS)。
下面我将从客户端实现的角度,分步讲解,并提供代码示例,最后会简要提及服务器端的实现思路。

第一步:准备工作 (Android Studio)
-
添加网络权限: 在
app/src/main/AndroidManifest.xml文件中,添加访问网络的权限。<uses-permission android:name="android.permission.INTERNET" />
注意:从 Android 9 (API 28) 开始,默认情况下,App 不能使用明文的 HTTP 流量,如果你的服务器使用的是
http://而不是https://,你需要在application标签中添加android:usesCleartextTraffic="true",或者在 Network Security Configuration 文件中进行配置。 -
添加网络请求库依赖: 强烈推荐使用成熟的网络库来简化开发,避免处理底层的线程和 IO 细节,这里我们以 Retrofit + OkHttp 为例,它们是目前 Android 开发中最主流的组合。
在
app/build.gradle文件的dependencies代码块中添加:
(图片来源网络,侵删)implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' // 用于解析 JSON 响应 implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' // 用于打印 OkHttp 的日志,方便调试
第二步:Android 客户端实现
选择图片
通常我们会使用 Intent 来启动系统的相册或相机,让用户选择或拍摄一张图片。
// 在 Activity 或 Fragment 中
private void pickImageFromGallery() {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, IMAGE_PICK_CODE);
}
// 处理返回的结果
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == IMAGE_PICK_CODE) {
Uri imageUri = data.getData();
// 得到图片的路径 (真实路径或 ContentProvider URI)
String imagePath = getPathFromUri(imageUri); // 需要自己实现这个方法
// 压缩图片并上传
compressAndUploadImage(imagePath);
}
}
// 将 URI 转换为文件路径 (这是一个简化的示例,实际处理更复杂)
private String getPathFromUri(Uri uri) {
String[] projection = { MediaStore.Images.Media.DATA };
Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String path = cursor.getString(column_index);
cursor.close();
return path;
}
return null;
}
压缩图片
直接上传原图会消耗大量流量和服务器资源,因此在上传前进行压缩是非常必要的,我们可以使用 Luban 或 Android-Universal-Image-Loader 等库,或者自己使用 BitmapFactory 进行压缩。
这里提供一个简单的自定义压缩方法:
public static File compressImage(Context context, String filePath) {
File outputFile = new File(context.getCacheDir(), "compressed_" + System.currentTimeMillis() + ".jpg");
int inWidth = 0;
int inHeight = 0;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
inWidth = options.outWidth;
inHeight = options.outHeight;
options.inSampleSize = calculateInSampleSize(options, 1024, 1024); // 压缩到最大边长为 1024px
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565; // 使用更节省内存的配置
Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
try {
FileOutputStream out = new FileOutputStream(outputFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out); // 质量为 80%
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
return outputFile;
}
// 计算采样率
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
构建 Retrofit 接口
定义一个接口,描述上传图片的 API。

import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
public interface UploadApiService {
// @Multipart 表示这是一个多部分请求
// @Part("file") 表示这个部分对应表单中的 "file" 字段
// RequestBody.create(...) 将文件转换为请求体
@Multipart
@POST("/api/upload") // 你的服务器上传接口地址
Call<UploadResponse> uploadImage(
@Part("file") RequestBody file,
@Part("description") RequestBody description // 可以同时上传其他文本数据
);
}
// 服务器返回的 JSON 数据对应的 Java 类 (GSON 会自动解析)
class UploadResponse {
private int code;
private String message;
private String imageUrl; // 服务器返回的图片访问 URL
// getters and setters
}
发送上传请求
将压缩后的文件,通过 Retrofit 发送到服务器。
private void uploadImage(File imageFile) {
// 1. 创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://your-api-base-url.com/") // 替换为你的服务器地址
.addConverterFactory(GsonConverterFactory.create())
.build();
// 2. 创建 API Service
UploadApiService apiService = retrofit.create(UploadApiService.class);
// 3. 准备要上传的文件
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpeg"), imageFile);
// 4. 准备其他文本数据 (可选)
RequestBody descriptionBody = RequestBody.create(MediaType.parse("text/plain"), "This is a test image");
// 5. 发起请求
Call<UploadResponse> call = apiService.uploadImage(fileBody, descriptionBody);
// 6. 异步执行请求
call.enqueue(new Callback<UploadResponse>() {
@Override
public void onResponse(Call<UploadResponse> call, Response<UploadResponse> response) {
if (response.isSuccessful()) {
UploadResponse uploadResponse = response.body();
if (uploadResponse != null && uploadResponse.getCode() == 200) {
// 上传成功
Log.d("Upload", "Upload successful! Image URL: " + uploadResponse.getImageUrl());
Toast.makeText(MainActivity.this, "上传成功", Toast.LENGTH_SHORT).show();
} else {
// 服务器返回业务逻辑错误
Log.e("Upload", "Upload failed: " + uploadResponse.getMessage());
Toast.makeText(MainActivity.this, "上传失败: " + uploadResponse.getMessage(), Toast.LENGTH_SHORT).show();
}
} else {
// HTTP 请求失败,如 404, 500 等
Log.e("Upload", "Upload failed with code: " + response.code());
Toast.makeText(MainActivity.this, "上传失败,网络错误", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<UploadResponse> call, Throwable t) {
// 网络连接等错误
Log.e("Upload", "Upload error: " + t.getMessage());
Toast.makeText(MainActivity.this, "上传失败: " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
第三步:服务器端实现 (简述)
服务器端需要处理一个 multipart/form-data 类型的 POST 请求,不同的后端技术处理方式不同。
Node.js (Express) 示例
使用 multer 中间件可以非常方便地处理文件上传。
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const port = 3000;
// 配置 multer 存储文件
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // 上传文件的目录
},
filename: function (req, file, cb) {
// 生成唯一文件名,避免覆盖
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({ storage: storage });
// 定义上传路由
app.post('/api/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ code: 400, message: 'No file uploaded.' });
}
// 文件上传成功,req.file 包含文件信息
const imageUrl = '/uploads/' + req.file.filename; // 假设可以通过这个 URL 访问文件
// 返回成功响应
res.json({
code: 200,
message: 'File uploaded successfully',
imageUrl: imageUrl
});
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Python (Flask) 示例
使用 Flask 的 request 对象可以轻松获取上传的文件。
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
# 确保上传目录存在
UPLOAD_FOLDER = 'uploads'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@app.route('/api/upload', methods=['POST'])
def upload_file():
# 检查是否有文件在请求中
if 'file' not in request.files:
return jsonify({'code': 400, 'message': 'No file part in the request'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'code': 400, 'message': 'No selected file'}), 400
if file:
# 安全地获取文件名
filename = secure_filename(file.filename)
# 保存文件
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
imageUrl = f"/uploads/{filename}"
return jsonify({
'code': 200,
'message': 'File uploaded successfully',
'imageUrl': imageUrl
})
if __name__ == '__main__':
app.run(debug=True)
总结与最佳实践
-
用户体验:
- 在上传过程中显示一个进度条或
ProgressBar,告知用户正在处理。 - 使用
Dialog或Snackbar提示用户上传成功或失败。
- 在上传过程中显示一个进度条或
-
线程安全:
- 所有网络请求都必须在后台线程中执行,Retrofit 的
enqueue方法已经帮我们处理了这一点,它会自动在子线程中执行请求,并在主线程中回调结果。千万不要在主线程(UI线程)中发起网络请求,否则会抛出NetworkOnMainThreadException异常。
- 所有网络请求都必须在后台线程中执行,Retrofit 的
-
错误处理:
全面处理各种可能的错误,如网络连接失败、服务器返回错误码、文件解析失败等,并向用户友好的提示。
-
安全性:
- HTTPS:生产环境的服务器必须使用 HTTPS 协议,以加密传输数据,防止中间人攻击。
- 文件类型校验:服务器端必须严格校验上传文件的类型(MIME type)和扩展名,防止上传恶意脚本(如
.php文件)。 - 文件大小限制:服务器端应限制上传文件的大小,防止恶意用户上传超大文件耗尽服务器空间。
-
权限:
- 如果你的 App 目标 API 是 33 (Android 13) 或更高,除了
INTERNET权限,你还需要在AndroidManifest.xml中声明android:maxSdkVersion="32",或者动态请求READ_MEDIA_IMAGES权限才能访问相册。
- 如果你的 App 目标 API 是 33 (Android 13) 或更高,除了
通过以上步骤,你就可以实现一个健壮的 Android 图片上传功能了。
