19、异步数据库操作:从过时方案到现代实践
说实话,Android 里的数据库操作,最让人头疼的就是「在主线程里搞 SQLite」。
你想想看,一个查询如果卡了几百毫秒,用户界面就直接冻住了。轻则掉帧,重则 ANR。我早期做项目时,就因为在主线程执行了一个大表的联表查询,被测试小姐姐追着骂了三天。
所以,异步操作是必须的。但怎么异步?这就有讲究了。今天我把几种主流方案都捋一遍,从过时的到现代的,你心里有个数就行。
核心观点:数据库操作必须异步执行,但选哪种方案取决于你的项目现状和技术栈。
19.1 AsyncTask + 数据库:时代的眼泪
AsyncTask 在 Android 早期版本里非常流行。它封装了线程池和 Handler,让你能轻松在后台线程干活,然后切回主线程更新 UI。
private class SaveUserTask extends AsyncTask<User, Void, Boolean> {
private DatabaseHelper dbHelper;
SaveUserTask(DatabaseHelper dbHelper) {
this.dbHelper = dbHelper;
}
@Override
protected Boolean doInBackground(User... users) {
// 后台线程执行
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", users[0].getName());
values.put("age", users[0].getAge());
long result = db.insert("users", null, values);
return result != -1;
}
@Override
protected void onPostExecute(Boolean success) {
// 主线程更新 UI
if (success) {
Toast.makeText(getApplicationContext(), "保存成功", Toast.LENGTH_SHORT).show();
}
}
}
// 调用
new SaveUserTask(dbHelper).execute(user);
嗯,这里要注意。AsyncTask 在 API 30 已经被标记为过时了。为什么?
- 生命周期问题:Activity 销毁了,AsyncTask 还在跑,容易内存泄漏。我踩过这个坑,用户旋转屏幕,Activity 重建,结果 AsyncTask 还在往旧的 Activity 实例里写数据,直接崩溃。
- 串行执行陷阱:Android 3.0 之后,AsyncTask 默认是串行执行的。如果你同时开多个任务,它们会排队,效率极低。
- 错误处理麻烦:doInBackground 里抛异常,你只能在 onPostExecute 里判断,非常别扭。
避坑指南:我曾经在一个老项目里看到 AsyncTask 嵌套 AsyncTask,代码像意大利面条一样。如果你还在维护老项目,建议尽快迁移。新项目绝对不要用。
19.2 HandlerThread 方案:手动控制线程
HandlerThread 本质上就是一个带有 Looper 的线程。你可以往它的 Handler 里发消息,消息会在子线程里排队执行。
public class DatabaseHandlerThread {
private HandlerThread handlerThread;
private Handler workerHandler;
private DatabaseHelper dbHelper;
public DatabaseHandlerThread(DatabaseHelper dbHelper) {
this.dbHelper = dbHelper;
handlerThread = new HandlerThread("DBWorker");
handlerThread.start();
workerHandler = new Handler(handlerThread.getLooper());
}
public void insertUser(User user, OnDbCallback callback) {
workerHandler.post(() -> {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", user.getName());
values.put("age", user.getAge());
long result = db.insert("users", null, values);
// 切回主线程通知结果
new Handler(Looper.getMainLooper()).post(() -> {
if (callback != null) {
callback.onResult(result != -1);
}
});
});
}
public void quit() {
handlerThread.quitSafely();
}
public interface OnDbCallback {
void onResult(boolean success);
}
}
这个方案的好处是:你完全控制线程的生命周期。坏处是:代码量不小,而且容易忘记 quit(),导致线程泄漏。
我个人习惯在 Fragment 或 Activity 的 onDestroy 里调用 quit()。但说实话,现在用 HandlerThread 的人越来越少了,因为协程和 RxJava 太香了。
19.3 协程 + 数据库:现代 Android 的标配
协程是 Kotlin 的异步利器。配合 Dispatchers.IO,数据库操作变得非常优雅。
// 在 ViewModel 或 Repository 中
fun saveUser(user: User) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val db = databaseHelper.writableDatabase
val values = ContentValues().apply {
put("name", user.name)
put("age", user.age)
}
db.insert("users", null, values)
}
// 自动回到主线程
_saveResult.value = true
}
}
// 查询示例
suspend fun getUser(id: Long): User? = withContext(Dispatchers.IO) {
val db = databaseHelper.readableDatabase
val cursor = db.query("users", null, "id=?", arrayOf(id.toString()), null, null, null)
cursor.use {
if (it.moveToFirst()) {
User(it.getLong(0), it.getString(1), it.getInt(2))
} else null
}
}
为什么协程好?
- 轻量级:一个协程的开销比线程小得多。你可以开几千个协程,但开几千个线程就炸了。
- 结构化并发:viewModelScope 会在 ViewModel 销毁时自动取消所有协程,不会泄漏。
- 代码可读性高:withContext 切换线程,看起来就像同步代码一样。
个人经验:我在一个日活百万的 App 里,把所有数据库操作都改成了协程。性能提升很明显,而且再也没出现过 ANR。如果你用 Room,它原生支持协程,连 withContext 都省了。
19.4 RxJava + 数据库:响应式编程
RxJava 在协程流行之前,是 Android 异步编程的王者。它用 Observable 和 Flowable 来管理数据流。
public class UserRepository {
private DatabaseHelper dbHelper;
public Single<Boolean> saveUser(User user) {
return Single.create(emitter -> {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", user.getName());
values.put("age", user.getAge());
long result = db.insert("users", null, values);
emitter.onSuccess(result != -1);
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public Flowable<List<User>> getAllUsers() {
return Flowable.create(emitter -> {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query("users", null, null, null, null, null, null);
List<User> users = new ArrayList<>();
while (cursor.moveToNext()) {
users.add(new User(cursor.getLong(0), cursor.getString(1), cursor.getInt(2)));
}
cursor.close();
emitter.onNext(users);
emitter.onComplete();
}, BackpressureStrategy.BUFFER)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
// 调用
repository.saveUser(user)
.subscribe(success -> {
if (success) {
Toast.makeText(context, "保存成功", Toast.LENGTH_SHORT).show();
}
}, throwable -> {
Log.e("DB", "保存失败", throwable);
});
RxJava 的优势在于:
- 强大的操作符:map、flatMap、debounce 等,处理复杂数据流非常方便。
- 背压支持:Flowable 可以处理生产者速度大于消费者速度的情况。
- 线程切换灵活:subscribeOn 和 observeOn 让你精确控制线程。
但缺点也很明显:学习曲线陡峭,而且 RxJava 的包体积不小。我个人觉得,如果项目已经用了 RxJava,那就继续用;如果新项目,直接上协程更香。
19.5 回调封装:自己造轮子
有时候,你不想引入协程或 RxJava,但又不想用 AsyncTask。那就自己封装一个回调式的异步工具。
public class DbAsyncExecutor {
private static ExecutorService executor = Executors.newFixedThreadPool(4);
private static Handler mainHandler = new Handler(Looper.getMainLooper());
public static <T> void execute(Callable<T> backgroundTask, OnResultCallback<T> callback) {
executor.execute(() -> {
try {
T result = backgroundTask.call();
mainHandler.post(() -> {
if (callback != null) {
callback.onSuccess(result);
}
});
} catch (Exception e) {
mainHandler.post(() -> {
if (callback != null) {
callback.onError(e);
}
});
}
});
}
public interface OnResultCallback<T> {
void onSuccess(T result);
void onError(Exception e);
}
}
// 使用
DbAsyncExecutor.execute(() -> {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", user.getName());
long id = db.insert("users", null, values);
return id;
}, new DbAsyncExecutor.OnResultCallback<Long>() {
@Override
public void onSuccess(Long result) {
Toast.makeText(context, "插入成功,ID: " + result, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(Exception e) {
Log.e("DB", "插入失败", e);
}
});
这个方案的好处是:轻量、无依赖、容易理解。坏处是:功能有限,没有协程的取消机制,也没有 RxJava 的操作符。
我的建议:如果你在维护一个老项目,不想引入新框架,回调封装是个不错的过渡方案。但新项目,请直接上协程。这是 2024 年的 Android 开发,协程已经是官方推荐的标准方案了。
19.6 方案对比总结
| 方案 | 推荐度 | 学习成本 | 代码量 | 生命周期安全 | 适用场景 |
|---|---|---|---|---|---|
| AsyncTask | ❌ 不推荐 | 低 | 中 | 差 | 仅限老项目维护 |
| HandlerThread | ⚠️ 可用 | 中 | 多 | 中 | 需要精确控制线程 |
| 协程 | ✅ 强烈推荐 | 中 | 少 | 好 | 新项目首选 |
| RxJava | ✅ 推荐 | 高 | 中 | 好 | 已使用 RxJava 的项目 |
| 回调封装 | ⚠️ 可用 | 低 | 少 | 中 | 轻量级项目或过渡期 |
好了,以上就是异步数据库操作的几种主流方案。我个人最推荐协程,它简洁、安全、高效。如果你还在用 AsyncTask,赶紧换吧,别等到 ANR 了才后悔。
公众号:蓝海资料掘金营,微信deep3321