凌峰创科服务平台

Android如何实现服务器图片上传?

  1. Android 客户端:选择图片、压缩图片、构建请求、发送到服务器。
  2. 服务器端:接收 HTTP 请求、解析上传的文件、保存到服务器。
  3. 网络通信:客户端和服务器之间使用的协议(通常是 HTTP/HTTPS)。

下面我将从客户端实现的角度,分步讲解,并提供代码示例,最后会简要提及服务器端的实现思路。

Android如何实现服务器图片上传?-图1
(图片来源网络,侵删)

第一步:准备工作 (Android Studio)

  1. 添加网络权限: 在 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 文件中进行配置。

  2. 添加网络请求库依赖: 强烈推荐使用成熟的网络库来简化开发,避免处理底层的线程和 IO 细节,这里我们以 Retrofit + OkHttp 为例,它们是目前 Android 开发中最主流的组合。

    app/build.gradle 文件的 dependencies 代码块中添加:

    Android如何实现服务器图片上传?-图2
    (图片来源网络,侵删)
    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;
}

压缩图片

直接上传原图会消耗大量流量和服务器资源,因此在上传前进行压缩是非常必要的,我们可以使用 LubanAndroid-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。

Android如何实现服务器图片上传?-图3
(图片来源网络,侵删)
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) 示例

使用 Flaskrequest 对象可以轻松获取上传的文件。

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)

总结与最佳实践

  1. 用户体验

    • 在上传过程中显示一个进度条或 ProgressBar,告知用户正在处理。
    • 使用 DialogSnackbar 提示用户上传成功或失败。
  2. 线程安全

    • 所有网络请求都必须在后台线程中执行,Retrofit 的 enqueue 方法已经帮我们处理了这一点,它会自动在子线程中执行请求,并在主线程中回调结果。千万不要在主线程(UI线程)中发起网络请求,否则会抛出 NetworkOnMainThreadException 异常。
  3. 错误处理

    全面处理各种可能的错误,如网络连接失败、服务器返回错误码、文件解析失败等,并向用户友好的提示。

  4. 安全性

    • HTTPS:生产环境的服务器必须使用 HTTPS 协议,以加密传输数据,防止中间人攻击。
    • 文件类型校验:服务器端必须严格校验上传文件的类型(MIME type)和扩展名,防止上传恶意脚本(如 .php 文件)。
    • 文件大小限制:服务器端应限制上传文件的大小,防止恶意用户上传超大文件耗尽服务器空间。
  5. 权限

    • 如果你的 App 目标 API 是 33 (Android 13) 或更高,除了 INTERNET 权限,你还需要在 AndroidManifest.xml 中声明 android:maxSdkVersion="32",或者动态请求 READ_MEDIA_IMAGES 权限才能访问相册。

通过以上步骤,你就可以实现一个健壮的 Android 图片上传功能了。

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