耗时操作
通过前面的开发工作,我们实现了一个具有基本功能的完整的笔记应用。然而,这里面缺仍然隐藏着一些隐患。其中比较突出的,就是在UI线程进行耗时操作的问题。
UI线程(UI thread)又被称为主线程(main thread),在程序运行中负责分发处理用户界面(UI)事件(event),如点击、按键等。UI事件通常随机产生并且要求尽快响应。因此,如果在UI线程中加入了耗时操作,就会妨碍接踵而至的UI事件的处理,造成程序卡顿乃至无响应。耗时操作包括但不限于:
- 网络访问
- 文件读写
- 数据库访问
- 进程间通信
- 图像处理
Android系统规定,UI线程被阻塞超过约5秒钟以上,则报告ANR(Application Not Responding,应用程序无响应)错误,在屏幕上弹出警告对话框,询问用户选择继续等待还是结束应用程序。
因此,我们在设计和实现应用程序时,不应当在UI线程中进行需要耗费大量时间的操作。针对耗时操作,通常的解决方案,是将其安排到UI线程之外的工作线程进行。待其处理结束,再通知UI线程进行响应,处理其操作结果:
运用AsyncTask处理耗时操作
Android SDK提供了AsyncTask(异步任务)机制来实现耗时操作的多线程处理。AsyncTask是一个抽象类。我们要在它的基础上实现我们自己的子类。
在我们的项目中,所有对数据库的访问都是耗时操作,都需要转移到工作线程中进行,包括:
- 全部笔记页面:从数据库加载全部笔记数据
- 新建笔记页面:将新的笔记保存到数据库
- 阅读笔记页面:从数据库根据指定的id读取一条笔记记录
1. 全部笔记页面
首先,我们对全部笔记页面的交互流程追加一些设计:
- 在异步执行数据库查询操作的过程中显示一个加载等待视图以使用户了解当前状态
- 在查询操作线程结束后,关闭加载等待视图,并显示查询结果。
等待视图如下:
所以,我们修改全部笔记页面的布局文件,在原来布局结构的最底部增加这个等待视图:
...
打开全部笔记页面对应的NoteListActivity类,从布局文件中取得加载等待视图对象。首先增加表示加载等待视图的成员变量mLoadingView:
private FrameLayout mLoadingView;
然后找到onCreate()方法,在靠前的位置取得加载等待视图对象:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_list);
// 获取布局中的加载等待视图对象
mLoadingView = (FrameLayout) findViewById(R.id.loading_view);
...
}
接下来,找到原来直接从数据库加载数据的代码位置。有两处,一处是onCreate()方法中:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 为适配器设定数据集
adapter.setNotes(noteRepository.getAllNotes());
...
}
将此行代码去掉:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 为适配器设定数据集
// adapter.setNotes(noteRepository.getAllNotes());
...
}
另一处是在onResume()方法中:
@Override
protected void onResume() {
super.onResume();
// 1. 重新设定数据集
adapter.setNotes(noteRepository.getAllNotes());
// 2. 触发RecyclerView重新绘制
adapter.notifyDataSetChanged();
}
将这两行代码全部注释掉:
@Override
protected void onResume() {
super.onResume();
// 1. 重新设定数据集
// adapter.setNotes(noteRepository.getAllNotes());
// 2. 触发RecyclerView重新绘制
// adapter.notifyDataSetChanged();
}
然后,紧挨着onResume()方法创建我们的异步任务类LoadAllNotesTask:
private class LoadAllNotesTask extends AsyncTask> {
}
注意,AsyncTask类有三个范型参数,这里我们只关心最后一个,也就是从数据库加载到的笔记列表,将其设置成ArrayList
根据报错,自动添加必须实现的doInBackground()方法:
private class LoadAllNotesTask extends AsyncTask> {
@Override
protected ArrayList doInBackground(Void... voids) {
return null;
}
}
顾名思义,doInBackground()方法运行在工作线程,负责完成耗时操作。我们在这个方法里面执行数据库查询操作,并且用得到的笔记列表作为返回值。由于数据量小,加载速度太快,令工作线程等待1秒钟以观看效果。编写代码如下:
@Override
protected ArrayList doInBackground(Void... voids) {
// 由于数据量小,加载速度太快,令工作线程等待2秒钟以观看效果
// 实际产品中应当去掉此代码
try { Thread.sleep(2000); } catch (InterruptedException e) { }
return noteRepository.getAllNotes();
}
但是,我们要做的工作不止这些:
- 在工作线程开始执行之前,需要将加载等待视图显示出来
- 在工作线程完成并返回笔记列表后,需要将笔记列表显示到UI,并且关闭加载等待视图
在LoadAllNotesTask类内部空白处按Ctrl+O热键以弹出重写方法对话框:
如上图,同时选中onPreExecute()方法和onPostExcute()方法,它们分别在处理工作线程运行前、后被调用,并且都运行在UI线程。点击“OK”确认,这两个方法就被自动加入进来了:
改写onPreExecute()方法
在此方法中,我们要将加载等待视图显示出来。只要设置该视图的可见性为VISIBLE(可见)即可:
@Override
protected void onPreExecute() {
mLoadingView.setVisibility(View.VISIBLE);
}
改写onPostExcute()方法
可以注意到,onPostExcute()方法的参数恰好是doInBackground()方法的返回值,即从数据库读出来的全部笔记列表。那么我们在这里首先将这个参数传进来的列表设置给RecyclerView列表视图的适配器对象并进行刷新,然后将加载等待视图关闭掉(设置其可见性为GONE):
@Override
protected void onPostExecute(ArrayList notes) {
// 为适配器设置新的笔记列表
adapter.setNotes(notes);
// 通知RecyclerView刷新
adapter.notifyDataSetChanged();
// 关闭加载等待视图
mLoadingView.setVisibility(View.GONE);
}
执行异步任务以加载数据
异步任务在何处执行呢?onResume()是个不错的选择——每当页面出现在用户面前时该方法都会重新调用,这样可以保证页面显示的数据与数据库保持一致。改写onResume()方法如下:
@Override
protected void onResume() {
super.onResume();
// 执行异步加载数据任务
LoadAllNotesTask task = new LoadAllNotesTask();
task.execute();
}
执行程序查看效果:
2. 新建笔记页面
本页面中涉及到的数据库操作就是保存笔记,我们在异步任务开始执行前通过一个Toast来通知用户“正在保存您的笔记”,待保存成功/失败之后再告知用户保存结果。
打开EditNoteActivity类,找到原来保存数据的代码,在onFinishEdit()方法中:
private void onFinishEdit() {
// 1. 从编辑区获取标题和内容字符串
String title = mTitleEdit.getEditableText().toString();
String content = mContentEdit.getEditableText().toString();
// 2. 创建笔记对象
Note note = new Note(0, title, content, System.currentTimeMillis());
// 3. 存储笔记
if (noteRepository.saveNote(note) != null) {
Toast.makeText(this, R.string.msg_note_saved, Toast.LENGTH_SHORT).show();
finish(); // 关闭窗口
} else {
Toast.makeText(this, R.string.msg_note_not_saved, Toast.LENGTH_SHORT).show();
}
}
用异步任务的方式改写。在前面,我们单独定义了一个内部类LoadAllNotesTask来实现异步。这里我们换一种方法,通过匿名类来实现异步任务:
private void onFinishEdit() {
AsyncTask task = new AsyncTask() {
@Override
protected void onPreExecute() {
Toast.makeText(EditNoteActivity.this, R.string.saving_note, Toast.LENGTH_SHORT).show();
}
@Override
protected void onPostExecute(Note note) {
if (note != null) {
Toast.makeText(EditNoteActivity.this, R.string.msg_note_saved, Toast.LENGTH_SHORT).show();
finish(); // 关闭窗口
} else {
Toast.makeText(EditNoteActivity.this, R.string.msg_note_not_saved, Toast.LENGTH_SHORT).show();
}
}
@Override
protected Note doInBackground(Void... voids) {
// 1. 从编辑区获取标题和内容字符串
String title = mTitleEdit.getEditableText().toString();
String content = mContentEdit.getEditableText().toString();
// 2. 创建笔记对象
Note note = new Note(0, title, content, System.currentTimeMillis());
return noteRepository.saveNote(note);
}
};
task.execute();
}
运行看效果:
3. 阅读笔记页面
阅读页面设计思路可以参数全部笔记页面,所不同的只是根据已知的笔记id查询一条笔记。
从全部笔记页面中拷贝加载等待视图相关部分,粘贴到阅读页面布局的最底部:
同样在ReadNoteActivity类中增加对应成员变量,并在onCreate()方法中初始化:
...
private FrameLayout mLoadingView;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_read_note);
...
mLoadingView = findViewById(R.id.loading_view);
...
}
在onCreate()方法中找到原来根据id加载数据的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
Intent intent = getIntent();
long noteId = intent.getLongExtra(EXTRA_NOTE_ID, 0);
Note note = noteRepository.getNote(noteId);
if (note == null) {
finish();
return;
}
mTimeView.setText(Utils.formatTime(note.getCreateTime()));
mTitleView.setText(note.getTitle());
mContentView.setText(note.getContent());
}
将这些代码去除(注释掉,异步处理中还可以用)。
创建异步处理类LoadNoteTask并自动生成三个方法,再分别实现之:
private class LoadNoteTask extends AsyncTask {
@Override
protected void onPreExecute() {
// 异步处理开始前显示加载视图
mLoadingView.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Note note) {
// 异步处理结束后关闭加载视图
mLoadingView.setVisibility(View.GONE);
// 如果获取到的笔记对象是null,则退出阅读页面
if (note == null) {
finish();
return;
}
// 用加载得到的note填充页面内容
mTimeView.setText(Utils.formatTime(note.getCreateTime()));
mTitleView.setText(note.getTitle());
mContentView.setText(note.getContent());
}
@Override
protected Note doInBackground(Void... voids) {
// 为了方便查看效果添加延时处理,将来要去掉
try { Thread.sleep(1000); } catch (InterruptedException e) { }
Intent intent = getIntent();
long noteId = intent.getLongExtra(EXTRA_NOTE_ID, 0);
return noteRepository.getNote(noteId);
}
}
最后,在onCreate()方法最末尾创建任务对象并执行:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
LoadNoteTask task = new LoadNoteTask();
task.execute();
}
运行看效果: