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

前言

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

1、Thread + Handler

《Android Loader 机制,让你的数据加载更加轻松》 Thread + Handler

2、AsyncTask

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

3、Loader

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

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

一、Loader 简介

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

  • 可用于每个 Activity 或 Fragment

  • 支持异步加载数据

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

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

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

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

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

二、相关类和 API 介绍

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

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

类 / 接口说明
LoaderManager一种与 Activity 或 Fragment 相关联的抽象类,用于管理一个或多个 Loader 实例。这有助于应用管理与 Activity 或 Fragment 生命周期相关的、运行时间较长的操作。它常见的用法是 与 CursorLoader 一起使用,不过应用也可以自由写入自己的加载器,用于加载其他类型的数据
LoaderManager.LoaderCallbacks回调接口,用于客户端与 LoaderManager 进行交互,例如,可以使用 onCreateLoader() 回调方法创建新的加载器
Loader一种执行异步加载数据的抽象类。这是加载器的基类。我们通常会使用 CursorLoader,但也可以实现自己的子类。当加载器处于活动状态时,应监控其数据源并在内容变化时传递新结果
AsyncTaskLoader提供 AsyncTask 来执行工作的抽象加载器
CursorLoaderAsyncTaskLoader 的子类,它将查询 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 了,需要完整代码的 点击这里。觉得有帮助的话,希望能帮忙给个喜欢,欢迎关注。

猜你喜欢

    原文作者:developerHaoz
    原文地址: https://www.jianshu.com/p/337c138a56c8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞