简介面向安卓初学者的实战工程基于Android Studio工具编写演示如何通过SharedPreferences保存QQ密码。工程覆盖数据存储、界面布局、按钮事件与简单加密解密逻辑包含保存和加载两个核心功能通过Editor写入键值对并对密码进行加密后再存储帮助理解移动应用本地持久化的常用方式。同时工程完整呈现了从输入框获取密码、触发按钮点击、更新界面回显的交互闭环适合作为Android入门练手。资源共941个文件以414个XML布局与配置文件、438张PNG图片为主另含Java源码、可直接安装的APK、Gradle构建脚本及少量class、jar等编译产物压缩包大小约6.31MB。其中XML负责界面与资源定义PNG提供视觉素材APK支持真机安装验证各类型文件协同构成完整可运行工程。已有2263人学习下载可用于课程设计、自学入门或项目模板参考。通过完整源码与配套界面能够掌握SharedPreferences读写流程、UI布局与事件监听的配合方法同时建立登录信息加密保存的安全意识为后续学习Android Keystore等更强方案奠定基础。1. 先认清这份“保存QQ密码”工程到底在解决什么问题拿到一份命名为“保存QQ密码”的Android Studio工程第一反应容易跑偏是保存别人的密码还是保存自己App里的登录密码拆开看之后可以确定这是一份标准的登录模块实战——仿QQ登录界面的“记住密码”功能把用户勾选“记住密码”后的账号和密码写到本地下次启动自动回填。它解决的是Android开发里三个高频问题登录态怎么持久化、密码存哪里才不至于被一览无余、界面怎么恢复上次的输入状态。适合刚能跑通Hello World、想拿完整工程练手的新手也适合要把登录模块复用到聊天App或毕设项目里的同学。我会照着这份源码把存储选型、读写封装、加密改造和几个翻车点挨个拆一遍。2. 存储方案选型密码该进SharedPreferences还是SQLite2.1 Android本地存储四兄弟定位和边界做这个Demo之前先得把Android给的本地存储路子盘清楚。我按使用频率排了个对比表后面选型全看它存储方式本质适合场景典型坑SharedPreferencesXML键值对小配置、登录态、开关项整个文件读入内存不适合大字段内部文件File私有目录文件图片、日志、大文本要自己管流、管编码SQLite关系型数据库多表关联、复杂查询、大量记录版本升级要写迁移容易崩RoomSQLite的注解封装需要响应式查询的正式项目编译期生成代码KSP配置麻烦SharedPreferences最朴素但它足够应付“账号密码勾选状态”这种两三个字段的小数据。很多人一上来就说“密码必须进SQLite”其实单条键值对这种场景上SQLite是杀鸡用牛刀——你要多写好几十行数据库Helper还要处理版本号和Cursor的关闭。我一般不会为一个记住密码功能去引一个数据库框架。内部文件File在这里更不合适。它适合存图片或日志这类二进制或大文本你要自己拼接、自己解析存一个结构化的登录配置反而不方便。Room是SQLite的封装适合那种需要按条件查询、列表展示数据的页面比如聊天记录、订单列表。登录配置是“读取整份覆盖整份”的访问模式正好落在SharedPreferences的甜区。2.2 为什么这份工程选SharedPreferences当主存储从源码的注释和代码习惯能看出原作者选型时主要考虑了四点。第一是数据量。这份工程需要持久化的只有账号字符串、密码字符串、布尔值的勾选状态总共三个字段。SharedPreferences底层是一个XML文件三个节点的文件打开和解析成本可以忽略。第二是访问模式。记住密码功能只需要“登录时写入、启动时读取、退出时清空”这三种整体操作没有“查一条”“按条件统计”这种需求键值对天然匹配。第三是生命周期。SQLite要求你管理数据库连接、事务和游标SharedPreferences连关闭都不用管系统加载完文件就放内存里了。第四是代码量。用SQLite实现同样的效果差不多要多写两倍样板代码对教学工程不友好。什么时候该换方案我给自己定的标准是字段超过五个、需要按时间排序、需要模糊搜索这三条命中任何一条就别硬撑SharedPreferences了直接上SQLite或Room。单纯保存登录信息SharedPreferences就是最优解不需要为了显得专业去过度设计。2.3 源码结构数据从按钮到XML的完整路径拿到这份工程打开包结构你会看到主要东西都集中在四个文件里MainActivity登录页包含账号输入框、密码输入框、记住密码勾选框和登录按钮。HomeActivity登录成功后的落地页用来验证登录流程是否走通。UserInfo实体类承载账号、密码、记住状态三个属性。SpUtilsSharedPreferences的封装工具类所有读写都走这里。整个流程是MainActivity里点击登录按钮做非空校验然后判断勾选框状态把三个字段交给SpUtils写入到shared_prefs目录下的XML文件下一次进入应用MainActivity的onCreate里读取同一份XML把账号回填到输入框密码回填到密码框勾选框状态也恢复。这点要特别说明一下这份工程叫“保存QQ密码”但保存的是自己应用里用户输入的登录密码不是去解析聊天软件的数据。真实App里密码原文根本不应该在本地落盘——登录成功后服务端会下发一个token客户端保存的应该是token不是密码。这个工程的价值在于演示“登录持久化”这套完整链路把链路里的每个环节跑通后面接真实接口时把保存内容从密码换成token就行。3. 把核心代码拆开写入、读取、清空与界面联动3.1 封装工具类SpUtils的读写与清空源码里最值得先看的是SpUtils。它把SharedPreferences的细节全部包起来Activity里调一个方法就拿结果不用关心键名和上下文。核心代码是这样的public class SpUtils { private static final String SP_NAME login_config; private static final String KEY_ACCOUNT account; private static final String KEY_PASSWORD password; private static final String KEY_REMEMBER remember; public static void saveLoginInfo(Context context, String account, String password, boolean remember) { SharedPreferences sp context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor sp.edit(); editor.putString(KEY_ACCOUNT, account); editor.putString(KEY_PASSWORD, remember ? password : ); editor.putBoolean(KEY_REMEMBER, remember); editor.apply(); } public static String[] getLoginInfo(Context context) { SharedPreferences sp context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); String account sp.getString(KEY_ACCOUNT, ); String password sp.getString(KEY_PASSWORD, ); boolean remember sp.getBoolean(KEY_REMEMBER, false); return new String[]{account, password, String.valueOf(remember)}; } public static void clearLoginInfo(Context context) { SharedPreferences sp context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); sp.edit().clear().apply(); } }这里有两个容易被忽略的动作。一个是写入时用了三目运算符remember为false时不写空字符串而是写入空串把密码字段清掉。这样即使账号还在下次启动也不会把密码回填出来。另一个是读取返回值用String数组下标0是账号、1是密码、2是记住状态。这种方式简单但不够优雅读代码的人容易忘记下标含义我一般建议改成返回UserInfo实体类。参数上要注意SP_NAME是文件名同一个应用里所有通过getSharedPreferences读取的地方必须用同一个名字拼错一个字母就是两个不同文件读出来永远是空。MODE_PRIVATE表示仅本应用可访问这是目前唯一推荐的模式。MODE_WORLD_READABLE这类直接别用从Android 7.0开始它们已经失效编译期也会被标记警告。3.2 登录按钮的完整逻辑与校验顺序MainActivity里登录按钮的点击逻辑源码里是这样组织的public void onLogin(View view) { String account etAccount.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (account.isEmpty() || password.isEmpty()) { Toast.makeText(this, 账号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } boolean remember cbRemember.isChecked(); SpUtils.saveLoginInfo(this, account, password, remember); Intent intent new Intent(this, HomeActivity.class); startActivity(intent); finish(); }逻辑顺序是先取文本、去空格再做非空校验处理完勾选状态后保存数据最后跳转。这里的校验只是“不为空”的弱校验真实项目一般会在Activity里简单判一下再去请求接口做账号密码校验成功后才走保存和跳转。把保存写在跳转之前是有讲究的。如果先跳转再保存页面切换的间隙里如果进程被杀数据可能丢先保存再做页面跳转保证数据落盘动作已经发生。finish()把登录页关掉避免用户按返回键时退回登录页这样“下一次启动才需要重新登录”的语义才完整。3.3 界面恢复下次启动自动回填数据存进去了下一次启动怎么拿出来MainActivity的onCreate里有一段读取逻辑String[] info SpUtils.getLoginInfo(this); if (info ! null !TextUtils.isEmpty(info[0])) { etAccount.setText(info[0]); etPassword.setText(info[1]); cbRemember.setChecked(Boolean.parseBoolean(info[2])); etPassword.setSelection(info[1].length()); }TextUtils.isEmpty的判断很重要它同时拦住了空串和null避免setText收到null导致崩溃。setSelection把光标移到密码末尾这样用户如果想修改密码直接接着输入而不是从头开始。这里有个小坑在Boolean.parseBoolean上。它只有在字符串是“true”时才返回true其他任何情况都返回false包括传入null也不会抛异常。这个特性用对了是省事用错了就会出现“我记得勾了记住密码结果重启后没勾上”的玄学问题——检查一下你就发现是signup时存的布尔字段没对齐。提示onCreate里做回填时一定要等setContentView执行完再拿控件引用否则拿到的是null调用setText会直接空指针。三方库的写法到这里就结束了这套原生代码没有引入任何额外依赖对学习SharedPreferences来说反而是好事。4. 别把密码当明文躺平从MODE_PRIVATE到加密存储4.1 明文存储的风险与真实场景边界SharedPreferences存进去的XML文件长什么样打开/data/data/应用包名/shared_prefs/login_config.xml你会看到类似这样的内容?xml version1.0 encodingutf-8 standaloneyes ? map string nameaccounttest_user/string string namepassword12345678/string boolean nameremember valuetrue / /map密码就是明晃晃的12345678。日常使用中普通用户看不到这个文件因为它在应用的私有目录里需要root权限才能访问。但换个角度想如果用户手机丢了、设备被root过、或者做了系统备份这份XML就会暴露。很多同学在毕设里直接把密码明文存进去答辩时被老师问一句“安全性体现在哪”就卡住了。所以真实App里登录密码的落盘规则一般是这样的本地永不保存密码原文服务端登录成功后签发一个token客户端把这个token存到私有存储里。密码只在登录请求发出的那一瞬间存在于内存中用完即焚。这份工程把密码存本地是教学演示你可以保留这个逻辑但要明白它是教学简化不是生产实践的模板。4.2 加密改造用EncryptedSharedPreferences替换原文件安全加密的落地方案我推荐直接用Android官方提供的Jetpack Security组件。它基于Android Keystore系统生成密钥加密后的文件不再是明文XML即使被拖走也只是一堆密文。先把依赖加上implementation androidx.security:security-crypto:1.1.0-alpha06然后在SpUtils里增加一个加密实例的初始化方法public static SharedPreferences getEncryptedPrefs(Context context) throws GeneralSecurityException, IOException { MasterKey masterKey new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); return EncryptedSharedPreferences.create( context, sec_login_config, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); }这里几个参数要解释清楚。MasterKey是主密钥由Android Keystore生成和管理密钥本身不出安全硬件。AES256_GCM是密钥的加密方案GCM模式带认证可以防止密文被篡改。文件名换成了sec_login_config和原来的明文XML分开存放。AES256_SIV针对key名称加密保证别人看不出你存了哪些字段AES256_GCM针对value加密保护实际数据内容。改造后的写入代码和原来一模一样只是把getSharedPreferences换成getEncryptedPrefs。对外暴露的接口不变调用方不用改一行代码。这是典型的“最小侵入改造”工程里的SpUtils只需要替换内部实现。4.3 兼容性与降级边界用了加密库之后要注意一点security-crypto要求minSdk 23Android 6.0。低于这个版本MasterKey依赖的Keystore能力不够库会直接抛异常。如果项目还要支持Android 5.0就不能全量启用加密实例。做法是先判断SDK版本高版本走加密分支低版本退回原来的明文SharedPreferences等用户升级系统后再慢慢淘汰。验证加密是否生效可以清掉应用数据重来一遍然后再次打开那个XML文件看到的应该是齐刷刷的Base64密文而不是原来的明文密码。5. 避坑指南五个我实际翻过车的点5.1 commit和apply差一个生命周期现象登录后保存密码然后立刻用返回键退出应用再启动时密码没被回填。在低版本机器上多按几次会出现。原因apply()是异步写盘它先把数据更新到内存里的SharedPreferences副本随后才在后台线程里写磁盘文件。进程在磁盘写入完成前被杀这次写入就静默丢了。commit()是同步写盘阻塞到写入完成才返回。解决登录保存这种低频写操作直接用commit()。高频的开关、进度条这种可以继续apply()性能差异只在频繁调用时才明显。我现在的习惯是登录信息这种“丢了要命”的数据一律commit其余用apply。5.2 getPreferences和getSharedPreferences用混现象保存成功换一个界面去读读出来的全是默认值。代码检查了好几遍键名没错、类型没错就是读不到。原因getSharedPreferences(SP_NAME, MODE_PRIVATE)打开的是自定义文件名的XML而getPreferences(MODE_PRIVATE)是Activity内部快捷键它的文件名是当前Activity的类名。两个文件不同读写自然对不上。解决全项目统一走SpUtils这个工具类不要在Activity里直接调用getPreferences。工具类里把文件名写死避免每个人各写一份。5.3 在适配器或工具类里拿不到Context现象在一个静态工具方法里调用SpUtils.saveLoginInfoIDE直接提示无法解析this。把Activity的this传进去又担心内存泄漏。原因SharedPreferences是Context提供的服务没有Context就无从谈起。静态方法里经常把Context弄丢这是封装时最常犯的问题。解决统一在Application初始化时把全局Context缓存下来。做法是写一个App类继承Application在onCreate里保存this然后SpUtils内部通过AppUtil.getContext()获取。这样任何地方都能调又不需要一层层传Context。5.4 密码框回填后明文显示现象保存密码后重启应用密码被回填到输入框里但显示的是明文字符旁边还默认带了一个可见性开关。原因EditText没有设置TYPE_TEXT_VARIATION_PASSWORD属性系统不知道这是密码框按普通文本处理。解决布局文件里给密码框加inputType代码设置的话是etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);这样输入和回填都会显示圆点。但要注意如果同时期望“密码可见”切换功能还要自己处理InputType被覆盖的问题用setTransformationMethod会更稳。5.5 用户点了系统“清除数据”后一切归零现象测试反馈“我明明勾了记住密码重启后又要重新登录”。查了一圈代码没问题最后发现对方是去系统设置里点了清除数据。原因SharedPreferences、数据库、内部文件都存放在应用私有目录清除数据就是把整个目录删掉这是系统行为不是代码缺陷。解决告诉用户这是预期结果。如果产品要求“清除应用数据后还能恢复登录”那这些数据就不能只存本地要么走服务端会话恢复要么使用Android的AccountManager做系统级账号同步。本地存储的定位永远是“便捷”不是“可靠”。6. 进阶技巧给本地登录态加上保鲜期与验证习惯6.1 加时间戳让密码不是永久有效现在这份工程的问题是只要勾了记住密码密码就永远躺在本地永不失效。正式项目里一般会要求登录态过期比如七天内免登录超过七天必须重新输密码。实现方式不复杂在保存时多写一个时间戳editor.putLong(KEY_LOGIN_TIME, System.currentTimeMillis());读取时判断long savedTime sp.getLong(KEY_LOGIN_TIME, 0L); if (System.currentTimeMillis() - savedTime 7 * 24 * 60 * 60 * 1000L) { SpUtils.clearLoginInfo(context); // 跳回登录页 }过期后清空本地信息用户下次启动就是全新登录流程。这个时间戳只解决“过期”这一个问题如果有人手动修改系统时间它也会跟着失真正式项目建议用服务端下发的时间或token过期时间来判断。6.2 养成固定验证习惯每次改完登录模块我都会在真机上强制走完这三个场景登录后杀进程重进验证回填是否成功把系统时间往后调几天再进应用验证过期逻辑是否触发去设置里清除应用数据验证是否需要重新登录。这三个动作能覆盖掉前五章说的大多数问题。这套“保存QQ密码”工程做教学素材是够格的关键是把默认的明文保存改成加密保存再把token概念代入理解一次你的登录模块就和其他人的不在一个水平线了。这个工程里没有用任何奇怪框架全部是原生API适合作为你改造自定义登录模块的底稿。希望帮到你。本文还有配套的精品资源点击获取