
简介这份资源是面向移动端开发学习者的双端通讯录类项目源码合集涵盖 iOS 与安卓两端 APP 及配套 PHP 后台适合想研究通讯录、相册、短信、定位等模块实现思路的开发者参考。压缩包共约 2000 个文件、298.59MB以 548 个 php 后台逻辑文件、475 个 png 界面素材、148 个 css 与 114 个 js 前端资源为主另含 186 个 md 说明、88 个 xml 配置、76 个 json 数据文件及 2 个 apk、2 个 ipa 打包产物结构上前后端分离清晰。项目基于 HBuilder X 导入打包后台采用 ThinkPHP 目录规范数据库配置与伪静态规则均有对应文件代码未加密便于对照阅读路由、接口与页面组织方式。目前已有 548 人学习下载适合具备一定 PHP 与移动端基础、希望拆解双端项目结构的读者参考作者明确仅供学习分享禁止违法用途。1. 双端通讯录源码拆解iOS 与安卓同源工程到底怎么跑起来拿到一个压缩包标题写着「iOS 安卓双端通讯录相册短信手机号定位」第一反应往往是这玩意儿到底能不能编译、跑起来是不是一堆红字。我拆过不少这类双端工程血泪经验是——标题里功能列得越全越要先看目录结构再动手否则很容易在依赖和权限上翻车。这份源码的核心价值在于它把通讯录、相册、短信、定位四类系统能力做成了双端可对照的实现适合想学移动端系统 API 调用、或者要快速搭一个本地通讯录管理 Demo 的开发者。新手能照着跑通基础页面熟手能直接看两端权限模型和数据结构设计的差异。下面按「能干什么 → 怎么跑 → 怎么改」的顺序把这份 ios 安卓 源码 通讯录 工程拆开讲。2. 工程结构与技术选型双端同源目录怎么读2.1 顶层目录与两端入口这类双端包解压后常见做法是根目录下并列两个工程文件夹外加一个共享资源目录。我一般先tree -L 2看一眼确认入口文件在哪再决定用哪个 IDE 打开。典型结构如下project-root/ ├── ios/ # Xcode 工程 │ ├── ContactsDemo.xcodeproj │ └── ContactsDemo/ │ ├── AppDelegate.swift │ ├── Info.plist # 权限声明集中在这里 │ └── Modules/ # 通讯录/相册/短信/定位分模块 ├── android/ # Gradle 工程 │ ├── build.gradle │ ├── settings.gradle │ └── app/src/main/ │ ├── AndroidManifest.xml │ └── java/com/demo/contacts/ └── shared/ # 双端共用的数据模型说明或资源读这个结构要抓两个点一是Info.plist和AndroidManifest.xml权限声明全在这两个文件里跑不起来十有八九是这里缺项二是Modules/目录四类功能各自独立成模块说明作者是按能力切分的二次开发时改一个模块不会牵动全局。技术栈上iOS 侧大概率是 Swift UIKit老工程也可能是 Objective-C安卓侧是 Java 或 Kotlin 原生 SDK没有引入跨平台框架——这点从双端各写一套就能看出来好处是系统 API 调用最直接坏处是两端逻辑要各维护一份。2.2 权限声明跑通前的第一道关通讯录、相册、短信、定位这四类能力在两端都属于敏感权限声明不全直接闪退或白屏。iOS 侧在Info.plist里补全描述字段安卓侧在清单文件里声明并在运行时申请。先看 iOS 的写法!-- ios/ContactsDemo/Info.plist 片段 -- keyNSContactsUsageDescription/key string需要访问通讯录以展示联系人列表/string keyNSPhotoLibraryUsageDescription/key string需要访问相册以展示本地图片/string keyNSLocationWhenInUseUsageDescription/key string需要定位以展示当前位置信息/string这三个 key 缺一个对应模块一调用就崩而且崩溃日志不会直接告诉你缺哪个得自己对着模块逐个排查。安卓侧对应的是清单声明加运行时请求!-- android/app/src/main/AndroidManifest.xml 片段 -- uses-permission android:nameandroid.permission.READ_CONTACTS/ uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE/ uses-permission android:nameandroid.permission.READ_SMS/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/注意安卓 6.0 以后光声明不够还得在代码里动态申请否则权限永远是拒绝态。参数上READ_SMS在部分新系统版本上受限较严如果只是做本地演示建议先注释掉短信模块的权限申请避免一启动就弹一堆授权框把用户吓退。定位用ACCESS_FINE_LOCATION就够别顺手加后台定位那是另一个量级的审核门槛。2.3 构建配置与最低版本两端工程都要确认最低支持版本这决定了你能用哪些 API。iOS 侧看project.pbxproj里的IPHONEOS_DEPLOYMENT_TARGET安卓侧看build.gradle的minSdkVersion。常见做法是 iOS 设 12.0 以上、安卓设 24 以上覆盖绝大多数在用设备。如果编译报「API not available」先回来改这个值而不是去改业务代码——这是我最常提醒新手的一点。Gradle 版本和 AGP 版本也要匹配老工程用新 Android Studio 打开经常卡在同步阶段把gradle-wrapper.properties里的 distributionUrl 调到与本地 JDK 兼容的版本即可。3. 核心模块拆解通讯录、相册、短信、定位四类能力实现3.1 通讯录读取两端数据模型对照通讯录模块是整个工程的主线两端都是「请求权限 → 查询系统数据库 → 映射成自定义模型 → 渲染列表」这条链路。iOS 侧用 Contacts 框架// ios/ContactsDemo/Modules/Contacts/ContactService.swift import Contacts func fetchContacts(completion: escaping ([ContactItem]) - Void) { let store CNContactStore() // 只取需要的字段避免全量拉取拖慢首屏 let keys [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey] as [CNKeyDescriptor] let request CNContactFetchRequest(keysToFetch: keys) var result: [ContactItem] [] do { try store.enumerateContacts(with: request) { contact, _ in let name contact.givenName contact.familyName let phone contact.phoneNumbers.first?.value.stringValue ?? result.append(ContactItem(name: name, phone: phone)) } completion(result) } catch { // 权限被拒或查询失败返回空列表由上层提示 completion([]) } }这段的关键在keysToFetch只声明需要的字段系统只返回这些字段性能和内存都更可控。enumerateContacts是同步遍历联系人量大时要放到后台队列否则首屏会卡。安卓侧对应的是 ContentResolver 查ContactsContract// android/.../contacts/ContactRepository.java public ListContactItem loadContacts(Context ctx) { ListContactItem list new ArrayList(); Cursor c ctx.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER}, null, null, null); if (c ! null) { while (c.moveToNext()) { // 列索引按投影顺序取别写死数字 list.add(new ContactItem(c.getString(0), c.getString(1))); } c.close(); // 游标必须关否则泄漏 } return list; }两端模型字段对齐name、phone是为了让上层列表逻辑可以复用同一套渲染思路。参数上安卓的投影数组顺序决定getString的索引改字段时两处要同步改这是最容易埋 bug 的地方。3.2 相册与短信系统能力调用的边界相册模块两端都走系统选择器或媒体库查询。iOS 用PHPhotoLibrary拉取资源安卓用MediaStore.Images查询。这里有个共同点都只读不写所以权限申请到读取级别即可别去碰写入权限能省掉不少授权摩擦。短信模块在 iOS 侧限制较多通常只能通过MFMessageComposeViewController发起发送读不了历史短信安卓侧READ_SMS可读但新版本管控趋严。所以这份源码里短信模块更可能是「发送入口 本地记录展示」的组合而不是全量读取。二次开发时如果要做短信备份得先确认目标系统版本是否还开放读取接口别照着老教程硬写。3.3 定位模块一次定位请求的完整链路定位模块两端都是「申请权限 → 发起单次定位 → 回调坐标 → 反查地址」四步。iOS 侧用 CoreLocation// ios/ContactsDemo/Modules/Location/LocationService.swift import CoreLocation class LocationService: NSObject, CLLocationManagerDelegate { private let manager CLLocationManager() var onResult: ((Double, Double) - Void)? func requestOnce() { manager.delegate self manager.desiredAccuracy kCLLocationAccuracyHundredMeters // 演示场景不必最高精度 manager.requestWhenInUseAuthorization() manager.requestLocation() // 单次请求省电 } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let loc locations.last else { return } onResult?(loc.coordinate.latitude, loc.coordinate.longitude) } }requestLocation()是单次请求比持续更新省电得多演示场景够用。精度设成百米级避免高精度带来的额外耗电和授权敏感。安卓侧对应FusedLocationProviderClient的getCurrentLocation思路一致。反查地址两端都要调地理编码服务注意这是网络请求要处理超时和失败别让定位成功但地址为空时界面卡住。4. 避坑与排查双端工程跑不起来的五类常见问题4.1 编译通过但一进通讯录页就闪退现象是列表页刚出现就退回桌面日志里只有一句笼统的异常。原因基本是权限描述缺失或权限被拒后没做兜底。解决先核对Info.plist三个 UsageDescription 是否齐全再在读取前判断授权状态拒绝时展示引导文案而不是直接调查询接口。我一般会在服务层入口加一个checkPermission前置判断把崩溃挡在业务层之外。4.2 安卓端 Gradle 同步失败现象是打开工程卡在 sync报插件版本不兼容。原因是老工程的 AGP 与本地 Android Studio 版本错配。解决改build.gradle里的 AGP 版本到与 IDE 匹配的区间同时把gradle-wrapper.properties的 distributionUrl 调成兼容版本改完清缓存重同步。别一上来就升级全部依赖先让工程能编译再逐个升。4.3 定位一直回调不到结果现象是权限给了、代码也调了但坐标永远为空。原因通常是模拟器没设虚拟位置或真机上精度参数与请求方式不匹配。解决模拟器里手动设一个位置再测真机上确认用的是单次请求而非持续更新并检查代理方法是否绑定在同一个 manager 实例上——delegate 被释放是这类问题的隐形杀手。4.4 相册列表空白但没报错现象是页面正常渲染图片数量为零。原因是媒体查询的 URI 或字段在新系统上变了或者只查了图片没查视频导致筛选条件过窄。解决先用系统相册确认设备里确实有图再把查询投影放宽到常用字段逐字段打印数量定位是哪一步归零。4.5 短信模块在部分机型上直接不可用现象是点进短信页提示无权限或功能灰掉。原因是新系统版本对短信读取收紧READ_SMS不再是声明即得。解决把短信模块降级为「发送入口 本地记录」读取失败时给明确提示而不是静默失败如果业务确实依赖读取得在目标机型上逐版本验证别拿一台机器测完就上线。5. 二次开发改造从 Demo 到可用工具的落地技巧拿到这份双端源码多数人不是原样用而是想改成自己的通讯录管理工具或客户资料 Demo。改造的第一步不是写新功能而是把数据层抽出来。现在两端各自查询、各自映射模型改字段要动两处容易漏。我的做法是在shared/下定义一份字段约定name、phone、avatar、lastContactAt两端映射时都对齐这份约定后续加字段只改映射层不动业务层。第二步是给权限流程加统一入口。两端各写一个PermissionGate把「检查 → 申请 → 拒绝引导」三步收口业务模块只调一个方法拿结果。这样新增模块时权限逻辑不用重写也避免了前面说的闪退问题。验证方法很直接在设置里手动关掉某一项权限再进对应页面看是否走引导而不是崩溃两端各测一遍。第三步是定位与通讯录的联动。这份源码里定位和通讯录是分开的但实际场景常需要「按距离排序联系人」。改造思路是给ContactItem加经纬度字段读取通讯录后异步补定位再用 Haversine 公式算距离排序。注意定位是异步的列表要先渲染再增量更新别阻塞首屏。改造点改动位置验证方式字段统一shared 约定 两端映射层加一个字段两端列表都能显示权限收口各端 PermissionGate关权限进页面不崩溃距离排序ContactItem 定位服务列表按距离升序且不卡顿最后说个习惯。这类双端包我每次动手前都强制走一遍「先跑通空壳、再加模块」的顺序先把两端工程编译起来、只留一个空白列表页确认构建链路没问题再逐个把通讯录、相册、短信、定位接回去。这样出问题时能立刻定位是新模块引入的还是基础工程本身的问题比一上来全量编译再大海捞针省太多时间。希望帮到你。本文还有配套的精品资源点击获取