
1. 项目概述与核心价值在移动应用开发中数据持久化是绕不开的核心环节。无论是记录用户的偏好设置、缓存网络请求结果还是构建一个功能完整的笔记、记账类应用都需要一个可靠、轻量且与平台深度集成的本地数据库方案。对于Android开发者而言SQLite几乎是这个角色的不二之选。它内置于Android系统无需额外部署零配置以单个文件的形式存在管理起来异常方便。然而很多刚接触Android开发的朋友在Android Studio中实际操作SQLite时往往会感到无从下手代码应该写在哪里SQLiteOpenHelper该怎么用数据库升级又该如何处理今天我们就来彻底解决这个问题。我将以一个典型的“用户信息管理”功能为例手把手带你完成从零开始在Android Studio中连接、创建SQLite数据库和数据表的全过程。这不仅仅是贴几段代码我会深入每个步骤背后的设计逻辑分享我多年实践中积累的、官方文档不会告诉你的那些“坑”和技巧。无论你是正在做课程设计的学生还是希望夯实基础的初级开发者这篇内容都能让你获得一个清晰、可复现、且具备生产环境参考价值的实操指南。2. 环境准备与项目初始化在开始编写数据库相关代码之前我们需要一个合适的“战场”。虽然理论上任何Android项目都可以但为了最佳实践我建议从一个干净的项目开始。2.1 创建新项目与配置首先打开Android Studio选择“New Project”。在模板选择界面为了聚焦于数据库逻辑我强烈推荐使用“Empty Views Activity”即传统的、基于View的Empty Activity而不是Compose。这能避免Jetpack Compose的声明式UI带来的额外概念干扰让我们更专注于数据层。项目命名可以随意例如SQLiteDemo。确保包名符合规范最低API级别Minimum SDK的选择需要谨慎。这里有一个关键点虽然SQLite本身很古老但Android框架对它的封装类如SQLiteOpenHelper是稳定的。然而如果你计划使用Room等更现代的ORM库就需要关注更高的API级别。对于本次纯SQLiteOpenHelper教学选择API 24 (Android 7.0) 或以上是一个兼顾市场覆盖和现代特性的平衡点。创建完成后检查项目app模块下的build.gradle.kts(或build.gradle) 文件。确保其中包含了基本的依赖。对于原生SQLite操作我们不需要额外添加依赖因为相关的类android.database.sqlite.*已经包含在Android SDK中。这是SQLite的一大优势——开箱即用。2.2 规划项目目录结构清晰的代码组织是良好可维护性的基础。我们将在java/你的包名/目录下或kotlin/你的包名/如果你用Kotlin创建几个关键的包packagedatabase: 这个包将存放所有数据库相关的核心类例如我们即将创建的DatabaseHelper。model: 存放数据模型或实体类Entity例如User类。这个类定义了数据库中一张表的结构。dao: 数据访问对象Data Access Object。虽然原生SQLite不强制要求但引入DAO模式能极大地提升代码的清晰度和可测试性。它定义了所有操作数据库的方法增删改查。可选repository: 如果业务逻辑复杂可以再抽象一层Repository用于协调多个数据源如本地数据库和网络。对于简单Demo我们可以暂时不用。现在右键点击你的包名选择New - Package依次创建上述包。一个结构良好的项目从一开始就成功了一半。3. 核心组件设计与原理解析在动手写代码之前理解Android框架为我们提供的工具至关重要。核心就是SQLiteOpenHelper类。3.1 SQLiteOpenHelper你的数据库管家SQLiteOpenHelper是一个抽象类它是Android系统为管理SQLite数据库创建和版本升级提供的一个超级好用的助手。你不应该直接去调用SQLiteDatabase.openOrCreateDatabase()这类底层方法而是应该继承SQLiteOpenHelper。它的核心工作机制是这样的延迟初始化它不会在应用启动时就创建或打开数据库只有在第一次调用getWritableDatabase()或getReadableDatabase()时才会触发。版本控制它通过一个整型的DATABASE_VERSION来追踪数据库模式Schema的版本。当检测到版本号增加时会自动回调onUpgrade()方法让你执行表结构变更、数据迁移等操作。单例模式为了避免多个数据库连接同时操作造成资源竞争和数据不一致通常将SQLiteOpenHelper实例以单例模式提供。这里有一个大坑在多线程环境下如果多个线程同时获取数据库实例可能会引发异常。幸运的是SQLiteOpenHelper内部已经使用synchronized关键字确保了getWritableDatabase()的线程安全但为了最佳性能我们仍需确保整个应用使用同一个Helper实例。3.2 设计数据模型Entity模型类是对数据库中一张表的Java/Kotlin对象映射。我们以User表为例先设计它的结构。表结构规划_id: INTEGER, 主键自增长。在Android中约定俗成使用_id作为主键列名这与CursorAdapter等组件兼容性更好。name: TEXT, 用户名非空。age: INTEGER, 年龄。email: TEXT, 邮箱唯一约束。对应的User模型类应该是一个简单的POJOPlain Old Java Object包含这些字段、构造方法以及getter和setter。在Kotlin中一个data class就能优雅地完成。为什么要有模型类直接使用ContentValues或Cursor虽然可以工作但代码会充斥魔法数字列名字符串难以维护且容易出错。模型类将数据库的列名定义为常量并通过对象化的方式操作数据是通往清晰架构的第一步。4. 实现DatabaseHelper类这是整个数据库层的基石。我们将在database包下创建一个名为DatabaseHelper的类。4.1 定义常量与构造函数package com.yourpackage.database; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; public class DatabaseHelper extends SQLiteOpenHelper { // 数据库信息常量 private static final String DATABASE_NAME MyApp.db; private static final int DATABASE_VERSION 1; // 表名常量 public static final String TABLE_USERS users; // 用户表列名常量 public static final String COLUMN_ID _id; public static final String COLUMN_NAME name; public static final String COLUMN_AGE age; public static final String COLUMN_EMAIL email; // 创建用户表的SQL语句 private static final String CREATE_TABLE_USERS CREATE TABLE TABLE_USERS ( COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, COLUMN_NAME TEXT NOT NULL, COLUMN_AGE INTEGER, COLUMN_EMAIL TEXT UNIQUE );; // 单例实例 private static DatabaseHelper instance; // 获取单例的静态方法使用Application Context避免内存泄漏 public static synchronized DatabaseHelper getInstance(Context context) { if (instance null) { // 使用Application Context而不是Activity Context instance new DatabaseHelper(context.getApplicationContext()); } return instance; } // 私有化构造函数强制使用getInstance private DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } }关键点解析常量定义所有字符串包括数据库名、表名、列名、SQL语句都必须定义为static final常量。这避免了在代码中硬编码字符串一旦需要修改只需改动一处且能有效防止因拼写错误导致的bug。SQL语句编写创建表的SQL是核心。注意语法PRIMARY KEY AUTOINCREMENT确保了主键唯一且自增。NOT NULL和UNIQUE是数据完整性的重要约束。务必在语句末尾加上分号这是一个好习惯。单例模式与Context这里实现了线程安全的懒加载单例。最需要关注的是context.getApplicationContext()。永远不要用Activity的Context去初始化SQLiteOpenHelper因为Activity生命周期短如果它被销毁但数据库连接还未关闭Helper持有该Context的引用就会导致内存泄漏。Application Context的生命周期与应用一致是安全的选择。4.2 实现onCreate与onUpgrade方法接下来在DatabaseHelper类中重写两个核心回调方法。Override public void onCreate(SQLiteDatabase db) { // 当数据库第一次被创建时调用版本号从0到1 try { db.execSQL(CREATE_TABLE_USERS); // 如果需要创建其他表可以继续在这里执行execSQL // db.execSQL(CREATE_TABLE_ORDERS); } catch (Exception e) { // 在实际项目中这里应该记录日志而不是简单打印 e.printStackTrace(); } } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 当DATABASE_VERSION增加时调用 // 警告这里的实现非常关键直接删除表会丢失所有用户数据 // 这只是演示生产环境需要更复杂的迁移策略。 try { db.execSQL(DROP TABLE IF EXISTS TABLE_USERS); onCreate(db); } catch (Exception e) { e.printStackTrace(); } }关于onUpgrade的深度讨论上面onUpgrade中的“删除-重建”策略是极其危险的仅适用于测试或数据库结构发生彻底颠覆性变更且可以接受数据丢失的情况。在生产环境中你必须实现无损的数据迁移。一个基本的迁移策略是使用ALTER TABLE语句。例如从版本1升级到版本2需要为users表新增一个phone列Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { for (int version oldVersion 1; version newVersion; version) { switch (version) { case 2: // 升级到版本2添加phone列 db.execSQL(ALTER TABLE TABLE_USERS ADD COLUMN phone TEXT;); break; case 3: // 升级到版本3可能重命名列或修改约束SQLite的ALTER功能有限可能需要创建新表并复制数据 // 这是一个复杂操作需要临时表 break; // ... 处理后续版本 default: break; } } }更复杂的迁移如删除列、修改列类型在SQLite中不支持直接的ALTER需要创建新表、复制数据、删除旧表、重命名新表等一系列操作。强烈建议在项目初期就使用Room Persistence Library它内置了强大的、基于注解的迁移方案能自动生成迁移代码省去无数烦恼。5. 实现数据访问层DAO有了DatabaseHelper我们可以获取到SQLiteDatabase对象来进行增删改查。为了不把数据库操作逻辑散落在各个Activity或Fragment中我们引入UserDao。在dao包下创建UserDao类。这个类将封装所有与users表交互的细节。5.1 插入数据Createpackage com.yourpackage.dao; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.yourpackage.database.DatabaseHelper; import com.yourpackage.model.User; import java.util.ArrayList; import java.util.List; public class UserDao { private SQLiteDatabase db; private DatabaseHelper dbHelper; public UserDao(Context context) { dbHelper DatabaseHelper.getInstance(context); // 获取可写数据库。注意在完成操作后我们不在DAO层关闭它。 // 通常在一个业务操作单元如插入一条用户完成后也不应立即关闭 // 因为频繁开关连接性能损耗大。SQLiteOpenHelper会管理连接池。 db dbHelper.getWritableDatabase(); } // 插入一个用户 public long insertUser(User user) { // ContentValues类似于Map用于存放要插入的键值对 ContentValues values new ContentValues(); values.put(DatabaseHelper.COLUMN_NAME, user.getName()); values.put(DatabaseHelper.COLUMN_AGE, user.getAge()); values.put(DatabaseHelper.COLUMN_EMAIL, user.getEmail()); // 执行插入返回新行的ID如果失败返回-1 long newRowId db.insert(DatabaseHelper.TABLE_USERS, null, values); return newRowId; } }实操心得ContentValues的使用put方法有重载可以接受String,Integer,Long,Double,byte[],Boolean等类型。对于Boolean通常转换为INTEGER存储0或1。insert方法的第二个参数nullColumnHack有点古怪。当ContentValues为空时即你想插入一个所有列都为NULL的行SQLite不允许此时这个参数指定一个允许为NULL的列名来“绕过”限制。在绝大多数有内容的插入场景直接传null即可。5.2 查询数据Read查询是数据库操作中最复杂的部分涉及Cursor的使用。// 查询所有用户 public ListUser getAllUsers() { ListUser userList new ArrayList(); // 定义要查询的列 String[] projection { DatabaseHelper.COLUMN_ID, DatabaseHelper.COLUMN_NAME, DatabaseHelper.COLUMN_AGE, DatabaseHelper.COLUMN_EMAIL }; // 排序按ID降序最新的在前面 String sortOrder DatabaseHelper.COLUMN_ID DESC; Cursor cursor null; try { cursor db.query( DatabaseHelper.TABLE_USERS, // 表名 projection, // 要返回的列 null, // WHERE子句的条件null表示无 null, // WHERE子句的参数 null, // GROUP BY null, // HAVING sortOrder // ORDER BY ); // 遍历Cursor while (cursor.moveToNext()) { User user new User(); // 根据列索引获取数据。使用getColumnIndex比硬编码数字更安全。 int idIndex cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_ID); int nameIndex cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_NAME); int ageIndex cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_AGE); int emailIndex cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_EMAIL); user.setId(cursor.getLong(idIndex)); user.setName(cursor.getString(nameIndex)); // 注意数据库中age可能为NULL需要检查 if (!cursor.isNull(ageIndex)) { user.setAge(cursor.getInt(ageIndex)); } else { user.setAge(null); // 或一个默认值 } user.setEmail(cursor.getString(emailIndex)); userList.add(user); } } catch (Exception e) { e.printStackTrace(); } finally { // 非常重要必须关闭Cursor否则会造成内存泄漏和资源耗尽。 if (cursor ! null) { cursor.close(); } } return userList; } // 根据ID查询单个用户 public User getUserById(long id) { String selection DatabaseHelper.COLUMN_ID ?; String[] selectionArgs { String.valueOf(id) }; Cursor cursor null; try { cursor db.query( DatabaseHelper.TABLE_USERS, null, // 查询所有列 selection, selectionArgs, null, null, null ); if (cursor.moveToFirst()) { User user new User(); // ... 从cursor填充user对象同上可抽取为独立方法 return user; } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor ! null) cursor.close(); } return null; }Cursor处理的黄金法则总是检查null和moveToFirst/moveToNext查询可能返回空结果集。总是使用getColumnIndexOrThrow它比getColumnIndex更安全如果列不存在会抛出异常便于早期发现问题。总是处理NULL值使用cursor.isNull(columnIndex)检查否则直接调用getInt等可能返回0与真实的NULL或0值混淆。总是在finally块中关闭Cursor这是铁律。未关闭的Cursor是Android内存泄漏的常见元凶。5.3 更新与删除数据Update Delete// 更新用户信息 public int updateUser(User user) { ContentValues values new ContentValues(); values.put(DatabaseHelper.COLUMN_NAME, user.getName()); values.put(DatabaseHelper.COLUMN_AGE, user.getAge()); values.put(DatabaseHelper.COLUMN_EMAIL, user.getEmail()); String selection DatabaseHelper.COLUMN_ID ?; String[] selectionArgs { String.valueOf(user.getId()) }; // 返回受影响的行数 return db.update(DatabaseHelper.TABLE_USERS, values, selection, selectionArgs); } // 删除用户 public int deleteUser(long userId) { String selection DatabaseHelper.COLUMN_ID ?; String[] selectionArgs { String.valueOf(userId) }; return db.delete(DatabaseHelper.TABLE_USERS, selection, selectionArgs); }更新和删除操作相对简单核心是构建正确的WHERE子句selection和selectionArgs。selectionArgs的使用可以有效防止SQL注入攻击永远不要用字符串拼接的方式来构造selection。6. 在UI层集成与调用数据库层和DAO层准备就绪后我们就可以在Activity中使用了。这里以MainActivity为例演示插入和查询。6.1 初始化与插入操作public class MainActivity extends AppCompatActivity { private UserDao userDao; private EditText etName, etAge, etEmail; private TextView tvResult; private Button btnInsert, btnQuery; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化DAO userDao new UserDao(this); // 绑定UI控件 etName findViewById(R.id.et_name); etAge findViewById(R.id.et_age); etEmail findViewById(R.id.et_email); tvResult findViewById(R.id.tv_result); btnInsert findViewById(R.id.btn_insert); btnQuery findViewById(R.id.btn_query); btnInsert.setOnClickListener(v - insertUser()); btnQuery.setOnClickListener(v - queryAllUsers()); } private void insertUser() { String name etName.getText().toString().trim(); String ageStr etAge.getText().toString().trim(); String email etEmail.getText().toString().trim(); if (name.isEmpty() || email.isEmpty()) { Toast.makeText(this, 姓名和邮箱不能为空, Toast.LENGTH_SHORT).show(); return; } Integer age null; if (!ageStr.isEmpty()) { try { age Integer.parseInt(ageStr); } catch (NumberFormatException e) { Toast.makeText(this, 年龄请输入有效数字, Toast.LENGTH_SHORT).show(); return; } } User user new User(); user.setName(name); user.setAge(age); user.setEmail(email); // 注意数据库操作是耗时的不应该在主线程执行 // 这里仅作演示实际必须使用线程或AsyncTask/Loader/协程等。 new Thread(() - { long result userDao.insertUser(user); runOnUiThread(() - { if (result ! -1) { Toast.makeText(MainActivity.this, 插入成功ID: result, Toast.LENGTH_SHORT).show(); clearInputs(); } else { Toast.makeText(MainActivity.this, 插入失败可能是邮箱重复, Toast.LENGTH_SHORT).show(); } }); }).start(); } private void clearInputs() { etName.setText(); etAge.setText(); etEmail.setText(); } }6.2 查询与显示操作private void queryAllUsers() { new Thread(() - { ListUser users userDao.getAllUsers(); runOnUiThread(() - displayUsers(users)); }).start(); } private void displayUsers(ListUser users) { if (users null || users.isEmpty()) { tvResult.setText(数据库中没有用户数据。); return; } StringBuilder sb new StringBuilder(); for (User user : users) { sb.append(ID: ).append(user.getId()) .append(, 姓名: ).append(user.getName()) .append(, 年龄: ).append(user.getAge() ! null ? user.getAge() : N/A) .append(, 邮箱: ).append(user.getEmail()) .append(\n---\n); } tvResult.setText(sb.toString()); } Override protected void onDestroy() { super.onDestroy(); // 在实际应用中如果DAO持有数据库引用可能需要在这里关闭。 // 但更常见的做法是让SQLiteOpenHelper管理生命周期不手动关闭。 // userDao.close(); // 如果DAO有close方法的话 } }至关重要的线程警告上面的代码为了清晰直接在ClickListener中创建了Thread。这在生产环境中是非常糟糕的做法。你会面临线程管理混乱、内存泄漏匿名内部类持有Activity引用、生命周期不一致等问题。现代Android开发中你应该使用AsyncTask(已废弃不推荐新项目使用)简单但易出错生命周期管理复杂。LiveDataViewModelRepository这是官方推荐的架构组件。Repository在后台线程执行数据库操作通过LiveData将结果通知给UI。ViewModel管理UI相关数据生命周期长于Activity屏幕旋转时数据不会丢失。Kotlin协程Coroutines如果使用Kotlin协程是处理异步操作的首选代码简洁可读性高。结合Room库和viewModelScope可以写出非常优雅的异步数据库代码。RxJava一个强大的响应式编程库但学习曲线较陡。7. 进阶话题与性能优化掌握了基础操作后我们来看看如何做得更好。7.1 使用事务Transaction当你需要执行一系列数据库操作如批量插入1000条记录并且希望它们要么全部成功要么全部失败时必须使用事务。事务能保证数据的一致性并且由于SQLite的写操作是串行的将多个操作包裹在事务中可以大幅提升性能。public void batchInsertUsers(ListUser users) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); // 开始事务 try { for (User user : users) { ContentValues values new ContentValues(); // ... 填充values db.insert(TABLE_USERS, null, values); } db.setTransactionSuccessful(); // 标记事务成功 } catch (Exception e) { e.printStackTrace(); // 如果发生异常事务会自动回滚 } finally { db.endTransaction(); // 结束事务如果未调用setTransactionSuccessful则回滚 } }关键点setTransactionSuccessful()必须在endTransaction()之前调用否则所有操作都会被回滚。endTransaction()是必须调用的它负责释放事务锁。7.2 索引与查询优化随着数据量增大查询速度可能变慢。为经常用于WHERE、ORDER BY或JOIN条件的列创建索引可以极大提升查询性能。创建索引的SQL可以在DatabaseHelper的onCreate或onUpgrade中执行private static final String CREATE_INDEX_USERS_EMAIL CREATE INDEX IF NOT EXISTS idx_users_email ON TABLE_USERS ( COLUMN_EMAIL );; Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE_USERS); db.execSQL(CREATE_INDEX_USERS_EMAIL); // 为邮箱创建索引 }注意索引会加快查询但会减慢插入、更新和删除的速度因为索引也需要维护并占用额外空间。不要为所有列都创建索引只为高选择性的、频繁查询的列创建。7.3 使用Room Persistence Library虽然原生SQLite API给了你完全的控制权但对于大多数应用我强烈建议直接使用Jetpack组件中的Room。Room在SQLite之上提供了一个抽象层让你能够用更少的样板代码、更安全的方式操作数据库。它的核心优势编译时校验SQL查询语句在编译时就会被检查语法和表名/列名是否正确避免运行时崩溃。方便的ORM通过注解Entity,Dao,Database自动完成对象关系映射。内置LiveData和RxJava支持轻松实现数据变化驱动UI更新。强大的迁移工具通过Migration类可以安全、清晰地处理数据库版本升级。从原生SQLite迁移到Room是一个平滑的过程你之前对SQL和数据库设计的理解完全适用只是换了一种更高效、更安全的方式来表达。8. 常见问题排查与调试技巧即使按照步骤操作你也可能会遇到问题。这里记录一些我踩过的坑和解决方法。8.1 数据库文件在哪里如何查看应用内创建的数据库文件默认位于/data/data/你的应用包名/databases/目录下。在真机上没有root权限无法直接访问。调试时有两种主要方法使用Android Studio的Device File Explorer在Android Studio中打开View - Tool Windows - Device File Explorer找到上述路径可以将数据库文件.db下载到电脑上。使用第三方工具查看下载到电脑后可以使用如DB Browser for SQLite一个免费、开源的图形化工具来打开、浏览和编辑数据库文件直观地检查表结构和数据这对调试非常有帮助。8.2 “database is locked” 或 “cannot open database” 错误这通常是由于多个线程或进程同时尝试写入数据库造成的。SQLite支持多线程读但写操作是串行的。确保使用单例的SQLiteOpenHelper如我们之前所做这能保证整个应用使用同一个数据库连接池。合理使用事务将多个写操作包裹在事务中可以减少数据库被锁定的总时间。避免长时间持有数据库连接不要在全局变量中长期持有SQLiteDatabase对象用的时候通过Helper获取操作完成后Android框架会管理连接池。检查是否在多个地方调用了close()通常不需要手动关闭SQLiteDatabaseSQLiteOpenHelper会管理它。手动关闭可能导致其他地方使用时报错。8.3 应用升级后数据丢失了这几乎肯定是onUpgrade方法实现有问题。回顾第4.2节如果你在onUpgrade中直接DROP TABLE那么每次版本号增加旧表都会被删除。请务必根据新旧版本号实现渐进式的、数据保留的迁移逻辑。对于重要的生产应用在发布新版本前务必在多种旧版本上测试升级路径。8.4 查询结果不对或Cursor异常列名拼写错误始终使用定义好的常量避免手打字符串。忘记调用cursor.moveToFirst()执行query后Cursor的指针在第一条记录之前必须调用moveToFirst或moveToNext才能获取数据。未处理NULL值如第5.2节所述使用isNull()检查。未关闭Cursor这不仅是内存泄漏问题在旧版本Android上可能会导致后续数据库操作异常。确保在finally块中关闭。8.5 使用adb shell进行调试对于高级调试你可以使用adb命令。确保手机已开启USB调试并连接电脑。进入adb shell:adb shell切换到你的应用数据目录:run-as com.yourpackage.name(你的应用包名)进入数据库目录:cd databases使用sqlite3命令行工具打开数据库:sqlite3 MyApp.db执行SQL命令:.tables(查看所有表)SELECT * FROM users;(查询数据).schema users(查看表结构).exit(退出)这个过程能让你在最底层确认数据库的状态是解决疑难杂症的终极手段。从连接、创建到增删改查我们完成了一个完整的Android SQLite集成流程。我个人的体会是虽然直接使用SQLiteOpenHelper让你对底层有更深的掌控感但在当今的Android开发生态中除非有极特殊的性能或控制需求否则Room库应该是你的默认选择。它能帮你规避掉90%的样板代码和潜在错误让开发者更专注于业务逻辑。理解原生API的工作原理是为了更好地使用和驾驭像Room这样的高级工具。当你下次启动Android Studio准备处理本地数据时希望这篇内容能成为你手边一份可靠的参考。