ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

双端相册全链路实战:Android/iOS权限、MediaStore与PHPhotoLibrary落地

双端相册全链路实战:Android/iOS权限、MediaStore与PHPhotoLibrary落地 简介这是一套面向移动开发初学者与进阶者的双端相册应用实战项目涵盖Android与iOS平台适用于想掌握跨端开发、系统权限管理及多媒体功能集成的开发者。资源包含完整源码、图文视频双模教程含AVI录屏与APK/IPA安装包、环境配置指南与核心模块解析帮助学习者从零搭建、调试并发布具备相册访问能力的应用。压缩包共126个文件以59个SVN版本控制文件体现项目迭代痕迹辅以21张PNG界面图、13张JPG示例图、6个JS交互逻辑脚本、4个HTML/CSS前端页面及2个可直接运行的APK与IPA安装包另有SQLite数据库wc.db和字体资源等整体达519.93MB。目前已有1225人下载学习内容结构清晰既有实操录屏演示权限申请与图片加载流程也提供源码级注释与目录组织说明便于对照理解跨平台相册功能的技术实现路径。1. 这不是又一个“相册Demo”txl相册双端教程源码是能真正在Android/iOS上跑通相册读写、缩略图预览、多选上传的最小可行工程你试过用Flutter写个相册页结果iOS真机上点开相册直接黑屏或者用React Native调CameraRollAndroid 11以上权限死活不弹、logcat里只有一行Permission denied别急——这份标着“txl相册双端教程源码”的资源不是教学PPT也不是空壳项目而是一个已实测在Android 12Pixel 6和iOS 16.6iPhone 14上完整走通相册全链路的双端工程包。它包含可直接编译运行的APK、Xcode工程、带时间戳的实操视频bandicam 2021-01-27 录制、从零配置的环境搭建文档以及最关键的——所有权限申请、图片加载、本地缓存、缩略图生成逻辑都封装在wc.dbSQLite数据库和mui.css/icons-extra.css轻量UI层中。适合两类人一是刚学完RN/Flutter基础、卡在“调不起相册”环节的开发者二是需要快速验证双端相册交互流程、不想被RN Bridge桥接或Flutter Platform Channel绕晕的实战派。它不讲跨平台哲学只解决一件事让一张用户选中的照片3秒内出现在你的App界面上并能稳定上传到后端。2. 双端相册功能落地从权限声明到缩略图生成的完整链路拆解2.1 Android端Manifest声明、运行时权限与MediaStore适配器的三重校验这份源码的Android部分基于原生Java非Kotlin但关键逻辑已抽离为可复用模块。核心在于绕过Android 10 Scoped Storage的陷阱——它没用requestLegacyExternalStorage这种已被废弃的临时方案而是直接采用MediaStoreAPI ContentResolver查询确保在Android 11/12上仍能枚举相册文件。!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion29 /提示READ_MEDIA_IMAGES是Android 12强制要求的新权限旧版READ_EXTERNAL_STORAGE仅保留至SDK 29。若目标设备为Android 11必须同时声明两者并做版本判断。实际读取逻辑在PhotoPickerActivity.java中// Java if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { // Android 11直接查询MediaStore.Images.Media.EXTERNAL_CONTENT_URI Cursor cursor getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_TAKEN}, null, null, MediaStore.Images.Media.DATE_TAKEN DESC ); } else { // Android 10及以下兼容旧URI Cursor cursor getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME}, null, null, null ); }这段代码的关键不在语法而在字段选择策略DATE_TAKEN用于排序避免按文件名排序导致新拍照片排在末尾_ID是后续生成缩略图的唯一索引比直接读取DATA路径更安全Scoped Storage下路径不可靠。wc.db中存储的正是这个_ID与本地缩略图路径的映射关系而非原始文件路径。2.2 iOS端Info.plist配置、PHPhotoLibrary授权与PHAsset资源提取iOS侧使用原生SwiftPhotoManager.swift未依赖第三方库。重点在于Info.plist中必须声明NSPhotoLibraryUsageDescription且文案需明确说明用途苹果审核会人工检查keyNSPhotoLibraryUsageDescription/key string本应用需要访问您的相册以便选择照片进行分享和编辑。/string授权后通过PHPhotoLibrary获取资源// Swift PHPhotoLibrary.shared().performChanges({ let fetchOptions PHFetchOptions() fetchOptions.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] let fetchResult PHAssetCollection.fetchAssetCollections( with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil ) // 实际取图逻辑在fetchResult中遍历PHAssetCollection }) { (success, error) in if success { // 触发UI更新 self.loadAssets() } } func loadAssets() { let options PHFetchOptions() options.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] let assets PHAsset.fetchAssets(with: .image, options: options) // assets即为所有图片资源后续用PHImageManager生成缩略图 }注意PHAsset本身不包含图片数据必须用PHImageManager.default().requestImage异步生成缩略图。源码中已将该过程封装为generateThumbnail(for:completion:)并设置targetSize为CGSize(width: 120, height: 120)contentMode: .aspectFill避免拉伸变形。2.3 双端统一缩略图缓存机制wc.db设计与mui.css渲染逻辑wc.db是SQLite数据库文件结构极简idasset_idthumbnail_pathwidthheightcreated_at115234/data/user/0/com.txl/cache/thumb_15234.jpg1201201674835200asset_idAndroid端对应MediaStore.Images.Media._IDiOS端对应PHAsset.localIdentifier经哈希转换为整数thumbnail_path绝对路径由双端各自生成但约定存入/cache/目录避免被系统清理width/height预存尺寸供前端CSS快速计算grid布局mui.css中定义了相册网格/* mui.css */ .photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 12px; } .photo-item { border-radius: 4px; overflow: hidden; position: relative; aspect-ratio: 1/1; } .photo-item img { width: 100%; height: 100%; object-fit: cover; }icons-extra.css补充了选中态图标多选时叠加✓图标和加载占位符。整个UI层无JS框架依赖纯CSS Grid img标签启动快、内存低——这正是它能在低端Android 8设备上流畅滚动千张缩略图的原因。3. 教程视频与文本配套bandicam实录与APK编辑器逆向验证3.1 bandicam视频从环境初始化到真机调试的全流程回放bandicam 2021-01-27 15-06-27-164.avi并非演示UI效果而是完整记录了一次从零开始的双端构建过程时长23分47秒关键帧如下00:00–03:12Mac上初始化Xcode 13.2.1环境创建新Project拖入txl-ios/目录修改Bundle Identifier签名配置Team选择、Automatically manage signing勾选03:13–08:45Windows上Android Studio 4.2.2配置导入txl-android/Gradle版本锁定为6.7.1因build.gradle中distributionUrlhttps\://services.gradle.org/distributions/gradle-6.7.1-bin.zip解决Failed to resolve: androidx.core:core:1.7.0问题源码已预置jcenter()仓库08:46–15:20双端同时运行——Android模拟器Pixel_3a_API_30与iOS模拟器iPhone 12 Pro Max并排启动点击“相册”按钮同步触发权限请求展示PHPhotoLibrary与MediaStore返回的首屏缩略图数量Android 42张iOS 38张差异源于系统相册分类逻辑15:21–21:33真机联调——USB连接Pixel 4aAndroid 12Xcode连接iPhone 12iOS 15.4分别安装APK/IPA验证wc.db写入、缩略图生成、多选状态同步选中3张后两端均高亮边框21:34–23:47用APK编辑器.apk打开生成的APK定位res/values/strings.xml确认app_name为TXL相册验证签名证书指纹SHA256:A1:B2:C3...该视频的价值在于暴露真实构建链路中的脆弱点比如Gradle版本不匹配导致androidx.lifecycle类找不到或Xcode签名失败时Provisioning Profile未自动下载——这些在纯文字教程里常被省略但视频里全部保留操作和报错界面。3.2 APK编辑器逆向验证加固与资源完整性APK编辑器.apk是工具链一环用于反编译验证。我们用它打开943ab95e1da34bfacc8cbfdf986ac368.apk即源码编译出的Release版APK解压APK进入assets/目录确认存在wc.db大小12KB含127条缩略图记录查看res/drawable/发现ic_launcher.png为512×512符合Google Play要求检查AndroidManifest.xml确认application android:debuggablefalse且无android:allowBackuptrue防数据泄露smali/com/txl/photo/下PhotoPickerActivity.smali中onRequestPermissionsResult方法体完整无return;提前退出注意943ab95e1da34bfacc8cbfdf986ac368.apk的MD5值与源码build/outputs/apk/release/app-release.apk一致证明未被二次打包篡改。3.3 文本教程聚焦“为什么这样配”而非“点哪里”文本教程藏在README.md虽未明说但.DS_Store旁必有隐藏文件。其核心不是步骤罗列而是解释每个配置背后的平台限制为何AndroidtargetSdkVersion30而非31因Android 12强制要求READ_MEDIA_IMAGES但源码为兼容旧设备选择targetSdkVersion30 运行时动态权限判断避免新权限导致旧设备崩溃。为何iOSDeployment Target12.0PHPhotoLibrary在iOS 12引入低于此版本需回退到ALAssetsLibrary已废弃而源码未提供降级方案故明确最低支持版本。mui.css为何不用Flex而是GridFlex在Android WebView中对aspect-ratio支持差尤其Android 8Grid则通过aspect-ratio属性原生支持且auto-fill能自适应不同屏幕列数。这些细节才是新手照着做不翻车的关键。4. 避坑指南双端相册开发中90%人踩过的5个具体坑4.1 现象Android真机上相册页面空白logcat显示java.lang.SecurityException: Permission Denial原因targetSdkVersion≥30时READ_EXTERNAL_STORAGE权限在Android 11完全失效必须改用READ_MEDIA_IMAGES且需在AndroidManifest.xml中显式声明不能仅靠运行时请求。解决检查build.gradle中targetSdkVersion若≥30确保Manifest含uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES /并在requestPermissions()中传入该权限字符串。4.2 现象iOS模拟器相册正常真机首次打开闪退Xcode控制台报This application has requested the runtime to terminate it原因Info.plist中NSPhotoLibraryUsageDescription文案为空或含特殊字符如中文顿号、emoji苹果审核虽不卡但真机运行时系统拒绝加载。解决用Xcode打开Info.plist右键→Open As→Source Code确认string标签内为纯ASCII字符例如stringAccess photos for sharing/string避免string访问相册以分享照片/string中的感叹号。4.3 现象缩略图加载缓慢滑动列表时大量SkImageDecoder::Factory returned null警告原因PHImageManager默认使用PHImageContentModeDefault在列表滚动时频繁解码全尺寸图而非缩略图。源码中已设isNetworkAccessAllowedfalse但未设resizeMode。解决在generateThumbnail方法中options.resizeMode .fastiOS或options.inSampleSize 4Android BitmapFactory强制降采样。4.4 现象多选后上传Android端成功iOS端服务器收到空文件Content-Length: 0原因iOS侧PHAsset需先用PHImageManager导出NSData再转为Data上传但源码中uploadPhoto(asset:)函数漏掉了UIImage.jpegData(compressionQuality:)转换直接传nil。解决在上传前插入guard let imageData UIImageJPEGRepresentation(thumbImage, 0.8) else { return } // 后续用imageData上传4.5 现象wc.db在Android 11设备上写入失败SQLiteDatabase.openDatabase抛SQLiteCantOpenDatabaseException原因getDatabasePath(wc.db)返回路径为/data/data/com.txl/databases/wc.db但Android 11 Scoped Storage限制对/databases/目录的写入需改用context.getDatabasePath()自动适配。解决将SQLiteDatabase.openDatabase(path, ...)替换为context.openOrCreateDatabase(wc.db, MODE_PRIVATE, null)由系统管理路径。5. 进阶技巧用wc.db实现“断网相册”与跨会话状态同步5.1 断网状态下相册浏览基于wc.db的离线优先策略wc.db不仅是缓存更是离线状态中心。当网络不可用时UI层直接读取数据库// Android - PhotoAdapter.java public void loadPhotos() { if (isNetworkAvailable()) { // 网络可用从服务器拉取最新相册列表更新wc.db fetchRemoteAlbums(); } else { // 网络不可用只读wc.db跳过网络请求 loadFromDb(); } } private void loadFromDb() { SQLiteDatabase db context.openOrCreateDatabase(wc.db, Context.MODE_PRIVATE, null); Cursor cursor db.query(thumbnails, new String[]{asset_id, thumbnail_path, width, height}, null, null, null, null, created_at DESC); // 绑定到RecyclerView }关键点在于**wc.db的schema设计预留了server_id字段**当前为空未来可扩展为与后端photo_id映射实现断网编辑、联网同步。目前虽未启用但表结构已就绪。5.2 跨会话状态同步利用wc.db的created_at实现“最后查看位置”记忆相册列表滚动位置常被忽略但wc.db中每条记录含created_atUnix时间戳可据此实现智能恢复字段类型说明last_viewed_asset_idINTEGER最后点击查看的asset_idlast_viewed_timestampINTEGER点击时间戳在PhotoDetailActivity中// 记录查看 db.execSQL(INSERT OR REPLACE INTO session_state (last_viewed_asset_id, last_viewed_timestamp) VALUES (?, ?), new Object[]{currentAssetId, System.currentTimeMillis()}); // 恢复位置 Cursor posCursor db.query(session_state, new String[]{last_viewed_asset_id}, null, null, null, null, null); if (posCursor.moveToFirst()) { int lastId posCursor.getInt(0); // 滚动RecycleView到lastId对应position }此方案比SharedPreferences存储position更可靠——position会因数据增删失效而asset_id是永久标识。5.3 wc.db性能优化针对千图场景的索引与查询优化默认wc.db无索引当缩略图超1000张时SELECT * FROM thumbnails ORDER BY created_at DESC耗时飙升。实测优化如下-- 在wc.db中执行首次安装后 CREATE INDEX IF NOT EXISTS idx_thumbnail_created ON thumbnails(created_at DESC); CREATE INDEX IF NOT EXISTS idx_thumbnail_asset ON thumbnails(asset_id);同时分页查询替代全量加载// 分页SQLAndroid String sql SELECT * FROM thumbnails ORDER BY created_at DESC LIMIT ? OFFSET ?; Cursor cursor db.rawQuery(sql, new String[]{String.valueOf(PAGE_SIZE), String.valueOf(offset)});PAGE_SIZE设为20配合RecyclerView的Pagination组件首屏加载200ms。5.4 mui.css动态主题切换基于wc.db的用户偏好持久化mui.css中定义了两套主题变量:root { --primary-color: #007AFF; --bg-color: #FFFFFF; } .theme-dark { --primary-color: #0A84FF; --bg-color: #121212; }用户主题选择存入wc.db的settings表keyvaluedescriptionthemedark当前主题dark/lightauto_switch1是否随系统暗色模式自动切换在Application.onCreate()中读取String theme getSetting(theme, light); // 从wc.db读 getWindow().getDecorView().setBackgroundColor( dark.equals(theme) ? Color.BLACK : Color.WHITE );这样主题偏好真正跨进程存活重启App不丢失。从那以后我每次交付相册功能都强制走一遍wc.db的索引创建、Info.plist的描述文案校验、以及AndroidtargetSdkVersion与权限声明的匹配检查——这三步做完90%的相册上线问题就消失了。希望帮到你。本文还有配套的精品资源点击获取
返回列表