Android Loader 机制,让你的数据加载更加轻松

4,615 阅读10分钟

前言

在 Android 中,任何耗时的操作都不能放在 UI 线程中,所以耗时的操作都需要使用异步加载来实现。其实,加载耗时数据的常用方式其实也挺多的,就让我们来看一下

1、Thread + Handler

Thread + Handler

2、AsyncTask

AsyncTask

3、Loader

Loader

前面两种异步加载方式,相信大家是比较熟悉的,但是第三种方式,可能有些人是没怎么接触过的,其实在 ContentProvider 中也可能存在耗时的操作,这时候也应该使用异步操作,而 Android 3.0 之后最推荐的异步操作就是 Loader,使用 Loader 机制能让我们高效地加载数据

一、Loader 简介


Android 3.0 中引入了 Loader 机制,让开发者能轻松在 Activity 和 Fragment 中异步加载数据,Loader 机制具有以下特征:

  • 可用于每个 Activity 或 Fragment

  • 支持异步加载数据

  • 监控数据源并在内容变化时传递新结果

  • 在某一配置更改后重建加载器时,会自动重新连接上一个加载器的游标。因此,它们无需重新查询其数据。

我们用一张图来直观地认识下 Loader 机制和另外两种做法之间的区别

从图片中可以看出 Loader 机制的写法是相当简洁的,可以让我们进行快速的开发,而且效率方面也是非常高的。

二、相关类和 API 介绍


本节内容大部分来自官方文档,详细内容可以 点击这里

在介绍 Loader 的使用之前,我们先来看一下与 Loader 机制相关的一些类和接口

类 / 接口 说明
LoaderManager 一种与 Activity 或 Fragment 相关联的抽象类,用于管理一个或多个 Loader 实例。这有助于应用管理与 Activity 或 Fragment 生命周期相关的、运行时间较长的操作。它常见的用法是 与 CursorLoader 一起使用,不过应用也可以自由写入自己的加载器,用于加载其他类型的数据
LoaderManager.LoaderCallbacks 回调接口,用于客户端与 LoaderManager 进行交互,例如,可以使用 onCreateLoader() 回调方法创建新的加载器
Loader 一种执行异步加载数据的抽象类。这是加载器的基类。我们通常会使用 CursorLoader,但也可以实现自己的子类。当加载器处于活动状态时,应监控其数据源并在内容变化时传递新结果
AsyncTaskLoader 提供 AsyncTask 来执行工作的抽象加载器
CursorLoader AsyncTaskLoader 的子类,它将查询 ContentResolver 并返回一个 Cursor。使用此加载器是从 ContentProvider 异步加载数据的最佳方式,而不用通过 Activity 或 Fragment 的 API 来执行托管查询

以上便是 Loader 机制相关的类,但并不是我们创建的每个加载器都要用到上述所有的类和接口。但是,为了初始化加载器以及实现一个 Loader 类(如 CursorLoader),我们需要引用 LoaderManager。

2.1 加载器的使用

使用加载器的应用通常包括:

  • Activity 或 Fragment

  • LoaderManager 的实例

  • 一个 CursorLoader,用于加载由 ContentProvider 支持的数据。当然我们也可以实现自己的 Loader 或 AsyncTaskLoader 子类,从其他的数据源中加载数据

  • 一个 LoaderManager.LoaderCallbacks 实现,可以使用它来创建新的加载器,并管理对现有加载器的引用

  • 显示加载器数据的方法,如 SimpleCursorAdapter

  • 使用 CursorLoader 时的数据源,如 ContentProvider

启动加载器

LoaderManager 可在 Activity 或 Fragment 内管理一个或多个 Loader 实例,每个 Activity 或 Fragment 中只有一个 LoaderManager。通过我们会在 Activity 的 onCreate() 方法或 Fragment 中的 onActivityCreate() 方法内初始化 Loader

getSupportLoaderManager().initLoader(0,null,this);

initLoader() 方法采用以下参数:

  • 用于标识加载器的唯一 ID,在代码示例中,ID 为 0

  • 在构建时提供给加载器的可选参数(在代码示例中,为 null)

  • LoaderManager.LoaderCallbacks 实现,LoaderManager 将调用该实现来报告加载器事件。在此示例中,本地类实现了 LoaderManager.LoaderCallbacks 接口,因此直接传递它对自身的引用 this

initLoader() 调用确保加载器已经初始化且处于活动状态,这可能会出现两种结果:

  • 如果指定 ID 的加载器已经存在,那么将重复使用上次创建的加载器

  • 如果指定 ID 的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 中的 onCreateLoader() 方法,在这个方法中,我们可以实现代码以实例化并返回新的加载器

无论何种情况,给定的 LoaderManager.LoaderCallbacks 实现均与加载器相关联,且在加载器状态变化时调用。如果在调用时,调用程序处于启动状态,且请求的加载器已存在并生成了数据,则系统将立即调用 onLoadFinish()

有一点要注意的是,initLoader() 方法将返回已创建的 Loader,但我们不用捕获它的引用。**LoaderManager 将自动管理加载器的生命周期。**LoaderManager 将根据需要启动和停止加载,并维护加载器的状态及其相关内容。这意味着我们将很少与加载器直接进行交互。当特定事件发生时,我们通常会使用 LoaderManager.LoaderCallbacks 方法干预加载进程。

重启加载器

当我们使用 initLoader(),它将使用含有指定 ID 的现有加载器(如有)。如果没有它会创建一个。但有时,我们想舍弃这些旧数据并重新开始。

要舍弃旧数据,我们需要使用 restartLoader(),例如,当用户的查询更改时,SearchView.OnQueryTextListener 实现将重启加载器。加载器需要重启,以便它能够使用修正后的搜索过滤器执行新查询:

public boolean onQueryTextChanged(String newText){
      mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
      getSupportLoaderManager().restartLoader(0, null, this);
      return true;
}

使用 LoaderManager 回调

LoaderManager.LoaderCallbacks 是一个支持客户端与 LoaderManager 交互的回调接口

加载器(特别是 CursorLoader)在停止运行后,仍需保留其数据,这样既可保留 Activity 或 Fragment 的 onStop() 和 onStart() 方法中的数据。当用户返回应用时,无需等待它重新加载这些数据。

LoaderManager.LoaderCallbacks 接口包括以下方法

  • onCreateLoader():针对指定的 ID 进行实例化并返回新的 Loader

  • onLoadFinished():将在先前创建的加载器完成加载时调用

  • onLoaderReset():将在先前创建的加载器重置且其数据因此不可用时调用

onCreateLoader()

当我们尝试访问加载器时(例如,通过 initLoader()),该方法将检查是否已存在由该 ID 指定的加载器。如果没有,它将触发 LoaderManager.LoaderCallbacks 中的 onCreateLoader() 方法。在此方法中,我们可以创建加载器,通过这个方法将返回 CursorLoader,但我们也可以实现自己的 Loader 子类。

在下面的示例中,onCreateLoader() 方法创建了 CursorLoader。我们必须使用它的构造方法来构建 CursorLoader。构造方法 需要对 ContentProvider 执行查询时所需的一系列完整信息

    public CursorLoader(Context context, Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        super(context);
        mObserver = new ForceLoadContentObserver();
        mUri = uri;
        mProjection = projection;
        mSelection = selection;
        mSelectionArgs = selectionArgs;
        mSortOrder = sortOrder;
    }
参数名 作用
uri 用于检索内容的 URI
projection 返回的列的列表。传递 null 时,将返回所有列,这样的话效率会很低
selection 一种用于声明返回那些行的过滤器,采用 SQL WHERE 子句格式。传递 null 时,将为指定的 URI 返回所有行
selectionArgs 我们可以在 selection 中包含 ?,它将按照在 selection 中显示的顺序替换为 selectionArgs 中的值
sortOrder 行的排序依据,采用 SQL ORDER BY 子句格式。传递 null 时,将使用默认排序顺序(可能并未排序)

示例代码:

public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    Uri baseUri;
    if (mCurFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(mCurFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
            + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
            + Contacts.DISPLAY_NAME + " != '' ))";
    return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION, select, null,
            Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}

onloadFinished

当先前创建的加载器完成加载时,将会调用此方法。该方法必须在为此加载器提供的最后一个数据释放之前调用。此时,我们应该移除所有使用的旧数据(因为它们很快就会被释放),但不要自行释放这些数据,因为这些数据归加载器所有,加载器会处理它们。

当加载器发现应用不再使用这些数据时,将会释放它们。例如,如果数据是来自 CursorLoader 的一个游标,则我们不应手动对其调用 close()。如果游标放置在 CursorAdapter 中,则应使用 swapCursor() 方法,使旧 Cursor 不会关闭

SimpleCursorAdapter mAdapter;

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    mAdapter.swapCursor(data);
}

onLoadReset

该方法将在 先前创建的加载器重置数据因此不可用 时调用,通过此回调,我们可以了解何时将释放数据,因此能够及时移除其引用。

此实现调用值为 null 的 swapCursor()

SimpleCursorAdapter mAdapter;

public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
}

三、Loader 机制的使用场景和使用方式


Loader 机制一般用于数据加载,特别是用于加载 ContentProvider 中的内容,比起 Handler + Thread 或者 AsyncTask 的实现方式,Loader 机制能让代码更加的简洁易懂,而且是 Android 3.0 之后最推荐的加载方式。

Loader 机制的 使用场景 有:

  • 展现某个 Android 手机有多少应用程序

  • 加载手机中的图片和视频资源

  • 访问用户联系人

下面用一个加载手机中的图片文件夹的例子,看看在实际开发中如何运用 Loader 机制进行高效加载。

3.1 实现自己的加载器

加载器是我们加载数据的工具,通过将对应的 URI 以及其他的查询条件传递给加载器,便可让加载器在后台高效地加载数据,等数据加载完成了便会返回一个 Cursor.

public class AlbumLoader extends CursorLoader {

    private static final Uri QUERY_URI = "content://media/external/file";

    private static final String[] PROJECTION = {
            "_id",
            "bucket_id",
            "bucket_display_name",
            "_data",
            "COUNT(*) AS " + "count"};

    private static final String SELECTION =
            "(media_type=? OR media_type =?) AND _size>0) GROUP BY (bucket_id"

    private static final String[] SELECTION_ARGS = {
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
            String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)
    };

    private static final String BUCKET_ORDER_BY = "datetaken DESC";

    private AlbumLoader(Context context, String selection, String[] selectionArgs) {
        super(context, QUERY_URI, PROJECTION, SELECTION, SELECTION_ARGS, BUCKET_ORDER_BY);
    }

    public static CursorLoader newInstance(Context context) {
        String selection = SELECTION;
        String[] selectionArgs = SELECTION_ARGS;
        return new AlbumLoader(context, selection, selectionArgs);
    }

    @Override
    public Cursor loadInBackground() {
        return super.loadInBackground();
    }
}

3.2 实现 LoaderCallbacks 进行客户端的交互

为了降低代码的耦合度,继承 LoaderManager.Loadercallbacks 实现 AlbumLoader 的管理类,将 Loader 的各种状态进行管理。

通过外部传入 Context,采用弱引用的方式防止内存泄露,获取 LoaderManager,并在 AlbumCollection 内部定义了相应的接口,将加载完成后返回的 Cursor 回调出去,让外部的 Activity 或 Fragment 进行相应的处理。

public class AlbumCollection implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final int LOADER_ID = 1;
    private WeakReference<Context> mContext;
    private LoaderManager mLoaderManager;
    private AlbumCallbacks mCallbacks;

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        Context context = mContext.get();
        return AlbumLoader.newInstance(context);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        Context context = mContext.get();
        mCallbacks.onAlbumLoad(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        Context context = mContext.get();
        mCallbacks.onAlbumReset();
    }

    public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks){
        mContext = new WeakReference<Context>(activity);
        mLoaderManager = activity.getSupportLoaderManager();
        mCallbacks = callbacks;
    }

    public void loadAlbums(){
        mLoaderManager.initLoader(LOADER_ID, null, this);
    }

    public interface AlbumCallbacks{
        void onAlbumLoad(Cursor cursor);
        void onAlbumReset();
    }
}

3.3 主界面中的逻辑

看到这代码是不是觉得特别简洁,让 MainActivity 中继承了 AlbumCollection 中的 AlbumCallback 接口,接着 onCreate() 中实例化了 AlbumCollection,然后让 AlbumCollection 开始加载数据。

等数据加载完成后,便将包含数据的 Cursor 回调在 onAlbumLoad() 方法中,我们便可以进行 UI 的更新。

可以看到采用 Loader 机制,可以让我们的 Activity 或 Fragment 中的代码变得相当的简洁、清晰,而且代码耦合程度也相当低。

public class MainActivity extends AppCompatActivity implements AlbumCollection.AlbumCallbacks{

    private AlbumCollection mCollection;
    private AlbumAdapter mAdapter;
    private RecyclerView mRvAlbum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCollection = new AlbumCollection();
        mCollection.onCreate(this, this);
        mCollection.loadAlbums();
    }

    @Override
    public void onAlbumLoad(Cursor cursor) {
        mRvAlbum = (RecyclerView) findViewById(R.id.main_rv_album);
        mRvAlbum.setLayoutManager(new LinearLayoutManager(this));
        mRvAlbum.setAdapter(new AlbumAdapter(cursor));
    }

    @Override
    public void onAlbumReset() {

    }

}

以上便是本文的全部内容,代码我已经放上 Github 了,需要完整代码的 点击这里。觉得有帮助的话,希望能帮忙点个赞,欢迎关注。


猜你喜欢