第十四章:使用 Loaders 来实现自动化地后台执行

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

Loader框架提供了一种健壮的方法来运行与内容提供程序或其他数据源的异步操作。框架可以异步加载数据,并在内容更改或添加到数据源时将其传递到应用程序。Loader框架沿着兼容性包一起被添加到Honeycomb(API级别11)中的Android平台。

您可以从Activity或Fragment连接到Loader框架。创建Loader对象时,需要请求一个管理与数据源的连接的加载程序。(Note我使用了fx2作为框架,使用fx2作为连接的对象。)

当您连接到内容提供程序时,框架包含一个名为CursorLoader的加载器,您可以挂接到该加载器。对于其他数据源,您可以编写自定义加载器。对于任何加载器类型,您必须定义三个回调:一个创建新加载器,一个在加载器交付新数据时运行,一个在加载器重置时运行-即,加载器停止传送数据。

Loader框架提供的一些功能包括:

Asynchronous data management -异步数据管理

加载器在后台对数据源做出反应,并在数据源有新数据时在应用中触发回调。

Lifecycle management -生命周期管理

当Activity或Fragment停止时,其加载器也会停止。此外,在后台运行的加载器在配置更改(例如方向更改)后继续工作。

Cached data - 缓存数据

如果异步数据加载的结果无法交付,则将其缓存,以便在接收方准备就绪时交付-例如,当Activity由于配置更改而重新创建时。

Leak protection -泄漏保护

如果活动经历配置更改,加载器框架将确保上下文对象不会因泄漏而丢失。框架只在应用程序上下文中运行,因此不会发生与线程相关的重大泄漏(参见第95页的“生命周期不匹配”)。

注意:正如我们所看到的,当Activity经历配置更改时正在运行的加载器保持活动状态,以便它们可以使用新的Activity再次运行。因为加载器是保留的,所以它们可能导致内存泄漏。

所有回调(最重要的是数据的传递)都在UI线程上报告。

因为加载器既可以使用Activity,也可以使用Fragment,所以在本章中我将使用术语客户端来指代Activity或Fragment。

本章分为两个主要部分:使用内容提供者提供的加载器和为另一个数据源创建自定义加载器。

Loader Framework -Loader框架

Loader框架是android.app-package中的一个API,它包含LoaderManager、Loader、AsyncTaskLoader和CursorLoader类。图14-1显示了它们之间的关系。这个API相当全面,但大部分内容仅在自定义加载器中需要,而在从客户端使用加载器时不需要。因此,在本节中,我将重点讨论如何从客户端使用加载器,而将框架的其他部分推迟到第233页的“实现自定义加载器”。

图 14-1 框架核心类

LoaderManager负责处理客户端中的加载器。加载器是基于Loader和AsyncTaskLoader类的具体实现。平台中唯一的具体加载器是CursorLoader,而定制加载器可以通过扩展AsyncTaskLoader来实现,并遵循Loader生命周期。

LoaderManager

LoaderManager是一个抽象类,用于管理Activity或Fragment使用的所有加载器。LoaderManager充当客户端和其加载器之间的中介。客户端拥有一个LoaderManager实例,可通过Activity或Fragment类访问:

LoaderManager getLoaderManager();

LoaderManager API主要由四个方法组成:

Loader<D> initLoader(int id, Bundle args, LoaderCallbacks<D> callback)
Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback)
Loader<D> getLoader(int id)
void destroyLoader(int id)

所有方法都包含一个标识符,该标识符表示LoaderManager应与之交互的加载器。每个加载器都应该有一个唯一的标识符。通常,应用程序只需调用initLoader或restartLoader来启动加载器。

客户端通过LoaderManager.LoaderCallbacks接口与LoaderManager交互,该接口必须由客户端实现。

下面是一个在Activity中设置回调的典型加载器的框架示例(例14-1)。

Example 14-1. Skeleton example of a typical loader setup with callbacks

public class SkeletonActivity extends Activity implements
        LoaderManager.LoaderCallbacks<D> {
    private static final int LOADER_ID = 0;
    public void onCreate(Bundle savedInstanceState) {
        getLoaderManager().initLoader(LOADER_ID, null, this);
    }
    // LoaderCallback methods
    public Loader<D> onCreateLoader(int id, Bundle args) {
        /* TODO: Create the loader. */
    }
    public void onLoadFinished(Loader<D> loader, D data) {
        /* TODO: Use the delivered data. */
    }
    public void onLoaderReset(Loader<D> loader) {
        /* TODO: The loader data is invalid, stop using it. */
    }
}

onCreateActivity调用onCreate中的加载器,它告诉框架调用代码中的第一个回调onCreateLoader()。在该回调中,客户端应返回将由平台管理的加载器实现。一旦创建了加载器,它就会启动数据加载。结果在UI线程上的onLoadFinished()中返回,以便客户端可以使用该结果使用最新数据更新UI组件。当以前创建的加载器不再可用时,将调用onLoadReset(),之后加载器处理的数据集将失效,不应再使用。

当客户端通过Activity.onStart()、Activity.onStop()等更改状态时,LoaderManager将在内部触发,这样应用程序本身就不必管理任何加载器的生命周期。例如,启动的Activity将启动数据加载并侦听内容更改。当Activity停止时,所有的加载器也会停止,这样就不会再进行数据加载或传递。

如果客户端希望保持活动状态,但不再需要数据集,则可以通过destroyLoader(id)显式销毁加载器。

initLoader vs restartLoader

LoaderManager使用initLoader()或restartLoader()调用加载器,它们具有相同的参数列表:

id -- 一个加载器标识符,对于同一客户端中的所有加载器必须是唯一的。两个不同客户端的加载器(每个客户端是一个单独的Activity或Fragment)可以使用相同的编号来标识它们的加载器,而不会相互干扰。

args -- 加载器的一组输入数据,打包在Bundle中。如果客户端没有输入数据,则此参数可以为null。参数被传递给LoaderCallbacks.on ObjectLoader()。通常,参数包含一组查询参数。

callbacks -- LoaderCallback接口的强制实现,其中包含框架要调用的回调方法。

尽管它们看起来很相似,但这两个方法调用之间存在重要差异:

--如果标识符匹配,则initLoader()重用可用的加载程序。如果不存在具有指定标识符的加载器,则onLoadLoader首先请求一个新的加载器,然后启动数据加载,并在onLoadFinished中交付结果。如果加载器标识符已经存在,则最新的数据加载结果直接在onLoadFinished中传递。initLoader()通常在创建客户端时调用,以便您可以创建新的加载器或检索现有加载器的结果。这意味着加载器在配置更改后被重用,并且不必进行新的数据加载:加载器中的缓存结果可以立即交付。

--restartLoader()不重用加载器。如果存在具有指定标识符的现有加载器,restartLoader()将销毁该加载器及其数据,然后通过调用onstartLoader创建一个新的Loader。这将启动一个新的数据加载。由于以前的加载器实例被销毁,它们的缓存数据被删除。

当底层数据源在整个客户端生命周期中相同时,应选择initLoader;例如,观察来自内容提供者的相同Cursor数据的Activity。其优点是,如果以前加载的数据可用,initLoader可以提供缓存结果,这在配置更改后很有用。基本设置如例14-1所示。

但是,如果底层数据源在客户机生命周期中可能发生变化,则应使用restartLoader。一个典型的变化是将查询更改为数据库,在这种情况下,以前加载的Cursor实例将过时,并且应该初始化可以返回新Cursor的新数据加载。

LoaderCallbacks

这些是强制性接口,用于建立或拆除LoaderManager和客户端之间的通信。该接口由三个方法组成:

public Loader<D> onCreateLoader(int id, Bundle args)
public void onLoadFinished(Loader<D> loader, D data)
public void onLoaderReset(Loader<D> loader)

接口的实现适合于要加载的内容。加载器定义为Loader,其中是与加载器返回的数据类型对应的泛型参数;例如,如果加载器是内容提供程序,则为Cursor。

回调的触发取决于加载器事件的发生,如图14-2所示。

事件的正常顺序是:

1,LoaderIntialization:

        通常,客户端在创建加载程序时初始化加载程序,以便尽快开始后台数据加载。加载器初始化是通过LoaderManager.initLoader()触发的,它为要初始化的加载器传递一个唯一的标识符。如果请求的标识符没有可用的加载器,则会调用onDataLoader-callback,以便客户端可以创建新的加载器并将其返回给LoaderManager。LoaderManager现在开始管理生命周期和从加载器加载数据。

如果客户端请求在现有的加载器标识符上初始化,则不需要创建新的加载器。相反,现有加载器将通过调用客户端的onLoadFinished回调来传递最后加载的结果。

2,Data loading:

        当数据源更新其内容或客户端准备好时,框架可以启动新数据加载。客户端本身也可以通过调用Loader.forceLoad()强制加载新数据。在任何情况下,结果都被传递给LoaderManager,后者通过调用客户端的onLoadFin ished回调来传递结果。

客户端还可以使用Loader.cancelLoad()取消已启动的加载。如果在加载开始之前发出此命令,则加载请求将被简单地取消。如果加载已经开始,则结果将被丢弃,并且不会传递到客户端。

3,Loader reset:

        当客户端被销毁或当它调用LoaderManager.destroyLoader(id)时,加载器被销毁。客户端通过上的LoaderReset(Loader)回调。此时,客户端可能希望释放之前加载的数据(如果不再需要的话)。

图 14-2  loader 回调时序图 

AsyncTaskLoader

        加载器异步执行环境由AsyncTaskLoader提供,它扩展了Loader类。该类包含一个AsyncTask来处理后台加载,并依赖AsyncTask.executeOnExecutor()方法来进行后台执行。因此,它不会受到不同版本Android上行为之间的差异的影响(在第167页的“后台任务执行”中进行了描述)。

注意:兼容包中的AsyncTaskLoader不依赖于平台中的公共AsyncTask,因为它可能会按顺序执行。相反,兼容性包与内部ModernAsyncTask实现捆绑在一起,该实现同时处理数据加载。

AsyncTaskLoader尝试保持同时任务的数量,即,活动线程-最小化。例如,使用force Load()强制连续加载的客户端可能会惊讶地发现,并非每个调用都将提供结果。原因是AsyncTaskLoader在启动新的加载之前会取消以前的加载。在实践中,这意味着在前面的加载完成之前重复调用forceLoad()将推迟结果的交付,直到最后一次调用的加载完成。

加载也可以由内容更改触发,如果底层数据集触发许多更改通知,例如,内容提供程序中的许多插入-UI线程可能在UI线程上接收许多onLoadFinished调用,其中UI组件被更新和重绘。因此,UI线程可能由于许多更新而失去响应性。如果您认为这是一个问题,您可以限制AsyncTaskLoader的数据传递,以便仅在特定延迟后才进行连续的数据加载。通过setUpdateThrottle(long delayMs)设置节流延迟。

注意:AsyncTaskLoader不受AsyncTask执行差异的影响,如第170页的“跨平台版本执行”中所述,尽管它可通过API级别4的支持包提供。该支持包实现了自己的ModernAsyncTask,它可以在API级别之间保持执行一致。

Painless Data Loading with CursorLoader -使用CursorLoader轻松加载数据

从加载器加载数据是由抽象Loader类处理的,该类应该是子类并连接到数据源。开箱即用,框架目前只支持ContentProvider数据源,使用CursorLoader。CursorLoader的伟大之处在于它所做的设计选择:它并不试图提供一种可以适应许多不同情况的通用异步技术。相反,它是一种特殊用途的功能,专注于从内容提供者加载数据,并仅简化该用例。

注意:CursorLoader只能用于从内容提供程序交付的Cursor对象,而不能用于来自SQLite数据库的Cursor对象。

Using the CursorLoader

CursorLoader是抽象AsyncTaskLoader类的扩展,实现异步执行(“AsyncTaskLoader”on page 225)。CursorLoader监视可从内容提供程序查询的Cursor对象。换句话说,它是一个Cursor数据类型的加载器,并通过Loader等调用传递给LoaderManager。CursorLoader在Cursor上注册一个ContentObserver来检测数据集中的变化。

内容提供者通过标识要查询的数据集的URI来标识。要监视的Cursor是使用通常用于提供程序的查询参数定义的,并且都可以在构造函数中定义:

CursorLoader(Context context, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)

Cursor生命周期由CursorLoader管理:它取代了引入CursorLoader后Activity类中不推荐使用的managedQuery和startManagingCursor方法。因此,客户端不应干扰此内部生命周期管理,并尝试自行关闭Cursor。

Example: Contact list

在我们深入了解框架的细节之前,让我们先看一个示例,它展示了Loader可以提供的强大功能和简单性。

下面的示例使用平台中可用的具体CursorLoader加载程序实现类列出联系人提供程序中的联系人。其概念是使用Activity或Fragment设置CursorLoader,并在LoaderCallback中实现方法:

public class ContactActivity extends ListActivity implements
        LoaderManager.LoaderCallbacks<Cursor>{
    private static final int CONTACT_NAME_LOADER_ID = 0;
    // Projection that defines just the contact display name
    static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {  //1
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME
    };
    SimpleCursorAdapter mAdapter;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initAdapter();
        getLoaderManager().initLoader(CONTACT_NAME_LOADER_ID, null, this);  //2
    }
    private void initAdapter() {
        mAdapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_1, null,
                new String[] { ContactsContract.Contacts.DISPLAY_NAME },
                new int[] { android.R.id.text1}, 0);
        setListAdapter(mAdapter);
    }
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) { //3
        return new CursorLoader(this, ContactsContract.Contacts.CONTENT_URI,
                CONTACTS_SUMMARY_PROJECTION, null, null,
                ContactsContract.Contacts.DISPLAY_NAME + " ASC");
    }
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {   //4
        ou mAdapter.swapCursor(c);
    }
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {  //5
        mAdapter.swapCursor(null);
    }
}

1,加载程序只查询联系人的显示名称。

2,使用LoaderManager初始化一个加载器,然后回调到

3,第一次回调。Activity创建一个加载器并将其移交给平台。

4,当数据在后台线程上加载完毕时,数据将在UI线程上提供。

5,当加载器重置时,将调用最后一个回调,Activity将释放对加载器数据的引用。

注意:CursorLoader在加载新数据集后关闭旧Cursor。因此,在onLoadFinished中使用swapCursor,而不是changeCursor排序的替代方案,因为这也关闭了旧的游标。

Adding CRUD Support

加载器旨在读取数据,但对于内容提供程序,通常还需要创建、更新和删除数据,而CursorLoader不支持这些操作。尽管如此,内容观察和自动后台加载也为完整的CRUD解决方案带来了简单性。但是,您确实需要一个非同步机制来处理对提供程序的写入,例如AsyncQueryQuery,如下面的示例所示。

示例:将CursorLoader与AsyncQueryObject配合使用

在本例中,我们为存储在内容提供程序中的Chrome浏览器书签创建一个基本管理器。该示例包含一个显示已存储书签列表的Activity和一个打开Fragment的按钮,在Fragment中可以添加新书签。

如果用户长时间单击某个项目,则该项目将直接从列表中删除。

因此,书签管理器调用三个应该异步处理的提供程序操作:

List bookmarks

使用CursorLoader查询提供者,这样我们就可以利用内容观察和自动数据加载的特性。

Add or delete a bookmark

使用AsyncQueryTab从片段中插入新书签,并在长时间单击列表项时删除书签。

大部分示例执行许多Android应用程序常见的显示和光标处理活动。评论将集中在使用游标加载器的特殊之处。在示例中,书签列表显示在ChromeBookmarkActivity中:

public class ChromeBookmarkActivity extends Activity implements
        LoaderManager.LoaderCallbacks<Cursor> {
    // Definition of bookmark access information.
    public interface ChromeBookmark {
        final static int ID = 1;
        final static Uri URI= Uri.parse(
                "content://com.android.chrome.browser/bookmarks");  //1
        final static String[] PROJECTION = {
                Browser.BookmarkColumns._ID,
                Browser.BookmarkColumns.TITLE,
                Browser.BookmarkColumns.URL
        };
    }
    // AsyncQueryHandler with convenience methods for
    // insertion and deletion of bookmarks.
    public static class ChromeBookmarkAsyncHandler extends AsyncQueryHandler {
        public ChromeBookmarkAsyncHandler(ContentResolver cr) {
            super(cr);
        }
        public void insert(String name, String url) {
            ContentValues cv = new ContentValues();
            cv.put(Browser.BookmarkColumns.BOOKMARK, 1);
            cv.put(Browser.BookmarkColumns.TITLE, name);
            cv.put(Browser.BookmarkColumns.URL, url);
            startInsert(0, null, ChromeBookmark.URI, cv);
        }
        public void delete(String name) {
            String where = Browser.BookmarkColumns.TITLE + "=?";
            String[] args = new String[] { name };
            startDelete(0, null, ChromeBookmark.URI, where, args);
        }
    }
    ListView mListBookmarks;
    SimpleCursorAdapter mAdapter;
    ChromeBookmarkAsyncHandler mChromeBookmarkAsyncHandler;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bookmarks);
        mListBookmarks = (ListView) findViewById(R.id.list_bookmarks);
        mChromeBookmarkAsyncHandler =
                new ChromeBookmarkAsyncHandler(getContentResolver());
        initAdapter();
        getLoaderManager().initLoader(ChromeBookmark.ID, null, this);
    }
    private void initAdapter() {
        mAdapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_1, null,
                new String[] { Browser.BookmarkColumns.TITLE },
                new int[] { android.R.id.text1}, 0);
        mListBookmarks.setAdapter(mAdapter);
        mListBookmarks.setOnItemLongClickListener(
                new AdapterView.OnItemLongClickListener() {
                    @Override
                    public boolean onItemLongClick(AdapterView<?> adapterView, View view,
                                                   int pos, long id) {
                        Cursor c =
                                ((SimpleCursorAdapter) adapterView.getAdapter()).getCursor();
                        c.moveToPosition(pos);
                        int i = c.getColumnIndex(Browser.BookmarkColumns.TITLE);
                        mChromeBookmarkAsyncHandler.delete(c.getString(i)); //2
                        return true;
                    }
                });
    }
    @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        return new CursorLoader(this, ChromeBookmark.URI,
                ChromeBookmark.PROJECTION, null, null,
                Browser.BookmarkColumns.TITLE + " ASC");    //3
    }
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor newCursor) {
        mAdapter.swapCursor(newCursor);
    }
    @Override
    public void onLoaderReset(Loader loader) {
        mAdapter.swapCursor(null);
    }
    public void onAddBookmark(View v) {
        FragmentTransaction ft = getFragmentManager().beginTransaction();
        Fragment prev = getFragmentManager().findFragmentByTag("dialog");
        if (prev != null) {
            ft.remove(prev);
        }
        ft.addToBackStack(null);
        // Create and show the dialog.
        DialogFragment newFragment = EditBookmarkDialog.
                newInstance(mChromeBookmarkAsyncHandler);
        newFragment.show(ft, "dialog");
    }
}

1,Chrome浏览器书签的提供程序URI。

2,异步删除书签。

3,使用CursorLoader进行异步数据检索。

新书签通过EditBookmarkDialog添加,该对话框包含一个按钮和两个输入字段:一个用于书签名称,一个用于书签URL。按下按钮时,书签名称和URL将插入提供程序中,对话框将关闭:

public class EditBookmarkDialog extends DialogFragment {
    static EditBookmarkDialog newInstance(
            ChromeBookmarkActivity.ChromeBookmarkAsyncHandler asyncQueryHandler) {
        EditBookmarkDialog dialog = new EditBookmarkDialog(asyncQueryHandler);
        return dialog;
    }
    ChromeBookmarkActivity.ChromeBookmarkAsyncHandler mAsyncQueryHandler;
    public EditBookmarkDialog(
            mChromeBookmarkActivity.ChromeBookmarkAsyncHandler asyncQueryHandler) {
        mAsyncQueryHandler = asyncQueryHandler;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.dialog_edit_bookmark, container,
                false);
        final EditText editName = (EditText) v.findViewById(R.id.edit_name);
        final EditText editUrl = (EditText) v.findViewById(R.id.edit_url);
        Button buttonSave = (Button) v.findViewById(R.id.button_save);
        buttonSave.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                String name = editName.getText().toString();
                String url = editUrl.getText().toString();
                mAsyncQueryHandler.insert(name, url);   //1
                dismiss();
            }
        });
        return v;
    }
}

1,在提供程序中异步插入书签。

一旦通过ChromeBookmarkAsyncUNK插入或删除书签,内容就会发生变化,CursorLoader会自动重新查询光标。因此,列表中的插入和删除会自动更新。

Implementing Custom Loaders -实现自定义Loaders

加载器最常与内容提供程序一起使用,因为它们已经由平台支持,但其他数据源可以使用自定义加载器进行处理。当然,这需要更多的工作和对框架的更深入的了解。应该实现自定义加载器,以便从客户端的角度来看,它们的行为符合预期。一个完全成熟的加载器应该支持一系列特性:

• Loader lifecycle
• Background loading
• Content management
• Deliver cached result

注意:典型的Activity和Fragment-自定义加载器必须被定义为静态类或外部类。如果不这样做,则当加载器从onToolLoader返回时会引发RuntimeEx ception。

Loader Lifecycle

加载器的基类是Loader。它保存加载器的状态,该状态定义是否应将数据传递到客户端。加载器包含一组状态转换方法,这些方法调用自定义加载器可能实现的方法:

void startLoading() -> void onStartLoading()
void stopLoading() -> void onStopLoading()
void reset() -> void onReset()
void abandon() -> void onAbandon()

加载器仅在启动状态下传递数据-即,调用Loader.startLoading()后,将更改状态并启动数据加载。

状态之间的转换以及这些方法的作用如图14-3所示。这些状态是:

reset:--加载程序的初始和最终状态,其中它已释放任何缓存的数据。

started:--启动异步数据加载并通过LoaderCallback. onLoadshed的回调调用传递结果。

stopped:--加载程序停止向客户端传递数据。它仍然可以在内容更改时在后台加载数据,但数据被缓存在加载器中,以便可以轻松检索最新数据,而无需启动新的数据加载。

abandoned:--复位前的中间状态,在此状态下,数据一直存储到新的加载程序连接到数据源。这很少使用; LoaderManager在重新启动时放弃加载器,以便在重新启动过程中数据可用。

图 14-3 Loader 状态转换

加载器的生命周期由LoaderManager控制,客户端通常不应该直接使用Loader方法修改状态。相反,客户端应该发出initLoader和restartLoader,以确保有一个已启动的加载器,并让LoaderManager与加载器进行交互。

可以通过强制更新来显式启动数据加载:

void forceLoad ()

forceLoad()与startLoading()的不同之处在于,它只强制新的数据加载,但不更改加载器的状态。Forceload应该只在启动状态下调用;否则,结果将不会传递给客户端。

Background Loading

数据应该在后台线程上异步加载,并且由加载器选择执行环境。通常,选择并不困难;平台提供了AsyncTaskLoader,它可以由自定义加载器扩展,以方便从UI线程卸载。AsyncTaskLoader的实现只能覆盖一个方法:

public D loadInBackground() {
    ...
}

loadInBackground在后台线程上调用,应该执行加载器的长任务并从加载返回内容。返回类型是泛型的,所以D必须被替换为底层内容的数据类型,回调应该返回该数据类型的数据。

Example: Simple custom loader

以下是一个基本加载器,它扩展AsyncTaskLoader以从虚拟数据源加载整数值。它模拟了一个很长的装载时间,以反映真实的装载机的环境:

public class BasicLoader extends AsyncTaskLoader<Integer>{
    public BasicLoader(Context context) {
        super(context);
    }
    @Override
    protected void onStartLoading() {
        super.onStartLoading();
        forceLoad();    //1
    }
    @Override
    public Integer loadInBackground() {
        return loadDummyData();     //2
    }
    private int loadDummyData() {
        SystemClock.sleep(1000);
        Random rand = new Random();
        return rand.nextInt(50);
    }
}

1,当客户端调用startLoading()时,加载器将状态更改为started并调用onStartLoading(),其中自定义加载器应该触发新的加载-即,调用forceLoad()。

2,在后台线程上加载长时间运行的任务并返回结果。

BasicActivity是一个客户端,它使用BasicLoader加载整数值并显示它们:

public class BasicActivity extends Activity implements
        LoaderManager.LoaderCallbacks<Integer>{
    private static final int BASIC_LOADER_ID = 0;
    TextView tvResult;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basic);
        tvResult = (TextView) findViewById(R.id.text_result);
        getLoaderManager().initLoader(BASIC_LOADER_ID, null, this);
    }
    @Override
    public Loader<Integer> onCreateLoader(int id, Bundle args) {
        return new BasicLoader(this);
    }
    @Override
    public void onLoadFinished(Loader<Integer> loader, Integer data) {
        tvResult.setText(Integer.toString(data));
    }
    @Override
    public void onLoaderReset(Loader<Integer> loader) {
        // Empty, the integer value is shown in a TextView
        // and should not be removed.
    }
}

BasicLoader在后台线程上执行长任务,并将附加到客户端生命周期,但除此之外,它缺乏加载器所期望的大部分好特性。例如,没有数据缓存,因此加载器将在每次重新创建客户端时重新加载相同的数据,而不是返回缓存的值。

Content Management

当底层数据集发生变化时,加载器应该自动启动新的后台数据加载。因此,底层数据集必须是可观察的,就像CursorLoader利用ContentObserver来获得有关内容提供程序中更新的通知一样。Observer机制依赖于底层数据集,但典型的机制有:

Observable and Observer

        Java对象的内存数据模型可以通过将模型实现为Observable来监视,该模型将更改报告给Observer类。这两个类都位于java.util-package中。

Broadcasted intent to a BroadcastReceiver

        一个独立于内容的内容通知程序,可以在应用程序中本地使用,也可以跨进程边界使用。

FileObserver

        使用android.os.FileObserver观察文件系统更改,该文件系统监视文件系统中的路径,并在发生更改时发送事件。报告的事件可以配置事件过滤器,该过滤器可以限制观察结果,例如添加、删除和移动。FileObserver的用法见第238页的“示例:自定义文件加载器”。

        当观察者收到更新通知时,由加载器异步加载新数据,这应该通过forceLoad或onContentChanged来完成。forceLoad触发独立于加载器状态的后台执行,而onContentChanged仅在状态启动时才启动数据加载。否则,它会将内容标记为已更改,这样当加载器重新启动时,它就可以检查是否有应加载并传递给客户端的内容更改。自定义加载器应该在启动加载器时使用takeContentChanged()检查是否有要加载的内容:

@Override
protected void onStartLoading() {
    super.onStartLoading();
    // Note: There are other interesting things to
    // implement here as well.
    if (takeContentChanged()) {
        forceLoad();
    }
}

注意:一旦takeContentChanged()被调用,内容就不再被标记为已更改,并且连续调用不会返回true,直到使用onContentChanged报告了新的内容更改。

从加载程序启动到重置,内容观察都应处于活动状态,以便即使在停止状态下也可以继续进行后台加载,并从该高速缓存传递新数据,如下一节所述。

Delivering Cached Results

当您子类化AsyncTaskLoader时,它会在从loadInBackground()返回新数据后提供结果。但是触发一个新的背景任务,在没有新数据时调用loadInBackground-is浪费资源。相反,您应该实现加载器,以便它可以加快向客户端交付结果的速度。例如,如果加载器已经交付了一个结果,并且没有报告任何内容更改,则没有必要启动新的后台任务。直接返回前一个结果会更快。因此,加载器应该缓存加载的数据-即,保留上次成功加载的结果。交付控制是通过结果缓存实现的。您还可以重写Loader.deliverResult(),如果启动加载程序,则调用该方法时,它将向客户端传递数据,如下面的示例所示。

Example: Custom File Loader

文件系统应该异步访问,加载器框架可以用于在发生任何更改时加载新数据。在本例中,我们创建了一个File Loader,它提供应用程序目录中的文件名。它会观察目录的更改,如果添加或删除文件,将启动异步加载,并将文件名列表传递到UI线程上的客户端。

FileLoader使用AsyncTaskLoader作为后台执行器,它被配置为处理文件名的数据源,如List:

public class FileLoader extends AsyncTaskLoader<List<String>> {
    // Cache the list of file names.
    private List<String> mFileNames;
    private class SdCardObserver extends FileObserver { //1
        public SdCardObserver(String path) {
            super(path, FileObserver.CREATE|FileObserver.DELETE);
        }
        @Override
        public void onEvent(int event, String path) {
            // This call will force a new asynchronous data load
            // if the loader is started otherwise it will keep
            // a reference that the data has changed for future loads.
            onContentChanged();
        }
    }
    private SdCardObserver mSdCardObserver;
    public FileLoader(Context context) {
        super(context);
        String path = context.getFilesDir().getPath();
        mSdCardObserver = new SdCardObserver(path);
    }
    @Override
    protected void onStartLoading() {   //2
        super.onStartLoading();
        // Start observing the content.
        mSdCardObserver.startWatching();
        if (mFileNames != null) {   //3
            // Return the cache
            deliverResult(mFileNames);
        }
        if (fileNames == null || takeContentChanged()) { //4
            forceLoad();
        }
    }
    @Override
    public List<String> loadInBackground() {
        File directory = getContext().getFilesDir();
        return Arrays.asList(directory.list());
    }
    @Override
    public void deliverResult(List<String> data) {
        if (isReset()) {
            return;
        }
        // Cache the data
        mFileNames = data;
        // Only deliver result if the loader is started.
        if (isStarted()) {
            super.deliverResult(data);
        }
    }
    @Override
    protected void onStopLoading() {
        super.onStopLoading();
        cancelLoad();   //5
    }
    @Override
    protected void onReset() {
        super.onReset();
        mSdCardObserver.stopWatching();     //6
        clearResources();   //7
    }
    private void clearResources() {
        mFileNames = null;
    }
}

1,为文件的添加和删除定义一个文件系统观察器。FileLoader的构造函数将其配置为观察使用getContext().getFilesDir()检索的应用程序文件目录。当检测到更改时,将调用onEvent方法。文件观察由Android特定的android.os.FileObserver类处理。

2,FileLoader被告知开始加载数据,通常是在Activity或Fragment启动并准备好显示数据时。此时,加载器被启动,并且FileLoader被期望观察底层数据集-即,文件系统-因此调用startWatching。

3,如果以前交付的数据集被缓存在加载器中,我们将其交付给客户端,这样我们就不需要进行另一次异步加载。

4,如果没有以前的数据,或者如果内容已标记为以前更改但未交付,则强制加载数据。

5,尝试取消正在进行的加载,因为无论如何都不会交付结果。

6,当加载器被重置时停止内容观察,因为不应该加载或缓存内容更改。

7,当加载程序被重置时,预计将不会再使用它,因此删除对该高速缓存的引用。

FileLoader用于填充显示在Fragment中的文件名列表:

public class FileListFragment extends ListFragment implements
        LoaderManager.LoaderCallbacks<List<String>>{
    private static final int FILE_LOADER_ID = 1;
    private ArrayAdapter<String> mFileAdapter;
    private List<String> mFileNames = new ArrayList<String>();
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(FILE_LOADER_ID, null, this);
        setEmptyText("No files in directory");
        setListShown(false);
        mFileAdapter = new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_1, android.R.id.text1, mFileNames);
        mFileAdapter.setNotifyOnChange(true);
        setListAdapter(mFileAdapter);
    }
    @Override
    public Loader<List<String>> onCreateLoader(int i, Bundle bundle) {
        return new FileLoader(getActivity());
    }
    @Override
    public void onLoadFinished(Loader<List<String>> fileLoader,
                               List<String> fileNames) {
        mFileAdapter.clear();
        mFileAdapter.addAll(fileNames); //1
        setListShown(true);
    }
    @Override
    public void onLoaderReset(Loader<List<String>> fileLoader) {
        mFileNames = null;
        mFileAdapter.clear();
    }
}

1,将加载的文件名添加到列表中并更新UI。

Handling Multiple Loaders

最常见的情况是,一个LoaderManager只管理一个加载器,在这种情况下,回调是从一个已知的加载器调用的:只有一个加载器存在。如果您创建多个加载器,回调应该检查标识符-即,invoke Loader.getId()-验证哪个加载器生成了回调。作为多个加载器模板的代码框架是:

public class SkeletonActivity extends Activity implements
        LoaderManager.LoaderCallbacks<D> {
    private static final int LOADER_ID_ONE = 1;
    private static final int LOADER_ID_TWO = 2;
    public void onCreate(Bundle savedInstanceState) {
        getLoaderManager().initLoader(LOADER_ID_ONE, null, this);
        getLoaderManager().initLoader(LOADER_ID_TWO, null, this);
    }
    // LoaderCallback methods
    public Loader<D> onCreateLoader(int id, Bundle args) {
        switch(id) {
            case LOADER_ID_ONE:
                /* TODO: Create the loader. */
                return ...;
            case LOADER_ID_TWO:
                /* TODO: Create the loader. */
                return ...;
        }
    }
    public void onLoadFinished(Loader<D> loader, D data) {
        switch(loader.getId()) {
            case LOADER_ID_ONE:
                /* TODO: Use the delivered data. */
                break;
            case LOADER_ID_TWO:
                /* TODO: Use the delivered data. */
                break;
        }
    }
    public void onLoaderReset(Loader<D> loader) {
        switch(loader.getId()) {
            case LOADER_ID_ONE:
                /* TODO: The loader data is invalid, stop using it. */
                break;
            case LOADER_ID_TWO:
                /* TODO: The loader data is invalid, stop using it. */
                break;
        }
    }
}

总结:

Loader框架是添加到Android平台的最新异步技术。它是一个异步执行的框架,当涉及到CursorLoader时,它会大放异彩,因为它封装了特定用例的困难-即,内容提供商,并有效地解决它。该框架还通过允许自定义加载器实现提供了灵活性,但这需要应用程序付出更多的努力,考虑其他异步技术可能会更好。

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值