Loader的初步学习笔记

时间:2021-09-19 00:19:45

Loader的初步学习笔记

Loader是一个异步加载数据的类,它和AsyncTask有类似也有不同,今天我们就先来学习下它。由于是对比学习,所以我们先来复习下AsyncTask的使用和特点。

 

一、AsyncTask

参考自:http://www.open-open.com/lib/view/open1417955629527.html

Loader的初步学习笔记
package com.example.jacktony.myapplication03;

import android.os.AsyncTask;

/**
* Created by Jack Tony on 2014/12/15.
*/
public class Task extends AsyncTask<String, Integer, Long>{

@Override
protected void onPreExecute() {
super.onPreExecute();
}

@Override
protected Long doInBackground(String... params) {
return null;
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}

@Override
protected void onPostExecute(Long aLong) {
super.onPostExecute(aLong);
}

@Override
protected void onCancelled(Long aLong) {
super.onCancelled(aLong);
}
}
Loader的初步学习笔记

 

1.1 它什么时候会被结束

关于AsyncTask存在一个这样广泛的误解,很多人认为一个在Activity中的AsyncTask会随着Activity的销毁而销毁。然后事实并非如此。AsyncTask会一直执行doInBackground()方法直到方法执行结束。一旦上述方法结束,会依据情况进行不同的操作。

  • 如果cancel(boolean)被调用了,则执行onCancelled(Result)方法
  • 如果cancel(boolean)没有被调用,则执行onPostExecute(Result)方法

AsyncTask的cancel方法需要一个布尔值的参数,参数名为mayInterruptIfRunning,意思是如果正在执行是否可以打断, 如果这个值设置为true,表示这个任务可以被打断,否则,正在执行的程序会继续执行直到完成。如果在doInBackground()方法中有一个循环操作,我们应该在循环中使用isCancelled()来判断,如果返回为true,我们应该避免执行后续无用的循环操作。

总之,我们使用AsyncTask需要确保AsyncTask正确地取消。

 

1.2 奇怪的cancel()

如果你调用了AsyncTask的cancel(false),doInBackground()仍然会执行到方法结束,只是不会去调用onPostExecute()方法。但是实际上这是让应用程序执行了没有意义的操作。那么是不是我们调用cancel(true)前面的问题就能解决呢?并非如此。如果mayInterruptIfRunning设置为true,会使任务尽早结束,但是如果的doInBackground()有不可打断的方法,则它就会失效,比如这个BitmapFactory.decodeStream() IO操作。但是你可以提前关闭IO流并捕获这样操作抛出的异常。但是这样会使得cancel()方法没有任何意义。

 

1.3 内存泄露

还有一种常见的情况就是,在Activity中使用非静态匿名内部AsyncTask类,由于Java内部类的特点,AsyncTask内部类会持有外部类的隐式引用。详细请参考细话Java:”失效”的private修饰符,由于AsyncTask的生命周期可能比Activity的长,当Activity进行销毁AsyncTask还在执行时,由于AsyncTask持有Activity的引用,导致Activity对象无法回收,进而产生内存泄露。所以很不建议用内部类来做异步处理。

1.4 结果丢失

另一个问题就是在屏幕旋转等造成Activity重新创建时AsyncTask数据丢失的问题。当Activity销毁并创新创建后,还在运行的 AsyncTask会持有一个Activity的非法引用即之前的Activity实例。导致onPostExecute()没有任何作用。

 

1.5 串行 or 并行

  • 在1.6之前,所有的AsyncTask在一个单独的线程中有序的执行。
  • 从1.6到2.3,这些AsyncTask在一个线程池中执行,但是有上限。
  • 从3.0开始,又使用最早的方案!他们在一个单独的线程中有序的执行,除非你调用executeOnExecutor,并且传入一个ThreadPoolExecutor。

Android团队从3.0开始认为,开发者可能并不喜欢让AsyncTask并行,于是Android团队又把AsyncTask改成了串行。当然这一次的修改并没有完全禁止 AsyncTask并行。你可以通过设置executeOnExecutor(Executor)来实现多个AsyncTask并行。关于API文档的描述如下

If we want to make sure we have control over the execution, whether it will run serially or parallel, we can check at runtime with this code to make sure it runs parallel:

Loader的初步学习笔记
public static void execute(AsyncTask as) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR1) {
as.execute();
}
else {
as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
//(This code does not work for API lvl 1 to 3)
Loader的初步学习笔记

 

1.6 真的需要AsyncTask么

并非如此,使用AsyncTask虽然可以以简短的代码实现异步操作,但是正如本文提到的,你需要让AsyncTask正常工作的话,需要注意很多条条框框。推荐的一种进行异步操作的技术就是使用Loaders。下面就来谈谈这个从3.0引入的类,support包中也对低版本添加了支持。

 

二、Loader的特点

Loaders使得在一个Activity或fragment中异步加载数据变得容易。loaders有这些特性:

  1. 提供异步加载数据的功能。
  2. 监视它们的数据资源,并在这些资源发生变化时发送新的结果。
  3. 当配置信息发生改变后重新被创建时,它会重连到上一次装载机的指示位置。而且,它们不需要重新查询数据。
  4. 允许一个Activity或者Fragment连接到Activity或者Loader被重新创建之前的Loader,并且重新取出里面的result
  5. 如果result在Loader从Activity/Fragmentdisconnected之后才得到,那么它会被保存在cache中,并且在Activity/Fragemtneon被重新创建之后传送回来。
  6. Loader可以监控他得数据源,在数据源数据变化后将新的数据deliver(传递)出来。
  7. Loader处理了result相关的资源的allocation/disallocation(比如Cursors)。

如果你想在一个Activity或者Fragment里面进行异步数据的加载,不要再使用AsyncTask了。也不要认为Loader只是用来做数据库相关的事情(CursorLoaders),他可以做很多事情。

 

三、Loader的管理者——LoaderManager

  Loader是由一个loaderManager来管理的,每一个Activity或Fragment都只有一个LoaderManager,但是一个LoaderManager可以包含多个加载器。

3.1 建立loaderManager

在activity或者fragment中用getSupportLoaderManager()或getLoaderManager()得到这个对象就行了。

getSupportLoaderManager()
getLoaderManager()

 

3.2 初始化loader的回调函数

getSupportLoaderManager().initLoader(0, null, new MyLoaderCallback());

 

通过initLoader就可以初始化一个loader了,这个初始化很不彻底,其实就是通过它能触发回调方法中的onCreateLoader()方法,而onCreateLoader()方法会返回一个loader,所以这里的初始化并没有真正new一个对象,而是调用了onCreateLoader()来初始化,所以你需要在回调函数中的onCreateLoader()进行处理。因为我们现在是学习loaderManager,还没涉及到loader,所以在onCreatLoader中我们没建立一个loader,返回一个null对象。这也侧面证明了initloader()并没有真正初始化一个真正的loader,真正的初始化是在onCreatLoader()中进行的。

Loader的初步学习笔记
    private class MyLoaderCallback implements LoaderManager.LoaderCallbacks {
final private String TAG = getClass().getSimpleName();

/**
*
@param id
*
@param args
*
@return Object returned from onCreateLoader must not be a non-static inner member class
*/
@Override
public Loader onCreateLoader(int id, Bundle args) {
Log.i(TAG,
"onCreateLoader");
//
return new MyAsyncLoader(getApplicationContext());
       return null; }

@Override
public void onLoadFinished(Loader loader, Object data) {
Log.i(TAG,
"onLoadFinished");
}

@Override
public void onLoaderReset(Loader loader) {
Log.i(TAG,
"onLoaderReset");
}
}
Loader的初步学习笔记

 

3.3 参数

  • 标识加载器的唯一Id,在这个例子中Id是0。在Activity或者Fragment中创建一个私有的常量ID就可以了。
  • 提供给加载器的可选参数(Bundle),在这个例子中是null。
  • 一个LoaderManager.loaderCallBacks实现,这里是MyLoaderCallback()

1.第一个参数需要在当前Activity范围内找一个唯一的id进行传入,方便控制loader。

2.第二个参数可以传入bundle,很多时候你不需要传入任何参数,置null即可。

         Bundle args = new Bundle();
args.putString(
"query",query);
getLoaderManager().restartLoader(LOADER_ID,args, loaderCallbacks);

在Callback中得到这个bundle

Loader的初步学习笔记
    @Override
public Loader onCreateLoader(int id, Bundle args) {
Log.i(TAG,
"onCreateLoader");
return new MyAsyncLoader(getApplicationContext());
}
Loader的初步学习笔记

 

3.4 结果

initLoader()的调用确保加载器被初始化和激活。它有两个可能的结果:

① 如果这个加载器指定的id已经存在,上一次被创建的加载器就被重用。

② 如果这个加载器指定的id不存在,initLoader()方法触发LoaderManager.LoaderCallbacks 方法onCreateLoader()。这是你实现实例化和返回一个新的加载器代码的地方。

无论在任何情况下,提供的LoaderManager.LoaderCallBacks实现都和加载器有关,而且将会在加载器状态改变时被调用。如果调用调用者是在它的开始状态,而且请求的加载器已经存在还产生了数据,接着系统就会直接调用onLoadFinished()方法(在initLoader()期间),所以你必须准备好这种情况的发生。也就是说,这个加载器已经被初始化并且启动了,那么再initLoader的时候就会直接调用onLoadFinished()方法。(这中文翻译的好蛋疼T_T)

 

3.5 初始化Loader后怎么要用它么?

initLoader()方法返回的是已经被创建的加载器(前提是你在callback中确实初始化了一个loader),但是你不需要获取对它的引用。LoaderManager自动管理加载器的生命。Loadermanager在必要的时候启动和停止加载,而且保持加载器的状态和它关联的内容。这意味着,你很少直接与加载器交互。当一个特别的事件发生时你通常使用LoaderManager.LoaderCallbacks的方法进行干预。总之,你现在进行的仅仅是一个初始化的动作,而不需要对这个初始化的loader做任何处理。

 

3.6 复用loader和重新初始化loader

当你像上面展示的那样使用initLoader()时,如果对于指定的id的加载器已经存在了一个那将使用时这个存在的。如果没有,将创建一个。但是有时你希望抛弃原来的老数据重新开始。

为了清除你的老数据,你需要使用restartLoader()方法。

getLoaderManager().restartLoader(0, null, this);

 

3.7 LoaderManager的一个Bug

当一个Fragment因为configuration变化被重新创建的时候(比如旋转屏幕,改变语言等),你在onActivityCreated()里面调用了initLoader()之后,他的LoaderManager调用了两次onLoadFinished。

解决方案:

1. 如果代码逻辑允许,可以不用处理。

2. 将之前的result保存起来,检查结果是否有变化。

3. 在onCreate()里面调用setRetainInstance(true)。

 

3.8 一次性的loader

有时候我们仅仅希望laoder启动一次,比如:我们点击一个按钮然后提交一些信息到服务器。在用户点击了一个按钮后,我们调用initLoader,这个时候用户旋转屏幕,在旋转完屏幕之后,我们想要使用之前loader得到的结果。

注意:在旋转屏幕的时候,我们的Loader还没有提交完数据。像我们之前用AsyncTask的话,是没有办法在Activity/Fragment重新创建之后拿到之前任务返回的result的。但是使用Loader就简单多了。

解决方案:

在loaderManager中删除掉这个id的loader,因为loaderManager可能管理多个loader,所以要用id来判断

Loader的初步学习笔记
      @Override
public void onLoadFinished(Loader loader, Object data) {
Log.i(TAG,
"onLoadFinished");
getLoaderManager().destroyLoader(LOADER_ID);
}
Loader的初步学习笔记

 

在Activity / Fragment创建时进行逻辑判断,如果当前这个id的loader真正运行,那么就重新初始化一下,如果没有这个id的loader,那么就不做任何处理(没实际测试过效果)。

这里用fragment举例:

Loader的初步学习笔记
    @Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
//Reconnect to the loader only if present
if(getLoaderManager().getLoader(LOADER_ID) != null) {
getLoaderManager().initLoader(LOADER_ID,
null, this);
}
}
Loader的初步学习笔记

 

3.9 避免出现FragmentManager exceptions

你不能在LoaderCallback这些回调里面直接创建FragmentTransaction(包括DialogFragment这种dialog)。解决方法就是使用Handler来处理FragmentTransaction。

 

四、Loader

终于来到了我们的重点了,话说前面说了好多好多啊,终于铺垫完毕了。

Loader一个执行异步加载数据的抽象类,这是一个加载器的基类。你一般可以使用AsyncTaskLoader或CursorLoader,当然你也可以实现自己的子类。当加载器被激活时,它们可以被用来监视它们的数据资源,和在内容改变时发送新的结果。

注意:Loader只能是static的,不然的话他们就会保持一个对outer class的引用。

4.1 常用类

AsyncTaskLoader

一个提供异步任务的加载器

CursorLoader

一个用来查询数据库相关的加载器,是AsyncTaskLoader的子类。

 

4.2 回调方法

重要的回调

Loader的初步学习笔记
onStartLoading()

onStopLoading()

onReset()

onForceLoad()
// from Loader

ORloadInBackground()
// from AsyncTaskLoader
Loader的初步学习笔记

可选的回调

deliverResult() [override]

 

4.3 建立一个Loader类(必须是static class)

Loader的初步学习笔记
    private class MyLoaderCallback implements LoaderManager.LoaderCallbacks {
final private String TAG = getClass().getSimpleName();

/**
*
@param id
*
@param args
*
@return Object returned from onCreateLoader must not be a non-static inner member class
*/
@Override
public Loader onCreateLoader(int id, Bundle args) {
Log.i(TAG,
"onCreateLoader");
return new MyAsyncLoader(getApplicationContext());
}

@Override
public void onLoadFinished(Loader loader, Object data) {
Log.i(TAG,
"onLoadFinished " + data);
}

@Override
public void onLoaderReset(Loader loader) {
Log.i(TAG,
"onLoaderReset");
}

}
Loader的初步学习笔记

 

Loader的初步学习笔记
    public static class MyAsyncLoader extends AsyncTaskLoader<String> {

final private String TAG = "MyAsyncLoader";

public MyAsyncLoader(Context context) {
super(context);
}

@Override
protected void onStartLoading() {
super.onStartLoading();
Log.i(TAG,
"onStartLoading");
forceLoad(); }

@Override
public String loadInBackground() {
Log.i(TAG,
"loadInBackground");
return "kale";
}

@Override
public void deliverResult(String data) {
Log.i(TAG,
"deliverResult");

}

@Override
protected void onStopLoading() {
super.onStopLoading();
Log.i(TAG,
"onStopLoading");
cancelLoad();
// Attempt to cancel the current load task if possible

}

@Override
protected void onReset() {
Log.i(TAG,
"onReset");
super.onReset();
}
}
Loader的初步学习笔记

 

输出日志:

initLoader时:

Loader的初步学习笔记

退出activity时

Loader的初步学习笔记

 

分析:

initLoader时首先调用callback中的onCreateLoader方法,返回一个loader,然后loaderManager开始管理这个loader,直接执行loader的onStartLoading方法,我们可以在这里做点准备工作,准备工作做完了后就通过forceLoad()来执行loadInBackground()方法,在这里进行加载各种数据,这个方法执行完毕后在callback中就会自动调用onLoadFinished()方法,告诉activity已经加载结束了,并且在public void onLoadFinished(Loader loader, Object data) 中,我们可以得到结果的data对象。

 

分析:回调方法所在的线程

 Loader的初步学习笔记

 这个完全可以类比到AsyncTask,之前也有预料到这个结果,loadInBackground()是在主线程之外的线程运行的,其余的回调方法都是在主线程运行的。所以可以这么理解onStartLoading()做一些初始化的工作,真正做处理的代码要放在loadInBackground()中,loadInBackground()中的代码执行完毕后会自动传输数据,我们在callback回调中接收就好了。至于怎么结束,我们可以不怎么管他,因为在当前activity退出时,它会自动调用onStop(),onRest(),咱们在这里面可以做点收尾工作。

 

4.4 一个较为完整的例子

一般我们不可能就这么简单的用loader,里面应该有一些逻辑处理,比如做点开始的准备和收尾工作之类的。

Loader的初步学习笔记
    public static class MyAsyncLoader extends AsyncTaskLoader<String> {

final private String TAG = "MyAsyncLoader";
private String mResult;

public MyAsyncLoader(Context context) {
super(context);
}

@Override
protected void onStartLoading() {
super.onStartLoading();
Log.i(TAG,
"onStartLoading");

if (mResult != null) {
//If we currently have a result available, deliver it immediately.
deliverResult(mResult);
}

if (takeContentChanged() || mResult == null) {
//If the data has changed since the last time it was loaded
//or is not currently available, start a load.
forceLoad(); // it'll call loadInBackground task
}
}

@Override
public String loadInBackground() {
Log.i(TAG,
"loadInBackground");
return "kale";
}

@Override
public void deliverResult(String data) {
Log.i(TAG,
"deliverResult");
if (isStarted()) {
//If the Loader is currently started, we can immediately deliver its results.
super.deliverResult(data);
}
}

@Override
protected void onStopLoading() {
super.onStopLoading();
Log.i(TAG,
"onStopLoading");
cancelLoad();
// Attempt to cancel the current load task if possible

}

@Override
protected void onReset() {
Log.i(TAG,
"onReset");
super.onReset();
mResult
= null;
}
}
Loader的初步学习笔记

 

 4.5 CursorLoader

CursorLoader继承自AsyncLoader,所以就不在多说了,它主要处理数据查询方面的工作。

    public static class KaleCursorLoader extends CursorLoader{public KaleCursorLoader(Context context) {
super(context);
}
}

 

五、LoaderManager.LoaderCallbacks回调的方法

之前说了很多asyncLoader的东西,这里正好用CursorLoader做例子。

5.1 onCreateLoader

当你试图访问一个装载器时(例如,通过initLoader()),它会检查那个装载器是否被已存在的id所指定。如果它没有,它将触发LoaderManager.LoaderCallbacks 的onCreateLoader()方法。

在下面例子中,onCreateLoader()回调方法创建了一个CursorLoader。你必须使用它的构造方法来创建一个CursorLoader,它需要对ContentProvider执行一个查询所需要的全套的信息。它可能需要:

  • uri--要检索内容的URI。
  • projection--要返回的某一列元素的列表。传递空将返回所有列的元素的集合,这是不高效的。
  • selection--声明要返回哪些行的过滤器,按照SQL的where语句格式化(包含where本身)。传递null将会返回给定URI的所有行。
  • selectionArgs--在Selection中可以包含多个?,它们将会被从selectionArgs中得到的值所替代,使它们显示在selection中。这些值将会被绑定为字符串。
  • sortOrder--如何对行进行排序,被格式化成SQL ORDER BY 子句(包含ORDER BY 本身)。传递空值将会使用默认排序,那也许是无序的。
Loader的初步学习笔记
 // If non-null, this is the current filter the user has provided.
String mCurFilter;
...
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri
= Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(mCurFilter));
}
else {
baseUri
= Contacts.CONTENT_URI;
}

// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
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");
}
Loader的初步学习笔记

这里怎么去理解呢?就是说这个loader是查询数据库的,我给交给这个loader进行处理的前先把数据的URI,查询的语句(select),排序方式(order by)等等作为构造函数的参数传递到loader中,这个loader用这些东西去查究好了。

 

5.2 onLoadFinished

当先前创建的装载器已经完成它的装载工作时此方法将会被调用。这个方法要保证在为这个装载器提供的最终数据释放之前被调用。在此刻你应该清除掉所有使用的旧数据(由于它将很快被释放),但是不要清除你自己传递(发布)的数据,因为它的装载机拥有它并将管理它。

一旦装载机知道应用程序不再使用那些数据就会释放它们。例如,如果数据是从一个CursorLoader返回的cursor,你自己就应该调用它的close()方法。如果游标被放置在一个CursorAdapter中,你应该使用swapCursor()方法以便旧的Cursor也被关掉。例如:

Loader的初步学习笔记
        @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// do something
data.close();
}
Loader的初步学习笔记
Loader的初步学习笔记
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
Loader的初步学习笔记

 

5.3 onLoaderReset

当先前被创建的装载机被重置的时候这个方法就被调用,从而使它的数据无效。这个回调可以让你找到数据什么时候将要被释放,这样你就可以释放掉对它的引用。如果游标被放置在一个CursorAdapter中,你应该实现调用swapCursor同时传递一个空值。

Loader的初步学习笔记
// This is the Adapter being used to display the list's data.
SimpleCursorAdapter mAdapter;
...

public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
Loader的初步学习笔记

 

 六、给Loader添加进度 & 错误处理机制

6.1 添加进度

Loader不像AsyncTask那样可以有一个进度的回调方法,所以这里要通过LocalBroadcastManager来进行处理。通过广播的形式来传递进度,下面仅仅是一个举例,和实际使用无关。

Loader的初步学习笔记
    @Override
protected void onStart() {
//Receive loading status broadcasts in order to update the progress bar
LocalBroadcastManager.getInstance(this).registerReceiver(loadingStatusReceiver, new IntentFilter(MyLoader.LOADING_ACTION));
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
LocalBroadcastManager.getInstance(
this).unregisterReceiver(loadingStatusReceiver);
}

@Override
public Result loadInBackground() {
// Show progress bar
Intent intent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA,true);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
try {
return doStuff();
}
finally {
// Hide progress bar
intent = newIntent(LOADING_ACTION).putExtra(LOADING_EXTRA, false);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
}
Loader的初步学习笔记

开始的时候建立广播对象,结束的时候注销广播,在运行的时候可以在某个时期发送广播。

 

6.2 添加错误处理

你遇到error的时候通常只能简单的返回null。

解决方法:

① 封装一个结果和exception。比如Pair<T,Exception>。你的Loader的cache需要更智能一些,他可以检查result是否为null或者是否有error。

② 在Loader中加入一个exception的变量。如果在出错的时候给这个变量赋值,在finish时传递出去这个值,然后我们就可以在callback中进行检查处理了。

Loader的初步学习笔记
public abstract class ExceptionSupportLoader<T>extends AsyncTaskLoader<T> {
private Exception lastException;
public ExceptionSupportLoader(Context context) { super(context);
}
public Exception getLastException() { return lastException;
}
@Override
public T loadInBackground() {
try{
return tryLoadInBackground();
}
catch(Exception e) {
this.lastException= e;
return null;
}
}
protected abstract T tryLoadInBackground() throws Exception;
}
@Override
public void onLoadFinished(Loader<Result>loader, Result result) {
if(result == null) {
Exception exception
= ((ExceptionSupportLoader<Result>) loader).getLastException();
//Error handling
} else {
//Result handling
}
}
Loader的初步学习笔记

 

英文资源参考(PPT):http://download.csdn.net/detail/shark0017/8265393

 

参考自:

http://blog.csdn.net/dxj007/article/details/7880417

http://www.open-open.com/lib/view/open1417955629527.html

http://blog.csdn.net/liaoqianchuan00/article/details/24094913