You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android通过Retrofit上传图片至Laravel服务器过慢的优化咨询

图片上传慢的优化方案

针对你用Retrofit向Laravel上传大图片耗时过长的问题,以下是从客户端到服务器端的全流程优化方案:

一、客户端图片预处理(最有效的前端优化)

1. 转换为WebP格式

WebP格式在相同视觉质量下,体积比JPEG小25%-35%,比PNG小50%以上。Android 4.0+原生支持WebP编码,实现代码如下:

// 将Bitmap转换为WebP格式的字节数组
public byte[] bitmapToWebP(Bitmap bitmap, int quality) {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.WEBP, quality, outputStream);
    return outputStream.toByteArray();
}

// 直接从文件转换(避免加载全图到内存)
public File convertImageToWebP(File sourceFile, int quality) throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565; // 减少内存占用
    Bitmap bitmap = BitmapFactory.decodeFile(sourceFile.getAbsolutePath(), options);
    
    File outputFile = new File(sourceFile.getParent(), "compressed_" + System.currentTimeMillis() + ".webp");
    FileOutputStream fos = new FileOutputStream(outputFile);
    bitmap.compress(Bitmap.CompressFormat.WEBP, quality, fos);
    fos.close();
    bitmap.recycle();
    return outputFile;
}

测试时可以从quality=80开始调整,找到质量和体积的平衡点。

2. 多维度压缩(质量+尺寸)

如果WebP还不够,结合尺寸压缩进一步减小体积:

  • 质量压缩:不改变图片分辨率,仅降低编码质量
public File compressImageQuality(File sourceFile, int quality) throws IOException {
    Bitmap bitmap = BitmapFactory.decodeFile(sourceFile.getAbsolutePath());
    File outputFile = new File(sourceFile.getParent(), "quality_compressed.jpg");
    FileOutputStream fos = new FileOutputStream(outputFile);
    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, fos);
    fos.close();
    bitmap.recycle();
    return outputFile;
}
  • 尺寸压缩:按比例缩小图片分辨率,比如缩放到屏幕宽度的2倍(足够满足大多数展示需求)
public File compressImageSize(File sourceFile, int maxWidth, int maxHeight) throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(sourceFile.getAbsolutePath(), options);
    
    // 计算缩放比例
    int widthRatio = Math.round((float) options.outWidth / maxWidth);
    int heightRatio = Math.round((float) options.outHeight / maxHeight);
    int inSampleSize = Math.min(widthRatio, heightRatio);
    if (inSampleSize < 1) inSampleSize = 1;
    
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    Bitmap bitmap = BitmapFactory.decodeFile(sourceFile.getAbsolutePath(), options);
    
    File outputFile = new File(sourceFile.getParent(), "size_compressed.jpg");
    FileOutputStream fos = new FileOutputStream(outputFile);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
    fos.close();
    bitmap.recycle();
    return outputFile;
}

3. 采样率压缩(避免OOM)

处理超大图片时,先通过inSampleSize减少加载到内存的像素数,再进行压缩:

public Bitmap decodeSampledBitmapFromFile(String filePath, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filePath, options);
    
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(filePath, options);
}

private 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上传请求优化

1. 启用GZIP压缩

通过OkHttpClient添加GZIP拦截器,压缩请求体体积:

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new GzipRequestInterceptor())
        .build();

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("YOUR_LARAVEL_BASE_URL")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

// Gzip请求拦截器实现
class GzipRequestInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
            return chain.proceed(originalRequest);
        }

        Request compressedRequest = originalRequest.newBuilder()
                .header("Content-Encoding", "gzip")
                .method(originalRequest.method(), gzip(originalRequest.body()))
                .build();
        return chain.proceed(compressedRequest);
    }

    private RequestBody gzip(final RequestBody body) {
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return body.contentType();
            }

            @Override
            public long contentLength() {
                return -1; // 无法提前计算压缩后的长度
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
                body.writeTo(gzipSink);
                gzipSink.close();
            }
        };
    }
}

2. 分块上传(处理超大文件)

如果图片还是很大(比如几十MB),可以把文件分成多个块上传,服务器端拼接:

  • Retrofit接口定义:
@Multipart
@POST("upload/chunk")
Call<ResponseBody> uploadChunk(
        @Part("file_name") RequestBody fileName,
        @Part("chunk_index") RequestBody chunkIndex,
        @Part("total_chunks") RequestBody totalChunks,
        @Part MultipartBody.Part chunk
);

@POST("upload/merge")
Call<ResponseBody> mergeChunks(
        @Query("file_name") String fileName,
        @Query("total_chunks") int totalChunks
);
  • 客户端分块逻辑:
public void uploadFileInChunks(File file, int chunkSize) throws IOException {
    String fileName = file.getName();
    long fileLength = file.length();
    int totalChunks = (int) Math.ceil((double) fileLength / chunkSize);
    
    for (int i = 0; i < totalChunks; i++) {
        long start = i * chunkSize;
        long end = Math.min(start + chunkSize, fileLength);
        
        File chunkFile = new File(file.getParent(), fileName + ".part" + i);
        FileInputStream fis = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(chunkFile);
        byte[] buffer = new byte[1024];
        int bytesRead;
        long bytesWritten = 0;
        
        fis.skip(start);
        while (bytesWritten < (end - start) && (bytesRead = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, bytesRead);
            bytesWritten += bytesRead;
        }
        fis.close();
        fos.close();
        
        // 上传当前块
        RequestBody requestFile = RequestBody.create(MediaType.parse("image/*"), chunkFile);
        MultipartBody.Part body = MultipartBody.Part.createFormData("chunk", chunkFile.getName(), requestFile);
        
        RequestBody fileNameBody = RequestBody.create(MediaType.parse("text/plain"), fileName);
        RequestBody chunkIndexBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(i));
        RequestBody totalChunksBody = RequestBody.create(MediaType.parse("text/plain"), String.valueOf(totalChunks));
        
        Call<ResponseBody> call = apiService.uploadChunk(fileNameBody, chunkIndexBody, totalChunksBody, body);
        Response<ResponseBody> response = call.execute();
        
        if (!response.isSuccessful()) {
            // 处理上传失败逻辑
            chunkFile.delete();
            return;
        }
        chunkFile.delete();
    }
    
    // 合并所有块
    Call<ResponseBody> mergeCall = apiService.mergeChunks(fileName, totalChunks);
    Response<ResponseBody> mergeResponse = mergeCall.execute();
    if (mergeResponse.isSuccessful()) {
        // 上传完成
    }
}
  • Laravel端分块接收与合并:
// 上传块的控制器方法
public function uploadChunk(Request $request) {
    $fileName = $request->input('file_name');
    $chunkIndex = $request->input('chunk_index');
    $totalChunks = $request->input('total_chunks');
    
    $chunk = $request->file('chunk');
    $chunkDir = storage_path('app/chunks/' . $fileName);
    if (!File::exists($chunkDir)) {
        File::makeDirectory($chunkDir, 0755, true);
    }
    
    $chunk->move($chunkDir, $chunkIndex);
    return response()->json(['status' => 'success']);
}

// 合并块的控制器方法
public function mergeChunks(Request $request) {
    $fileName = $request->input('file_name');
    $totalChunks = $request->input('total_chunks');
    $chunkDir = storage_path('app/chunks/' . $fileName);
    $outputFile = storage_path('app/uploads/' . $fileName);
    
    $output = fopen($outputFile, 'wb');
    for ($i = 0; $i < $totalChunks; $i++) {
        $chunkPath = $chunkDir . '/' . $i;
        $chunk = fopen($chunkPath, 'rb');
        stream_copy_to_stream($chunk, $output);
        fclose($chunk);
        unlink($chunkPath);
    }
    fclose($output);
    File::deleteDirectory($chunkDir);
    
    return response()->json(['status' => 'success', 'file_path' => $outputFile]);
}

三、Laravel服务器端优化

1. 调整PHP上传配置

修改php.ini中的以下参数,确保能处理大文件(配合分块上传可以不用调太大):

upload_max_filesize = 10M
post_max_size = 10M
memory_limit = 256M
max_execution_time = 300

2. 异步处理上传后的图片

上传完成后,不要同步处理图片(比如生成缩略图),用Laravel队列异步处理:

// 在上传合并完成后,分发队列任务
dispatch(new ProcessImageUpload($outputFile));

// 队列任务类
class ProcessImageUpload implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    private $filePath;
    
    public function __construct($filePath) {
        $this->filePath = $filePath;
    }
    
    public function handle() {
        // 生成缩略图、转码等操作
        $image = Image::make($this->filePath);
        $image->resize(300, null, function ($constraint) {
            $constraint->aspectRatio();
        })->save(storage_path('app/thumbnails/' . basename($this->filePath)));
    }
}

3. 启用服务器GZIP压缩

在Nginx配置中添加GZIP压缩,减少响应体积:

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;

内容的提问来源于stack exchange,提问作者AhmadVahedi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 02:07:07