资讯详情 双端通讯录源码拆解:iOS与安卓通讯录、相册、短信、定位模块实现
📅 2026/10/23 20:26:38
简介这是一套面向移动端开发学习者的双端通讯录类项目源码覆盖iOS与安卓两个平台适合具备一定前端与PHP基础、希望研究移动应用数据采集与后台管理实现方式的开发者参考。项目采用HBuilder X进行APP打包后端基于ThinkPHP框架搭建包含完整的后台管理入口与数据库配置说明可帮助读者理解从客户端到服务端的数据交互链路。压缩包共约2000个文件整体298.59MB其中548个php文件构成后端业务逻辑475个png与148个css、114个js负责界面与交互资源另有186个md文档、88个xml配置及apk、ipa安装包等目录层次分明便于按模块定位学习。目前已有545人学习关注。资源涵盖通讯录、相册、短信、定位及已安装应用信息等模块的开源实现代码未加密读者可据此研究移动端权限调用、接口设计与后台数据管理的具体写法并借助说明文档完成本地环境部署与调试适合作为课程设计或技术预研的参考素材。1. 双端通讯录源码拆解从通讯录到相册短信一套代码怎么跑通 iOS 和安卓通讯录、相册、短信、手机号定位——这四个词摆在一起做过移动端的人大概能猜到这不是一个简单的单点功能 Demo而是一套完整的双端应用源码。iOS 和安卓各一套工程功能模块覆盖了通讯录管理、相册浏览、短信读取和基于手机号的定位能力。拿到这份源码的开发者通常有两类诉求一类是想快速跑起来看看效果验证功能是否完整另一类是想在现有基础上做二次开发比如换 UI、接自己的后端、或者把某个模块单独抽出来用。这套源码的价值在于它把双端工程结构和核心功能模块都摆出来了省去了从零搭框架的时间。接下来我会按「项目结构 → 环境搭建 → 核心模块 → 避坑 → 二次开发」的顺序把这份源码拆开讲清楚。2. 工程结构与环境搭建双端目录怎么分、依赖怎么装2.1 双端工程目录结构拿到压缩包解压后常见的目录组织方式是按平台分目录iOS 和安卓各占一个顶层文件夹共享的资源文件单独放。我一般会先扫一遍根目录确认工程入口在哪。# 解压后典型目录结构以实际源码为准 AddressBookSource/ ├── iOS/ # iOS 端工程目录 │ ├── AddressBook.xcodeproj │ ├── AddressBook/ # 主工程源码 │ │ ├── Controllers/ # 视图控制器 │ │ ├── Models/ # 数据模型 │ │ ├── Views/ # 自定义视图 │ │ └── Utils/ # 工具类 │ └── Podfile # CocoaPods 依赖描述 ├── Android/ # 安卓端工程目录 │ ├── app/ │ │ ├── src/main/java/ # Java/Kotlin 源码 │ │ ├── src/main/res/ # 资源文件 │ │ └── build.gradle # 模块构建脚本 │ ├── build.gradle # 项目级构建脚本 │ └── settings.gradle └── README.md # 说明文件这个结构是移动端双端项目的常见做法。iOS 端用 Xcode 工程组织安卓端用 Gradle 构建。两边的业务逻辑各自独立但功能模块的划分是对齐的——通讯录、相册、短信、定位各对应一组控制器和工具类。这样设计的好处是如果你只关心某一个平台可以直接打开对应目录不用管另一边。2.2 iOS 端环境准备iOS 端需要 macOS 系统安装 Xcode版本建议 14 以上。如果工程用了 CocoaPods 管理依赖先装好 CocoaPods然后在 iOS 目录下执行安装命令。# 进入 iOS 工程目录 cd AddressBookSource/iOS # 安装 CocoaPods 依赖如果存在 Podfile pod install # 安装完成后用 .xcworkspace 打开而不是 .xcodeproj open AddressBook.xcworkspace这里有个细节如果源码里没有 Podfile说明依赖可能是手动拖入的 framework或者用了 Swift Package Manager。打开工程后先看 Build Phases 里的 Link Binary With Libraries确认依赖是否完整。常见依赖包括通讯录框架 Contacts、相册框架 Photos、短信框架 MessageUI这些都是系统框架不需要额外安装。2.3 安卓端环境准备安卓端需要 Android StudioJDK 版本看 build.gradle 里的配置。打开 Android 目录等待 Gradle 同步完成。# 进入安卓工程目录 cd AddressBookSource/Android # 查看 gradle 版本配置 cat gradle/wrapper/gradle-wrapper.properties # 命令行构建可选 ./gradlew assembleDebugGradle 同步时最容易卡在依赖下载上。如果公司网络有限制可以在项目级 build.gradle 里把仓库地址换成国内镜像。同步完成后连接真机或启动模拟器直接点 Run 就能装上去。注意安卓 6.0 以上需要动态申请权限通讯录、相册、短信、定位这四个权限都得在运行时请求否则功能会直接崩。2.4 权限配置清单双端都需要在配置文件里声明权限这是跑通功能的前提。平台配置文件需要声明的权限iOSInfo.plistNSContactsUsageDescription、NSPhotoLibraryUsageDescription、NSLocationWhenInUseUsageDescription安卓AndroidManifest.xmlREAD_CONTACTS、READ_EXTERNAL_STORAGE、READ_SMS、ACCESS_FINE_LOCATIONiOS 的权限描述文案必须写清楚用途否则审核会被拒。安卓这边READ_SMS 属于敏感权限Google Play 对它有额外限制如果只是自己用或者企业内部使用问题不大。3. 核心模块拆解通讯录、相册、短信、定位各怎么实现3.1 通讯录模块读取、搜索与展示通讯录模块的核心是读取系统联系人数据然后做本地搜索和列表展示。iOS 用 Contacts 框架安卓用 ContentResolver 查询 ContactsContract。先看 iOS 端的读取逻辑// iOS 通讯录读取核心代码 import Contacts func fetchContacts() { let store CNContactStore() // 请求授权 store.requestAccess(for: .contacts) { granted, error in guard granted else { return } // 指定要读取的字段 let keys [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey] as [CNKeyDescriptor] let request CNContactFetchRequest(keysToFetch: keys) do { try store.enumerateContacts(with: request) { contact, stop in // 组装数据模型 let name \(contact.familyName)\(contact.givenName) let phones contact.phoneNumbers.map { $0.value.stringValue } // 存入本地数组刷新 UI self.contactList.append(Contact(name: name, phones: phones)) } DispatchQueue.main.async { self.tableView.reloadData() } } catch { print(读取失败: \(error)) } } }这段代码的关键点CNContactFetchRequest的keysToFetch决定了你能拿到哪些字段不指定就只返回姓名。enumerateContacts是同步遍历数据量大时建议放到后台线程。读取完成后必须回主线程刷新 UI否则列表不更新。安卓端的查询逻辑// 安卓通讯录查询核心代码 private void loadContacts() { ContentResolver resolver getContentResolver(); Cursor cursor resolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME ASC ); if (cursor ! null) { while (cursor.moveToNext()) { String name cursor.getString(cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); String number cursor.getString(cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NUMBER)); contactList.add(new Contact(name, number)); } cursor.close(); } adapter.notifyDataSetChanged(); }安卓这边用ContentResolver查询Phone.CONTENT_URI按姓名排序。注意 cursor 用完必须 close否则内存泄漏。搜索功能一般是在本地列表上做过滤不重新查数据库这样响应更快。3.2 相册模块缩略图加载与全屏预览相册模块要解决两个问题一是快速加载缩略图二是点击后全屏预览。iOS 用 Photos 框架安卓用 MediaStore。iOS 端获取相册资源// iOS 相册资源获取 import Photos func fetchAssets() { let options PHFetchOptions() options.sortDescriptors [NSSortDescriptor(key: creationDate, ascending: false)] let assets PHAsset.fetchAssets(with: .image, options: options) assets.enumerateObjects { asset, _, _ in // 用 PHImageManager 请求缩略图 let manager PHImageManager.default() let size CGSize(width: 200, height: 200) manager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { image, _ in self.thumbnails.append(image) } } }PHFetchOptions的排序规则决定了相册的展示顺序一般按创建时间倒序。requestImage是异步的targetSize 设小一点可以加快缩略图加载速度。全屏预览时再请求原图避免一次性加载大图导致内存暴涨。安卓端用 MediaStore 查询图片// 安卓相册查询 String[] projection {MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA}; Cursor cursor getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED DESC ); while (cursor.moveToNext()) { String path cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); imageList.add(path); } cursor.close();安卓 10 以后分区存储生效DATA字段可能拿不到真实路径需要用ContentResolver.openInputStream配合 Uri 来读取。这是很多老代码在新系统上翻车的地方。3.3 短信模块读取会话与消息列表短信模块在 iOS 上限制比较多系统不提供公开 API 读取短信内容所以 iOS 端的短信功能通常是发送短信MessageUI或者展示本地模拟数据。安卓端可以通过 ContentResolver 读取短信数据库。安卓读取短信// 安卓短信读取 Uri uri Uri.parse(content://sms/); Cursor cursor getContentResolver().query(uri, new String[]{_id, address, body, date}, null, null, date DESC); while (cursor.moveToNext()) { String address cursor.getString(1); // 发送方号码 String body cursor.getString(2); // 短信内容 long date cursor.getLong(3); // 时间戳 smsList.add(new Sms(address, body, date)); } cursor.close();content://sms/是短信数据库的 URI查询时需要 READ_SMS 权限。注意这个 URI 在不同安卓版本上行为可能不一致部分厂商 ROM 会限制访问。如果只是做展示建议加个 try-catch拿不到数据时给个空状态提示。iOS 端发送短信// iOS 发送短信 import MessageUI func sendSMS(to number: String, body: String) { guard MFMessageComposeViewController.canSendText() else { return } let controller MFMessageComposeViewController() controller.recipients [number] controller.body body controller.messageComposeDelegate self present(controller, animated: true) }MFMessageComposeViewController只能调起系统发送界面不能静默发送这是 iOS 的安全限制。3.4 手机号定位模块基站与网络定位的调用方式手机号定位这个功能需要说清楚单纯靠手机号本身是拿不到实时位置的必须结合设备端的定位能力。这套源码里的定位模块通常是调用系统定位 API 获取经纬度然后做逆地理编码显示城市和地址。iOS 端定位// iOS 定位核心 import CoreLocation class LocationManager: NSObject, CLLocationManagerDelegate { let manager CLLocationManager() func start() { manager.delegate self manager.desiredAccuracy kCLLocationAccuracyHundredMeters manager.requestWhenInUseAuthorization() manager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location locations.last else { return } // 逆地理编码 CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in let city placemarks?.first?.locality ?? 未知 print(当前城市: \(city)) } } }desiredAccuracy设成百米级就够了精度越高越耗电。逆地理编码是异步的返回结果里包含国家、省份、城市、街道等信息。安卓端定位// 安卓定位 LocationManager lm (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 10, locationListener); }安卓这边可以用 GPS_PROVIDER 或 NETWORK_PROVIDER前者精度高但耗电后者快但精度低。实际项目里一般两个都用根据场景切换。4. 避坑与常见问题权限、兼容性、数据读取的翻车现场4.1 权限申请后功能仍然不可用现象代码里明明申请了权限用户也点了允许但读取通讯录还是返回空。原因iOS 端requestAccess是异步的如果在回调外直接调用读取逻辑此时授权还没完成。安卓端在 Android 6.0 以上权限申请和实际使用之间如果隔了页面跳转可能被系统回收。解决iOS 端把所有读取逻辑放在授权回调的granted true分支里。安卓端在onRequestPermissionsResult里确认授权后再执行查询不要依赖checkSelfPermission的瞬时结果。4.2 安卓 10 以上相册路径拿不到现象MediaStore.Images.Media.DATA返回 null 或者无效路径。原因安卓 10 引入分区存储应用不能直接访问外部存储的绝对路径。解决改用ContentResolver.openInputStream(uri)读取图片流或者申请MANAGE_EXTERNAL_STORAGE权限但上架审核会麻烦。推荐用 Glide 或 Coil 直接加载 Uri它们内部已经处理了分区存储的兼容问题。4.3 iOS 短信功能被审核拒绝现象App 提交审核时被拒理由是短信权限使用不当。原因iOS 不允许读取短信内容只能通过MFMessageComposeViewController发送。如果代码里试图访问短信数据库会被直接拒。解决iOS 端的短信模块只保留发送功能展示功能用本地数据或后端接口。如果产品需求必须展示短信考虑改成展示通知或者让用户手动导入。4.4 定位权限弹窗不出现现象调用requestWhenInUseAuthorization后没有任何反应。原因Info.plist 里缺少NSLocationWhenInUseUsageDescription字段或者字段值为空。解决在 Info.plist 里补上描述文案内容要具体说明用途比如「用于展示您当前所在城市」。安卓端同理AndroidManifest.xml 里必须声明ACCESS_FINE_LOCATION否则运行时申请会被直接拒绝。4.5 双端数据模型不一致导致联调困难现象iOS 和安卓返回的联系人字段名不同后端接口对不上。原因两端各自定义 Model没有统一字段命名规范。解决在项目初期就约定好数据交换格式比如都用{name, phone, avatar}这样的结构。可以在共享目录放一份 JSON Schema两端按这个来序列化。5. 二次开发与改造思路换 UI、接后端、抽模块5.1 替换 UI 层的切入点这套源码的 UI 层和业务逻辑基本是分离的iOS 端 Controller 负责布局Model 负责数据View 负责展示。如果你想换一套 UI只需要改 View 和 Controller 的布局代码Model 和工具类不用动。安卓端同理把 res/layout 下的 XML 换掉Adapter 里的绑定逻辑调整一下就行。我一般会先找到列表页的 Controller 或 Activity把它的布局文件替换成自己的设计然后跑一遍看数据能不能正常渲染。如果数据能出来但样式不对说明只改布局就够了如果数据出不来那可能是 Adapter 或 DataSource 的绑定逻辑需要调整。5.2 接入自有后端接口源码默认读的是本地数据要接后端的话需要把数据源从系统 API 换成网络请求。iOS 端可以用 URLSession 或 Alamofire安卓端用 Retrofit 或 OkHttp。改造步骤// iOS 端替换数据源示例 func loadContactsFromServer() { guard let url URL(string: https://your-api.com/contacts) else { return } URLSession.shared.dataTask(with: url) { data, _, error in guard let data data, error nil else { return } do { let contacts try JSONDecoder().decode([Contact].self, from: data) DispatchQueue.main.async { self.contactList contacts self.tableView.reloadData() } } catch { print(解析失败: \(error)) } }.resume() }关键点是数据模型要跟后端字段对齐用CodingKeys做映射。安卓端用 Gson 或 Moshi 解析Retrofit 的接口定义里把返回类型写成ListContact。5.3 把定位模块抽成独立组件如果你只想用定位功能可以把 LocationManager 相关的代码单独抽出来。iOS 端把 CoreLocation 的封装类复制到新工程安卓端把定位工具类连同权限申请逻辑一起搬过去。注意抽离时要保留权限声明否则新工程里功能会失效。5.4 改造后的验证方法改完代码后我习惯按这个顺序验证先跑通编译确认没有语法错误然后装到真机上逐个功能点一遍最后看日志有没有异常输出。通讯录和相册这种依赖系统数据的模块最好在真机上测模拟器的数据往往不完整。定位功能在室内可能拿不到 GPS 信号需要到窗边或室外测试。从那以后我每次拿到双端源码都强制先跑一遍权限申请流程确认四个权限都能正常弹出再往下看业务代码。希望帮到你。本文还有配套的精品资源点击获取