Android实例解析:身份证校验、手机归属地与区号查询APP实现
2026/9/9 13:42:32 网站建设 项目流程

简介:一份面向Android初学者的实用查询工具实例,集合了手机号码归属地、身份证号码归属地区和国内区号三项查询功能,界面直观、操作简单,且每个功能都与日常生活密切相关,适合用来熟悉Android工程的基本组织方式与开发流程。压缩包内共69个文件,其中25个Java源文件是核心业务逻辑,18个XML文件承担界面布局、菜单与配置声明,22张PNG图片用于按钮、背景等视觉资源,另有classpath、properties、cfg和project等工程文件辅助项目运行,整体压缩包大小仅76KB,轻量易得。目前已有1656人学习浏览,特别适合刚接触Android的学生或转行者对照练习。通过该项目可以掌握查询类APP的共性实现套路,包括输入控件监听、号码段/身份证编码规则判断、结果页面呈现等基础技能,也能从src、res、assets标准目录划分中体会Android项目规范,从而为独立开发同类小工具打下坚实基础。 拿到“Android身份证、手机号码、区号查询APP实例.rar”这个项目包的时候,我直接把它解压丢进Android Studio跑了一遍。先说结论:这不是什么高大上的项目,但作为Android入门阶段的练手工程,它的知识覆盖相当完整。一个输入框、一个查询按钮、一套数据匹配逻辑、一个结果展示页,这几乎是所有工具类APP的通用骨架。如果你刚学完Activity、Intent、ListView/RecyclerView,正愁没有一个“能拿得出手”的小项目,这个实例值得仔细拆一遍。

整个APP的核心就三件事:身份证号解析、手机号归属地查询、国内长途区号查询。看上去只是“输入一段号码,返回一个结果”,但里面牵扯到数据校验、编码规则、离线数据组织、界面状态处理等一堆细节。这篇文章我就把自己从解压到跑通、再到改造这个项目的完整过程写出来,包括我对每个功能模块的实现思路、关键代码、踩过的坑,以及后续可以怎么扩展。

1. 项目到底做了什么:三个查询功能的业务逻辑

先说业务层,也就是“每个查询到底在查什么”。这三个功能看起来都是“查询”,但背后的规则和数据处理方式完全不同,千万别一上来就写代码。

1.1 身份证查询:18位编码里藏着多少信息

身份证号码是国内居民身份编码的缩影,18位数字(最后一位可能是X)由四段信息组成:

  • 前6位:户籍所在地的行政区划代码,格式是“省(2位)+ 地市(2位)+ 区县(2位)”,比如110105代表北京市朝阳区。这部分可以映射到省市区名称。
  • 第7到14位:出生日期,格式为YYYYMMDD,比如19900308。
  • 第15到17位:顺序码,表示同一地址码区域内同年同月同日出生的人的排列顺序。其中第17位(倒数第二位)是性别标识,奇数表示男性,偶数表示女性。
  • 第18位:校验码,由前17位通过加权因子计算得出,可以是数字0到9,也可能是X。

这个查询功能的关键点不在“查到”,而在“校验”。一个合格的身份证查询APP,首先得能判断这个号码格式是否合法,然后再去解析省市区、出生日期、性别。如果用户输入“11010519900308001X”这种明显不对的号码,你得先拦住,而不是硬去匹配数据。

1.2 手机号归属地查询:数据量越来越大

手机号归属地查询的核心依据是“号段”。早期手机号是11位,前3位是运营商号段(如139、150、188),前4位能初步判定省份,但如果要精确到市,一般要看前7位。这些年随着携号转网和虚拟运营商的出现,号段归属已经出现了“号在人不在”的情况,即号码注册地与实际使用地分离。做这个功能时要清楚:无论APP本地库还是在线API,能查到的都是“号码最初分配时的归属地”,不是机主当前所在的位置。

数据组织的常见方式有两种:

  • 离线内置:把号段和归属地做成一个映射表,打包进APP。查询快、不耗流量、稳定,但更新要靠发版,而且号段数据文件会随着记录增多越来越大。
  • 在线接口:调用第三方API,数据实时且准确,但依赖网络,还要申请接口权限和Key,免费接口往往有调用次数限制。

作为教学实例,离线内置是更合适的选择,既不需要处理网络异步回调,又能在包体积可控的范围内完成功能闭环。别贪多,先把离线方案跑通。

1.3 区号查询:最简单但容易忽略细节

区号查询就是输入一个城市名,返回长途电话区号,比如“北京”返回“010”,“广州”返回“020”。反过来也可以输入区号查询城市。这个功能逻辑最简单,数据结构就是一个“城市名—区号”的键值对,但要注意两点:

  • 城市名的输入法问题。用户可能输入“北京市”“北京”“beijing”等各种形式,你需要做归一化处理,最简单的做法是数据存储时就用“北京”作为标准名,输入时做去“市/省”等后缀处理。
  • 区号不是全国统一长度,有三位(如010、021)也有四位(如0755),展示时别硬凑格式。

2. APP的架构设计思路:小项目也要有层次

很多初学者拿到这种“多查询功能”的需求,第一反应是把所有代码塞进一个Activity里。我强烈不建议这么做。这项目虽然小,但按功能分层能让后面加功能、换数据源都轻松不少。

2.1 导航与页面结构:三种常见方案

三个查询功能摆在一个APP里,页面结构有三种常见选择:

第一种:一个MainActivity放三个Tab(底部导航),每个Tab对应一个Fragment,查询结果各自展示。这是最主流的结构,适合功能平行、互相独立的工具类APP。

第二种:一个MainActivity放三个入口按钮,点击进入各自独立的Activity。优点是每个查询逻辑完全隔离,缺点是页面切换重,代码重复度高。

第三种:一个Activity内做状态切换,靠ViewFlipper或条件判断显示不同查询界面。最简单,但代码容易越来越乱,只适合极致精简的场景。

这个实例选哪种取决于原作者的工程习惯。从我解压后的项目结构来看,主流做法是第一种,即一个主界面 + 三个查询模块,数据层单独抽出来做查询工具类。这样写的好处是:以后加一个新的查询功能,只需新增一个Fragment和对应的查询器,不需要改动现有模块。

2.2 数据从哪来:本地数据组织的关键取舍

我把项目里的数据文件翻了一遍,发现数据的组织方式决定了查询性能。身份证区划代码和区号的数据量不算大,用一个JSON文件或一个SQLite表就能搞定。手机号段归属地稍有不同,如果收录了全国所有号段,数据量会到几十万条级别。

这里有个关键取舍:

  • 全量号段放JSON里,启动时一次性加载到内存,查询时用HashMap做Key,速度可以非常快,但首次解析大JSON会有几百毫秒的延迟和一定的内存占用。
  • 放到SQLite里,用“号段”字段建索引,查询走索引效率很高,但需要处理数据库文件的导入、初始化版本等问题,对新手不太友好。

我的建议是:入门阶段用JSON加载到HashMap的方式,代码简单、逻辑清晰。等控制到数据量瓶颈,再考虑引入SQLite或者Room。这个项目本身就是学习用的,没必要一开始就把持久化方案搞得太重。

2.3 输入校验与交互设计:小细节决定体验

别小看一个输入框。我测试过这个实例的原始版本,发现几个交互层面的细节问题很典型:

  • 输入框没有类型限制。身份证输入框应该用android:inputType="textPersonName"或设置digits属性限制只能输入数字和X;手机号输入框限制为数字且长度11位;区号查询需要同时支持城市名和区号两种输入模式。
  • 查询按钮没有做空输入拦截。用户没输入内容直接点击查询,程序会走到数据匹配逻辑,结果自然是查不到,还容易被误认为是Bug。
  • 结果展示区域没有空状态。查询成功后结果直接显示在TextView是常见的做法,但查询前最好显示一个类似“输入号码后点击查询”的提示,查询失败时也有明确的反馈,而不是让界面干在那里没反应。

这些小细节是区分“能跑的Demo”和“能用的工具”的重要标准,后文我会在实现环节给出具体的处理方式。

3. 从解压到跑通:核心模块的实现细节

下面进入正题,把我实际运行这个项目、以及重新整理关键模块的过程逐步拆开。这里我不会贴全量代码,而是挑每个模块里最有价值、最容易出错的部分详细说,最后你拿这些片段组合到自己的工程里就能直接跑。

3.1 身份证号码校验算法:代码只有三十行,却要处理三个坑

身份证校验是这整个项目里“技术含量最高”的部分,因为它涉及国家标准(GB 11643-1999)里的校验码生成算法。原理不复杂:前17位号码分别乘以不同的加权因子,求和后除以11,得到的余数对应一个校验码。

加权因子固定为:7、9、10、5、8、4、2、1、6、3、7、9、10、5、8、4、2。余数0到10对应的校验码依次是:1、0、X、9、8、7、6、5、4、3、2。也就是说,余数为2时,校验码是X。

我写了个工具方法,传入18位身份证号,返回布尔值:

public static boolean isValidIdCard(String idCard) { if (idCard == null || idCard.length() != 18) { return false; } String code = "10X98765432"; int[] factors = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; int sum = 0; for (int i = 0; i < 17; i++) { char c = idCard.charAt(i); if (c < '0' || c > '9') { return false; } sum += (c - '0') * factors[i]; } char expected = code.charAt(sum % 11); return expected == Character.toUpperCase(idCard.charAt(17)); }

这段代码看似简单,实际踩坑点有三个:

第一,最后一位X的大小写。用户可能输入小写的x,所以校验前必须统一转成大写,否则会把合法的身份证号判成非法。上面代码里已处理。

第二,只校验校验码还不够。GB 11643要求第7到14位必须是合法日期,也就是不仅格式是数字,还要通过月份和日期的真实天数校验。比如第7到14位如果是“19901301”,13月本身就不存在,生日解析应该失败。我补充了完整的生日判断:

public static boolean checkBirthday(String idCard) { int year = Integer.parseInt(idCard.substring(6, 10)); int month = Integer.parseInt(idCard.substring(10, 12)); int day = Integer.parseInt(idCard.substring(12, 14)); if (year < 1900 || year > Calendar.getInstance().get(Calendar.YEAR)) { return false; } if (month < 1 || month > 12) { return false; } int[] monthDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (month == 2) { boolean leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); return leap ? day <= 29 : day <= 28; } return day >= 1 && day <= monthDays[month - 1]; }

第三,地址码(前6位)不能只在区划表里查到就完事,还要考虑县级行政区划变更。实操中我建议对“查不到地址码”的情况做单独提示,比如“号码格式合法,但区划代码不在当前数据库中”,而不是直接报“号码无效”,这样可以避免把用户因旧地址码被判无效的身份证号误伤了。

3.2 性别解析与结果组装:一个字符判断就搞定

通过校验后,性别、出生日期、地区就可以准确解析了。性别看第17位(下标16)的奇偶性:

String gender = (Integer.parseInt(String.valueOf(idCard.charAt(16))) % 2 == 1) ? "男" : "女"; String birthday = idCard.substring(6, 10) + "-" + idCard.substring(10, 12) + "-" + idCard.substring(12, 14);

省份和城市信息从区划数据里匹配,这步就要用到项目里的JSON文件。我通常把区划代码按省、市、区县三级拆成三层Map,先匹配前2位定位省份,再匹配前4位定位城市,最后用完整6位匹配区县。逐级回退的好处是:即便某个县级代码已经废弃,至少还能返回省份和城市等级别信息,不会让整个解析失败。这种“能返回多少是多少”的容错思路,比直接返回空结果对用户友善得多。

3.3 手机号归属地查询:从JSON加载到HashMap匹配

离线查询手机号归属地,核心是把“号段前缀—归属地”的映射关系加载到内存。项目中数据文件一般放在assets/phone.json,结构大概是:

{ "130": "河北石家庄", "1300": "安徽合肥", "1301": "北京", ... }

加载代码建议用原生的AssetManager配合org.json.JSONObject

public static Map<String, String> loadPhoneLocation(Context context) { Map<String, String> map = new HashMap<>(); try { InputStream is = context.getAssets().open("phone.json"); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); String json = new String(buffer, "UTF-8"); JSONObject obj = new JSONObject(json); Iterator<String> keys = obj.keys(); while (keys.hasNext()) { String key = keys.next(); map.put(key, obj.getString(key)); } } catch (Exception e) { e.printStackTrace(); } return map; }

查询时先取手机号前7位做Key,查不到再往前3位查。这里必须做“长度递减匹配”的逻辑,因为不同运营商的号段分配粒度不同。我的实践经验是:前7位查不到时,依次尝试前6位、前4位、前3位,越短匹配到的结果越粗糙,但至少能给出运营商或省份级别的结果。

另外补充一点:查询前一定要判断手机号合法性。11位、1开头、第二位是3到9的才可能是正常手机号。否则像“12345678901”这种号码进去查询,返回结果没有意义,纯属浪费计算。

3.4 区号查询:简单但要做数据归一化

区号模块我建议在HashMap基础上做一层“城市名清洗”。用户输入“北京市”,你不能直接拿“北京市”做Key去查,而是先去掉结尾的“市”“省”“自治区”“壮族”“回族”“维吾尔”等后缀,再拿核心地名去查。反过来,如果用户输入的是“010”这种区号,就去查反查表。

项目里我会同时维护两个Map,一个是“城市名 — 区号”,一个是“区号 — 城市名”,用同一份数据源初始化:

private Map<String, String> cityToCode = new HashMap<>(); private Map<String, String> codeToCity = new HashMap<>(); public void init(List<AreaCodeItem> dataList) { for (AreaCodeItem item : dataList) { cityToCode.put(item.getCityName(), item.getAreaCode()); codeToCity.put(item.getAreaCode(), item.getCityName()); } } public String search(String input) { input = input.trim().replace("市", "").replace("省", ""); if (cityToCode.containsKey(input)) { return cityToCode.get(input); } else if (codeToCity.containsKey(input.trim())) { return codeToCity.get(input.trim()); } else { return null; } }

这段代码来自我自己的实现习惯,原始项目可能用的是更朴素的遍历List方式。从效率上看,Map在数据量增大后优势很明显,而且代码可读性更好,值得你直接替换原始实现。

3.5 界面层:输入框、按钮、结果文本的联动状态

界面部分我用一个Fragment展示查询界面,布局文件包含一个输入框(EditText)、一个查询按钮(Button)、一个结果区域(TextView或LinearLayout)。三个查询功能对应三个类似布局的Fragment,只是查询器不同。

一个关键细节:查询按钮的点击事件里必须处理空输入和非法输入。我在项目中写了一个统一的处理工具:

private void doQuery() { String input = editText.getText().toString().trim(); if (TextUtils.isEmpty(input)) { resultText.setText("请先输入查询内容"); return; } if (input.length() < 11 && currentMode == MODE_PHONE) { resultText.setText("手机号长度不正确,应为11位"); return; } String result = queryHelper.query(input); resultText.setText(result == null ? "未查询到对应结果,请检查输入内容" : result); }

界面状态处理上有个容易被忽略的体验点:查询结果出来之后,如果用户修改了输入内容,旧的结果应该被清空或置灰,否则用户会误以为修改后的内容已经查询过了。我在TextWatcher的onTextChanged里做了结果区域透明度变化,算是一个很小但能提升质感的小技巧。

4. 编译运行常见问题与排查实录

这部分是我实际跑项目时踩过的坑,都整理出来了。如果你在导入或运行这个工程时遇到问题,大概率能在这里找到答案。

4.1 SDK版本与Gradle版本不匹配

这是导入任何老项目时最容易遇到的问题。解压后如果发现Gradle构建报错,先检查build.gradle(Project级别)里声明的Gradle版本和你本机安装的Android Studio版本是否兼容。我遇到过项目声明Gradle 3.5.2,但本机Android Studio是新版本,直接同步就报错。

处理方法我建议升级而不是降级:打开gradle-wrapper.properties,把distributionUrl改成当前Android Studio配套的版本,然后点击Sync Now。一般Android Studio会自动提示升级DSL的写法,例如compile改成implementationandroidx依赖补全等。这类机械性的升级工作量不大,半小时内能处理完。

4.2 资源文件编码导致中文乱码

项目里如果涉及中文字符串或JSON数据,最常见的坑是文件编码不是UTF-8。JSON文件用GBK编码保存的话,在Android Studio中显示正常,但APP运行到真机上加载时,String里全是乱码,查询结果直接没法看。

彻底解决方法是:打开出问题的文件,在Android Studio右下角状态栏把文件编码强制改为UTF-8,然后重新输入中文内容或转换编码。注意转换前备份原始文件,防止编码转换过程中出现内容损坏。

4.3 数据量过大导致的启动卡顿

这个问题容易出现在手机归属地查询模块。如果作者把全国几百万条号段数据全放进去,第一次加载JSON到内存时会明显卡顿,甚至可能触发Application Not Responding(ANR)。如果你遇到这种情况,尝试这样优化:

  • 把数据按省份拆成多个JSON文件,用户查询时先根据号码前三位判定大致省份,只加载对应文件。
  • 或者在首次启动时把JSON数据导入SQLite,后续查询走数据库索引,速度可以快一个量级。
  • 再或者,把查询逻辑放到子线程,通过Handler或协程回传结果,不让主线程承担大JSON解析工作。

我实际测试发现,对10万条左右的号段数据,用HashMap全量加载大约耗时300到500毫秒,真机启动时勉强可以接受,但不如数据库方案稳定。作为学习项目可以接受,后续如果要上线,还是建议换SQLite。

4.4 隐私与合规性提醒

这里必须提一句:涉及到身份证号、手机号归属地的工具类APP,在上线前要注意个人信息保护相关要求。身份证号码本身是敏感个人信息,这类查询工具在功能设计上应该:

  • 不提供通过姓名反向查询身份证号的功能。
  • 不记录、不上传用户查询的完整号码。
  • 查询结果不包含除归属地之外的隐私字段。
  • 如果需要网络权限,必须在隐私政策中说明用途。

合规不是软件功能问题,但它是这个项目能不能上线、能不能在应用商店存在的先决条件,写代码的时候就要有这根弦。这也是为什么我坚持推荐离线本地查询方案的原因之一——不上传数据,隐私风险天然低很多。

5. 这个项目还能怎么玩:三个扩展方向

跑通现有的三个查询功能只是第一步。我在重构过程中发现,这个工程稍加改造就可以变成更完整的练手项目,这里提供三个可选的扩展方向。

5.1 从“单次查询”升级为“批量查询与历史记录”

给APP增加一个“查询历史”功能非常实用。用一个SQLite表记录查询时间、查询内容、查询结果,在主界面下方用ListView或RecyclerView展示最近20条记录。这样涉及到的知识点包括:数据库CRUD、列表展示、点击历史记录回填输入框等,几乎覆盖了Android中“数据持久化 + 列表展示”的完整链路。

5.2 把手机归属地查询改成API版本

如果你觉得内置数据的更新太麻烦,可以做一版使用聚合数据或天行数据等平台免费接口的在线查询。这里需要处理网络请求库(用OkHttp和Retrofit都行)、异步回调、权限声明(INTERNET权限)、JSON解析和数据缓存。在线版本与离线版本可以做一个设置开关,用户可以选择用本地数据还是实时接口,这样就把整个项目从“工具类APP”提升成“具备工程架构思维的网络应用”了。

5.3 支持扫描二维码识别手机号/身份证号

Android中可以用CameraX或ZXing库快速实现二维码扫描,识别出文本内容后自动判断是手机号还是身份证号,直接跳转到对应的查询模块。这个功能虽然有一定工作量,但都是成熟库的集成应用,做完后项目的完整度会提升一大截。

以上三个方向选任何一个做下去,这个实例就能从“照抄练习”变成“自己的作品”。代码量不大,但每个方向都用到了真实App开发中的核心知识,简历上写项目经历时也能写得更充实。

最后说句实际操作层面的经验:拿到任何一个项目压缩包,别急着看代码,先把README(如果有的话)和数据文件结构过一遍,搞清楚数据规模和数据格式,再跑代码。这个习惯能帮你省掉大量定位“是不是代码写错了”的时间——大部分情况下,这类查询失效的问题都出在数据文件和格式上,而不是逻辑本身。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询