- EXIF 参数:照片的元数据,由相机在拍摄时写入,包括拍摄时间、相机型号、光圈、快门速度、ISO、GPS 地理位置、是否使用闪光灯等。
- 图像文件参数:图像文件本身的基本属性,图像的分辨率(宽度和高度)、文件大小、格式(JPEG, PNG)、位深度等。
下面我将详细讲解如何获取这两类参数,并提供完整的代码示例。

(图片来源网络,侵删)
获取 EXIF 参数 (元数据)
EXIF 信息通常存储在 JPEG 和 DNG 文件的头部,Android 提供了 ExifInterface 类来方便地读写这些信息。
关键步骤:
-
添加依赖:较新版本的
ExifInterface已经包含在 Android SDK 中,但为了确保使用最新功能,最好在build.gradle文件中显式声明。// app/build.gradle dependencies { implementation 'androidx.exifinterface:exifinterface:1.3.6' // 使用最新版本 } -
获取
ExifInterface实例:传入照片文件的 URI。 -
调用
getAttribute()方法:使用预定义的常量来获取你需要的参数。
(图片来源网络,侵删)
常用 EXIF 参数列表:
| 常量 | 描述 | 示例值 |
|---|---|---|
TAG_DATETIME |
拍摄日期时间 | "2025:10:27 15:30:00" |
TAG_MAKE |
相机制造商 | "samsung" |
TAG_MODEL |
相机型号 | "SM-G998B" |
TAG_F_NUMBER |
光圈值 | "2.4" |
TAG_EXPOSURE_TIME |
曝光时间 (快门速度) | "1/250" |
TAG_ISO |
ISO 感光度 | "100" |
TAG_FLASH |
闪光灯状态 | "1" (闪光灯已开) |
TAG_FOCAL_LENGTH |
焦距 | "6.0" |
TAG_GPS_LATITUDE |
纬度 | "39.9" |
TAG_GPS_LONGITUDE |
经度 | "116.4" |
TAG_GPS_LATITUDE_REF |
纬度参考 | "N" (北纬) 或 "S" (南纬) |
TAG_GPS_LONGITUDE_REF |
经度参考 | "E" (东经) 或 "W" (西经) |
TAG_WHITE_BALANCE |
白平衡模式 | "auto" |
代码示例:
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.IOException
object PhotoMetadataUtils {
// 从文件路径获取 EXIF 数据
fun getExifFromFile(context: Context, filePath: String): Map<String, String> {
val exifData = mutableMapOf<String, String>()
try {
val exif = ExifInterface(filePath)
// 获取各个属性
exifData["Make"] = exif.getAttribute(ExifInterface.TAG_MAKE)
exifData["Model"] = exif.getAttribute(ExifInterface.TAG_MODEL)
exifData["DateTime"] = exif.getAttribute(ExifInterface.TAG_DATETIME)
exifData["FNumber"] = exif.getAttribute(ExifInterface.TAG_F_NUMBER)
exifData["ExposureTime"] = exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)
exifData["ISO"] = exif.getAttribute(ExifInterface.TAG_ISO)
exifData["Flash"] = exif.getAttribute(ExifInterface.TAG_FLASH)
exifData["FocalLength"] = exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)
exifData["WhiteBalance"] = exif.getAttribute(ExifInterface.TAG_WHITE_BALANCE)
// 处理 GPS 信息
val lat = exif.latLong
if (lat != null && lat.size == 2) {
exifData["GPSLatitude"] = lat[0].toString()
exifData["GPSLongitude"] = lat[1].toString()
}
} catch (e: IOException) {
e.printStackTrace()
}
return exifData
}
// 从 Uri 获取 EXIF 数据 (推荐方式)
fun getExifFromUri(context: Context, photoUri: Uri): Map<String, String> {
val filePath = getRealPathFromUri(context, photoUri)
return if (filePath != null) {
getExifFromFile(context, filePath)
} else {
// 如果无法获取真实路径,尝试直接从 Uri 读取 (Android 10+)
try {
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(photoUri, "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
if (fileDescriptor != null) {
val exif = ExifInterface(fileDescriptor)
// ... 同上,从 exif 对象获取数据 ...
parcelFileDescriptor.close()
return mapOf("Note" to "Read from Uri directly")
}
} catch (e: Exception) {
e.printStackTrace()
}
mapOf("Error" to "Could not read EXIF data")
}
}
// 这是一个辅助函数,用于将 Content URI 转换为文件路径
// 注意:在 Android 10+ (API 29) 及以上,这个方法可能返回 null,因为Scoped Storage限制了直接访问
private fun getRealPathFromUri(context: Context, uri: Uri): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return getDataColumn(context, uri, null, null)
}
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
// DownloadsProvider
val id = DocumentsContract.getDocumentId(uri)
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id))
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
// MediaProvider
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
if ("image".equals(type, ignoreCase = true)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else if ("video".equals(type, ignoreCase = true)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
} else if ("audio".equals(type, ignoreCase = true)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
}
} else if ("content".equals(uri.scheme, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) {
uri.lastPathSegment
} else {
getDataColumn(context, uri, null, null)
}
} else if ("file".equals(uri.scheme, ignoreCase = true)) {
// File
return uri.path
}
return null
}
private fun getDataColumn(context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}
获取图像文件参数 (分辨率、大小等)
获取这些参数更直接,主要通过 BitmapFactory 和 ContentResolver 来完成。
关键步骤:
- 获取
InputStream:通过ContentResolver打开照片 URI 的输入流。 - 获取文件大小:直接从
InputStream或AssetFileDescriptor获取。 - 获取图像尺寸:使用
BitmapFactory.Options的inJustDecodeBounds属性,这个属性允许你在不真正解码图片的情况下获取其原始宽度和高度,非常节省内存。 - (可选)获取图像格式:
BitmapFactory会根据文件扩展名自动识别格式。
代码示例:
import android.content.ContentResolver
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import java.io.File
import java.io.FileInputStream
import java.io.IOException
object ImageFileUtils {
// 从 Uri 获取图像参数
fun getImageParametersFromUri(context: Context, photoUri: Uri): Map<String, Any> {
val parameters = mutableMapOf<String, Any>()
// 1. 获取文件大小
parameters["FileSize"] = getFileSizeFromUri(context, photoUri)
// 2. 获取图像分辨率
val (width, height) = getImageDimensionsFromUri(context, photoUri)
parameters["Width"] = width
parameters["Height"] = height
// 3. 获取文件格式 (通常可以从 Uri 或文件名推断)
// parameters["Format"] = getImageFormat(photoUri) // 这个需要更复杂的实现
return parameters
}
// 从 Uri 获取文件大小 (单位: KB)
private fun getFileSizeFromUri(context: Context, uri: Uri): Long {
return try {
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream != null) {
val available = inputStream.available()
inputStream.close()
available.toLong()
} else {
0L
}
} catch (e: IOException) {
e.printStackTrace()
0L
}
}
// 从 Uri 获取图像尺寸 (宽度和高度)
private fun getImageDimensionsFromUri(context: Context, uri: Uri): Pair<Int, Int> {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true // 只解码边界,不加载像素数据
try {
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream != null) {
BitmapFactory.decodeStream(inputStream, null, options)
inputStream.close()
}
} catch (e: IOException) {
e.printStackTrace()
}
return Pair(options.outWidth, options.outHeight)
}
// 如果你有文件的绝对路径,可以直接操作 File 对象
fun getImageParametersFromFile(filePath: String): Map<String, Any> {
val parameters = mutableMapOf<String, Any>()
val file = File(filePath)
// 1. 获取文件大小
parameters["FileSize"] = file.length()
// 2. 获取图像分辨率
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
parameters["Width"] = options.outWidth
parameters["Height"] = options.outHeight
return parameters
}
}
完整使用示例 (在 Activity 或 Fragment 中)
假设你已经通过 Intent 获取到了一张照片的 URI。
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var tvMetadata: TextView
private val PICK_IMAGE_REQUEST = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnPickImage: Button = findViewById(R.id.btn_pick_image)
tvMetadata = findViewById(R.id.tv_metadata)
btnPickImage.setOnClickListener {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, PICK_IMAGE_REQUEST)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK) {
val photoUri: Uri? = data?.data
if (photoUri != null) {
processImageMetadata(photoUri)
}
}
}
private fun processImageMetadata(uri: Uri) {
// 获取 EXIF 参数
val exifData = PhotoMetadataUtils.getExifFromUri(this, uri)
// 获取图像文件参数
val imageParams = ImageFileUtils.getImageParametersFromUri(this, uri)
// 合并并显示结果
val allParams = mutableMapOf<String, Any>()
allParams.putAll(exifData)
allParams.putAll(imageParams)
val resultText = StringBuilder()
allParams.forEach { (key, value) ->
resultText.append("$key: $value\n")
}
tvMetadata.text = resultText.toString()
}
}
重要注意事项
- 运行时权限:访问照片和存储需要权限,对于 Android 13 (API 33) 及以上,你需要请求
READ_MEDIA_IMAGES权限,对于更早的版本,需要READ_EXTERNAL_STORAGE权限。 - Scoped Storage (作用域存储):从 Android 10 (API 29) 开始,应用默认无法直接访问共享外部存储上的任意文件,从
ContentProvider返回的Uri可能没有直接对应的文件路径,这就是为什么推荐直接使用Uri来操作(如ExifInterface支持从FileDescriptor读取),而不是尝试获取一个已废弃的文件路径。 - 性能:对于非常大的图片,使用
inJustDecodeBounds来获取尺寸是最佳实践,因为它避免了将整个图片加载到内存中,直接解码大图片可能会导致OutOfMemoryError。 - EXIF 数据可能不存在:并非所有图片都有完整的 EXIF 数据,从微信等社交软件保存的图片通常会丢失 EXIF 信息,在读取时应该进行空值检查,避免应用崩溃。

(图片来源网络,侵删)
