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 的项目
回调封装 ⚠️ 可用 轻量级项目或过渡期
Android 异步数据库操作方案演进 AsyncTask 已过时 HandlerThread 手动控制 回调封装 轻量方案 RxJava 响应式 协程 ⭐ 推荐 选择建议 • 新项目:直接使用 协程,配合 Room 或原生 SQLite • 已有 RxJava 项目:继续使用,但考虑逐步迁移到协程 • 老项目维护:回调封装作为过渡,尽快升级 • 避免使用:AsyncTask(已过时)、HandlerThread(代码冗余) 时间线 → 从过时到现代

好了,以上就是异步数据库操作的几种主流方案。我个人最推荐协程,它简洁、安全、高效。如果你还在用 AsyncTask,赶紧换吧,别等到 ANR 了才后悔。


公众号:蓝海资料掘金营,微信deep3321