ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

调用系统相机进行拍照并保存:TaoToken 统一 Key 通道下的移动端图像采集实践

调用系统相机进行拍照并保存:TaoToken 统一 Key 通道下的移动端图像采集实践 1. 移动端拍照保存的真实痛点为什么你的相机回调总是拿不到图做移动端开发的朋友大概率都遇到过这个场景产品经理说加个拍照上传功能你心想这不就是调个系统相机的事吗结果真机一跑问题全来了。Android 上onActivityResult回来了但data.getData()是 nulliOS 上UIImagePickerController拍完照存到临时目录过一会儿就没了用户拍完切后台再回来图片直接消失。这些坑我在过去几年做智能硬件配套 App 的时候几乎踩了个遍。调用系统相机拍照并保存听起来简单实际上是一条完整的链路权限申请 → 拉起相机 Intent/AVCapture → 拿到临时文件 → 转存到相册或沙盒目录 → 保留 EXIF 信息 → 失败时回滚清理。任何一环出问题用户看到的就是拍照没反应或者图片丢了。这篇文章聚焦 Android 和 iOS 双端把这条链路拆开讲透。适合谁看正在做 App 内图像采集的移动端开发者、做智能硬件配网/识别需要拍照上传的工程师、以及被FileUriExposedException和PHPhotoLibrary权限折磨过的同学。我会给出可直接复制的权限声明、拍照回调、文件保存代码并附上真机验证步骤和常见异常排查清单。另外提一句如果你在做的是 AI 应用拍照之后往往还要把图片送给大模型做识别或理解这时候一个统一的 Key 通道能省掉不少对接成本。TaoToken 就是干这个的后面会讲怎么把拍照链路和模型调用串起来。先说结论Android 侧的核心是FileProviderMediaStoreiOS 侧的核心是PHPickerViewController或UIImagePickerControllerPHPhotoLibrary保存。下面逐个拆。2. TaoToken 前置准备统一 Key 通道怎么接入拍照后的 AI 链路在讲具体代码之前先把这个前置环节说清楚因为很多同学拍照功能做完之后下一步就是要把图片送去 AI 处理比如 OCR 识别、物体检测、图像描述。如果每个模型都单独申请 Key、单独配 Base URL维护成本很高。TaoToken 提供的是一个统一的 API 通道你只需要一个 Key就能调用多种模型。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。接入步骤很直接第一步注册后在控制台创建 API Key。控制台地址https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。创建完记得复制保存Key 只显示一次。第二步如果你用的是 Claude Code 这类编码工具可以直接用 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它把 Base URL 和 Key 的配置都简化了。第三步验证 Key 是否可用。最直接的方式是用模型对话页面测一下https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。输入一句话看有没有正常返回。对于拍照场景你拿到图片的 base64 或者 URL 之后就可以通过统一的 API 端点发给模型。比如图像理解类请求请求体里带上model和messages图片以 base64 编码放在 content 里。这样你的 App 拍照链路和 AI 处理链路就串起来了不用为每个模型单独维护一套鉴权逻辑。需要说明的是TaoToken 在这里的角色是统一调用通道不是替代你的编辑器或相机功能它解决的是拍完照之后怎么方便地调模型这个问题。如果你暂时不需要 AI 处理这一节可以先跳过直接看后面的拍照代码。3. Android 可复制配置FileProvider 声明与拍照保存完整代码Android 这块是重灾区尤其是 Android 7.0 之后file://URI 被禁止必须用FileProvider。我见过太多项目还在用Uri.fromFile()在低版本上跑得好好的一到新机型就崩。3.1 权限声明与 FileProvider 配置先在AndroidManifest.xml里声明权限和 Providermanifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.takephoto uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-feature android:nameandroid.hardware.camera android:requiredfalse / application ... provider 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 /application /manifest然后在res/xml/file_paths.xml里定义可共享的路径?xml version1.0 encodingutf-8? paths external-files-path nameimages pathPictures/ / cache-path namecache_images pathimages/ / external-cache-path nameext_cache pathimages/ / /paths注意WRITE_EXTERNAL_STORAGE加了maxSdkVersion28因为 Android 10 之后分区存储生效写自己沙盒目录不需要这个权限了。READ_MEDIA_IMAGES是 Android 13 读取相册需要的。3.2 拍照 Intent 与临时文件创建关键点临时文件要放在getExternalFilesDir()或cacheDir下不要直接往DCIM写否则权限和路径都会出问题。private lateinit var photoUri: Uri private lateinit var tempFile: File private fun createImageFile(): File { val timeStamp SimpleDateFormat(yyyyMMdd_HHmmss, Locale.getDefault()).format(Date()) val storageDir getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile(JPEG_${timeStamp}_, .jpg, storageDir) } private fun takePhoto() { try { tempFile createImageFile() } catch (e: IOException) { Toast.makeText(this, 创建文件失败, Toast.LENGTH_SHORT).show() return } photoUri FileProvider.getUriForFile( this, ${applicationId}.fileprovider, tempFile ) val intent Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } startActivityForResult(intent, REQUEST_TAKE_PHOTO) }这里EXTRA_OUTPUT指定了输出 URI相机拍完直接写到这个文件回调里data可能是 null这是正常的别去data.getData()。3.3 回调处理与保存到相册拍完之后临时文件在沙盒里用户在图库看不到。要持久化到相册用MediaStore插入override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode REQUEST_TAKE_PHOTO resultCode RESULT_OK) { saveToGallery(tempFile) } } private fun saveToGallery(file: File) { val values ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, file.name) put(MediaStore.Images.Media.MIME_TYPE, image/jpeg) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES /MyApp) put(MediaStore.Images.Media.IS_PENDING, 1) } } val resolver contentResolver val uri resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) uri?.let { resolver.openOutputStream(it)?.use { out - FileInputStream(file).use { input - input.copyTo(out) } } if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(it, values, null, null) } } // 保存成功后清理临时文件 file.delete() }IS_PENDING这个标记很重要Android 10 上写入过程中其他应用看不到这张图写完置 0 才可见避免半截文件被扫描。3.4 EXIF 保留如果你需要保留拍摄时间、GPS、方向等信息用ExifInterface读取临时文件再写回目标文件val srcExif ExifInterface(tempFile.absolutePath) val dstExif ExifInterface(savedFilePath) val orientation srcExif.getAttribute(ExifInterface.TAG_ORIENTATION) dstExif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation) dstExif.saveAttributes()方向信息尤其重要很多手机拍出来是横的就是 EXIF orientation 没处理。4. iOS 可复制配置UIImagePickerController 与 PHPhotoLibrary 保存iOS 相对规范一些但权限和保存逻辑也有讲究。这里用UIImagePickerController举例PHPickerViewController是更新的方案但拍照场景UIImagePickerController更直接。4.1 Info.plist 权限声明keyNSCameraUsageDescription/key string需要使用相机拍摄照片用于识别/string keyNSPhotoLibraryAddUsageDescription/key string需要保存拍摄的照片到相册/string keyNSPhotoLibraryUsageDescription/key string需要访问相册选择照片/string三个都要写少一个审核会被拒或者运行时崩溃。4.2 拉起相机与回调import UIKit import Photos class CameraViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { func openCamera() { guard UIImagePickerController.isSourceTypeAvailable(.camera) else { print(模拟器不支持相机) return } let picker UIImagePickerController() picker.sourceType .camera picker.delegate self picker.cameraCaptureMode .photo present(picker, animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.dismiss(animated: true) guard let image info[.originalImage] as? UIImage else { return } saveToPhotoLibrary(image) } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) } }4.3 保存到相册与沙盒func saveToPhotoLibrary(_ image: UIImage) { PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in guard status .authorized || status .limited else { print(无相册写入权限) return } PHPhotoLibrary.shared().performChanges { PHAssetChangeRequest.creationRequestForAsset(from: image) } completionHandler: { success, error in if success { print(保存相册成功) } else { print(保存失败: \(error?.localizedDescription ?? )) } } } }如果不想存相册只存沙盒直接写文件func saveToSandbox(_ image: UIImage) - URL? { guard let data image.jpegData(compressionQuality: 0.9) else { return nil } let dir FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let url dir.appendingPathComponent(photo_\(Int(Date().timeIntervalSince1970)).jpg) do { try data.write(to: url) return url } catch { print(写入失败: \(error)) return nil } }4.4 EXIF 保留iOS 用CGImageSource读取原始 EXIF再写入新文件func preserveExif(from srcURL: URL, to dstURL: URL) { guard let src CGImageSourceCreateWithURL(srcURL as CFURL, nil), let type CGImageSourceGetType(src), let dst CGImageDestinationCreateWithURL(dstURL as CFURL, type, 1, nil) else { return } let properties CGImageSourceCopyPropertiesAtIndex(src, 0, nil) CGImageDestinationAddImageFromSource(dst, src, 0, properties) CGImageDestinationFinalize(dst) }5. 真机验证与常见异常排查清单代码写完真机跑一遍才算数。下面是我整理的验证步骤和踩过的坑。5.1 真机验证步骤Android 侧装到真机点拍照按钮确认相机拉起拍完返回打开系统相册看有没有新图用adb shell进getExternalFilesDir目录确认临时文件已删除旋转手机拍一张检查图片方向是否正确。iOS 侧真机运行首次会弹相机权限和相册权限都允许拍完去照片App 确认切后台再回来确认图片还在用 Xcode 的 Devices 窗口看沙盒目录。5.2 常见报错对照报错信息原因解决FileUriExposedExceptionAndroid 7 用了file://URI改用FileProvider.getUriForFileonActivityResult中data为 null指定了EXTRA_OUTPUT直接用自己创建的photoUri别读 datalocal proxy failed网络请求走了本地代理但代理没起检查网络配置确认 API 端点可达401 UnauthorizedAPI Key 无效或未带检查请求头Authorization: Bearer keyreading choices相关解析错误模型返回格式与预期不符检查请求体 JSON 结构确认 model 字段正确OAuth相关报错鉴权方式不匹配确认用的是 API Key 而非 OAuth 流程iOS 保存相册无反应缺NSPhotoLibraryAddUsageDescription补上 plist 声明图片方向不对EXIF orientation 丢失读取并写回 orientation 属性5.3 失败回滚拍照失败或保存失败时一定要清理临时文件否则沙盒会越积越大private fun rollback() { if (::tempFile.isInitialized tempFile.exists()) { tempFile.delete() } }在resultCode ! RESULT_OK时调用以及保存相册异常时调用。5.4 接入 AI 处理时的配置三件套如果你拍完照要调模型不管是 Claude Code、Cline MCP 还是 Codex配置都离不开三件套Base URL、Key、Model ID。以 Codex 的auth.json为例{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }Cline MCP 的配置类似在设置里填 Base URL 和 Key选好 Model ID。Claude Code 的话用 Coding Plan 页面给的配置直接替换即可。三件套缺一不可尤其是 Model ID 写错会直接报模型不存在。6. 从拍照到 AI 识别把图像采集链路接到统一通道拍照保存只是第一步真正的价值在于拍完之后能做什么。比如智能硬件 App 拍设备铭牌做 OCR、拍商品做识别、拍文档做摘要这些都需要把图片送给模型。接入方式很简单拍照拿到图片文件后转成 base64 或上传拿到 URL然后通过统一 API 端点发请求。请求头带上你的 Key请求体里指定 model 和 messages图片放在 content 数组里。curl https://taotoken.net/api/v1/messages \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [ { role: user, content: [ {type: text, text: 描述这张图片的内容}, {type: image, source: {type: base64, media_type: image/jpeg, data: ...}} ] } ] }这样你的拍照链路和 AI 链路就打通了。Key 的管理、模型的切换都在一个通道里完成不用为每个模型单独维护配置。需要 Key 的话去控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的示例。想先测模型效果就去模型对话页https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码和 Agent 的可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后说个实用技巧拍照临时文件一定要放在getExternalFilesDir或cacheDir别放DCIM保存相册用MediaStore的IS_PENDING机制EXIF 的 orientation 一定要处理不然用户拍竖图显示横图。这三点做到移动端拍照保存基本就稳了。
返回列表