- 准备工作
- PHP 服务器端代码
- Android 客户端代码
- 运行与测试
- 重要注意事项与最佳实践
准备工作
服务器端:
- Web 服务器: 你需要一个可以运行 PHP 的环境,最简单的是 XAMPP (包含 Apache 和 PHP)。
- 目录: 在你的 Web 服务器根目录(XAMPP 的
htdocs)下创建一个文件夹,我们称之为upload_image。 - 权限: 确保
upload_image文件夹有写入权限,以便 PHP 可以将文件保存进去,在 Linux/Mac 上,你可能需要执行chmod 755 upload_image或chmod 777 upload_image(777 权限较大,生产环境需谨慎)。
Android 客户端:
- 开发环境: Android Studio。
- 网络权限: 在
AndroidManifest.xml中必须添加网络权限。 - 图片选择: 我们将使用 Android 7.0 (API 24) 及以上的官方方式
ActivityResultLauncher来选择图片。
PHP 服务器端代码
在 upload_image 文件夹中创建一个名为 upload.php 的文件。

upload.php 代码详解
这个 PHP 脚本负责接收从 Android 发送过来的图片数据,进行验证,然后保存到服务器上。
<?php
// 设置响应头为 JSON,方便 Android 客户端解析
header('Content-Type: application/json');
// --- 1. 配置 ---
// 允许上传的图片类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
// 上传目录 (相对于当前脚本位置)
$uploadDir = 'uploads/';
// 最大文件大小 (5MB)
$maxFileSize = 5 * 1024 * 1024;
// --- 2. 创建上传目录 ---
if (!is_dir($uploadDir)) {
if (!mkdir($uploadDir, 0777, true)) {
echo json_encode(['success' => false, 'message' => '无法创建上传目录。']);
exit;
}
}
// --- 3. 检查是否有文件上传 ---
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['success' => false, 'message' => '没有文件上传或上传出错。']);
exit;
}
$file = $_FILES['image'];
// --- 4. 验证文件大小 ---
if ($file['size'] > $maxFileSize) {
echo json_encode(['success' => false, 'message' => '文件过大,最大允许 5MB。']);
exit;
}
// --- 5. 验证文件类型 (MIME Type) ---
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedType = $finfo->file($file['tmp_name']);
if (!in_array($detectedType, $allowedTypes)) {
echo json_encode(['success' => false, 'message' => '不支持的文件类型,仅支持 JPG, PNG, GIF。']);
exit;
}
// --- 6. 生成唯一的文件名,防止文件名冲突 ---
// 获取原始文件名和扩展名
$originalName = pathinfo($file['name'], PATHINFO_FILENAME);
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
// 生成新的唯一文件名
$newFileName = uniqid('img_', true) . '.' . $extension;
$destination = $uploadDir . $newFileName;
// --- 7. 移动文件到最终目录 ---
if (move_uploaded_file($file['tmp_name'], $destination)) {
// 上传成功,返回成功信息和文件路径
$response = [
'success' => true,
'message' => '图片上传成功!',
'file_path' => $destination // 返回相对路径
];
echo json_encode($response);
} else {
// 上传失败
echo json_encode(['success' => false, 'message' => '文件移动失败,请检查目录权限。']);
}
?>
代码解释:
- 配置: 定义了允许的图片类型、上传目录和最大文件大小。
- 创建目录: 检查
uploads文件夹是否存在,如果不存在则自动创建。 - 检查请求: 通过
$_FILES超全局变量检查是否有文件被上传。 - 验证大小: 检查文件大小是否超过限制。
- 验证类型: 使用
finfo扩展来检测文件的真实 MIME 类型,这比单纯检查文件后缀更安全。 - 生成唯一文件名: 使用
uniqid()生成一个独特的文件名,避免因文件名相同而覆盖已有文件。 - 移动文件:
move_uploaded_file()是 PHP 中专门用于处理上传文件的函数,它会把客户端临时文件移动到服务器指定的位置。
Android 客户端代码
这里我们使用现代的 ActivityResultLauncher 和 OkHttp 库来实现。
步骤 3.1: 添加依赖
在 app/build.gradle 文件的 dependencies 代码块中添加 OkHttp 库。

dependencies {
// ... 其他依赖
implementation("com.squareup.okhttp3:okhttp:4.12.0") // 使用最新版本
}
步骤 3.2: 添加网络权限
在 app/src/main/AndroidManifest.xml 文件中添加以下权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 对于 Android 13 (API 33) 及以上,需要更精确的权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
步骤 3.3: 编写上传逻辑
在你的 Activity 或 Fragment 中,实现图片选择和上传功能。
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
class MainActivity : AppCompatActivity() {
private lateinit var selectImageButton: Button
private lateinit var uploadButton: Button
private var selectedImageUri: Uri? = null
// 使用 ActivityResultLauncher 处理图片选择
private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
selectedImageUri = it
uploadButton.isEnabled = true // 启用上传按钮
Toast.makeText(this, "图片已选择", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
selectImageButton = findViewById(R.id.btn_select_image)
uploadButton = findViewById(R.id.btn_upload_image)
uploadButton.isEnabled = false // 初始禁用上传按钮
selectImageButton.setOnClickListener {
// 打开图片选择器
pickImageLauncher.launch("image/*")
}
uploadButton.setOnClickListener {
selectedImageUri?.let { uri ->
uploadImage(uri)
} ?: run {
Toast.makeText(this, "请先选择一张图片", Toast.LENGTH_SHORT).show()
}
}
}
private fun uploadImage(imageUri: Uri) {
// 将 Uri 转换为 File 对象
val file = File(imageUri.path)
if (!file.exists()) {
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show()
return
}
// 创建 OkHttp 客户端
val client = OkHttpClient()
// 创建 MultipartBody.Part,用于上传文件
val requestBody = file.asRequestBody("image/*".toMediaType())
val multipartBody = MultipartBody.Part.createFormData(
"image", // 这里的 key 必须与 PHP 中 $_FILES['image'] 的 'image' 对应
file.name,
requestBody
)
// 构建 Request
val request = Request.Builder()
.url("http://10.0.2.2/upload_image/upload.php") // 使用 10.0.2.2 访问本地服务器
.post(multipartBody)
.build()
// 异步执行请求
client.newCall(request).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
// 在主线程中更新 UI
runOnUiThread {
Toast.makeText(this@MainActivity, "上传失败: ${e.message}", Toast.LENGTH_LONG).show()
}
}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
response.use {
if (it.isSuccessful) {
val responseBody =
