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
相关产品推荐
相关产品推荐

