
1. 从零到一为什么我们需要一个图片选择器做Android开发特别是涉及到用户头像上传、内容发布、社交分享这些功能时图片选择是一个绕不开的坎。早期我们可能直接用系统自带的Intent.ACTION_PICK或者Intent.ACTION_GET_CONTENT简单几行代码就能拉起系统相册。但用过的朋友都知道这玩意儿就是个“毛坯房”界面简陋、功能单一、不同厂商手机上的表现天差地别更别提什么自定义UI、多选、拍照、预览、压缩、视频选择这些进阶需求了。于是社区里诞生了各种优秀的第三方图片选择器库而PictureSelector就是其中的佼佼者。它几乎成了国内Android开发者在处理媒体选择时的“标配”之一。我最早接触的是2.x版本功能已经很强大了。而PictureSelector 3.0以下简称PS3在架构、性能和易用性上又做了大幅升级特别是全面拥抱了AndroidX和Android 10API 29以上的存储权限模型Scoped Storage让我们在适配新系统时省了不少心。简单来说如果你正在开发一个需要用户选择图片或视频的App并且希望这个功能拥有统一的用户体验无论用户用什么品牌的手机界面和操作逻辑都是一致的。丰富的功能支持图片/视频单选、多选、混合选择、直接拍照、录视频、预览支持缩放、播放、图片压缩、主题自定义等。轻松的权限与存储适配帮我们处理好从Android 6.0的动态权限到Android 10的分区存储这些令人头疼的兼容性问题。清晰的回调与数据获取选择完成后能方便地拿到图片的路径、宽高、大小、旋转角度等信息。那么PictureSelector 3.0就是一个非常值得投入学习和使用的解决方案。接下来我就以一个实际集成者的角度带你从集成、配置、使用到踩坑完整地走一遍PS3的实战之路。2. 项目集成与环境配置避开第一个坑集成第三方库的第一步永远是看官方文档但文档有时会滞后或者一些细节没讲透。这里我结合最新稳定版以本文撰写时的v3.11.2为例和实际经验把步骤和注意事项捋清楚。2.1 依赖引入与仓库配置PS3已经发布到了Maven Central所以确保你的项目根目录build.gradle文件里配置了该仓库。// 项目根目录的 build.gradle allprojects { repositories { google() mavenCentral() // 确保有这一行 // 其他仓库... } }然后在你的App模块的build.gradle文件中添加依赖。这里有个关键点PS3的核心库和各个功能模块是分开的你可以按需引入。// app模块的 build.gradle dependencies { // 核心模块必须 implementation io.github.lucksiege:pictureselector:v3.11.2 // 图片压缩模块按需如果需要压缩功能 implementation io.github.lucksiege:compress:v3.11.2 // UCrop裁剪模块按需如果需要裁剪功能 implementation io.github.lucksiege:ucrop:v3.11.2 // 相机模块按需如果要用库内置的相机否则用系统相机 implementation io.github.lucksiepe:camerax:v3.11.2 // 注意这里包名是lucksiepe不是lucksiege是个小细节 // 如果你需要支持Android 10的视频预览可能需要额外的视频播放器库 // implementation com.github.chrisbanes:PhotoView:latest.release.here }注意模块的groupIdio.github.lucksiege和相机模块的artifactId前缀camerax要仔细核对拼写错误会导致依赖拉取失败。这是新手容易踩的第一个坑。2.2 权限声明与动态申请PS3需要一些权限来访问媒体文件和相机。你需要在AndroidManifest.xml中声明它们并在运行时动态申请针对Android 6.0。在AndroidManifest.xml中添加权限!-- 从外部存储读取媒体文件Android 10以下必需10以上在Scoped Storage下作用有限但建议保留 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- 向外部存储写入文件用于保存拍照、压缩后的图片 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 9 (API 28)及以下需要 -- !-- 使用相机 -- uses-permission android:nameandroid.permission.CAMERA / !-- 如果targetSdkVersion 33 (Android 13)还需要添加媒体权限 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / !-- Android 14 的视频权限细化 -- uses-permission android:nameandroid.permission.READ_MEDIA_VISUAL_USER_SELECTED /运行时权限申请你需要在启动图片选择器之前检查并申请相关权限。PS3内部不帮你申请权限这是合理的因为权限申请应该与你的App业务逻辑结合。通常我们在按钮点击事件里做这件事// 以Kotlin为例使用Activity Result API private val requestPermissionLauncher registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions - val allGranted permissions.entries.all { it.value } if (allGranted) { // 权限全部授予启动图片选择 launchPictureSelector() } else { // 处理权限被拒绝的情况例如给用户一个提示 Toast.makeText(this, 需要权限才能选择图片, Toast.LENGTH_SHORT).show() } } fun onSelectImageClick() { val requiredPermissions mutableListOfString() requiredPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE) requiredPermissions.add(Manifest.permission.CAMERA) if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { // Android 13 使用新的媒体权限 requiredPermissions.add(Manifest.permission.READ_MEDIA_IMAGES) // 如果还需要选视频 // requiredPermissions.add(Manifest.permission.READ_MEDIA_VIDEO) } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.P) { // Android 9.0及以下需要写权限 requiredPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } // 检查是否已授权 val ungrantedPermissions requiredPermissions.filter { ContextCompat.checkSelfPermission(this, it) ! PackageManager.PERMISSION_GRANTED } if (ungrantedPermissions.isEmpty()) { // 已有权限直接启动 launchPictureSelector() } else { // 申请未授予的权限 requestPermissionLauncher.launch(ungrantedPermissions.toTypedArray()) } }这段代码看起来有点长但它是健壮的。它考虑了不同Android版本的权限差异特别是Android 13API 33引入的细粒度媒体权限。很多开发者在适配Android 13时发现相册打不开了问题就出在忘了添加READ_MEDIA_IMAGES权限。2.3 Android 10 分区存储 (Scoped Storage) 适配这是另一个大坑但幸运的是PS3 3.0已经很好地处理了它。你只需要确保正确配置了FileProvider。在AndroidManifest.xml的application标签内添加FileProviderapplication ... ... 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 / !-- 指向一个xml资源文件 -- /provider /application在res/xml/目录下创建file_paths.xml文件如果没有则新建?xml version1.0 encodingutf-8? paths xmlns:androidhttp://schemas.android.com/apk/res/android !-- 外部存储根目录Android 10后无法直接访问但FileProvider可以用 -- external-path nameexternal path. / !-- App专属外部存储目录无需权限 -- external-files-path nameexternal_files path. / !-- 外部缓存目录 -- external-cache-path nameexternal_cache path. / !-- 内部缓存目录 -- cache-path nameinternal_cache path. / !-- 内部文件目录 -- files-path nameinternal_files path. / /paths这个配置定义了FileProvider可以访问的目录。PS3在拍照、缓存压缩图片时会用到这些路径。${applicationId}.fileprovider中的authorities必须唯一通常使用你的App包名这样避免与其他App冲突。完成以上三步集成与配置的基础工作就做好了。接下来我们进入核心的使用环节。3. 核心API调用与配置详解打造你的专属选择器PS3的使用核心是PictureSelector这个类它提供了链式调用的API非常清晰。我们从一个最简单的单选图片例子开始逐步增加复杂度。3.1 基础单选打开相册选一张图import com.luck.picture.lib.basic.PictureSelector import com.luck.picture.lib.config.SelectMimeType import com.luck.picture.lib.entity.LocalMedia import com.luck.picture.lib.interfaces.OnResultCallbackListener fun launchPictureSelector() { PictureSelector.create(this) // this 是 Activity 或 Fragment .openGallery(SelectMimeType.ofImage()) // 打开图片相册 .setSelectionMode(SelectionModeConfig.SINGLE) // 单选模式 .forResult(object : OnResultCallbackListenerLocalMedia { override fun onResult(result: ArrayListLocalMedia?) { // 选择成功回调result是选中项的集合单选时size为1 result?.firstOrNull()?.let { media - // 获取图片路径 val path: String if (media.isCut) { // 如果图片被裁剪过取裁剪后的路径 media.cutPath } else { // 原始路径或压缩后的路径如果开启了压缩 media.availablePath } // 使用Glide、Coil等加载图片 // Glide.with(thisYourActivity).load(path).into(imageView) } } override fun onCancel() { // 用户取消了选择 Toast.makeText(thisYourActivity, 已取消, Toast.LENGTH_SHORT).show() } }) }这就是最基础的调用。openGallery表示打开相册SelectMimeType.ofImage()指定选择类型为图片。setSelectionMode设置单选。forResult注册回调结果在onResult中返回类型是LocalMedia它包含了媒体文件的所有信息。几个关键点availablePath: 这是你最应该使用的路径。它是一个“智能”路径会按优先级返回压缩后的路径如果启用压缩且成功 - 裁剪后的路径如果裁剪过 - 原始路径。直接用这个路径去显示图片能保证你看到的是用户最终处理过的结果。isCut: 布尔值标记该图片是否被裁剪过。onCancel: 回调很重要特别是如果你的选择器是弹窗形式需要在取消时做界面恢复。3.2 丰富配置多选、最大数量、过滤、预览现在我们来加一些常用配置。PictureSelector.create(this) .openGallery(SelectMimeType.ofAll()) // 可以选图片和视频 .setSelectionMode(SelectionModeConfig.MULTIPLE) // 多选模式 .isDisplayCamera(true) // 相册内显示拍照入口 .setMaxSelectNum(9) // 最多选择9个 .setMinSelectNum(1) // 最少选择1个 .setMaxVideoSelectNum(2) // 最多选择2个视频在混合选择中限制视频数量 .setImageSpanCount(4) // 相册列表的列数 .isPreviewImage(true) // 点击可预览 .isPreviewVideo(true) // 预览视频 .isPreviewFullScreenMode(true) // 预览全屏模式 .setRecyclerAnimationMode(AnimationType.ALPHA_IN_ANIMATION) // 列表动画 .isGif(true) // 显示GIF图片 .setLanguage(LanguageConfig.CHINESE) // 设置语言中文 .forResult(callback) // 使用上面定义的回调配置解析与避坑setMaxSelectNum和setMaxVideoSelectNum在混合选择ofAll()时两者都生效。setMaxSelectNum是总上限setMaxVideoSelectNum是视频的上限。比如总上限9视频上限2那么用户最多可以选9个文件但其中视频不能超过2个。isDisplayCamera为true时相册列表第一个item会是相机入口。点击会调用系统相机或PS3内置相机如果引入了camerax模块。这里有个大坑如果你在Activity或Fragment的onActivityResult里也处理了相机拍照结果可能会和PS3的内部处理冲突。建议要么全交给PS3管理要么自己处理相机逻辑不要混用。isPreviewImage和isPreviewVideo强烈建议开启。预览时用户可以进行单选/取消选择在多选模式下体验更好。预览视频需要设备有解码能力PS3内部会调用系统播放器或你配置的播放器。isGif如果你需要支持GIF这里要打开。同时在显示GIF时比如用Glide也要确保你的图片加载库支持GIF。3.3 图片压缩与裁剪配置压缩和裁剪是图片选择器最常用的后期处理功能。图片压缩配置PictureSelector.create(this) .openGallery(SelectMimeType.ofImage()) .setSelectionMode(SelectionModeConfig.SINGLE) // 压缩配置 .isCompress(true) // 开启压缩 .setCompressEngine(ImageCompressEngine()) // 使用PS3默认压缩引擎需要引入compress模块 .setMinimumCompressSize(100) // 小于100KB的图片不压缩单位KB .setCompressQuality(80) // 压缩质量80% .forResult(callback)压缩是异步进行的在onResult回调被触发时压缩可能还没完成。LocalMedia的availablePath会等待压缩完成后才返回压缩路径。你可以通过media.isCompressed来判断是否已压缩但通常直接使用availablePath即可。裁剪配置PictureSelector.create(this) .openGallery(SelectMimeType.ofImage()) .setSelectionMode(SelectionModeConfig.SINGLE) // 裁剪配置 .isCut(true) // 开启裁剪 .setCutEngine(ImageCropEngine()) // 使用PS3默认裁剪引擎基于UCrop需要引入ucrop模块 .setCutListData() // 设置裁剪比例列表可选 .setCutRectCorner(true) // 裁剪框显示圆角 .setCutBarColor(Color.BLUE) // 裁剪界面底部工具栏颜色 .setCutCropStyle(CropImageView.Style.RECTANGLE) // 矩形裁剪框 .setCutCircleDimmedLayer(true) // 圆形裁剪时是否显示阴影层 .setCutFrameColor(Color.WHITE) // 裁剪框颜色 .setFreeStyleCropEnabled(true) // 开启自由裁剪可旋转、缩放 .forResult(callback)裁剪是同步操作用户会在一个单独的裁剪界面完成操作并确认后才会返回结果。裁剪后的图片会保存在LocalMedia.cutPath中同时isCut会变为true。重要经验压缩和裁剪不要同时开启。因为裁剪本质上已经生成了一张新的图片尺寸、比例都变了如果先裁剪再压缩压缩的是裁剪后的图这是合理的。但如果先压缩再裁剪逻辑上就有点奇怪而且可能引发路径引用问题。官方示例和常见实践都是只开一个或者开启裁剪裁剪本身也是一种尺寸和质量的调整。如果既想裁剪又想进一步压体积可以在拿到裁剪后的路径cutPath后自己调用压缩库如Luban进行二次压缩。3.4 直接启动相机或录制视频除了从相册选择PS3也支持直接启动相机拍照或录像。直接拍照PictureSelector.create(this) .openCamera(SelectMimeType.ofImage()) // 打开相机拍照 .isCompress(true) // 拍照后是否压缩 .forResult(callback)拍照完成后结果同样通过OnResultCallbackListener回调返回一个LocalMedia列表只有一个元素。注意直接拍照不会经过相册选择界面拍完就回调。直接录像PictureSelector.create(this) .openCamera(SelectMimeType.ofVideo()) // 打开相机录像 .setRecordVideoMaxSecond(15) // 最大录制时长15秒 .setRecordVideoMinSecond(2) // 最小录制时长2秒 .setVideoQuality(1) // 视频质量1为高清 .forResult(callback)4. 结果处理与进阶技巧凌波微步拿到LocalMedia结果列表后我们该如何使用除了简单的显示还有哪些进阶场景和坑需要注意4.1 深入理解 LocalMedia 对象LocalMedia是一个信息宝库正确理解它的字段能帮你省很多事。result?.forEach { media - // 基础信息 val id media.id // 媒体库ID val mimeType media.mimeType // 媒体类型如 image/jpeg, video/mp4 val width media.width // 宽 val height media.height // 高 val duration media.duration // 视频时长毫秒 val size media.size // 文件大小字节 // 路径信息最重要 val path media.path // 原始路径Android Q以前是绝对路径Q以后可能是相对路径或Uri val realPath media.realPath // 绝对路径PS3尝试获取的可能为空 val availablePath media.availablePath // 推荐使用的可用路径 val cutPath media.cutPath // 裁剪后的路径如果裁剪过 val compressPath media.compressPath // 压缩后的路径如果压缩过 // 状态标记 val isCut media.isCut val isCompressed media.isCompressed val isOriginal media.isOriginal // 是否选择了原图未压缩 // Android Q (10) 及以上特别重要 val androidQToPath media.androidQToPath // 在Android Q上如果文件被复制到App私有目录这里是目标路径 val bucketId media.bucketId // 相册文件夹ID val isAndroidQ media.isAndroidQ // 是否来自Android Q的媒体库 }路径选择黄金法则始终优先使用availablePath。这是PS3设计来给开发者用的“最终路径”它已经帮你处理了压缩、裁剪的逻辑。如果需要判断文件是否经过处理用isCut和isCompressed。在Android 10及以上直接使用path可能无法用File对象打开因为可能是content://格式的Uri。availablePath在内部会处理好这个转换如果文件在外部公共目录PS3可能会将其复制到App的私有目录androidQToPath然后availablePath指向这个私有目录的路径。所以在Android Q上请务必使用availablePath不要自己再去解析path。4.2 图片加载与显示拿到路径后通常我们用Glide或Coil来加载显示。// 使用Glide Glide.with(imageView.context) .load(media.availablePath) // 关键用 availablePath .placeholder(R.drawable.placeholder) .error(R.drawable.error) .into(imageView) // 如果是视频需要加载第一帧作为封面 if (media.mimeType?.startsWith(video) true) { Glide.with(imageView.context) .load(media.availablePath) .frame(1000) // 取第1秒的帧 .into(imageView) }注意视频封面加载Glide加载视频封面需要GlideModule支持或者使用RequestOptions.frame()。确保你的Glide版本和配置支持视频解码。4.3 上传到服务器上传时我们需要的是文件的File对象或字节流。从availablePath获取val file File(media.availablePath) if (file.exists()) { // 使用OkHttp、Retrofit等上传file } else { // 罕见情况路径可能是一个Uricontent:// // 此时需要通过ContentResolver打开InputStream val inputStream context.contentResolver.openInputStream(Uri.parse(media.availablePath)) // 上传流 }同样availablePath在绝大多数情况下已经是可用的文件路径。如果遇到content://Uri在极少数直接从MediaStore选取且未复制的情况下availablePath会保持Uri格式你需要按上述方式处理。4.4 主题与UI自定义PS3提供了强大的UI自定义能力几乎可以改成任何你想要的样子。全局主题设置推荐在Application中初始化class MyApp : Application() { override fun onCreate() { super.onCreate() // 初始化图片选择器 PictureSelector.init(this) { setTheme(if (isDarkMode()) R.style.PictureDarkStyle else R.style.PictureDefaultStyle) } } }你可以继承并修改PictureDefaultStyle或PictureDarkStyle来自定义颜色、文字等。单次调用覆盖全局主题PictureSelector.create(this) .openGallery(SelectMimeType.ofImage()) .setSelectorUIStyle(SelectorUIStyle()) // 传入自定义的UI样式对象 .setPictureStyle(PictureSelectorStyle()) // 传入完整的图片选择器样式对象 .forResult(callback)通过SelectorUIStyle和PictureSelectorStyle你可以细粒度控制导航栏颜色、按钮文字、选择框样式、空白页面提示图等等。具体有哪些属性可以设置需要查阅源码或官方文档中的Style类定义。自定义UI是个细致活建议先跑通基础功能再根据UI设计稿一点点调整。4.5 处理Activity Result API的兼容问题如果你在Activity或Fragment中使用了Android新的Activity Result APIregisterForActivityResult需要注意PS3的forResult方法内部可能还是基于旧的startActivityForResult。为了更好的兼容性和生命周期安全PS3也提供了对应的支持。在Activity中使用// 在Activity中定义Launcher private val pictureLauncher registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result - if (result.resultCode Activity.RESULT_OK) { val data: Intent? result.data val selectList PictureSelector.obtainSelectorList(data) // 处理 selectList它是 ListLocalMedia handleResult(selectList) } } // 启动选择器 fun launchSelector() { val intent PictureSelector.create(this) .openGallery(SelectMimeType.ofImage()) .build() pictureLauncher.launch(intent) }在Fragment中使用类似。这种方式将结果回调的控制权完全交给了开发者更符合现代Android开发的最佳实践避免了在onActivityResult中处理一堆requestCode的混乱。我强烈推荐在新建项目中使用这种方式。5. 实战避坑与疑难杂症排查用了这么久PS3我也踩过不少坑。下面把这些“血泪教训”总结一下希望能帮你绕过去。5.1 坑一拍照后图片不显示或旋转90度现象调用PS3拍照拍出来的图片在预览或上传后方向不对特别是竖屏拍照变成横屏或者在某些设备上直接不显示。根因分析Exif信息丢失图片文件包含Exif元数据其中Orientation标签指示了图片的正确方向。有些设备尤其是国产定制ROM的相机App可能不会正确写入Exif或者图片处理过程中压缩、复制丢失了Exif信息。图片加载库的差异Glide等库在加载图片时会自动读取Exif并纠正方向但如果你直接读取Bitmap或者使用的加载库不支持Exif就会出问题。PS3内置相机与系统相机的差异如果你引入了camerax模块并使用PS3内置相机它的行为可能与系统相机不同。解决方案方案A推荐坚持使用availablePath并配合Glide等能自动处理Exif的图片加载库。这是最简单的办法让专业的库做专业的事。方案B如果必须自己处理Bitmap则需要读取Exif信息并旋转。fun rotateImageByExif(filePath: String): Bitmap? { try { val exif ExifInterface(filePath) val orientation exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) val bitmap BitmapFactory.decodeFile(filePath) return when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 - rotateBitmap(bitmap, 90f) ExifInterface.ORIENTATION_ROTATE_180 - rotateBitmap(bitmap, 180f) ExifInterface.ORIENTATION_ROTATE_270 - rotateBitmap(bitmap, 270f) else - bitmap } } catch (e: Exception) { e.printStackTrace() } return null } private fun rotateBitmap(source: Bitmap, angle: Float): Bitmap { val matrix Matrix() matrix.postRotate(angle) return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true) }方案C在PS3配置中尝试关闭某些可能干扰的选项或者明确指定使用系统相机。PictureSelector.create(this) .openCamera(SelectMimeType.ofImage()) .isUseSystemCamera(false) // 明确不使用系统相机用PS3内置如果引入了camerax模块 // 或者 .isUseSystemCamera(true) // 明确使用系统相机 .setCameraInterceptListener { context, cameraConfig - // 这里可以拦截相机启动进行自定义 false // 返回false让PS3处理true则自己处理 } .forResult(callback)5.2 坑二在Fragment中使用时回调不执行或崩溃现象在Fragment里调用PS3选择图片后onResult回调没触发或者Fragment已经销毁导致空指针崩溃。根因分析这通常是生命周期管理问题。如果使用旧的forResult回调PS3内部持有的是Activity的引用。如果Fragment在等待回调时被销毁或从Activity分离就可能出问题。解决方案使用Activity Result API如前文4.5节所述在Fragment中也使用registerForActivityResult来启动和接收结果这是最安全的方式。确保使用正确的Context创建PictureSelector时传入的必须是Fragment的requireActivity()或requireContext()不能是getActivity()可能为null。在onDestroy中清理如果因为某些原因必须用旧回调在Fragment的onDestroy中取消可能存在的回调。override fun onDestroy() { super.onDestroy() // 如果PictureSelector有提供取消回调的方法在这里调用 // 通常更推荐使用Activity Result API来避免此问题 }5.3 坑三Android 11 上无法选择图片或提示无权限现象在Android 11API 30或更高版本的设备上即使授予了所有权限PS3相册里仍然空白或者选择图片失败。根因分析从Android 11开始即使有了READ_EXTERNAL_STORAGE权限对媒体文件的访问也受到了进一步限制。应用默认只能访问自己创建的和MediaStore中的部分媒体文件。需要MANAGE_EXTERNAL_STORAGE权限所有文件访问权限才能访问所有文件但这个权限上架Google Play很麻烦且需要向用户说明理由。解决方案 PS3 3.0已经适配了Scoped Storage。问题可能出在权限遗漏检查是否在AndroidManifest.xml和运行时申请了READ_MEDIA_IMAGES和READ_MEDIA_VIDEO权限针对Android 13。FileProvider配置错误确保file_paths.xml配置正确且FileProvider的authorities与代码中匹配。使用PS3的媒体库加载PS3在Android Q上会使用MediaStore API来查询媒体文件这符合Scoped Storage规范。确保你的targetSdkVersion 29并且没有禁用Scoped StoragerequestLegacyExternalStorage在Android 11上已失效。测试真机多样性在某些深度定制的国产ROM上即使有权限MediaStore的查询也可能返回空。可以尝试在PS3初始化时设置自定义的媒体加载器高级用法需查阅PS3高级文档或者反馈给设备厂商。5.4 坑四多选模式下选择图片后返回结果很慢现象选择了大量图片比如9张后点击完成App要卡顿好几秒才回调结果。根因分析这通常发生在开启了图片压缩的情况下。压缩是多张图片顺序执行的是一个CPU密集型操作如果在主线程进行必然会阻塞UI。解决方案 PS3的压缩默认是在子线程进行的但压缩完成后的一些处理如更新LocalMedia对象可能涉及IO。如果还是慢可以优化压缩参数提高setMinimumCompressSize的值让太小的图片不压缩。或者降低setCompressQuality牺牲一些质量换取速度。异步处理结果在onResult回调中不要立即进行复杂的操作如批量上传。先将结果保存起来然后在后台线程处理压缩图片的上传等。分步操作对于“选择9张图并立即上传”这种场景可以考虑改为“先选择9张图不压缩或快速压缩并预览”用户确认后再单独触发一个后台压缩上传任务并给用户进度提示。// 选择时不压缩快速返回 PictureSelector.create(this) .openGallery(SelectMimeType.ofImage()) .setSelectionMode(SelectionModeConfig.MULTIPLE) .setMaxSelectNum(9) .isCompress(false) // 关闭即时压缩 .forResult { result - // 快速拿到结果展示预览 showPreview(result) // 用户点击“上传”后在后台线程进行压缩 lifecycleScope.launch(Dispatchers.IO) { result?.forEach { media - val compressedFile compressImage(File(media.availablePath)) // 你自己的压缩方法 uploadFile(compressedFile) } } }5.5 坑五自定义UI时某些样式不生效现象按照文档设置了PictureSelectorStyle里的各种颜色和文字但运行起来发现界面没变化。根因分析设置时机不对全局样式需要在PictureSelector.init时设置。单次调用的样式需要在openGallery或openCamera之前调用setPictureStyle。样式属性覆盖不全PS3的样式类属性很多你可能只改了一部分另一部分还保持着默认值或主题值。资源ID冲突如果你自定义的样式里引用了drawable或color确保这些资源在你的App模块中存在且没有与PS3库内部的资源ID重名虽然概率小。排查步骤检查代码顺序create() - setPictureStyle(...) - openGallery(...) - ... forResult()。使用一个完整的、从官方示例中拷贝的PictureSelectorStyle对象只修改其中一两个属性看是否生效逐步定位。查看LogcatPS3在初始化或应用样式时是否有错误日志。最直接的方式下载PS3的源码把它作为一个模块引入你的项目然后直接调试源码看你的样式对象是如何被应用的。这是解决自定义UI问题最彻底的方法。PictureSelector 3.0是一个功能强大、社区活跃的库它能覆盖绝大多数图片选择场景。它的核心价值在于把Android平台上碎片化、复杂化的媒体选择与处理逻辑封装成了一个简单易用的接口。掌握它不仅能提升开发效率更能让你的App在媒体处理这一块拥有稳定、一致的体验。希望这篇从集成、使用到避坑的详细指南能帮助你顺利地在项目中驾驭它。记住遇到问题多查源码和Issues社区里很可能已经有现成的解决方案了。