
1. Android系统拍照与选图功能实现解析在移动应用开发中调用系统相机拍照和相册选图是最基础但最容易踩坑的功能之一。最近在实现用户头像上传功能时我重新梳理了这套流程发现从Android 7.0开始引入的FileProvider机制让不少开发者头疼。本文将详细讲解如何正确处理不同Android版本下的拍照、选图和裁剪功能特别针对FileProvider的配置和URI处理给出完整解决方案。关键提示从Android 7.0API 24开始直接使用file:// URI会触发FileUriExposedException必须改用FileProvider提供的content:// URI。2. 核心功能实现步骤2.1 拍照功能实现首先在AndroidManifest.xml中添加必要的权限声明uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /拍照功能的核心代码实现private fun openCamera() { // 创建临时文件存储拍照结果 val outputImage File(externalCacheDir, output_${System.currentTimeMillis()}.jpg) try { if (outputImage.exists()) outputImage.delete() outputImage.createNewFile() } catch (e: IOException) { e.printStackTrace() return } // 根据版本选择URI生成方式 imageUri if (Build.VERSION.SDK_INT 24) { FileProvider.getUriForFile( this, ${packageName}.fileprovider, outputImage ) } else { Uri.fromFile(outputImage) } // 启动相机Intent val intent Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, imageUri) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } startActivityForResult(intent, REQUEST_CAMERA) }2.2 FileProvider配置详解在AndroidManifest.xml中配置FileProviderprovider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider创建res/xml/file_paths.xml文件?xml version1.0 encodingutf-8? paths external-cache-path namecamera_cache path. / external-files-path nameexternal_files path. / cache-path nameinternal_cache path. / /paths经验之谈建议配置多个路径类型适配不同存储需求。name属性会成为URI路径的一部分path.表示允许访问该目录下的所有文件。3. 相册选图功能实现3.1 基础选图实现private fun pickFromGallery() { val intent Intent(Intent.ACTION_GET_CONTENT).apply { type image/* addCategory(Intent.CATEGORY_OPENABLE) } startActivityForResult( Intent.createChooser(intent, 选择图片), REQUEST_GALLERY ) }3.2 兼容不同Android版本的URI处理在onActivityResult中处理返回结果override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_GALLERY - { if (resultCode RESULT_OK) { data?.data?.let { uri - val imageUri if (Build.VERSION.SDK_INT Build.VERSION_CODES.KITKAT) { handleImageOnKitKat(uri) } else { handleImageBeforeKitKat(uri) } imageUri?.let { startCrop(it) } } } } // 其他case处理... } } TargetApi(Build.VERSION_CODES.KITKAT) private fun handleImageOnKitKat(uri: Uri): Uri? { var path: String? null if (DocumentsContract.isDocumentUri(this, uri)) { val docId DocumentsContract.getDocumentId(uri) when (uri.authority) { com.android.providers.media.documents - { val id docId.split(:)[1] val selection ${MediaStore.Images.Media._ID}? path getImagePath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, arrayOf(id) ) } com.android.providers.downloads.documents - { val contentUri ContentUris.withAppendedId( Uri.parse(content://downloads/public_downloads), docId.toLong() ) path getImagePath(contentUri, null, null) } } } else if (content.equals(uri.scheme, true)) { path getImagePath(uri, null, null) } else if (file.equals(uri.scheme, true)) { path uri.path } return path?.let { FileProvider.getUriForFile( this, $packageName.fileprovider, File(it) ) } } private fun handleImageBeforeKitKat(uri: Uri): Uri { val path getImagePath(uri, null, null) return Uri.fromFile(File(path)) } private fun getImagePath( uri: Uri, selection: String?, selectionArgs: ArrayString? ): String? { var path: String? null contentResolver.query(uri, null, selection, selectionArgs, null)?.use { cursor - if (cursor.moveToFirst()) { path cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)) } } return path }4. 图片裁剪功能实现4.1 系统裁剪功能调用private fun startCrop(uri: Uri) { // 创建裁剪后图片的保存路径 val outputFile File( getExternalFilesDir(Environment.DIRECTORY_PICTURES), cropped_${System.currentTimeMillis()}.jpg ) val cropIntent Intent(com.android.camera.action.CROP).apply { setDataAndType(uri, image/*) // 适配Android 7.0 if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } // 裁剪参数配置 putExtra(crop, true) putExtra(aspectX, 1) putExtra(aspectY, 1) putExtra(outputX, 300) putExtra(outputY, 300) putExtra(scale, true) putExtra(return-data, false) putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile)) putExtra(outputFormat, Bitmap.CompressFormat.JPEG.toString()) } // 检查是否有可处理的Activity if (cropIntent.resolveActivity(packageManager) ! null) { startActivityForResult(cropIntent, REQUEST_CROP) } else { Toast.makeText(this, 未找到可用的图片裁剪应用, Toast.LENGTH_SHORT).show() } }4.2 裁剪结果处理在onActivityResult中补充裁剪结果处理REQUEST_CROP - { if (resultCode RESULT_OK) { // 获取裁剪后的图片文件 val croppedFile File( getExternalFilesDir(Environment.DIRECTORY_PICTURES), cropped_${System.currentTimeMillis()}.jpg ) if (croppedFile.exists()) { // 显示或上传裁剪后的图片 val bitmap BitmapFactory.decodeFile(croppedFile.absolutePath) imageView.setImageBitmap(bitmap) } } }5. 常见问题与解决方案5.1 权限问题处理在Android 6.0需要动态申请权限private fun checkPermissions(): Boolean { val permissions mutableListOfString() if (ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA ) ! PackageManager.PERMISSION_GRANTED ) { permissions.add(Manifest.permission.CAMERA) } if (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) ! PackageManager.PERMISSION_GRANTED ) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } if (permissions.isNotEmpty()) { ActivityCompat.requestPermissions( this, permissions.toTypedArray(), REQUEST_PERMISSIONS ) return false } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_PERMISSIONS) { if (grantResults.all { it PackageManager.PERMISSION_GRANTED }) { // 权限已授予 openCamera() } else { Toast.makeText(this, 需要相机和存储权限, Toast.LENGTH_SHORT).show() } } }5.2 其他常见问题FileProvider路径配置错误症状出现FileProvider异常或找不到文件解决方案检查file_paths.xml中的路径配置是否与代码中使用的位置一致裁剪后图片方向不正确症状裁剪后的图片出现旋转解决方案在显示图片前检查EXIF信息并纠正方向部分机型无法返回裁剪结果症状某些国产ROM上裁剪后不返回结果解决方案使用第三方裁剪库如uCrop作为备选方案大图片处理OOM症状加载大图时内存溢出解决方案使用BitmapFactory.Options进行采样压缩6. 性能优化建议图片压缩处理在显示或上传前对图片进行适当压缩private fun compressImage(file: File): File { val options BitmapFactory.Options().apply { inJustDecodeBounds true } BitmapFactory.decodeFile(file.absolutePath, options) val reqWidth 1024 val reqHeight 1024 val (width, height) options.run { outWidth to outHeight } var inSampleSize 1 if (height reqHeight || width reqWidth) { val halfHeight height / 2 val halfWidth width / 2 while (halfHeight / inSampleSize reqHeight halfWidth / inSampleSize reqWidth) { inSampleSize * 2 } } options.inJustDecodeBounds false options.inSampleSize inSampleSize val bitmap BitmapFactory.decodeFile(file.absolutePath, options) // 保存压缩后的图片 val outputFile File.createTempFile(compressed_, .jpg, cacheDir) FileOutputStream(outputFile).use { out - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out) } bitmap.recycle() return outputFile }缓存管理及时清理不再需要的临时文件使用LRU缓存管理频繁使用的图片异步处理使用协程或RxJava将耗时操作移到后台线程private fun processImageAsync(file: File) { CoroutineScope(Dispatchers.IO).launch { val compressedFile compressImage(file) withContext(Dispatchers.Main) { // 更新UI imageView.setImageBitmap(BitmapFactory.decodeFile(compressedFile.absolutePath)) } } }在实际项目中我发现很多开发者会忽略Android版本差异带来的兼容性问题特别是从相册获取图片路径的处理。通过封装一个统一的图片选择器工具类可以大大减少重复代码和潜在错误。另外对于要求较高的图片裁剪需求建议考虑使用专业的第三方库如uCrop它们通常提供更稳定的裁剪体验和更丰富的功能选项。